# Projects and Specialists

Two systems drive the village's requests of the digger. `ProjectManager` spawns, tracks, fulfils, and expires villager tasks from `data/projects.json`; `SpecialistManager` delivers the four Astrolabe specialists gated by dig progress, rare finds, and population.

Both are autoloads. `ProjectManager` runs its own `_process` timer; `SpecialistManager` reacts to `EventBus` signals.

## ProjectManager

`scripts/village/ProjectManager.gd` loads task templates once from `res://data/projects.json` in `_ready()` into `_templates`, then spawns concrete tasks over time into `_active_projects` (an `Array[Dictionary]`). It emits its own `projects_changed` signal for the task UI to re-render — the classic journal tab, or the talisman skin's journal drawer and crest bell badge — plus `EventBus` events for individual task lifecycle moments.

### Spawn timing and idle freeze

`_process(delta)` first does idle detection: if there has been no mouse movement or key press it accumulates `_idle_seconds`, and once past `_IDLE_THRESHOLD` (`60` s) it returns early — expiry and spawn timers freeze so an idle player isn't punished. Browsing menus, crafting, and building all count as active because it tracks Godot input idle time, not player position.

The spawn cadence is a countdown on `_spawn_timer`. The base interval is `_SPAWN_INTERVAL_BASE` (`0.5` in-game days, averaging two tasks a day) jittered ±0.2, scaled in `_current_interval()`:

```gdscript
var speed_mult := 1.0 + clampf(depth * _DEPTH_FACTOR_PER_10 / 10.0, 0.0, _DEPTH_FACTOR_CAP - 1.0)
var pile_penalty := 1.0 + _active_projects.size() * _PILE_UP_FACTOR
return _base_interval() / speed_mult * pile_penalty
```

Deeper digging speeds spawns (up to `_DEPTH_FACTOR_CAP` = 3×); more open tasks slows them (`_PILE_UP_FACTOR` = `0.20` per active task). The first task waits `_FIRST_SPAWN_DAYS` (`1.5` days) so it doesn't crowd the opening.

### The first-task gate

Before the first task can spawn, `_first_task_released()` must pass. It releases when any of these is true, then latches into the run blob (`current_run["first_task_released"]`) so a later metric dip never re-gates:

- All three village supports sit at or above population, or
- the elder chain graduates the player (`current_run["village_chain_done"]`), or
- day 3 is reached.

### Eligibility and weighted pick

`_try_spawn()` requires `population >= 5`, then filters `_templates` by `min_depth`/`max_depth` and `_objective_spawnable(...)`. `_weighted_pick()` biases the choice by each template's `spawn_weight` (default `1.0`), so iron-heavy asks surface proportionally less often without leaving the pool.

`_objective_spawnable(tmpl, depth)` branches on `objective_type`:

| Objective type | Spawnable when |
|---|---|
| `deliver` (default) | Every required material is reachable at current depth (`_materials_reachable`) — exotic materials require the Astrolabe ritual to have seeded them. |
| `find_discovery` | Any layer the discovery appears in is reachable. |
| `mine_material` | The target material (or, for `any_new`, some undug material) is reachable and not already in `_dug_materials`. |
| `reach_depth` | A full `depth_delta` descent still fits above the world floor (`_world_floor_depth()`). |

### Building a project

`_build_project(template_id, depth)` rolls the concrete task:

- **Materials**: each template range is scaled by `depth_scale` = `1.0 + clampf(depth/100, 0, _DEPTH_SCALE_MAX - 1.0)` (`_DEPTH_SCALE_MAX` = `4.0`), then a random amount in the scaled range.
- **Reward**: if the template has a weighted `reward_pool`, one option is rolled (`_pick_reward_from_pool`) so the same task pays different rewards across spawns; otherwise the flat `reward_type`/`reward_scale` is used. Rewards are depth-scaled too, except types in `_NO_DEPTH_SCALE_REWARDS` (`reinforced_frame` — a single upgrade regardless of depth). Epic tasks may carry a `reward_bundle` granting several rewards at once.
- **Safety reward conversion**: `_resolve_safety_reward()` converts a `safety_boost` reward into `strange_root` (the wall-binding material) so the card, payout, and `project_fulfilled` signal all agree.
- **Deny penalty**: `deny_penalty_pct` is rolled from the template range; food/water penalties are scaled by `FOOD_WATER_PENALTY_SCALE` (`0.65`) because they now drain a percent of total stores.
- **Expiry**: `_EXPIRY_DAYS[category]` gives a day range converted to seconds into `expires_in`.

### Objective progress

Event-objective tasks are marked met by signal handlers, not by fulfilment. `_on_tile_dug_progress` (on `EventBus.tile_dug`) tracks `mine_material` and `reach_depth` tasks and updates `_dug_materials`; `_on_discovery_found_progress` (on `EventBus.discovery_found`) handles `find_discovery`. `_mark_met()` sets `objective_met`, toasts, and re-emits `projects_changed`. A met objective freezes — it never expires while waiting to be collected.

### Fulfil, claim, deny, expire

| Method | Effect |
|---|---|
| `fulfill(id)` | Delivery tasks only. Requires `can_fulfill` (all materials present), removes them, grants the reward, increments `tasks_fulfilled`, emits `project_fulfilled`. |
| `claim(id)` | Event tasks whose `objective_met` is true. No material cost; grants reward and increments `tasks_fulfilled`. |
| `deny(id)` | Applies `deny_penalty`, increments `tasks_denied`, emits `project_denied`. |
| `_expire_project(id)` | Applies the penalty ×`_EXPIRE_PENALTY_MULT` (`1.75`) — ignoring a task until it lapses strands the villager, so it costs more than an active deny. |

`_grant_reward()` grants each `reward_bundle` entry or the single reward via `_apply_reward()`, which routes by type: `food`/`water` to `SurvivalManager`, `population` joins villagers, `gold`/material types to `Inventory`, and `safety_boost` (legacy/in-flight) re-resolves to strange root. `_apply_penalty()` routes `food`/`water` to the drain-percent calls and `safety` to `VillageManager.apply_safety_debuff()` with magnitude and duration scaled from the rolled penalty.

### Persistence

`get_save_data()` / `load_save_data()` serialize `_active_projects`; the loader drops legacy `reach_depth` tasks whose target now sits below the dig floor (unwinnable, silently discarded). `_dug_materials` round-trips via `get_dug_materials`/`set_dug_materials`. `reset()` clears everything and waits a full day before the first task. `seed_initial_projects()` spawns 1–2 tasks immediately for saves that predate the projects system.

## SpecialistManager

`scripts/village/SpecialistManager.gd` tracks the four specialists who unlock the Terrestrial Astrolabe. Each arrival shows a unique walk-in popup, writes a gold journal entry, emits `EventBus.specialist_arrived`, and appends to `current_run["specialists_arrived"]` — so arrival state saves and restores with the rest of the run.

### The four specialists and their gates

Arrival order is fixed: archivist → stonewarden → assayer → artificer. Each is triggered by a different `EventBus` signal handler wired in `_ready()`:

| Specialist | Trigger | Handler / condition |
|---|---|---|
| `archivist` | First bell rings | `_on_village_bell_rang` — arrives if not already here. |
| `stonewarden` | Digging into the quartz layer | `_on_tile_dug` — `DataRegistry.get_layer_id_at_depth(pos.y) == "quartz"`. |
| `assayer` | Acquiring any exotic material | `_on_inventory_changed` — `material_id in EXOTIC_IDS` (`ember_essence`, `void_iron`, `ancient_clay`, `prismatic_shard`) with amount > 0. |
| `artificer` | Population threshold, with the other three present | `_on_population_changed` — see below. |

### The Artificer gate

The Artificer is the slowest gate in the run. `_on_population_changed(new_pop)` requires **all** of:

- `new_pop >= ARTIFICER_POPULATION`, and
- the archivist, stonewarden, and assayer have all already arrived.

`ARTIFICER_POPULATION` is read in `_ready()` from `DataRegistry.balance["village"]["artificer_population"]`, defaulting to `120` (the field default declared on the variable is also `120`).

> Note: the codebase guide (CLAUDE.md) describes the Artificer as "gated by `village.artificer_population`", which matches the balance key read here. The in-code default is `120`, and the population check does not fire until the three earlier specialists have arrived — the population threshold alone is not sufficient.

### Arrival flow

`_arrive(id)` records the specialist, emits the narrative and `specialist_arrived` signal, then defers `_show_popup` by `0.4` s so the journal renders first. `_show_popup` builds a `CanvasLayer` (layer 8) with a dimmer, a walking `Sprite2D` (character art from `res://assets/images/characters/<id>_full.png`), and a centered `PanelContainer` styled per specialist. The `_process` state machine (`_WalkState`) walks the sprite in, shows the panel on arrival, and walks it out on dismiss before `_cleanup_popup()` frees the layer.

`is_popup_active()` returns true from popup build to full cleanup; `VillageManager._try_ring_bell()` checks it so a bell never lands on top of a specialist arrival, and `_cleanup_popup()` calls `VillageManager.try_ring_pending_bell()` so a held bell surfaces on exit. The popup itself defers if the village bell recap is showing, so the two modals never overlap.

Query helpers: `has_arrived(id)`, `all_arrived()`, `get_arrived()`.

## Key files

- `scripts/village/ProjectManager.gd` — task spawn/eligibility/fulfil/deny/expire, reward and penalty routing.
- `scripts/village/SpecialistManager.gd` — the four Astrolabe specialists, their gates, and the walk-in popup.
- `data/projects.json` — task templates (materials, reward pools/bundles, deny penalties, depth gates, objectives).

## Related

- [Survival and Village](/docs/underroot/survival-and-village)
- [The Astrolabe](/docs/underroot/the-astrolabe-and-harrow)
- [Machines](/docs/underroot/machines)
- [DataRegistry and the Data Files](/docs/underroot/dataregistry-and-the-data-files)
- [EventBus and the Signal Convention](/docs/underroot/eventbus-and-the-signal-convention)