# Counters and progress stages (world.stages) ## What it is for `world.stages` is the long game of a sandbox: numbers that move, and marks along the way. The kit does not know what a number means. Counters are plain numbers per id, pushed up or down by machines, scripts or other kits, kept between a `min` and a `max`. Derived counters are a sum, or the share of one counter in a group. A stage is reached once, when all its conditions hold, and runs actions (unlock a recipe or a structure, set a variable, start a mission). Use it for terraforming, a growing town, corruption, or any long-term goal. The progress panel is a `ui.webui` fragment shown in the HUD. ## Turn it on ```json { "id": "world.stages", "config": { "counters": [{ "id": "oxygen", "label": "Oxygen", "max": 100 }, { "id": "water" }], "index": { "id": "ti", "label": "Index" } } } ``` Stages are documents of the `world_stages` collection. ## First example Two stages on the counter `oxygen` (max 100): ```json { "id": "air", "name": "Air", "counter": "oxygen", "atLeast": 50, "actions": [{ "type": "add_counter", "counter": "water", "amount": 7 }] } { "id": "full", "name": "Full", "counter": "oxygen", "atLeast": 100 } ``` `add("oxygen", 60)` reaches `Air` once: `stage.reached` is emitted and `water` gains 7. `add("oxygen", 500)` is clamped to 100 and reaches `Full`; `Air` is never reached again. The variables `stage.current` and `stage.next` follow. ## Settings and defaults | Setting | Type | Default | Meaning | |---|---|---|---| | `counters` | array (max 24) | `[]` | Counter definitions: `id`, `label`, `unit`, `start`, `color`, `min`, `max`, `target` (`[low, high]` band), `world`. Other counters appear when something pushes them. | | `derived` | array (max 24) | `[]` | Read-only counters: `sum: [ids]`, or `share: { of, among }` times `scale`. Usable wherever a counter id is. | | `index` | object | none | One counter that is the sum of `of` (default: all): `id`, `label`, `of`. | A stage document is `{ id, name, counter, atLeast?, atMost?, when?, actions?, world? }`. `counter` + `atLeast` is a mark to rise to, `atMost` a mark to come down to, both a range. `when` is a list of `{ counter, atLeast?, atMost? }` that must all hold at once (a balance). A stage reached stays reached. ## Services, events, variables - `world.stages` (service): `counters()`, `value(id)`, `rate(id)`, `add(id, amount)` (a gain is scaled by multipliers, a loss is not; nothing on a derived id), `set(id, value)`, `multiply(id, factor, source)` (a lasting multiplier on what a counter gains), `multiplier(id)`, `band(id)`, `inBand(id)` (true without a band), `stages()`, `current()`, `next()`, `scope(id)`, `scopeId()`. - `stage.reached` (event): `{ stage, name, counter, value }`, once per stage. - `counter.changed` (event): `{ counter, value }`, at most twice a second per counter, derived ones included. - Variables: `stage.current`, `stage.next`, `stage.progress`, and `counter.`. - Action `add_counter` (`{ counter, amount }`); conditions `counter_at_least`, `counter_at_most`, `counter_between`, `stage_reached`. - HUD view `progress`: the stage reached, the way to the next one and each counter with its rate and band. Progress of a rising condition starts from the last mark passed on the same counter; of a falling one, from where the counter was when the previous stage was reached. ## Hooks and modding `stage.reach` (hook): `{ stage, name, counter, value, actions }`, asked right before a stage is reached. Mutable `actions` (false: reached and told, actions do not run). Cancellable: the stage is not reached; it is asked again once a counter of its conditions has moved. ```json { "id": "story_gate", "hook": "stage.reach", "match": { "stage": "blue_sky" }, "if": [{ "var": "story.open", "op": "==", "value": false }], "cancel": true } ``` A mod may override `counters`, `derived` and `index`, and add, patch or remove documents of `world_stages`. ## Worlds With several worlds (`travel.worlds`), `scope(id)` puts the live counters, stages, multipliers and baselines aside and takes the set kept for `id` (a fresh one the first time). A counter or stage with `world` exists only in that world. A world put aside is frozen. All sets are saved. ## Engines The logic is the same Rust implementation everywhere; the progress panel is a `ui.webui` fragment. ## When it does not do what you expect - A stage never fires: its conditions were never all true at once, or a `stage.reach` tap cancels it. - `add` does nothing on a derived counter: derived counters are read-only. - A counter does not rise past a number: it has a `max`. ## Limits It does not decide what counters mean or produce them; machines, scripts or other kits push them. ## Versions Kit 1.5.0; it provides `world.stages` 1.3.0 and requires `game.engine@^1`.