# Idle and Offline Simulation

Underroot is an idle game. It keeps producing and consuming while a task runs in the background, and it catches the base up when the player returns after closing the tab. Two pieces handle that: `TaskQueue`, the queue of pending dig work, and `OfflineSimulator`, which replays the elapsed time across every subsystem on resume.

## TaskQueue

`scripts/idle/TaskQueue.gd` is a thin FIFO over an `Array` of task dictionaries. A task has the shape `{ type, position, material_id, tool_id, progress, duration }`.

| Method | Effect |
|---|---|
| `add_task(task)` | Append to `queued_tasks`. |
| `get_current_task()` | The head task, or `{}` if empty. |
| `complete_current_task()` | Pop the head. |
| `cancel_task_at(index)` | Remove a task by index. |
| `has_tasks()` | Whether the queue is non-empty. |
| `clear()` | Empty the queue. |
| `get_tasks_for_save()` / `load_from_save(data)` | Deep-copy round-trip for persistence. |

The queue holds no timing logic of its own; progress is advanced by whoever ticks it, live play or the offline sim. It's just the persistent record of dig work that survives a close.

## OfflineSimulator

`scripts/idle/OfflineSimulator.gd` has one public entry point, `simulate(elapsed_seconds)`. It builds a `summary` dictionary, runs each subsystem's catch-up in a deliberate order, emits `EventBus.offline_return(summary)`, and returns it (the offline summary UI reads this).

**It has two callers, not one:**

| Caller | When |
|---|---|
| Game load | A previous timestamp exists — the ordinary resume path. |
| `BlackHollowManager.resolve_expedition()` | A Black Hollow expedition banks or busts, spending its `days_away` of village time. |

The second one has a sharp edge worth knowing about — see [Called while the game is paused](#called-while-the-game-is-paused) below.

### Order of operations

The order matters — production must be credited before consumption so a net-positive base does not starve, and the digger must eat before the village rations. The whole run is bracketed by the weather's offline flag, because weather does not advance during a catch-up and its bonuses must not credit the catch-up's digs:

```gdscript
WeatherManager.begin_offline_sim()
_simulate_tasks(elapsed_seconds, summary)
_simulate_machines(elapsed_seconds, summary)   # production first
_simulate_survival(elapsed_seconds, summary)   # digger's own drain
_check_starvation(summary)                      # digger death check
_simulate_village_rationing(elapsed_seconds, summary)
_simulate_shelter_fuel(elapsed_seconds)
_simulate_black_rot(elapsed_seconds, summary)
_simulate_maw(elapsed_seconds, summary)
_check_warnings(summary)
WeatherManager.end_offline_sim()
```

### Tasks

`_simulate_tasks` ticks `TaskQueue` down against the elapsed budget. For each task it completes within budget, it credits `TerrainDigging.roll_yield(material_id)` to both `Inventory` and `summary.resources_gained`; a partially-completed task keeps its `progress` for next time.

### Machines

`_simulate_machines` calls `MachineManager.simulate_offline(elapsed_seconds, SurvivalManager.get_idle_mult())`. The machine sim advances on the **idle clock**: every fueled machine burns *and* produces over `elapsed * idle_mult` seconds, so the fuel cost per unit of output matches live play, and the returned gains are credited 1:1 — no second scaling. (The old scheme scaled only the gains while burning the full window's fuel, which emptied a full sawmill tank for ~15% of the wood.)

Routing: food (hunting lodge) fills the survival meter and overflows into the silo via `SurvivalManager.add_supplies`; pump water goes to the **well** via `add_well_water`, exactly like the live tick; everything else is added to `Inventory`.

The pump has its own branch in the machine sim — it credits `water_output / lifespan` per idle-clock second and burns down its lifespan clock; it never touches tiles. The apothecary is the deliberate exception to idle scaling: it brews on the full clock, because its doses pace the offline death sims (Black Rot, post-ritual losses), which also run full-rate.

### Survival and the death check

`_simulate_survival` calls `SurvivalManager.simulate_offline(elapsed_seconds)`, which drains only the **digger's** personal share (idle-scaled, cosmetic and challenge modifiers applied, well trickled at full rate) and returns `{food_consumed, water_consumed}`.

`_check_starvation` runs immediately after, between the digger's drain and the village's — so it sees exactly what the digger personally needed and had. It totals food/water (adding silo/tower storage and `well_water` where owned) and, on a zero, appends a warning, calls `SurvivalManager.mark_dead()`, and emits `EventBus.player_died` with a "while away" cause string. Because `mark_dead()` sets the dead flag without emitting, the simulator owns the emit and can construct the right cause.

### Village rationing

`_simulate_village_rationing` draws the village's share of stored food/water **after** the digger (digger eats first), idle-scaled. When storage runs dry mid-window it computes `dry_days` and, if at least a day's worth, calls `VillageManager.simulate_hunger_offline(dry_days, idle_mult)` — the same attrition tiers the live daily tick applies to an unsupported village, on the idle clock — instead of killing the digger. The number lost and a warning go into the summary.

### Shelter, Black Rot, and the Maw

- `_simulate_shelter_fuel` calls `VillageManager.simulate_shelter_offline(days_passed, idle_mult)` to burn fuel over the window.
- `_simulate_black_rot` calls `VillageManager.simulate_black_rot_offline(days_passed)`. This is the challenge's defining drain — without it, closing the game would bypass the rot entirely. Losses are recorded via `GameManager.record_black_rot_deaths` (raising graveyard crosses, matching the live tick) and reported in the summary. The rot is a discrete daily tax, not idle-scaled.
- `_simulate_maw` calls `MawController.simulate_offline(...)`, stores the damage in `summary.maw_damage`, and then calls `MawController.refresh_threat_state()` so the summary headline and the live danger vignette reflect the worst front after catch-up, not a stale reading. The offline front loop only chews and advances — it never touches the light-surge timer, so **no surge fires during a catch-up**.

### Warnings

`_check_warnings` scans `MawController.fronts`; if any front's `threat_state` is `"critical"` it appends "The Maw is at your gates." (covering either side under the Two Fronts challenge).

## Called while the game is paused

`simulate()` is an ordinary synchronous function, so `get_tree().paused` does not hold it — and neither does the pause hold the signals it emits, or anything scheduled with `call_deferred`.

That matters on the Black Hollow path. `resolve_expedition()` runs the whole catch-up **while the game is paused and the expedition's end screen is still up**, so a full village tick — bells, population milestones, a possible death, toasts, narrative lines — lands *during* what the player experiences as the mini-game. Any listener that is `PROCESS_MODE_ALWAYS` reacts immediately; `AudioManager` is the obvious one, and it is why village music once started over the expedition track.

**When you add a signal to any `_simulate_*` step, or an always-on listener anywhere, check it against this path.** "The game is paused so it can't fire" is wrong here. See [Black Hollow](/docs/underroot/black-hollow).

## The summary dictionary

`simulate` returns a summary that the offline-return UI consumes. Its keys:

| Key | Contents |
|---|---|
| `elapsed_seconds`, `elapsed_display` | Raw and formatted away time. |
| `resources_gained` | `{material_id -> amount}` from tasks and machines. |
| `food_consumed`, `water_consumed` | Totals across digger and village. |
| `village_food`, `village_water` | The village's share specifically. |
| `maw_damage`, `walls_destroyed` | Maw catch-up results. |
| `villagers_lost_hunger`, `villagers_lost_black_rot` | Attrition counts. |
| `discoveries`, `warnings` | Discovery hits and player-facing warning strings. |

## Key files

- `scripts/idle/TaskQueue.gd` — FIFO of pending dig tasks; persistence round-trip.
- `scripts/idle/OfflineSimulator.gd` — resume catch-up across tasks, machines, survival, village, shelter, Black Rot, and the Maw.

## Related

- [Survival and Village](/docs/underroot/survival-and-village)
- [Machines](/docs/underroot/machines)
- [The Maw](/docs/underroot/the-maw)
- [Black Hollow](/docs/underroot/black-hollow)
- [Weather](/docs/underroot/weather)
- [GameManager and the Run Lifecycle](/docs/underroot/gamemanager-and-the-run-lifecycle)
- [Save System and Migration](/docs/underroot/save-system-and-migration)