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
Sign in to join this conversation.
Public: anyone can read this; the crew can change it.
forked from trevato/cs
src/client/main.tskeeps the fixed-step accumulator andsrc/client/input.tskeeps 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.tsholds 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 thinstart/stopdrives it from an injectedrequestAnimationFrameandnowso a test never needs a browser.src/engine/loop.test.tswould 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.tstakes 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, anddetach(). It knows no CS: noInput, noNO_INPUT, no idea whatslot1means. It takes its event targets as parameters typed structurally, so a stub withaddEventListener/removeEventListenercan drive it.src/engine/controls.test.tsdrives that stub headlessly: keydown then keyup adds and removes the action, an unbound code is ignored, arepeatkeydown does not re-fire, mouse movement accumulates andtakeLook()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, anddetach()removes every listener it added.src/client/binding.tsis the game's half:CS_KEYSwith today's exact bindings, and one function from the held-action set (plus the stick) to the game'sInput— the forward/side sum clamped to ±1, jump/duck/walk/reload/use/fire/scope,scoreboard, andslotWantedwhere 3 beats 2 beats 1.src/client/binding.test.tspins that mapping.src/client/input.tsis gone,src/client/main.tsis shorter, andboundary.test.tsis still green withloop.tsandcontrols.tsinside the engine.preventDefaultstill on Tab, Space and every touch handler.bun testgreen (145 passing before this), the bundle rebuilt withbun run scripts/build.tsand the newstatic/app.<hash>.jscommitted, and a round on the preview still turns, fires and jumps the way it did — read#fpsbefore 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 callpreventDefault; 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.tsis also touched by #5, and every pull here rewritesstatic/app.<hash>.js: rebase onmainand 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.