# T1 — Force ledger and route simulation Read `docs/living-island/README.md` first, including the rules section. Follow it. ## Goal Introduce a server-owned record of every enemy formation on the island that does not require those formations to exist as entities. Dormant records move along routes using arithmetic on a slow tick. This is the foundation for everything else in the living-island work. ## What to build Create `addons/core/Scripts/Game/ERR/ERR_ForceLedger.c`: ``` enum ERR_ForceKind { GARRISON, PATROL, CHECKPOINT, CONVOY } enum ERR_ForceDuty { HOLD, PATROL_ROUTE, MOVE_TO, SEARCH, RETURN } class ERR_ForceRoute // ordered waypoints + loop flag class ERR_ForceRecord // id, kind, position, heading, route, progress, // strength, casualties, duty, dutyTarget, // importance, radioCapability, group (null when dormant), // lastSimulatedAt, stateSince class ERR_ForceLedger // owns records; register/unregister/find/count/status ``` Create `addons/core/Scripts/Game/ERR/ERR_ForceSimulation.c` with a **pure rule class** plus the tick that applies it: ``` class ERR_ForceRules { // pure, engine-free, unit-testable static float Speed(ERR_ForceKind kind, ERR_ForceDuty duty); // m/s static int Advance(...); // returns new route index static vector Step(vector from, vector to, float speed, float seconds, out bool arrived); static bool Due(int id, float now, float lastSimulatedAt, float interval); // staggered } ``` Rules: - Dormant records advance along their route polyline by `speed * elapsed`. Walking speed default 1.4 m/s, vehicle convoy 11 m/s, a holding garrison 0. - **Do not raycast, do not query the navmesh, do not touch the world** for dormant records. Straight-line interpolation between authored route points is the intended fidelity. Authored routes follow roads; that is the content's job, not the simulation's. - Y coordinate: keep the authored route point's Y. Only call `ERR_Actors.Ground()` at the moment a record is materialised (that is T2's job, not yours). - Tick interval default 2000 ms, staggered by `id % 8` so records do not all update on the same frame. - Cap the ledger at 128 records. Refuse beyond that and log once. ## Wiring - `ERR_GameMode` owns the ledger, created next to the existing services (`m_Sites`, `m_Discovery`, …). Expose `ERR_ForceLedger Forces()`. - Tick it from the existing game-mode tick path. **Do not add a new timer or `EOnFrame`.** - `ERR_SiteService` registers a `GARRISON` record for each site guard group it already creates, so the ledger has real content from day one. The site's existing patrol cycle waypoints become the record's `ERR_ForceRoute`. - Records whose group dies must be marked eliminated and stop simulating. ## Acceptance criteria 1. With five authored sites, the ledger holds five garrison records after preparation, and `Status()` reports them. 2. A record with a 4-point looping route returns to its start after the expected time, within 5% — verified by the Python test, not by eye. 3. Advancing 128 records for a simulated hour performs no world queries and no per-record allocation. 4. `python3 tools/check.py` passes. ## Tests to write `tests/test_force_ledger.py`, using `tests/policy_harness.py`: - Extract `ERR_ForceRules.Step` and `Advance` with `body()` and run them as C++: a 4-leg loop advanced in 2 s steps arrives at each leg in the expected order and wraps correctly. - `Speed` returns 0 for `HOLD`, and a convoy outruns a foot patrol. - `Due` staggers: across ids 0..7 at the same `now`, not all are due. - Off-by-one on the final leg of a non-looping route ends at the last point and reports arrival exactly once. ## Do not - Do not spawn or despawn anything in this task. T2 owns materialisation. - Do not reference `ERR_HQ` — this is core. - Do not add a `ref array` member that grows without a cap.