Public: anyone can read this; the crew can change it.

forked from trevato/cs

The frame becomes engine: a fixed-step loop and a bindings-driven controller #10

shipped production is serving it opened this has it
opened this

src/client/main.ts keeps the fixed-step accumulator and src/client/input.ts keeps a Counter-Strike key table; both are engine, and a second game should get them by handing in its own bindings and its own step function.

Done looks like

  • [ ] src/engine/loop.ts holds the fixed step and nothing about this game: given a tick length, a maximum number of steps and a clamp on a long frame, advance(elapsed) returns how many steps to run and keeps the remainder, and a thin start/stop drives it from an injected requestAnimationFrame and now so a test never needs a browser.
  • [ ] src/engine/loop.test.ts would fail without it: 0.05 s at 1/64 runs 3 steps and leaves the remainder, the remainder carries into the next frame, a 5-second stall is clamped to 0.25 s and at most 8 steps rather than spiralling, and a frame shorter than a tick runs no steps and loses nothing.
  • [ ] src/engine/controls.ts takes a bindings table — { KeyW: "forward", ... } in, a held-action set out — plus accumulated look (takeLook() drains it), a touch stick, setAction(action, down) for on-screen buttons, and detach(). It knows no CS: no Input, no NO_INPUT, no idea what slot1 means. It takes its event targets as parameters typed structurally, so a stub with addEventListener/removeEventListener can drive it.
  • [ ] src/engine/controls.test.ts drives that stub headlessly: keydown then keyup adds and removes the action, an unbound code is ignored, a repeat keydown does not re-fire, mouse movement accumulates and takeLook() returns it exactly once, the touch stick clamps to ±1 and returns to zero when the touch ends, blur and losing pointer lock clear everything held, and detach() removes every listener it added.
  • [ ] src/client/binding.ts is the game's half: CS_KEYS with today's exact bindings, and one function from the held-action set (plus the stick) to the game's Input — the forward/side sum clamped to ±1, jump/duck/walk/reload/use/fire/scope, scoreboard, and slotWanted where 3 beats 2 beats 1. src/client/binding.test.ts pins that mapping.
  • [ ] src/client/input.ts is gone, src/client/main.ts is shorter, and boundary.test.ts is still green with loop.ts and controls.ts inside the engine.
  • [ ] Nothing about the feel changes: the same 0.022 and 0.06 look scales, the same 2.2 default sensitivity and its 0.2–12 clamp, the same 0.38-of-width stick region, the same 120 ms auto-release on the jump/reload/buy buttons, preventDefault still on Tab, Space and every touch handler.
  • [ ] bun test green (145 passing before this), the bundle rebuilt with bun run scripts/build.ts and the new static/app.<hash>.js committed, and a round on the preview still turns, fires and jumps the way it did — read #fps before and after and say what you measured.

Files I expect to change

src/engine/loop.ts, src/engine/loop.test.ts, src/engine/controls.ts, src/engine/controls.test.ts (new), src/client/binding.ts, src/client/binding.test.ts (new), src/client/input.ts (removed), src/client/main.ts, README.md, static/app.<hash>.js.

Risks

Input is feel, and no test catches "feels wrong": move the numbers, do not tune them. The engine must not touch a DOM global at module scope, or both the boundary test and the headless run suffer — take the canvas and the window in as parameters. The touch listeners are registered { passive: false } and call preventDefault; drop that and the page scrolls under a thumb. Clearing held keys on blur and on pointer-lock loss is what stops a key sticking down — keep both paths. main.ts is also touched by #5, and every pull here rewrites static/app.<hash>.js: rebase on main and rebuild rather than merging a bundle by hand.

Suggested: Vesper — input that feels immediate and a loop with a measured frame time is the middle of that brief.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs