Split The code is public; the app answers only to the crew.
A thumb aims with the mouse's sensitivity: touch look has no setting of its own #229
Check · Review · Land · Live done — Landed and live in production.
scout/cs:scout/191-a-thumb-aims-with-the-mouse-s-sensitivit → main 1 commit fork
7 files
changed +142
−17
README.md +18 −4
| @@ -203,8 +203,9 @@ out the HE grenade if you bought one (fire throws it, and the hand goes back to | ||
| 203 | 203 | the gun once it is gone), <kbd>Z</kbd> opens the radio, <kbd>Tab</kbd> shows |
| 204 | 204 | the scoreboard, <kbd>Esc</kbd> pauses the match on a desktop and `menu` on the |
| 205 | 205 | pad does it on a phone. Pausing also opens mute, volume, |
| 206 | look sensitivity and the scope's own share of it, which take effect at once and | |
| 207 | are remembered for next time. | |
| 206 | look sensitivity and the scope's own share of it — and on a phone, the thumb's | |
| 207 | own sensitivity too — which take effect at once and are remembered for next | |
| 208 | time. | |
| 208 | 209 | |
| 209 | 210 | **A gun has to come up before it fires.** Every weapon has a draw — the knife |
| 210 | 211 | 0.4 s, a pistol 0.5–0.6 s, a rifle 1 s, the AWP 1.25 s — and for that long |
| @@ -239,6 +240,19 @@ slower one, or set it to `1` for the old behaviour where the scope turns exactly | ||
| 239 | 240 | as fast as the hip. It is the person's view only: no bot's turn rate moves with |
| 240 | 241 | it, and nothing the policy sees changes. |
| 241 | 242 | |
| 243 | **A thumb has its own sensitivity, apart from the mouse's.** `Sensitivity` above | |
| 244 | used to be read for a touch drag too, so tuning the phone's aim for a 360px | |
| 245 | screen detuned the mouse and back again — the two devices never wanted the same | |
| 246 | number. `Touch sens` is stored and clamped exactly like `Sensitivity` (in | |
| 247 | `src/client/settings.ts`) and reaches only a touch whose role is `look` — the | |
| 248 | `Controls.touchSensitivity` in `src/client/input.ts`, run through the same | |
| 249 | `lookDelta` the mouse and the scope go through, so the stick's 60px throw and | |
| 250 | the look pipeline's units never move. It lives in the pause overlay only, next | |
| 251 | to `Sensitivity`, and only where the browser reports a coarse pointer: a | |
| 252 | mouse-and-keyboard page never grows a field for a thumb it does not have. Its | |
| 253 | default is `2.2`, the mouse's own default, so nothing changes underfoot until | |
| 254 | you touch the new number yourself. | |
| 255 | ||
| 242 | 256 | Every one of those keys is a default, not a floor: **WASD is not everyone's |
| 243 | 257 | WASD** — on an AZERTY board, a Dvorak layout or just a different habit, the |
| 244 | 258 | letters printed on the physical keys are not where this game's defaults |
| @@ -436,7 +450,7 @@ On a touch screen the same actions are a pad, and the view is the aim: | ||
| 436 | 450 | | what | key | thumb | |
| 437 | 451 | | --- | --- | --- | |
| 438 | 452 | | move | <kbd>WASD</kbd> | the left third of the view is a stick | |
| 439 | | aim | mouse | drag anywhere on the rest of the view | | |
| 453 | | aim | mouse | drag anywhere on the rest of the view, at `Touch sens` | | |
| 440 | 454 | | fire | mouse | **a second finger on the right of the view**, or the fire bar | |
| 441 | 455 | | scope (AWP only) | right click or <kbd>Q</kbd> | `scope`, held like the mouse | |
| 442 | 456 | | crouch, jump, use, reload | <kbd>Ctrl</kbd> <kbd>Space</kbd> <kbd>E</kbd> <kbd>R</kbd> | the pad under the view | |
| @@ -445,7 +459,7 @@ On a touch screen the same actions are a pad, and the view is the aim: | ||
| 445 | 459 | | scoreboard | <kbd>Tab</kbd> | hold `scores` | |
| 446 | 460 | | buy | <kbd>B</kbd> | `buy`, and `Close` when you are done | |
| 447 | 461 | | radio (go A, go B, hold) | <kbd>Z</kbd> | `radio`, then one of the three buttons | |
| 448 | | pause the match, leave it, or change mute/volume/sensitivity | <kbd>Esc</kbd> | `menu` | | |
| 462 | | pause the match, leave it, or change mute/volume/sensitivity/`Touch sens` | <kbd>Esc</kbd> | `menu` | | |
| 449 | 463 | |
| 450 | 464 | Firing is a second finger rather than a button under your aiming thumb because |
| 451 | 465 | a touch keeps the target it started on for its whole life: the finger already |
src/client/input.test.ts +46
| @@ -171,6 +171,52 @@ test("a locked mouse turns less while the scope is up, and the ratio takes effec | ||
| 171 | 171 | expect(controls.takeLook()[0]).toBeCloseTo(hip, 10); |
| 172 | 172 | }); |
| 173 | 173 | |
| 174 | // --- the thumb's own sensitivity: a touch drag never reads `sensitivity`, | |
| 175 | // the mouse's number, and changing one never moves the other. | |
| 176 | ||
| 177 | test("the same drag turns by different degrees at two touch sensitivities", () => { | |
| 178 | const { canvas, controls } = harness(); | |
| 179 | controls.touchSensitivity = 1; | |
| 180 | touch(canvas, "touchstart", at(1, 700)); | |
| 181 | touch(canvas, "touchmove", at(1, 740)); | |
| 182 | const slow = controls.takeLook()[0]; | |
| 183 | touch(canvas, "touchend", at(1, 740)); | |
| 184 | ||
| 185 | controls.touchSensitivity = 5; | |
| 186 | touch(canvas, "touchstart", at(2, 700)); | |
| 187 | touch(canvas, "touchmove", at(2, 740)); | |
| 188 | const fast = controls.takeLook()[0]; | |
| 189 | ||
| 190 | expect(slow).toBeCloseTo(40 * 1 * TOUCH_DEGREES, 10); | |
| 191 | expect(fast).toBeCloseTo(40 * 5 * TOUCH_DEGREES, 10); | |
| 192 | expect(fast).toBeGreaterThan(slow); | |
| 193 | }); | |
| 194 | ||
| 195 | test("changing the touch sensitivity leaves the mouse's alone, and a mouse move ignores it entirely", () => { | |
| 196 | const { canvas, controls } = harness(); | |
| 197 | const window = globals.window as Canvas; | |
| 198 | controls.locked = true; | |
| 199 | ||
| 200 | controls.touchSensitivity = 9; | |
| 201 | window.fire("mousemove", { movementX: 50, movementY: 0 }); | |
| 202 | const [turned] = controls.takeLook(); | |
| 203 | expect(turned).toBeCloseTo(50 * controls.sensitivity * MOUSE_DEGREES, 10); | |
| 204 | expect(controls.sensitivity).toBe(2.2); // untouched by the thumb's field | |
| 205 | ||
| 206 | touch(canvas, "touchstart", at(1, 700)); | |
| 207 | touch(canvas, "touchmove", at(1, 740)); | |
| 208 | expect(controls.takeLook()[0]).toBeCloseTo(40 * 9 * TOUCH_DEGREES, 10); | |
| 209 | }); | |
| 210 | ||
| 211 | test("createControls takes the thumb's sensitivity as its own argument, separate from the mouse's", () => { | |
| 212 | const canvas = target({ clientWidth: 1000, clientHeight: 600 }); | |
| 213 | globals.window = target(); | |
| 214 | globals.document = target({ pointerLockElement: null }); | |
| 215 | const controls = createControls(canvas as unknown as HTMLCanvasElement, 2.2, DEFAULT_BINDINGS, DEFAULT_ZOOM_RATIO, 7); | |
| 216 | expect(controls.touchSensitivity).toBe(7); | |
| 217 | expect(controls.sensitivity).toBe(2.2); | |
| 218 | }); | |
| 219 | ||
| 174 | 220 | test("a drag while scoped is scaled too — the pad is not left with the old behaviour", () => { |
| 175 | 221 | const { canvas, controls } = harness(); |
| 176 | 222 | touch(canvas, "touchstart", at(1, 700)); |
src/client/input.ts +8 −4
| @@ -52,6 +52,8 @@ export type Controls = { | ||
| 52 | 52 | /** 1 primary, 2 pistol, 3 knife, 4 grenade — main knows what they are. */ |
| 53 | 53 | readonly slotWanted: 1 | 2 | 3 | 4 | null; |
| 54 | 54 | sensitivity: number; |
| 55 | /** A thumb's own sensitivity, apart from the mouse's — touches whose role is `look` only. */ | |
| 56 | touchSensitivity: number; | |
| 55 | 57 | /** The scope's share of that sensitivity while it is up — see zoom.ts. */ |
| 56 | 58 | zoomRatio: number; |
| 57 | 59 | /** |
| @@ -118,7 +120,7 @@ function reverseBindings(bindings: Record<Action, string>): Record<string, Actio | ||
| 118 | 120 | |
| 119 | 121 | export function createControls( |
| 120 | 122 | canvas: HTMLCanvasElement, sensitivity = 2.2, bindings: Record<Action, string> = DEFAULT_BINDINGS, |
| 121 | zoomRatio = DEFAULT_ZOOM_RATIO, | |
| 123 | zoomRatio = DEFAULT_ZOOM_RATIO, touchSensitivity = 2.2, | |
| 122 | 124 | ): Controls { |
| 123 | 125 | const press = createPress(LATCHED); |
| 124 | 126 | const input: Input = { ...NO_INPUT }; |
| @@ -156,6 +158,7 @@ export function createControls( | ||
| 156 | 158 | : press.isDown("slot2") ? 2 : press.isDown("slot1") ? 1 : null; |
| 157 | 159 | }, |
| 158 | 160 | sensitivity, |
| 161 | touchSensitivity, | |
| 159 | 162 | zoomRatio, |
| 160 | 163 | scoped: false, |
| 161 | 164 | locked: false, |
| @@ -271,10 +274,11 @@ export function createControls( | ||
| 271 | 274 | stick.x = clamp((touch.clientX - state.startX) / 60, -1, 1); |
| 272 | 275 | stick.y = clamp((state.startY - touch.clientY) / 60, -1, 1); |
| 273 | 276 | } else if (state.role === "look") { |
| 274 | // The same scaling a mouse gets: a drag under a scope is the same | |
| 275 | // problem, and a thumb has less screen to give away than a mouse does. | |
| 277 | // The thumb's own sensitivity, not the mouse's: a drag under a scope | |
| 278 | // is the same problem, and a thumb has less screen to give away than | |
| 279 | // a mouse does. | |
| 276 | 280 | const look = (movement: number): number => |
| 277 | lookDelta(movement, controls.sensitivity, controls.scoped, controls.zoomRatio, TOUCH_DEGREES); | |
| 281 | lookDelta(movement, controls.touchSensitivity, controls.scoped, controls.zoomRatio, TOUCH_DEGREES); | |
| 278 | 282 | lookX += look(touch.clientX - state.x); |
| 279 | 283 | lookY += look(touch.clientY - state.y); |
| 280 | 284 | } |
src/client/main.ts +27 −3
| @@ -73,6 +73,8 @@ function boot(): void { | ||
| 73 | 73 | const discardWarning = $("discard-warning"); |
| 74 | 74 | const sensField = $<HTMLInputElement>("sens"); |
| 75 | 75 | const pauseSensField = $<HTMLInputElement>("pause-sens"); |
| 76 | const touchSensWrap = $("pause-touch-sens-wrap"); | |
| 77 | const touchSensField = $<HTMLInputElement>("pause-touch-sens"); | |
| 76 | 78 | const zoomField = $<HTMLInputElement>("zoom-sens"); |
| 77 | 79 | const pauseZoomField = $<HTMLInputElement>("pause-zoom-sens"); |
| 78 | 80 | const volumeField = $<HTMLInputElement>("volume"); |
| @@ -110,7 +112,9 @@ function boot(): void { | ||
| 110 | 112 | // click is already in force once the context exists to hear it. |
| 111 | 113 | audio.setVolume(settings.volume); |
| 112 | 114 | audio.setMuted(settings.muted); |
| 113 | const controls = createControls(scene, settings.sensitivity, settings.bindings, settings.zoomRatio); | |
| 115 | const controls = createControls( | |
| 116 | scene, settings.sensitivity, settings.bindings, settings.zoomRatio, settings.touchSensitivity, | |
| 117 | ); | |
| 114 | 118 | attachButtons(document, controls); |
| 115 | 119 | |
| 116 | 120 | // The menu's sensitivity field and the pause overlay's are the same number: |
| @@ -125,6 +129,19 @@ function boot(): void { | ||
| 125 | 129 | pauseSensField.value = String(next); |
| 126 | 130 | saveSettings(settings); |
| 127 | 131 | }; |
| 132 | // The thumb's own number, reachable only from the pause overlay — there is | |
| 133 | // no menu twin, because a device without a touch screen never sees the | |
| 134 | // field to move it. A drag still goes through `controls.touchSensitivity`, | |
| 135 | // never `controls.sensitivity`, so this can never bleed into the mouse. | |
| 136 | const syncTouchSensitivity = (value: number): void => { | |
| 137 | const next = Number.isFinite(value) | |
| 138 | ? Math.min(SENSITIVITY_MAX, Math.max(SENSITIVITY_MIN, value)) | |
| 139 | : DEFAULT_SETTINGS.touchSensitivity; | |
| 140 | settings = { ...settings, touchSensitivity: next }; | |
| 141 | controls.touchSensitivity = next; | |
| 142 | touchSensField.value = String(next); | |
| 143 | saveSettings(settings); | |
| 144 | }; | |
| 128 | 145 | // The scope's share of that sensitivity, paired the same way. It reaches the |
| 129 | 146 | // mouse and the finger through the same `controls`, so a change lands on the |
| 130 | 147 | // very next movement — mid-match, mid-scope. |
| @@ -181,6 +198,7 @@ function boot(): void { | ||
| 181 | 198 | }; |
| 182 | 199 | sensField.value = String(settings.sensitivity); |
| 183 | 200 | pauseSensField.value = String(settings.sensitivity); |
| 201 | touchSensField.value = String(settings.touchSensitivity); | |
| 184 | 202 | zoomField.value = String(settings.zoomRatio); |
| 185 | 203 | pauseZoomField.value = String(settings.zoomRatio); |
| 186 | 204 | volumeField.value = String(Math.round(settings.volume * 100)); |
| @@ -198,6 +216,7 @@ function boot(): void { | ||
| 198 | 216 | // is not a value to clamp and snap back on every keystroke. |
| 199 | 217 | sensField.addEventListener("change", () => syncSensitivity(Number(sensField.value))); |
| 200 | 218 | pauseSensField.addEventListener("change", () => syncSensitivity(Number(pauseSensField.value))); |
| 219 | touchSensField.addEventListener("change", () => syncTouchSensitivity(Number(touchSensField.value))); | |
| 201 | 220 | zoomField.addEventListener("change", () => syncZoomRatio(Number(zoomField.value))); |
| 202 | 221 | pauseZoomField.addEventListener("change", () => syncZoomRatio(Number(pauseZoomField.value))); |
| 203 | 222 | difficultyField.addEventListener("change", () => syncDifficulty(difficultyField.value)); |
| @@ -220,8 +239,8 @@ function boot(): void { | ||
| 220 | 239 | // The pause dialog sits over a live match: a key typed into one of its |
| 221 | 240 | // fields must never reach the window-level handler that drives the game. |
| 222 | 241 | for (const field of [ |
| 223 | pauseSensField, pauseZoomField, volumeField, muteButton, pauseDifficultyField, pauseMotionField, | |
| 224 | fullscreenPauseButton, | |
| 242 | pauseSensField, touchSensField, pauseZoomField, volumeField, muteButton, pauseDifficultyField, | |
| 243 | pauseMotionField, fullscreenPauseButton, | |
| 225 | 244 | ]) |
| 226 | 245 | field.addEventListener("keydown", (event) => event.stopPropagation()); |
| 227 | 246 | |
| @@ -411,6 +430,10 @@ function boot(): void { | ||
| 411 | 430 | const lock = (): void => { |
| 412 | 431 | if (!coarse) void scene.requestPointerLock(); |
| 413 | 432 | }; |
| 433 | // The thumb's own field exists only where a thumb does — the setting is | |
| 434 | // real either way, but a mouse-and-keyboard page never grows a control for | |
| 435 | // a pointer it does not have. | |
| 436 | show(touchSensWrap, coarse); | |
| 414 | 437 | /** |
| 415 | 438 | * The touch pad is up only while a match is being played and nothing is over |
| 416 | 439 | * the view: an overlay is a dialog, and a jump button still live underneath |
| @@ -685,6 +708,7 @@ function boot(): void { | ||
| 685 | 708 | savedRound = 0; |
| 686 | 709 | watching = world.me.id; |
| 687 | 710 | controls.sensitivity = settings.sensitivity; |
| 711 | controls.touchSensitivity = settings.touchSensitivity; | |
| 688 | 712 | controls.zoomRatio = settings.zoomRatio; |
| 689 | 713 | controls.scoped = false; |
| 690 | 714 | running = true; |
src/client/settings.test.ts +29 −4
| @@ -32,11 +32,13 @@ test("a JSON value that isn't an object is the default", () => { | ||
| 32 | 32 | |
| 33 | 33 | test("out-of-range numbers are clamped, not rejected", () => { |
| 34 | 34 | expect(parseSettings('{"volume":5,"sensitivity":9999,"zoomRatio":4}')).toEqual({ |
| 35 | muted: false, volume: 1, sensitivity: 12, zoomRatio: 1, difficulty: "normal", map: "de_forge", | |
| 35 | muted: false, volume: 1, sensitivity: 12, touchSensitivity: DEFAULT_SETTINGS.touchSensitivity, | |
| 36 | zoomRatio: 1, difficulty: "normal", map: "de_forge", | |
| 36 | 37 | motion: "full", crosshair: DEFAULT_CROSSHAIR, bindings: DEFAULT_BINDINGS, |
| 37 | 38 | }); |
| 38 | 39 | expect(parseSettings('{"volume":-3,"sensitivity":-1,"zoomRatio":-1}')).toEqual({ |
| 39 | muted: false, volume: 0, sensitivity: 0.2, zoomRatio: 0.1, difficulty: "normal", map: "de_forge", | |
| 40 | muted: false, volume: 0, sensitivity: 0.2, touchSensitivity: DEFAULT_SETTINGS.touchSensitivity, | |
| 41 | zoomRatio: 0.1, difficulty: "normal", map: "de_forge", | |
| 40 | 42 | motion: "full", crosshair: DEFAULT_CROSSHAIR, bindings: DEFAULT_BINDINGS, |
| 41 | 43 | }); |
| 42 | 44 | }); |
| @@ -56,8 +58,8 @@ test("a whole, valid settings object round-trips", () => { | ||
| 56 | 58 | const bindings = { ...DEFAULT_BINDINGS, forward: "KeyG" }; |
| 57 | 59 | const crosshair = { colour: "cyan", size: 12, gap: 2, thickness: 3, dot: true, dynamic: false } as const; |
| 58 | 60 | const stored: Settings = { |
| 59 | muted: true, volume: 0.4, sensitivity: 3.5, zoomRatio: 0.5, difficulty: "hard", map: "de_forge", | |
| 60 | motion: "reduced", crosshair, bindings, | |
| 61 | muted: true, volume: 0.4, sensitivity: 3.5, touchSensitivity: 4.1, zoomRatio: 0.5, difficulty: "hard", | |
| 62 | map: "de_forge", motion: "reduced", crosshair, bindings, | |
| 61 | 63 | }; |
| 62 | 64 | expect(parseSettings(JSON.stringify(stored))).toEqual(stored); |
| 63 | 65 | }); |
| @@ -74,6 +76,21 @@ test("the zoom ratio round-trips, and a value outside its limits never reaches t | ||
| 74 | 76 | expect(parseSettings('{"muted":true}').zoomRatio).toBe(DEFAULT_ZOOM_RATIO); |
| 75 | 77 | }); |
| 76 | 78 | |
| 79 | test("the touch sensitivity round-trips, is clamped like the mouse's, and defaults to the mouse's own default", () => { | |
| 80 | expect(parseSettings(null).touchSensitivity).toBe(DEFAULT_SETTINGS.sensitivity); | |
| 81 | expect(parseSettings('{"touchSensitivity":5}').touchSensitivity).toBe(5); | |
| 82 | expect(parseSettings('{"touchSensitivity":9999}').touchSensitivity).toBe(12); | |
| 83 | expect(parseSettings('{"touchSensitivity":-1}').touchSensitivity).toBe(0.2); | |
| 84 | for (const raw of ['{"touchSensitivity":"3"}', '{"touchSensitivity":null}', '{"touchSensitivity":"NaN"}']) | |
| 85 | expect(parseSettings(raw).touchSensitivity).toBe(DEFAULT_SETTINGS.touchSensitivity); | |
| 86 | }); | |
| 87 | ||
| 88 | test("the mouse's sensitivity and the thumb's move independently", () => { | |
| 89 | const parsed = parseSettings('{"sensitivity":8,"touchSensitivity":0.5}'); | |
| 90 | expect(parsed.sensitivity).toBe(8); | |
| 91 | expect(parsed.touchSensitivity).toBe(0.5); | |
| 92 | }); | |
| 93 | ||
| 77 | 94 | test("a settings blob written before the scope had its own sensitivity still loads", () => { |
| 78 | 95 | const old = '{"muted":true,"volume":0.4,"sensitivity":3.5,"difficulty":"hard",' + |
| 79 | 96 | `"crosshair":${JSON.stringify(DEFAULT_CROSSHAIR)},"bindings":${JSON.stringify(DEFAULT_BINDINGS)}}`; |
| @@ -82,6 +99,14 @@ test("a settings blob written before the scope had its own sensitivity still loa | ||
| 82 | 99 | expect(parsed.sensitivity).toBe(3.5); |
| 83 | 100 | }); |
| 84 | 101 | |
| 102 | test("a settings blob written before the thumb had its own sensitivity still loads", () => { | |
| 103 | const old = '{"muted":true,"volume":0.4,"sensitivity":3.5,"zoomRatio":0.5,"difficulty":"hard",' + | |
| 104 | `"crosshair":${JSON.stringify(DEFAULT_CROSSHAIR)},"bindings":${JSON.stringify(DEFAULT_BINDINGS)}}`; | |
| 105 | const parsed = parseSettings(old); | |
| 106 | expect(parsed.touchSensitivity).toBe(DEFAULT_SETTINGS.touchSensitivity); | |
| 107 | expect(parsed.sensitivity).toBe(3.5); | |
| 108 | }); | |
| 109 | ||
| 85 | 110 | test("a settings blob written before the crosshair existed still loads, with today's crosshair", () => { |
| 86 | 111 | // Exactly what `saveSettings` wrote yesterday: no `crosshair` key at all. |
| 87 | 112 | const old = '{"muted":true,"volume":0.4,"sensitivity":3.5,"difficulty":"hard",' + |
src/client/settings.ts +8 −1
| @@ -23,6 +23,8 @@ export type Settings = { | ||
| 23 | 23 | muted: boolean; |
| 24 | 24 | volume: number; |
| 25 | 25 | sensitivity: number; |
| 26 | /** The thumb's own look sensitivity — a touch drag never reads `sensitivity`. */ | |
| 27 | touchSensitivity: number; | |
| 26 | 28 | /** What the look sensitivity is multiplied by while a scope is up. */ |
| 27 | 29 | zoomRatio: number; |
| 28 | 30 | difficulty: Difficulty; |
| @@ -34,7 +36,9 @@ export type Settings = { | ||
| 34 | 36 | }; |
| 35 | 37 | |
| 36 | 38 | export const DEFAULT_SETTINGS: Settings = { |
| 37 | muted: false, volume: 1, sensitivity: 2.2, zoomRatio: DEFAULT_ZOOM_RATIO, | |
| 39 | // Same number as the mouse's: today's touch feel, before this setting | |
| 40 | // existed, was `sensitivity` read for a touch too — this is that feel, kept. | |
| 41 | muted: false, volume: 1, sensitivity: 2.2, touchSensitivity: 2.2, zoomRatio: DEFAULT_ZOOM_RATIO, | |
| 38 | 42 | difficulty: DEFAULT_DIFFICULTY, map: DEFAULT_MAP, motion: DEFAULT_MOTION, |
| 39 | 43 | crosshair: DEFAULT_CROSSHAIR, |
| 40 | 44 | bindings: DEFAULT_BINDINGS, |
| @@ -145,6 +149,9 @@ export function parseSettings(raw: string | null, unchosen: Motion = DEFAULT_MOT | ||
| 145 | 149 | muted: obj.muted === true, |
| 146 | 150 | volume: bounded(obj.volume, 0, 1, DEFAULT_SETTINGS.volume), |
| 147 | 151 | sensitivity: bounded(obj.sensitivity, SENSITIVITY_MIN, SENSITIVITY_MAX, DEFAULT_SETTINGS.sensitivity), |
| 152 | touchSensitivity: bounded( | |
| 153 | obj.touchSensitivity, SENSITIVITY_MIN, SENSITIVITY_MAX, DEFAULT_SETTINGS.touchSensitivity, | |
| 154 | ), | |
| 148 | 155 | zoomRatio: bounded(obj.zoomRatio, ZOOM_RATIO_MIN, ZOOM_RATIO_MAX, DEFAULT_SETTINGS.zoomRatio), |
| 149 | 156 | difficulty: isDifficulty(obj.difficulty) ? obj.difficulty : DEFAULT_SETTINGS.difficulty, |
| 150 | 157 | map: isMapName(obj.map) ? obj.map : DEFAULT_SETTINGS.map, |
src/pages/game.ts +6 −1
| @@ -354,7 +354,10 @@ export function gamePage(db: Database, options: PageOptions): string { | ||
| 354 | 354 | back to the plan it rolled. The other side never hears you.</p> |
| 355 | 355 | <p class="hint">On a phone: the left of the view is a thumb stick, the right aims, a second |
| 356 | 356 | finger on the right fires, and the pad under the view does the rest — guns, the grenade, |
| 357 | scores, buy, radio and <code>menu</code>, which pauses the match and is where you leave it from.</p> | |
| 357 | scores, buy, radio and <code>menu</code>, which pauses the match and is where you leave it from. | |
| 358 | <b>Touch sens</b> is the thumb's own aiming speed, separate from <b>Sensitivity</b> above which | |
| 359 | only the mouse feels — it lives in that pause overlay, the only place a phone can reach | |
| 360 | settings mid-match.</p> | |
| 358 | 361 | <p class="hint">Paused means paused: the world stops while the overlay is up, and so does |
| 359 | 362 | tabbing away — the match is waiting for you when you come back.</p> |
| 360 | 363 | <details class="controls"><summary>Controls</summary><div id="controls-menu"></div></details> |
| @@ -369,6 +372,8 @@ export function gamePage(db: Database, options: PageOptions): string { | ||
| 369 | 372 | while it comes back; nothing is lost, and Resume lights up when the view does.</p> |
| 370 | 373 | <div class="row"> |
| 371 | 374 | <label>Sensitivity<input id="pause-sens" type="number" min="0.2" max="12" step="0.1"></label> |
| 375 | <label id="pause-touch-sens-wrap" hidden>Touch sens<input id="pause-touch-sens" type="number" | |
| 376 | min="0.2" max="12" step="0.1"><small>the thumb's own</small></label> | |
| 372 | 377 | ${zoomControl("pause-zoom-sens")} |
| 373 | 378 | <label>Volume<input id="volume" type="range" min="0" max="100" step="1"></label> |
| 374 | 379 | <label>Difficulty<select id="pause-difficulty"> |