Skip to main content

Act Progression

Context

This page owns the runtime carrier of Valenar's Act ladder: which scope holds CurrentAct, which system advances it, what fires when an Act starts, and what gate predicate decides when Act N becomes Act N+1.

The per-Act content lives in the eight Act docs under ../acts/README.md. The Saga Arc envelope above the Acts lives in ./gd-quest-and-lore-design.md. What is committed here is the runtime state machine that walks the Act ladder.

The binding authority is ../lore/adr/ad-0010-act-progression-actstate-scope.md; this page is the owner-doc surface that ad-0010 routes Act-progression state-machine concerns to.

The cross-cutting canonical-pattern ADR ../lore/adr/ad-0013-scope-state-channel-template-archetype-pattern.md records that ActState is a pattern instantiation: scope ActState plus CurrentAct channel; the template archetype is implicit because ActState is a saga-scoped singleton and its initial state is authored as campaign-start data rather than as a per-instance template (ad-0013 lines 102-108).

ActState Scope Shape

scope ActState is the saga-scoped singleton carrier of the campaign's Act position. Exactly one row exists per game, created by the existing one-shot setup path that creates the MainCharacter row (the existing MapGenerationSystem).

Fields committed by ad-0010 lines 55-66:

  • CurrentAct: int (0..7). The Act the campaign is currently in. Range is committed; once Act 7's OnActCompleted fires the campaign transitions to the CampaignEnded follow-on state surfaced by the CampaignEnded event, not by raising CurrentAct beyond 7.
  • EnteredOnDay: int. The day the current Act was entered. This field is a recorded fact for read-model surfaces and post-mortem inspection; it is not a gate predicate input. Gate predicates may not read it. See "Gate Predicate Rule" below.
  • PreviousAct: int. The Act value before the most recent transition. Available for OnActStarted / OnActCompleted handlers that need both ends of the transition.

Additional fields needed by per-Act gate predicates are <deferred-to-Wave-5a-runtime-backing> per ad-0010 lines 59-60 and will be authored alongside the per-Act gate-spec docs.

Act Ladder

Nine states. Acts 0 through 7 are the running campaign; CampaignEnded is the post-campaign follow-on state.

Act valueOwner docGate to next Act
0 (Arrival and Core)../acts/gd-act-0-arrival-and-core.mdcore_established == true named-state fact written by the Act 0 mission spine.
1 (First People)../acts/gd-act-1-first-people.md<authored-in-gd-act-1-gate-spec.md> per ad-0010 lines 86-92.
2 (First Outpost)../acts/gd-act-2-first-outpost.md<authored-in-gd-act-2-gate-spec.md> per ad-0010 lines 86-92.
3 (Settlement)../acts/gd-act-3-settlement.md<authored-in-gd-act-3-gate-spec.md> per ad-0010 lines 86-92.
4 (Banner Network)../acts/gd-act-4-banner-network.md<authored-in-gd-act-4-gate-spec.md> per ad-0010 lines 86-92.
5 (Province)../acts/gd-act-5-province.md<authored-in-gd-act-5-gate-spec.md> per ad-0010 lines 86-92.
6 (Crown)../acts/gd-act-6-crown.md<authored-in-gd-act-6-gate-spec.md> per ad-0010 lines 86-92.
7 (Reclamation)../acts/gd-act-7-reclamation.mdOnActCompleted for Act 7 fires the follow-on CampaignEnded event per ad-0010 lines 101-104.
CampaignEndedSaga-arc finalizer (binding deferred to Wave 5a)terminal state; no further transition.

Act labels are spoiler-safe per the existing Act 0 label discipline. The runtime field is the integer CurrentAct; the player-facing label is supplied by the read-model surface.

ActProgressionSystem Behavior

system ActProgression is the per-tick driver. Committed by ad-0010 lines 68-80:

  • Phase. Runs in the Phases.Progression phase. The phase name is plain C# game vocabulary per ../../../../../.claude/rules/secs-concepts.md and per the existing PhaseDeclaration pattern; it is not a SECS keyword. If the Phases.Progression symbol does not already exist in the Generated phase-declaration tree, it is authored in the Wave 5a runtime-backing wave.
  • One-shot activation. The singleton ActState row is created by the existing MapGenerationSystem at campaign start. There is no per-tick row-creation responsibility on ActProgressionSystem.
  • Per-tick body. The system iterates the singleton ActState row, reads the named gate predicate for CurrentAct + 1, and if the predicate is true:
    1. Writes PreviousAct = CurrentAct.
    2. Writes CurrentAct = CurrentAct + 1.
    3. Writes EnteredOnDay = <current campaign day> (the campaign-day source is the existing campaign-day channel; field name <deferred-to-Wave-5a-runtime-backing> per ad-0010 lines 59-60).
    4. Fires OnActCompleted(ActProgressionArgs{previous, new, day}) for the outgoing Act.
    5. Fires OnActStarted(ActProgressionArgs{previous, new, day}) for the incoming Act.
  • Act 7 completion. When OnActCompleted fires for Act 7, the follow-on CampaignEnded event fires. The CampaignEnded event surfaces the campaign-end ceremony that the Saga Arc resolves into; its exact handler binding is <deferred-to-Wave-5a-runtime-backing> per ad-0010 lines 101-104.

The system writes to the ActState row through the existing SECS scope-write path. No host-side int _currentAct field outside the ActState scope row may stand in for CurrentAct. No predicate function may be called from outside ActProgressionSystem to advance the Act.

ActStateContract

One contract carries two lifecycle hooks, committed by ad-0010 lines 62-67:

  • OnActStarted(ActProgressionArgs). Fires immediately after CurrentAct advances. Handlers bind in Wave 5a.
  • OnActCompleted(ActProgressionArgs). Fires immediately before CurrentAct advances (logically the outgoing-Act handler runs before the incoming-Act handler).

ActProgressionArgs is the typed args carried by both events. At minimum: previous-Act value, new-Act value, day-of-transition value. Exact field roster is <deferred-to-Wave-5a-runtime-backing> per ad-0010 lines 63-66.

Both events are declared with trigger on_action per the SECS on_action allowlist in ../../../../../.claude/rules/behavior-vocabulary.md. The two event names — OnActStarted and OnActCompleted — are committed labels (not working names); they appear in ad-0010 line 197 as the canonical name set.

Gate Predicate Rule

Every Act gate predicate reads named state on existing or already-committed scopes only. Per ad-0010 lines 93-99, gate inputs are drawn from:

Forbidden gate inputs:

  • EnteredOnDay or any day-count value.
  • PreviousAct as a sole gate input (a gate that fires "after Act N ended" with no further state predicate is a hidden day-count).
  • Real-time clock values of any kind.
  • Time-since-start counters.
  • Any host-side timer that is not itself driven by a named-state predicate.

The "Never tune parameters as a substitute for fixing architecture" tenet applies directly here. Gates are state predicates, not timers.

Per-Act Gate References

Per-Act gate predicates for the seven Act 1 → Act 7 transitions are authored in seven gd-act-N-gate-spec.md docs under ../acts/README.md, one per transition. Those docs are net-new authoring in this same wave (the Wave 3a-B authoring track) — they are not deferred past Wave 3a. Each gate-spec doc names the source Act, the target Act, the gate predicate body in terms of named state on committed scopes, the required facts, the blocking conditions, and the fallback behavior at runtime if the predicate cannot be evaluated.

The Act 0 → Act 1 gate is already concrete in the Act 0 mission spine:

core_established == true

per ../acts/gd-act-0-arrival-and-core.md line 163 and ad-0010 lines 87-90. gd-act-1-gate-spec.md codifies that gate verbatim in the per-Act gate-spec authoring pattern; no further authoring of that gate occurs in this page.

CampaignEnded Follow-On

When OnActCompleted fires for Act 7, the CampaignEnded event fires as a follow-on. CampaignEnded is the campaign-end ceremony surface that the Saga Arc resolves into. The CampaignEnded handler binding is <deferred-to-Wave-5a-runtime-backing> per ad-0010 lines 101-104; this page commits the follow-on relationship between Act 7 completion and CampaignEnded firing.

CampaignEnded is a terminal state. ActState.CurrentAct does not advance past 7 after CampaignEnded fires; the saga-arc finalizer authored in Wave 5a is responsible for any post-campaign surface (end-of-campaign read model, post-mortem screen, new-game-plus seed).

Runtime Backing Status

Status: contract-only.

  • Host/runtime owner: proposed legacy/v1/examples/valenar/Host/Systems/ActProgressionSystem.cs hosts the system body; proposed legacy/v1/examples/valenar/Host/Data/ActStateData.cs carries the singleton ActState host data. Neither exists today. Authored in the Wave 5a runtime-backing wave per ad-0010 lines 152-158.
  • Generated/.secs owner: legacy/v1/examples/valenar/Content/acts/scopes.secs declares scope ActState; legacy/v1/examples/valenar/Content/acts/contracts.secs declares contract ActStateContract with the two lifecycle hooks; legacy/v1/examples/valenar/Content/acts/systems.secs declares system ActProgression; legacy/v1/examples/valenar/Content/acts/events.secs declares event OnActStarted and event OnActCompleted. Matched by the parallel legacy/v1/examples/valenar/Generated/Acts/ stand-in tree.
  • Read-model/UI owner: proposed legacy/v1/examples/valenar/Host/ReadModels/ActStateReadModel.cs. Does not exist today. The Valenar React client's saga-position UI surface reads ActState.CurrentAct. UI consumers are ../ux/gd-current-plan.md and ../ux/gd-core-screen.md. The existing Act 0 — Arrival and Core spoiler-safe label discipline for player-facing labels carries forward.
  • Tests: <deferred-to-wave-7> — Wave 7 test-authoring wave authors unit tests against each per-Act transition. Test names use the ActState<Source>To<Target>Test convention (e.g. ActState0To1Test).
  • Known gaps: per-Act gate predicates for Acts 1 through 7 (authored in parallel by the seven gd-act-N-gate-spec.md docs); the exact ActProgressionArgs field roster; the CampaignEnded event handler binding and the campaign-end ceremony surface; the Phases.Progression declaration if not already present; the campaign-day channel field name for EnteredOnDay.
  • Illegal fallback behavior: no host-side int _currentAct field outside the ActState scope row may stand in for CurrentAct. No predicate function may be called from outside ActProgressionSystem to advance the Act. No day-count or real-time clock value may stand in for a state predicate in any gate expression. Any attempt to set CurrentAct to a value that is not PreviousAct + 1 (or 0 at campaign start) raises an explicit IllegalTransitionException rather than silently jumping Acts.
  • Next closure wave: Wave 5a runtime-backing wave authors the host data, the scope, the contract, the system, the events, and the matching Generated stand-ins. Per-Act gate-spec docs author in parallel under ../acts/README.md.

Glossary Propagation

The following terms must land in the canonical glossary surfaces (../gd-glossary.md, ../gd-canon.md, ../README.md) per ../../../../../.claude/rules/valenar-contract-backing.md § Canonical Glossary Targets. Glossary propagation itself is the next docs wave (Wave 3c); this section records the obligation:

  • ActState (scope primitive — saga-scoped singleton).
  • CurrentAct (Base channel on ActState; the 0..7 Act index).
  • PreviousAct (field on ActState).
  • EnteredOnDay (recorded-fact field on ActState; not a gate input).
  • ActProgression (system name).
  • ActProgressionSystem (host-side system class name).
  • OnActStarted (event with trigger on_action).
  • OnActCompleted (event with trigger on_action).
  • ActStateContract (contract carrying the two lifecycle hooks).
  • ActProgressionArgs (typed args carried by the two events).
  • CampaignEnded (follow-on event fired at Act 7 completion).
  • Phases.Progression (game-vocabulary phase declaration; not a SECS keyword).

Cross-References