Skip to content

W84 — Incidents That Happen On Their Own - #360

Merged
The-Running-Dev merged 2 commits into
mainfrom
slice/W84
Aug 21, 2026
Merged

W84 — Incidents That Happen On Their Own#360
The-Running-Dev merged 2 commits into
mainfrom
slice/W84

Conversation

@The-Running-Dev

Copy link
Copy Markdown
Owner

What changed, and why. System 16 (incidents) previously only expired duration-bound
occurrences; it never rolled a new one, so litter was the only incident that could ever occur.
It now visits scopes in world → zone id → building id order, rolls each eligible definition
against its own declared chance from a single per-tick tick:${tick}:incidents handle,
picks among successes by weight when more than one passes, and blocks a
definition/scope that is still active or inside its cooldown. It also resolves an occurrence
on a true resolutionCondition, not only on expiry, writing resolvedAtTick before that
occurrence's resolve effects run. A roll's onStart effects apply before system 17
(objectives), and incident.raised now joins the already-delivered incident.resolved.

Registered incident_raised as an audit reason — like the existing "indirect five", it can
reach a visible StateChange via an onStart effect's EffectContext.reason. Added a
dormant storm incident to the MVP campaign, gated on every one of the three staff roles
being hired at once (no existing fixture hires all three), plus a new
world-graph-mvp-incidents replay fixture that exercises the roll for real.

A decision the contract (20-contract.md §4.18) left open: it does not specify how a
scope's own roll behaves when more than one definition is eligible for it in the same tick.
I implemented "each eligible definition rolls its own chance independently against its own
rollChanceBasisPoints, then a weighted choice picks one occurrence among the successes" —
this matches W84.1's "an eligible definition rolls against its declared chance" (per
definition, not per scope) while still reading as one "draw chance and then weighted choice"
per scope when (as in every fixture here) only one definition is ever eligible for a given
scope at once.

Closes #355

Verified

Not yet run — the gates run next and this section is replaced with their report.


Agent detail
  • Slice: W84 — design/30-slices.md § W84 @ 6fd30e04106b4db4db7614faa0e474ff1b160405
  • Criteria met: W84.1, W84.2, W84.3, W84.4, W84.5, W84.6, W84.7, W84.8
  • Left undone: W84.9 — npm run typecheck, npm run lint and npm test all pass locally
    (confirmed manually), but this is /pr's gate run to record formally, not /slice's

System 16 rolls declared-scope incidents (world, then zone id, then building
id) against each eligible definition's chance and a weighted choice among
successes, blocks a definition/scope already active or still in cooldown,
and now also resolves on a true resolutionCondition, not only expiry.
Allocated occurrences apply their onStart effects before system 17, and
incident.raised joins the already-delivered incident.resolved.

Registers incident_raised as an audit reason (it reaches a visible
StateChange via onStart's EffectContext exactly like the existing indirect
five), and adds a dormant "storm" definition to the MVP campaign — gated on
every staff role being hired at once, which no existing fixture does — plus
the new world-graph-mvp-incidents replay fixture proving the roll fires.
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts Outdated
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts Outdated
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts Outdated
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts Outdated
Comment thread src/engine/src/kinds/world-graph/tick/pipeline.ts Outdated
Comment thread src/engine/src/campaigns/world-graph-mvp.ts
@The-Running-Dev

Copy link
Copy Markdown
Owner Author

Two additional findings from the code review that can't be posted as inline comments because they land in files this PR doesn't touch:

src/engine/src/kinds/world-graph/validate.ts:171 — correctness (high). catalogEffectErrors never forbids an undeferrable wear-meter delta in an incident's onStart list, even though this PR makes onStart live exclusively via system 16's post-system-14 rolls. design/90-decisions.md explicitly anticipated this fork ("if onStart runs only for system 16's rolls... the §9.2 rejection should extend to it") and left it as the deciding slice's call — this is that slice, and it shipped without extending the rejection.

src/engine/src/kinds/world-graph/tick/effects.ts:183 — correctness (high). The start_incident effect handler builds the incident inline and never applies the definition's onStart effects, so onStart is permanently dead for any incident whose triggerCondition is null (effect-started-only content). This is the other, unresolved half of the same W47 fork noted in design/90-decisions.md.

Extends the undeferrable-wear-delta validation to incidents[].onStart,
per the fork design/90-decisions.md left for W84 to resolve (onStart
now only ever runs from system 16, after system 14 has closed its
broken-transition check). Also removes a content-dependent RNG-draw
skip in the weighted incident selection, collapses incidentBlocked's
scope match to a plain equality check, extracts the duplicated
IntegerRange-duration-collapse logic into a shared resolveDuration
helper, and hoists the per-scope eligible-definitions computation out
of the per-instance loop.
@The-Running-Dev

Copy link
Copy Markdown
Owner Author

Update on the two findings above:

  • validate.ts:171 (undeferrable wear delta not validated for onStart) — fixed in 53f40ae. catalogEffectErrors now calls forbidUndeferrableWearDelta on incidents[].onStart unconditionally, since onStart only ever runs from system 16's roll (after system 14).
  • effects.ts:183 (onStart never fires for effect-started incidents) — left open. A correct fix means every caller of applyWorldEffects that can emit a start_incident effect must issue a second, top-level applyWorldEffects call for the new incident's onStart — mirroring what pipeline.ts's roll loop already does — rather than recursing inside applyWorldEffects itself, which its own docstring says not to do ("applies one system's authored effect list without invoking another effect or system") and would misattribute the nested effects' StateChange.system. That's a real fix but not a minimal, self-contained edit; needs a deliberate design decision per the still-open design/90-decisions.md W47 fork.

@The-Running-Dev
The-Running-Dev merged commit 8fdded3 into main Aug 21, 2026
22 checks passed
@The-Running-Dev
The-Running-Dev deleted the slice/W84 branch August 21, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W84 — Incidents That Happen On Their Own

1 participant