# Challenges

Challenges are opt-in run modifiers (meta-progression) that make a run harder in exchange for mastery rewards. `scripts/core/ChallengeManager.gd` stores the active set, exposes effect multipliers, and gets consulted by hook sites across the codebase.

## Overview

The `ChallengeManager` autoload owns three things:

- The **catalog** the selection UI renders (`CATALOG`, `ORDER`, `ACCENT`, comic art).
- The **active set** for the current run, read from `GameManager.current_run.challenges`.
- The **effect-multiplier accessors** that every hook site consults to apply a Challenge's mechanical changes.

The active set is an `Array` of Challenge ids. `ChallengeManager` doesn't own that array; `GameManager` does, inside `current_run`, so it rides the save blob and survives save-load like any other run state. `ChallengeManager` only reads it.

## The catalog

Every Challenge is a key in the `CATALOG` dictionary. Six are defined and rendered in `ORDER`:

| id | Name | Tier | Effects (summary) |
|---|---|---|---|
| `lone_villager` | The Lone Villager | `free` | Population starts at 1; gathering bell 2x slower; begin with 10 food/10 water; berries pick 1 at a time; pond starts near dry and seeps back slowly |
| `brittle_world` | Brittle World | `free` | Tool durability x0.5; wall HP x0.5 |
| `eye_of_the_storm` | Eye of the Storm | `earned` | Near-constant rain plus random storms; storms speed the Maw x1.75; shelter fuel x2 |
| `ravenous_maw` | The Ravenous Maw | `earned` | Grace period 360s to 60s; Maw learns x3; escalation x3; ritual chew increase x2 |
| `black_rot` | The Black Rot | `earned` | 1 strange root per villager per day, or they die; starting root buffer plus boosted root trade |
| `two_fronts` | Two Fronts | `earned` | A second Maw crosses in from the west, with its own outskirt wall line and breach |

Each catalog entry carries `name`, `icon` (an emoji glyph from the bundled Noto subset), `tier`, `implemented`, `effects`, `unlock_hint`, `backstory`, and `aid_pool`. Two fields gate behavior:

- `tier` is `"free"` (selectable the moment the system unlocks) or `"earned"` (must be unlocked by playing).
- `implemented` is `false` for a catalog entry whose effects are not wired yet; such a Challenge shows on the locked grid but can never be selected.

`ACCENT` maps each id to a thematic hex accent colour; resolve it with `accent_color(id)` (returns a `Color`, defaulting to `#9a8f6e` on miss). `comic_path(id)` returns the `res://` path to a Challenge's one-page comic intro, or `""` if none.

## The unlock flow

The whole system is gated behind the Astrolabe (referred to only as "the Ritual" in player-facing copy). In `_on_ritual_performed()` — connected to `EventBus.astrolabe_activated` — once `AstrolabeManager.get_uses_count() >= 3`, `ChallengeManager` calls `SaveManager.mark_challenges_unlocked()` and announces the unlock with fireworks in the six accent colours plus a toast. The unlocked flag lives account-wide in `config.json` (`SaveManager.challenges_unlocked()`).

Earned Challenges unlock individually through `_evaluate_earn()`, connected to `EventBus.new_day` and `EventBus.generation_started`. Conditions are endurance / resilience / stacking based, never depth. Each earned Challenge unlocks when ANY one of its conditions is met **during a Challenge run** (a run with at least one active Challenge):

| Challenge | Earn condition |
|---|---|
| `eye_of_the_storm` | Survive 15 days |
| `ravenous_maw` | Survive 20 days, or grow a lineage of 3 |
| `black_rot` | Survive 14 days with 2 or more Challenges stacked |
| `two_fronts` | Survive 25 days, or perform 3 Rituals |

`_earn(id)` records the earn via `SaveManager.add_challenge_earned(id)` (persisted in `config.json` under `challenges_earned`) and toasts once. `is_selectable(id)` returns true for an `implemented` free Challenge, or for an earned Challenge whose id is in `SaveManager.get_challenges_earned()`.

## From selection to active run

The selection overlay stages the player's chosen ids on `ChallengeManager.pending_selection` (an `Array[String]`). `GameManager.reset_for_new_run()` reads that into `current_run.challenges`, then clears it. From that point the active set is queried with:

```gdscript
func active_ids() -> Array:
	var raw: Variant = GameManager.current_run.get("challenges", [])
	return raw if raw is Array else []

func is_active(id: String) -> bool:
	return id in active_ids()
```

## Effect multipliers and hook sites

Each mechanical change is exposed as an accessor that a hook site elsewhere in the codebase multiplies into its own value. The pattern is uniform: the accessor returns `1.0` (or the neutral value) when the Challenge is inactive, and its Challenge value when active. Each accessor also folds in the matching **Artificer's Harrow** dial (`HarrowManager.dial_value` / `dial_t`), which is the identity when no harrow rides the run, and clamps the product to a floor or cap so stacked configs stay disastrous but never degenerate.

Representative accessors:

| Accessor | Backing Challenge | What it scales |
|---|---|---|
| `tool_durability_mult()` | `brittle_world` | Tool durability (x0.5), floor `_FLOOR_DURABILITY` = 0.2 |
| `wall_hp_mult()` | `brittle_world` | Wall HP (x0.5), floor `_FLOOR_WALL_HP` = 0.2 |
| `bell_interval_mult()` | `lone_villager` | Gathering-bell interval (x2), cap `_CAP_BELL` = 5.0 |
| `berry_harvest_amount()` | `lone_villager` | Berries per pick (returns 1 when active) |
| `pond_regen_mult()` | `lone_villager` | Pond seep-back rate (x0.25) |
| `shelter_fuel_mult()` | `eye_of_the_storm` | Shelter fuel burn (x2) |
| `maw_storm_mult()` | `eye_of_the_storm` | Maw speed, only while `WeatherManager.state == STORM` (x1.75), cap 2.5 |
| `rain_interval_mult()` / `storm_interval_mult()` | `eye_of_the_storm` | Gaps `WeatherManager` schedules between rain / storms |
| `maw_grace_period(base)` | `ravenous_maw` | Absolute grace seconds (60s when active) |
| `maw_learning_mult()` | `ravenous_maw` | `MawAdaptation` familiarity growth (x3), cap 6.0 |
| `pressure_growth_mult()` | `ravenous_maw` | Long-term pressure escalation rate (x3), cap 6.0 |
| `ritual_chew_mult()` | `ravenous_maw` | Permanent chew added per Ritual (x2), cap 4.0 |

A few accessors (`food_drain_mult()`, `water_drain_mult()`, `surge_interval_mult()`, `surge_boost_mult()`) have no backing Challenge — they are Harrow-only levers and return the identity unless a harrow is active.

> Because the multipliers compose by multiplication and are individually clamped, a hook site should call the accessor once per tick and multiply — never re-derive the Challenge condition itself. Adding a new hook means adding an accessor here, not an `is_active()` check at the call site.

## Run-start application

Some Challenges need imperative setup that a multiplier cannot express. `apply_run_start()` is called from `GameManager.reset_for_new_run()` **after** the normal manager resets, so its overrides win:

- `lone_villager` — `SurvivalManager.reset(10, 10, 0, 0)`, `VillageManager.population = 1`, and `ForestManager.pond_water = 10`.
- `black_rot` — adds a `strange_root` buffer of `population * 2` (read after Lone Villager and the souls dial so a 1-pop run gets 2 root).
- `two_fronts` — calls `MawController.spawn_west_front()` to append the western Maw after `fronts[0]` (east) already exists.

## Day-20 Elder's Aid

On the first day-20 of a Challenge run, `_maybe_deliver_aid()` (connected to `EventBus.new_day`) delivers one care package per active Challenge, rolled from that Challenge's `aid_pool` via `_roll_aid_package()` (3–4 distinct entries, each quantity in its `[min, max]`). Food and water route through `SurvivalManager.add_supplies()`; everything else through `Inventory.add()`. The package is deposited immediately and gated single-fire by the `challenge_aid_delivered` flag on `current_run`, so it survives save-load and resets with the run. `EventBus.challenge_aid_offered` drives the ceremony modal.

## Mastery rewards

Performing the Ritual with a Challenge active earns that Challenge's trophy cosmetic. That flow lives in `scripts/core/CosmeticManager.gd` (`CHALLENGE_TROPHY`, `_check_challenge_mastery()`), which reads `ChallengeManager.active_ids()` and `ChallengeManager.CATALOG`. See the cosmetics article for detail.

## Key files

| File | Role |
|---|---|
| `scripts/core/ChallengeManager.gd` | Catalog, active-set queries, effect multipliers, unlock/earn flow, day-20 aid, run-start application |
| `scripts/core/GameManager.gd` | Owns `current_run.challenges`; calls `apply_run_start()` from `reset_for_new_run()` |
| `scripts/core/SaveManager.gd` | Persists `challenges_unlocked` and `challenges_earned` in `config.json` |
| `scripts/core/CosmeticManager.gd` | Challenge mastery trophies |

## Related

- [The Artificer's Harrow](/docs/underroot/the-artificer-s-harrow)
- [The Astrolabe](/docs/underroot/the-astrolabe-and-harrow)
- [The Maw](/docs/underroot/the-maw)
- [Weather](/docs/underroot/weather)
- [Cosmetics and Digger Forms](/docs/underroot/cosmetics-and-digger-forms)
- [Adding a Challenge](/docs/underroot/adding-a-challenge)