# Hybrid Tick Loop Single 24 Hz base tick; subsystems self-select frequency via stride scheduling. The sim is headless (`sim` crate) and advanced tick by tick from the game loop. --- ## Driving the sim from the render loop Rendering runs at frame rate; the sim runs in whole ticks. `Game::update` accumulates frame time and calls `Sim::step()` once per elapsed `TICK_MS` (`game/src/game.rs`): ```rust self.tick_accum_ms += (dt as f32).min(MAX_FRAME_MS); while self.tick_accum_ms >= TICK_MS { self.tick_accum_ms -= TICK_MS; if self.sim.step() { // true when a movement window executed self.track_lerp(); // entity positions may have changed self.sync_route(); // click-to-move bookkeeping self.step_movement(input);// schedule the next step right away } } ``` `MAX_FRAME_MS` (250 ms) caps how much a single slow frame catches up, so a debugger pause or window drag drops time instead of spiralling into a burst of ticks. Rendering interpolates entity positions between tiles over one movement interval (`EntityLerp`), so the 6 Hz movement cadence looks smooth at any frame rate. --- ## Stride scheduling All subsystems hang off one monotonic `Sim::tick` counter. Each subsystem fires when its stride condition is met. **Current implementation:** ``` tick % 1 == 0 → (game loop) input processing, intent scheduling tick % 4 == 0 → entity_tick() — movement / collision (sim.rs, 6 Hz) tick % 8 == 0 → (reserved) slower AI routines, pathfinding refresh tick % 192 == 0 → (reserved) world simulation (8-sec cycle: weather, daylight, respawns) ``` 24 Hz is highly composite (divisors: 1, 2, 3, 4, 6, 8, 12, 24), giving flexible stride options without resorting to co-prime tricks. `tick >> 3` yields 3 Hz pulses; world-sim at `% 192` is exactly an 8-second cycle. **Why `entity_tick` runs at 6 Hz, not 12 Hz:** without the action-point model (below), one `entity_tick` = one tile of movement, so the tick rate *is* the movement rate. 6 Hz yields the target ~6 tiles/sec. Once `energy`/`speed` are reintroduced, `entity_tick` can move up to 12 Hz (`% 2`) and per-entity `speed` sets the effective movement rate instead — decoupling sim rate from movement rate. --- ## Intents: one action per entity per movement window Actors do not act directly on the world; they schedule an intent (`Sim::set_action`). The sim keeps exactly one intent per entity, setting it again replaces it, and executing the movement window consumes it. So an actor moves at most one tile per window no matter how often it changes its mind in between, and a tap shorter than a window still registers. The player's intent is (re)set every frame from held keys or the click-to-move route; NPCs will use the same call from their `think()` (roadmap 08). Validation goes through `sim::step_allowed` on both sides — the pathfinder plans with the exact function the sim executes, so a planned route never contains a step the sim will reject. --- ## Action-point model (entity AI) > **Status: not yet implemented.** Removed from `entity.rs` as premature (no NPCs need it yet). > Until it returns, `entity_tick` runs at 6 Hz to set the movement rate directly (see above). > Reintroduce alongside roadmap item 08 (NPCs). Layered on top of `entity_tick()`. Replaces fixed per-entity cooldown timers. Each entity carries: - `energy: i32` — accumulates each tick - `speed: u8` — added to energy every `entity_tick()` An entity acts when `energy >= ACTION_COST`, then pays the cost: ```rust // inside entity_tick() entity.energy += entity.speed as i32; if entity.energy >= ACTION_COST { entity.act(&mut world); entity.energy -= ACTION_COST; } ``` Fast entities (`speed >= ACTION_COST`) act every tick. Slow entities act every N ticks naturally without any scheduler involvement. Haste and slow effects become simple `speed` modifiers — no special-case scheduling needed.