1
0

feat: unify life-sim play controls

This commit is contained in:
2026-08-19 03:04:53 -07:00
parent 120eac878a
commit 264daaab61
14 changed files with 1697 additions and 234 deletions
+246 -22
View File
@@ -765,6 +765,72 @@
than mystery glyphs: the controls disappear everywhere space is tight
except on the devices that cannot drive without them. */
.drive-controls, .walk-controls { display: none; }
.play-hud {
position: fixed;
top: calc(var(--s4) + env(safe-area-inset-top));
left: 50%;
z-index: 4;
min-width: min(27rem, calc(100vw - 12rem));
max-width: calc(100vw - var(--s4) * 2);
transform: translateX(-50%);
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: baseline;
gap: var(--s2) var(--s3);
padding: var(--s2) var(--s3);
pointer-events: none;
color: var(--ink-2);
background: rgba(8, 12, 17, 0.78);
border: 1px solid var(--hairline);
border-radius: var(--r);
box-shadow: var(--shadow);
backdrop-filter: var(--blur);
-webkit-backdrop-filter: var(--blur);
font-size: 10px;
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
}
.play-hud[hidden] { display: none; }
.play-hud-mode { color: var(--amber-ink); text-transform: uppercase; letter-spacing: 0.11em; }
.play-hud-primary { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.play-hud-status { text-align: right; color: var(--ink-3); }
.play-hud-status.warning { color: #ff9d72; }
.mode-dock {
position: fixed;
left: 50%;
bottom: calc(var(--s4) + env(safe-area-inset-bottom));
z-index: 4;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 3px;
padding: 4px;
border: 1px solid var(--hairline);
border-radius: 999px;
background: rgba(8, 12, 17, 0.82);
box-shadow: var(--shadow);
backdrop-filter: var(--blur);
-webkit-backdrop-filter: var(--blur);
}
.mode-dock button {
min-width: 44px;
min-height: 38px;
padding: 5px 10px;
border: 0;
border-radius: 999px;
color: var(--ink-3);
background: transparent;
font: inherit;
font-size: 9px;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
}
.mode-dock button[aria-pressed="true"] {
color: var(--amber-ink);
background: rgba(242, 177, 52, 0.2);
}
.mode-dock button[hidden] { display: none; }
@media (pointer: coarse) {
.drive-controls:not([hidden]), .walk-controls:not([hidden]) {
position: fixed;
@@ -807,6 +873,142 @@
body:has(.drive-controls:not([hidden])) .source,
body:has(.walk-controls:not([hidden])) .source { bottom: 7.5rem; }
}
body.touch-capable .drive-controls:not([hidden]),
body.touch-capable .walk-controls:not([hidden]) {
position: fixed;
left: 50%;
bottom: calc(var(--s3) + env(safe-area-inset-bottom));
transform: translateX(-50%);
z-index: 5;
width: min(23rem, calc(100vw - var(--s4) * 2));
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: var(--s1);
padding: var(--s1);
border: 1px solid var(--hairline);
border-radius: var(--r);
background: var(--glass-strong);
backdrop-filter: var(--blur);
-webkit-backdrop-filter: var(--blur);
box-shadow: var(--shadow);
}
body.touch-capable .drive-control,
body.touch-capable .walk-control {
min-height: 48px;
border: 1px solid var(--hairline);
border-radius: var(--r-sm);
background: rgba(255, 255, 255, 0.08);
color: var(--ink);
font: inherit;
font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
body.touch-capable .drive-control[aria-pressed="true"],
body.touch-capable .walk-control[aria-pressed="true"] {
border-color: rgba(242, 177, 52, 0.65);
background: rgba(242, 177, 52, 0.28);
color: var(--amber-ink);
}
body:has(.drive-controls:not([hidden])) .mode-dock,
body:has(.drive-controls:not([hidden])) .rail { bottom: calc(8rem + env(safe-area-inset-bottom)); }
body:has(.walk-controls:not([hidden])) .mode-dock,
body:has(.walk-controls:not([hidden])) .rail { bottom: calc(11.5rem + env(safe-area-inset-bottom)); }
/* GTA-like touch surface: analogue movement on the left, only the
actions meaningful to the current vehicle/actor on the right. */
.touch-play-controls { display: none; }
@media (pointer: coarse) {
.touch-play-controls:not([hidden]) { display: block; }
}
body.touch-capable .touch-play-controls:not([hidden]) { display: block; }
.touch-play-controls:not([hidden]) {
position: fixed;
inset: 0;
z-index: 5;
pointer-events: none;
}
.play-stick {
--stick-x: 0px;
--stick-y: 0px;
position: absolute;
left: max(var(--s3), env(safe-area-inset-left));
bottom: calc(var(--s3) + env(safe-area-inset-bottom));
width: clamp(7.5rem, 31vw, 9rem);
aspect-ratio: 1;
border-radius: 50%;
pointer-events: auto;
touch-action: none;
user-select: none;
-webkit-user-select: none;
background: radial-gradient(circle, rgba(255,255,255,.08) 0 38%, rgba(8,12,17,.78) 40% 100%);
border: 1px solid rgba(255,255,255,.18);
box-shadow: inset 0 0 24px rgba(0,0,0,.35), var(--shadow);
backdrop-filter: var(--blur);
-webkit-backdrop-filter: var(--blur);
}
.play-stick-ring {
position: absolute;
inset: 21%;
border: 1px solid rgba(242,177,52,.32);
border-radius: 50%;
}
.play-stick-knob {
position: absolute;
left: 50%;
top: 50%;
width: 3.25rem;
height: 3.25rem;
transform: translate(calc(-50% + var(--stick-x)), calc(-50% + var(--stick-y)));
border-radius: 50%;
background: rgba(242,177,52,.28);
border: 1px solid rgba(242,177,52,.72);
box-shadow: 0 4px 14px rgba(0,0,0,.45);
transition: transform 80ms ease-out;
}
.play-stick.active .play-stick-knob { transition: none; }
.touch-actions {
position: absolute;
right: max(var(--s3), env(safe-area-inset-right));
bottom: calc(var(--s3) + env(safe-area-inset-bottom));
width: min(10rem, 42vw);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 5px;
pointer-events: auto;
}
.touch-actions button {
min-height: 46px;
padding: 5px 7px;
border: 1px solid rgba(255,255,255,.18);
border-radius: 999px;
background: rgba(8,12,17,.78);
color: var(--ink-2);
box-shadow: var(--shadow);
backdrop-filter: var(--blur);
-webkit-backdrop-filter: var(--blur);
font: inherit;
font-size: 9px;
letter-spacing: .06em;
text-transform: uppercase;
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
.touch-actions button[hidden] { display: none; }
.touch-actions button[aria-pressed="true"] {
border-color: rgba(242,177,52,.72);
background: rgba(242,177,52,.28);
color: var(--amber-ink);
}
body:has(.touch-play-controls:not([hidden])) .mode-dock {
bottom: calc(10rem + env(safe-area-inset-bottom));
}
body:has(.touch-play-controls:not([hidden])) #hint { display: none; }
body:has(.touch-play-controls:not([hidden])) .source { bottom: 10rem; }
/* ---- Responsive -------------------------------------------------------
Two breakpoints and no more. At 900 the left column stops being furniture
@@ -850,6 +1052,18 @@
`deviceProfile()` in `stage.ts` keys off this exact 600px edge — so the
stylesheet and the pixel budget cannot drift apart. */
@media (max-width: 600px) {
.play-hud {
top: calc(var(--s3) + env(safe-area-inset-top) + 2.65rem);
min-width: 0;
width: calc(100vw - var(--s3) * 2);
}
.mode-dock {
max-width: calc(100vw - var(--s3) * 2);
bottom: calc(var(--s3) + env(safe-area-inset-bottom));
}
.mode-dock button { padding-inline: 8px; }
body:has(.drive-controls:not([hidden])) #hint,
body:has(.walk-controls:not([hidden])) #hint { display: none; }
/* Everything hit by a finger clears 44px, which is 11 steps of the 4px
rhythm and the smallest target anyone has managed to defend. `#help`
was 17px tall. */
@@ -1039,28 +1253,36 @@
</div>
</div>
<div id="drive-controls" class="drive-controls" aria-label="Vehicle controls" hidden>
<button class="drive-control" data-drive-key="a" aria-pressed="false">Left</button>
<button class="drive-control" data-drive-key="w" aria-pressed="false">Throttle</button>
<button class="drive-control" data-drive-key="s" aria-pressed="false">Brake</button>
<button class="drive-control" data-drive-key="d" aria-pressed="false">Right</button>
<button class="drive-control" data-drive-key=" " aria-pressed="false">Handbrake</button>
<button class="drive-control" data-drive-action="assist">Assist</button>
<button class="drive-control" data-drive-action="reset">Reset</button>
<button class="drive-control" data-drive-action="camera">Camera</button>
</div>
<section id="play-hud" class="play-hud" aria-live="polite" hidden>
<span id="play-hud-mode" class="play-hud-mode"></span>
<strong id="play-hud-primary" class="play-hud-primary"></strong>
<span id="play-hud-status" class="play-hud-status"></span>
</section>
<div id="walk-controls" class="walk-controls" aria-label="Walking controls" hidden>
<button class="walk-control" data-walk-key="a" data-walk-label="Left" data-aircraft-label="Roll left" aria-pressed="false">Left</button>
<button class="walk-control" data-walk-key="w" data-walk-label="Forward" data-aircraft-label="Pitch up" aria-pressed="false">Forward</button>
<button class="walk-control" data-walk-key="s" data-walk-label="Back" data-aircraft-label="Pitch down" aria-pressed="false">Back</button>
<button class="walk-control" data-walk-key="d" data-walk-label="Right" data-aircraft-label="Roll right" aria-pressed="false">Right</button>
<button class="walk-control flight-only" data-walk-key="q" data-walk-label="Descend" data-aircraft-label="Yaw left" aria-pressed="false">Descend</button>
<button class="walk-control flight-only" data-walk-key="e" data-walk-label="Climb" data-aircraft-label="Yaw right" aria-pressed="false">Climb</button>
<button class="walk-control crow-only" data-walk-key="g" data-walk-label="Glide" aria-pressed="false" hidden>Glide</button>
<button class="walk-control aircraft-only" data-walk-key=" " data-aircraft-label="Throttle" aria-pressed="false" hidden>Throttle</button>
<button class="walk-control aircraft-only" data-aircraft-action="assist" hidden>Assist</button>
<button class="walk-control aircraft-only" data-aircraft-action="reset" hidden>Reset</button>
<nav id="mode-dock" class="mode-dock" aria-label="Control mode">
<button type="button" data-control-mode="overview" aria-pressed="true">View</button>
<button type="button" data-control-mode="drive" aria-pressed="false" hidden>Drive</button>
<button type="button" data-control-mode="actor" aria-pressed="false" hidden>Explore</button>
<button type="button" data-control-mode="aircraft" aria-pressed="false" hidden>Fly</button>
<button type="button" data-control-mode="office-walk" aria-pressed="false" hidden>Walk</button>
</nav>
<div id="touch-play-controls" class="touch-play-controls" aria-label="Touch play controls" hidden>
<div id="play-stick" class="play-stick" role="application"
aria-label="Movement joystick. Drag in any direction.">
<span class="play-stick-ring" aria-hidden="true"></span>
<span id="play-stick-knob" class="play-stick-knob" aria-hidden="true"></span>
</div>
<div class="touch-actions">
<button id="touch-primary" data-play-control="primary" aria-pressed="false">Action</button>
<button id="touch-secondary" data-play-control="secondary" aria-pressed="false" hidden>Glide</button>
<button id="touch-pitch-up" data-play-control="pitch-up" aria-pressed="false" hidden>Pitch +</button>
<button id="touch-pitch-down" data-play-control="pitch-down" aria-pressed="false" hidden>Pitch </button>
<button id="touch-assist" hidden>Assist</button>
<button id="touch-reset" hidden>Reset</button>
<button id="touch-camera" hidden>Camera</button>
<button id="touch-map" aria-pressed="false">Map</button>
</div>
</div>
<p id="source" class="source"></p>
@@ -1082,6 +1304,7 @@
<dt><kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd></dt><dd>Drive, walk, or manually fly the California aircraft</dd>
<dt><kbd>V</kbd></dt><dd>Walk through an office / return to the dollhouse view</dd>
<dt><kbd>Q</kbd> / <kbd>E</kbd></dt><dd>Descend / climb while flying as a crow</dd>
<dt><kbd>I</kbd> / <kbd>K</kbd></dt><dd>Pitch the crow up / down</dd>
<dt><kbd>Space</kbd></dt><dd>Handbrake while driving</dd>
<dt><kbd>P</kbd> / <kbd>R</kbd></dt><dd>Resume assisted drive or flight / reset the vehicle</dd>
<dt><kbd>C</kbd></dt><dd>Switch chase / driver-height camera</dd>
@@ -1099,7 +1322,8 @@
<dt>One finger</dt><dd>Orbit</dd>
<dt>Two fingers</dt><dd>Pinch to zoom, drag to move over the ground</dd>
<dt>Tap a marker</dt><dd>Its card, at the bottom of the screen</dd>
<dt>Route controls</dt><dd>Choose 101 or I-5; the driving pad appears at the bottom</dd>
<dt>Play joystick</dt><dd>Move with the lower-left stick; current actions appear lower right</dd>
<dt>Map</dt><dd>The play action opens or closes the local-player plan</dd>
<dt>Tap the map</dt><dd>Dismiss the card; tap the ☰ sheet's scrim to close it</dd>
<dt>Plan view</dt><dd>The button on the rail shows or hides it, as a sheet above the rail</dd>
</dl>