World stages
Keep long-term numbers (oxygen, heat, population) as counters with bands, derived counters, and stages reached when counters pass marks: config, actions, conditions, the stage.reach hook.
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 #
{ "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 #
<!-- test: kits/game_engine/host/tests/world_stages.rs::stages_are_reached_once_and_run_their_actions --> Two stages on the counter oxygen (max 100):
{ "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, andcounter.<id>. - Action
add_counter({ counter, amount }); conditionscounter_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.
{ "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.reachtap cancels it. adddoes 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.