---
phase: 06-client-rebuild-mvp-gate-cli-08-hard-milestone
plan: 13
subsystem: game-logic
tags: [movement, physics, constants, gml, reverse-engineering, bno-fidelity, tdd]

requires:
  - phase: 01-extraction
    provides: "extracted GML files (objects/0000-server/events/, rooms/0058-BNCentral/meta.json)"
  - phase: 04-server-rebuild-mvp
    provides: "packages/game-logic/src/step.ts pure function; PlayerSim/WorldState types"
  - phase: 06-client-rebuild-mvp-gate-cli-08-hard-milestone (earlier plans)
    provides: "predictor.ts + TICK_MS accumulator (06-06); step() interface stable"

provides:
  - "packages/game-logic/src/constants.ts — 9 movement constants derived from extracted Client 5-8 GML, each with SOURCE: comment and GML citation"
  - "packages/game-logic/src/step.ts — BNO-faithful step() consuming constants; diagonal-normalize, snap-round, instant-stop, 1-px collision step"
  - "packages/game-logic/test/movement-constants.test.ts — 9 pinning tests, one per constant export"
  - "packages/game-logic/test/step-bno-fidelity.test.ts — 12 golden-trajectory tests covering D-32 behaviors"
  - "apps/client/src/prediction/predictor.ts — annotated as no-override; fully delegates to step()"
  - "06-13-MOVEMENT-DERIVATION.md — full derivation trail with D-32 sections; auditable GML evidence"

affects:
  - "06-16 sprite-state machine (builds on these constants for 8-direction animation)"
  - "06-17 UAT / CLI-08 milestone (tests BNO-faithful feel against original)"
  - "Any future plan that modifies WALK_SPEED or movement model"

tech-stack:
  added: []
  patterns:
    - "SOURCE: comment pattern — every constant cites its extracted GML file + line"
    - "BNO direction+speed model — scalar fspeed + direction angle via trig decomposition rather than separate hsp/vsp"
    - "Tick-ratio scaling — dt_ms / BNO_TICK_MS to maintain correct px/tick at any server Hz"

key-files:
  created:
    - packages/game-logic/src/constants.ts
    - packages/game-logic/test/movement-constants.test.ts
    - packages/game-logic/test/step-bno-fidelity.test.ts
    - .planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-13-MOVEMENT-DERIVATION.md
  modified:
    - packages/game-logic/src/step.ts
    - apps/client/src/prediction/predictor.ts

key-decisions:
  - "WALK_SPEED = 3 px/tick (BNO native 30 Hz) from Create.gml:23 global.curspeed=3"
  - "DIAGONAL_NORMALIZATION_MODE = normalize — BNO uses lengthdir trig decomposition; no 1.414x advantage"
  - "SUB_PIXEL_ACCUMULATOR_MODE = snap-round — Step.gml:224 applies round() before pixel loop"
  - "ACCEL = FRICTION = 0 — instant set/stop confirmed; no ramp or decay observed"
  - "TICK_RATE_HZ = 30 (BNO native) vs server 20 Hz; step() uses tick-ratio scaling; full reconciliation deferred to 06-17"
  - "vx/vy zeroed each tick — BNO has no persistent velocity; position is direct displacement"

requirements-completed: [REQ-CLI-04]

duration: 45min
completed: 2026-05-10
---

# Phase 6 Plan 13: Movement Derivation and BNO-Faithful Constants Summary

**Movement numeric constants reverse-engineered from extracted Client 5-8 GML (Create.gml + Step.gml + Keyboard-37..40 events), pinned in constants.ts with SOURCE: comments, and consumed by a rewritten step() + D-32 golden-trajectory fidelity tests**

## Performance

- **Duration:** ~45 min
- **Started:** 2026-05-10T22:50:00Z
- **Completed:** 2026-05-10T23:00:00Z
- **Tasks:** 2 (Task 1: derivation doc + constants; Task 2 TDD: tests + step() + predictor update)
- **Files modified:** 6

## Accomplishments

- Derived all 9 movement constants directly from extracted GML with auditable SOURCE: citations — no arbitrarily invented values
- D-32 coverage complete: diagonal normalization (normalize via trig), collision step (1 px loop), keyboard buffering (0 ms / continuous held), sub-pixel accumulator (snap-round)
- Rewrote step() to consume constants (no inline magic numbers); applies BNO-faithful instant-set/stop model with tick-ratio scaling
- 43 tests GREEN across 6 test files; client typecheck clean; server 61 tests unaffected

## Final Values of All 9 Derived Constants

| Constant | Value | GML Source |
|----------|-------|------------|
| `TICK_RATE_HZ` | `30` | BNCentral/meta.json:9 `"speed": 30` |
| `WALK_SPEED_PX_PER_TICK` | `3` | Create.gml:23 `if(global.curspeed==0) global.curspeed=3` |
| `ACCEL_PX_PER_TICK_SQ` | `0` | Absent — Keyboard-37.gml:17 sets fspeed directly |
| `FRICTION_PX_PER_TICK_SQ` | `0` | Absent — KeyRelease-37.gml:11 sets fspeed=0 instantly |
| `KEY_BUFFER_WINDOW_MS` | `0` | Absent — GM5 Keyboard events fire continuously while held |
| `SUB_PIXEL_ACCUMULATOR_MODE` | `'snap-round'` | Step.gml:224 `move = round(lengthdir_x(...))` |
| `DIAGONAL_NORMALIZATION_MODE` | `'normalize'` | Step.gml:224 trig decomposition; no 1.414× advantage |
| `COLLISION_STEP_PX` | `1` | Step.gml:226-233 pixel-by-pixel for-loop with break |
| `STARTING_DIRECTION_DEGREES` | `270` | Convention; Create.gml `//dir=2` comment; NaviStandD default |

## D-32 Resolution

- **Diagonal normalization**: `'normalize'` — BNO uses single `direction` angle (e.g. 135° for UP-LEFT) + scalar `fspeed`; `move = round(lengthdir_x(3, 135°)) = -2`. Each diagonal axis gets `round(WALK_SPEED / √2) ≈ 2` px — inherently normalized. No 1.414× advantage. Evidence: Keyboard-37.gml:12-19 sets `direction = DIR_UL` and `fspeed = global.curspeed`; Step.gml:224 applies the trig decomposition.

- **Collision-step granularity**: `1 px` — Step.gml:226-243 implements a manual pixel-by-pixel loop (x-axis then y-axis separately) using `for(i=0; i<abs(move); i+=1) { ... x+=1; else break; }`. Same 1-px pattern in the ice-tile section. GM5 `move_contact_solid` equivalent.

- **Keyboard buffering**: `0 ms` — GM5 Keyboard events (37-40) fire every step while the key is held; this is `keyboard_check()` semantics (continuous). No `keyboard_check_pressed`, no timestamp arrays, no grace window detected in any of the 8 Keyboard/KeyRelease event files.

- **Sub-pixel accumulator**: `'snap-round'` — `round()` applied to the displacement before the pixel loop. `lengthdir_x(fspeed, direction)` returns a float which is rounded to an integer before advancing. Final x/y are always integer-valued. TypeScript: `dx = Math.round(WALK_SPEED * tickRatio * cos(dirRad))`.

## GML Constructs That Don't Translate Directly

- `lengthdir_x/y(spd, dir_degrees)` → `spd * Math.cos/sin(dir * Math.PI / 180)`. GM5 uses degrees counter-clockwise from east (0°=right, 90°=up, 270°=down).
- `global.curspeed` (global mutable) → `WALK_SPEED_PX_PER_TICK` constant. JokerShell item sets it to 7 — out of scope for MVP.
- `DIR_UL/UR/DL/DR` constants → inferred as ≈ 45° diagonal angles; exact values depend on a constants-object not visible in extracted files.
- Movement tile system (`srspeed`, `slspeed`, etc.) → conveyor system, separate from player-input motion; not ported.

## Cross-Reference Outcome — RebnoRoom.ts

RebnoRoom.ts has no hardcoded movement constants. It imports `TICK_MS as GAME_LOGIC_TICK_MS` from `@rebno/game-logic` (accumulator.ts = 50ms / 20 Hz) and passes it to `setSimulationInterval` and `step()`. The server calls `step(state, inputs, TICK_MS)` where TICK_MS = 50ms. The new step.ts handles this correctly via `tickRatio = dt_ms / BNO_TICK_MS = 50 / 33.33 = 1.5` — each 20 Hz server tick produces 1.5 BNO-ticks worth of movement (4.5 px/tick → round(4.5) = 5 px at cardinal). Full rate reconciliation (upgrade server to 30 Hz or adjust scaling) is deferred to 06-17 UAT.

**Outcome: server's existing `TICK_MS` value verified to match game-logic; no drift introduced.**

## 06-16 Foundation Readiness

The 06-16 sprite-state machine plan has a stable numeric foundation:
- `WALK_SPEED_PX_PER_TICK = 3` — confirmed BNO baseline
- `DIAGONAL_NORMALIZATION_MODE = 'normalize'` — 8-direction sprite selection needs to handle diagonal moves differently from cardinal
- `TICK_RATE_HZ = 30` — sprite animation timing aligns with BNO's step rate
- `STARTING_DIRECTION_DEGREES = 270` — default idle sprite = facing DOWN

## Task Commits

1. **Task 1: Derivation doc + constants.ts** — `8ca848f` (feat)
2. **Task 2 RED: Pinning tests + fidelity tests** — `33065ab` (test)
3. **Task 2 GREEN: step.ts + predictor.ts** — `c138dc1` (feat)

## Files Created/Modified

- `packages/game-logic/src/constants.ts` — 9 movement constants derived from extracted GML, each with SOURCE: comment
- `packages/game-logic/src/step.ts` — BNO-faithful step() consuming constants; diagonal normalize, snap-round, instant-stop
- `packages/game-logic/test/movement-constants.test.ts` — 9 pinning tests, one per export
- `packages/game-logic/test/step-bno-fidelity.test.ts` — 12 golden-trajectory D-32 tests
- `apps/client/src/prediction/predictor.ts` — header comment confirming no override
- `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-13-MOVEMENT-DERIVATION.md` — full derivation trail

## Decisions Made

- **WALK_SPEED = 3** from Create.gml:23 — unambiguous single value; JokerShell item (7) documented but not in baseline constant
- **DIAGONAL_NORMALIZATION_MODE = 'normalize'** from trig decomposition in Step.gml — not `accept-1.414x` as originally assumed likely for GM5 era; the lengthdir approach is naturally normalizing
- **TICK_RATE_HZ = 30 documents BNO native rate** — server continues at 20 Hz; step() adapts via tick-ratio; this is a known temporary discrepancy pending 06-17 UAT
- **vx/vy zeroed each tick** — BNO has no velocity accumulation; the old FRICTION=0.85 model was architecturally wrong for BNO fidelity
- **TDD committed with 3 commits** (derivation, RED tests, GREEN implementation) per the plan's TDD protocol

## Deviations from Plan

None — plan executed exactly as written. The derivation confirmed that the "most conservative" defaults specified in the plan (instant-stop, no buffering) are exactly what BNO uses. DIAGONAL_NORMALIZATION_MODE = 'normalize' was correctly anticipated.

## Uncertainties Flagged

- `STARTING_DIRECTION_DEGREES = 270`: The `//dir = 2` comment in Create.gml is commented out. Best-guess assumption. Unit test pins this value so future GML evidence can falsify it.
- `DIR_UL/UR/DL/DR` exact angles: Not visible in extracted files; inferred as standard ±45° diagonal angles. Confidence high but not from direct GML line.
- Tick-rate discrepancy (30 Hz BNO vs 20 Hz REBNO server): Documented in constants.ts and this summary. Round-at-20Hz produces slightly different pixel values than BNO's 30 Hz (5 px vs 3 px cardinal). Deferred to 06-17 UAT for final reconciliation.

## Trace Tag Verification — REQ-CLI-04

```
pnpm trace:list | grep CLI-04
→ [OK] REQ-CLI-04  required: [doc, impl, unit]  stages: +doc +impl +unit +int
```

All required stages satisfied. Tags in:
- `packages/game-logic/src/constants.ts` — `// [impl->REQ-CLI-04]`
- `packages/game-logic/src/step.ts` — `// [impl->REQ-CLI-04]`
- `packages/game-logic/test/movement-constants.test.ts` — `// [unit->REQ-CLI-04]`
- `packages/game-logic/test/step-bno-fidelity.test.ts` — `// [unit->REQ-CLI-04]`
- `apps/client/src/prediction/predictor.ts` — `// [impl->REQ-CLI-04]`
- `06-13-MOVEMENT-DERIVATION.md` — `[doc->REQ-CLI-04]`

---
*Phase: 06-client-rebuild-mvp-gate-cli-08-hard-milestone*
*Completed: 2026-05-10*
