# Machines

Placed machines are stored, kept spatially indexed, ticked each frame, and simulated offline. It all runs through the `MachineManager` autoload and the `MachineData` value object it manages.

## The machines dictionary and spatial index

`scripts/machines/MachineManager.gd` holds two dictionaries that must stay in lockstep:

```gdscript
var machines: Dictionary = {}        # id -> MachineData
var _occupied_tiles: Dictionary = {} # Vector2i -> String (machine_id)
```

`machines` is the authoritative set of placed machines, keyed by `MachineData.id`. `_occupied_tiles` maps every tile a machine's footprint covers to that machine's id, so footprint checks are `O(footprint)` lookups instead of a nested loop over every placed machine.

A desync would corrupt placement checks, so the index is only mutated through two private helpers, called on every place, remove, relocate, and load:

- `_register_machine(machine)` — writes each `machine.position + (dx, dy)` tile (over the machine's oriented footprint) into `_occupied_tiles`. Call **after** adding to `machines`.
- `_unregister_machine(machine)` — erases those same tiles. Call **before** removing from `machines`.

`reset()` clears both together; `load_from_save(data)` clears both, rebuilds each `MachineData` via `MachineData.from_dict`, and re-registers it. `occupied_tiles()` returns a read-only view (used by the Astrolabe tile-restore so it never refills a tile a surviving machine sits on).

### Footprint orientation

`oriented_footprint(type, dir)` returns the `(w, h)` footprint, swapping width and height for a directional machine (only `steam_excavator`, flagged `directional` in `data/machines.json`) aimed right or left. `dig_face_offsets(machine)` returns the cut-face tile offsets (length = `dig_width`) perpendicular to travel, and `_dig_travel(machine)` gives the advance delta once a face is clear (down by default; right/left for directional).

### Footprint clearance checks

Three predicates read `_occupied_tiles` plus the terrain grid:

| Method | Passes when |
|---|---|
| `is_footprint_clear(pos, w, h, exclude_id)` | Every tile is empty terrain **and** unoccupied by another machine. Used for underground placement. |
| `is_footprint_solid(pos, w, h, exclude_id)` | Every tile is solid terrain and unoccupied — for machines that must sit on rock. |
| `is_footprint_occupied(pos, w, h, exclude_id)` | Any tile is occupied by another machine — for surface machines where terrain at y=0 is solid by design. |

`exclude_id` lets a machine ignore its own footprint (e.g. when relocating).

## MachineData

`scripts/machines/MachineData.gd` is a `class_name MachineData extends RefCounted` value object. Beyond identity (`id`, `machine_type`, `position`, `target_position`, `dig_direction`) it carries per-machine runtime state used by the various tick paths:

| Field group | Fields |
|---|---|
| Fuel / activity | `fuel_material_id`, `fuel_remaining`, `fuel_quality`, `is_active`, `work_rate` |
| Durability | `uses_completed`, `next_break_at` (0 = not yet rolled), `is_broken` |
| Processor | `process_recipe`, `process_progress`, `process_duration`, `auto_mode`, `is_upgraded`, `pending_batches`, `output_inventory` |
| Sawmill | `wood_progress`, `sawmill_upgrade`, `sawmill_wood_target` |
| Hunting lodge | `lodge_timer`, `lodge_hunting`, `lodge_upgrade`, `lodge_food_target` |
| Apothecary | `apoth_upgrade`, `apoth_stock`, `apoth_target`, `apoth_progress` |

`create(type, pos)` builds a fresh instance with `id = "<type>_<x>_<y>"`. `to_dict()` / `from_dict()` serialize the full state; `from_dict` re-derives `process_recipe` from the machine def when the saved value is empty, and pulls `work_rate` from the def.

## Placement

`place_machine(type, tile_pos, dig_direction)` is the general path (drills, excavators, pumps, lodge). It requires one of the machine item in `Inventory`, enforces the def's `max_placed` (counting only non-broken machines of that type), consumes the item, creates the `MachineData`, sets type-specific fields, adds it to `machines`, calls `_register_machine`, and emits `EventBus.machine_placed`. Type-specific setup: the pump gets its `fuel_remaining` from `lifespan_days` **and claims the water it sits against** — `_claim_water_pockets` consumes every adjacent water discovery via `DiscoveryManager.pump_discovery` (no dig reward; the pump's well delivery IS the extraction, and a claimed pocket can't feed a second pump). The lodge starts its hunting timer.

`place_static_machine(type, tile_pos)` is the processor/structure path: it checks `is_footprint_clear` for the raw def footprint, sets `process_recipe` from the def's `recipe_id`, and registers the machine. Directional/miner target resets are handled by `relocate_machine`, which unregisters the old footprint, moves the machine, resets a miner's `target_position`, and re-registers — again keeping the index in sync.

## Processing machines

Machines flagged `is_processor` in `data/machines.json` (smelter, forge, blast furnace, kiln, crucibles, etc.) convert inputs to outputs on a timer.

- `start_processing(id)` checks the machine is idle with empty output, that `Inventory.can_craft(recipe)` holds, consumes the recipe inputs, sets `process_duration` from `DataRegistry.get_craft_duration`, and activates it.
- `_tick_processor(machine, delta)` advances `process_progress`; upgraded machines tick at `UPGRADE_TICK_MULT` (`1.334`). On completion it deposits `output_amount` of `output_id` into `output_inventory`, increments `uses_completed`, and either breaks (once `uses_completed >= next_break_at`), auto-restarts (`auto_mode`, consuming inputs again and setting `process_duration = base_time * AUTO_PENALTY`), or emits `machine_idle`.

`AUTO_PENALTY` (`1.334`) and `UPGRADE_TICK_MULT` (`1.334`) are exact inverses, so an upgraded auto machine runs at manual speed. `effective_process_seconds(recipe, auto, upgraded)` mirrors this exactly for UI labels. `collect_output(id)` moves `output_inventory` into `Inventory`, clears `pending_batches`, and emits `machine_idle` to force a redraw.

## The per-frame tick

`process_machines(delta)` (driven by the `World.gd` heartbeat) loops active machines and dispatches by kind: `_tick_processor` for recipe machines, `_tick_lodge` / `_tick_apothecary` / `_tick_pump` / `_tick_sawmill` for special buildings, and `_tick_machine` for fuel-burning miners (which damage tiles by `work_rate * fuel_quality * delta`, harvest via `TerrainDigging.roll_yield`, and `_advance_target` down the column). After the loop it removes worn-out fuel machines — **except** `repairable` surface buildings (lodge/sawmill/apothecary), which stay standing to be repaired — calling `_unregister_machine` before erasing each and emitting `machine_broken`.

Surface buildings regulate themselves. The sawmill, apothecary, and lodge "bank the fire" once their target (`sawmill_wood_target`, `apoth_target`, `lodge_food_target`) is met, burning nothing until stock drops. The lodge alternates hunting and resting phases and can, rarely, lose a villager (`hunt_death_chance`).

## Durability, upgrades, and repair

`_roll_break_at(def)` rolls `next_break_at` between `durability_min` and `durability_max` (0 means no durability, e.g. the drill). `upgrade_machine(id)` spends one `reinforced_frame` to set `is_upgraded`. Broken machines are fixed by `repair_machine(id)` (spends `repair_cost` gold) or `repair_machine_with_materials(id)` (surface buildings use `repair_materials`; processors derive 2× their recipe inputs); both re-roll `next_break_at`. `remove_machine(id)` unregisters and erases.

## Offline simulation

`simulate_offline(elapsed_seconds, idle_mult)` returns a `{material_id -> amount}` gains dict, catching each machine up over the away window on the **idle clock**: fuel burn and output both advance over `elapsed * idle_mult` seconds, and `OfflineSimulator` credits the gains 1:1, so offline fuel economics match live play. It skips processors, but handles the lodge (hunt/rest loop capped by fuel and the food target), the pump (credits well water against its lifespan clock — it has its own branch and never falls into the miner loop), the sawmill (banks the fire at its wood target), and generic miners (mining tiles against idle-scaled fuel time). The apothecary deliberately ignores `idle_mult` and brews on the full clock, consuming resources directly rather than through the gains dict — its doses pace the offline death sims, which also run full-rate. Food gains go back through `SurvivalManager`; pump water lands in the well; other materials are inventory items. See the offline sim page for how this fits the wider catch-up.

## Key files

- `scripts/machines/MachineManager.gd` — machines dict, `_occupied_tiles` index, placement, ticking, offline sim, repair/upgrade.
- `scripts/machines/MachineData.gd` — per-machine value object and its serialization.
- `data/machines.json` — machine definitions (footprint, `is_processor`, `recipe_id`, durability, `max_placed`, upgrade tables).

## Related

- [Adding a Machine](/docs/underroot/adding-a-machine)
- [World and Terrain](/docs/underroot/world-and-terrain)
- [Idle and Offline Simulation](/docs/underroot/idle-and-offline-simulation)
- [DataRegistry and the Data Files](/docs/underroot/dataregistry-and-the-data-files)
- [The Astrolabe](/docs/underroot/the-astrolabe-and-harrow)