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'sOnActCompletedfires the campaign transitions to theCampaignEndedfollow-on state surfaced by theCampaignEndedevent, not by raisingCurrentActbeyond 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 forOnActStarted/OnActCompletedhandlers 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 value | Owner doc | Gate to next Act |
|---|---|---|
0 (Arrival and Core) | ../acts/gd-act-0-arrival-and-core.md | core_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.md | OnActCompleted for Act 7 fires the follow-on CampaignEnded event per ad-0010 lines 101-104. |
CampaignEnded | Saga-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.Progressionphase. The phase name is plain C# game vocabulary per../../../../../.claude/rules/secs-concepts.mdand per the existingPhaseDeclarationpattern; it is not a SECS keyword. If thePhases.Progressionsymbol 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
MapGenerationSystemat campaign start. There is no per-tick row-creation responsibility onActProgressionSystem. - Per-tick body. The system iterates the singleton ActState row,
reads the named gate predicate for
CurrentAct + 1, and if the predicate is true:- Writes
PreviousAct = CurrentAct. - Writes
CurrentAct = CurrentAct + 1. - 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). - Fires
OnActCompleted(ActProgressionArgs{previous, new, day})for the outgoing Act. - Fires
OnActStarted(ActProgressionArgs{previous, new, day})for the incoming Act.
- Writes
- Act 7 completion. When
OnActCompletedfires for Act 7, the follow-onCampaignEndedevent 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 afterCurrentActadvances. Handlers bind in Wave 5a.OnActCompleted(ActProgressionArgs). Fires immediately beforeCurrentActadvances (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:
scope Realmand itsPolityRankchannel (per../lore/adr/ad-0009-realm-scope-and-polity-rank-orthogonality.md).scope Settlementand its tier / building / population fields.scope QuestThreadandscope Missionand theirLifecycleStatechannels (per./gd-quest-thread-and-mission-state-machine.md).scope ThreatSourceandscope FrontCandidate(per./gd-front-emergence.md).scope Siteand its discovery / clearance state (per./gd-state-axes-and-thresholds.md).scope MainCharacterand its trait / condition / inventory state.- The Act 0 spine's
core_establishednamed-state fact (per../acts/gd-act-0-mission-spine.mdline 163).
Forbidden gate inputs:
EnteredOnDayor any day-count value.PreviousActas 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.cshosts the system body; proposedlegacy/v1/examples/valenar/Host/Data/ActStateData.cscarries 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.secsdeclaresscope ActState;legacy/v1/examples/valenar/Content/acts/contracts.secsdeclarescontract ActStateContractwith the two lifecycle hooks;legacy/v1/examples/valenar/Content/acts/systems.secsdeclaressystem ActProgression;legacy/v1/examples/valenar/Content/acts/events.secsdeclaresevent OnActStartedandevent OnActCompleted. Matched by the parallellegacy/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 readsActState.CurrentAct. UI consumers are../ux/gd-current-plan.mdand../ux/gd-core-screen.md. The existingAct 0 — Arrival and Corespoiler-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 theActState<Source>To<Target>Testconvention (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.mddocs); the exactActProgressionArgsfield roster; theCampaignEndedevent handler binding and the campaign-end ceremony surface; thePhases.Progressiondeclaration if not already present; the campaign-day channel field name forEnteredOnDay. - Illegal fallback behavior: no host-side
int _currentActfield outside the ActState scope row may stand in forCurrentAct. No predicate function may be called from outsideActProgressionSystemto 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 setCurrentActto a value that is notPreviousAct + 1(or 0 at campaign start) raises an explicitIllegalTransitionExceptionrather 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 withtrigger on_action).OnActCompleted(event withtrigger 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
../lore/adr/ad-0010-act-progression-actstate-scope.md— binding authority.../lore/adr/ad-0013-scope-state-channel-template-archetype-pattern.md— canonical pattern.../lore/adr/ad-0007-quest-thread-and-mission-scope-primitives.md—ActScopeKindbinding.../lore/adr/ad-0009-realm-scope-and-polity-rank-orthogonality.md— Realm scope and PolityRank ladder orthogonality.../acts/gd-act-0-arrival-and-core.md— Act 0 content owner.../acts/gd-act-0-mission-spine.md—core_establishedgate fact (Act 0 → Act 1).../acts/gd-act-1-first-people.mdthrough../acts/gd-act-7-reclamation.md— Acts 1 through 7 content owners.- The seven per-Act gate-spec docs under
../acts/README.md(gd-act-1-gate-spec.mdthroughgd-act-7-gate-spec.md) — per-Act gate predicates authored in parallel. ./gd-quest-and-lore-design.md— Saga Arc envelope above the Act ladder../gd-quest-thread-and-mission-state-machine.md—ActScopeKindreadsActState.CurrentAct../gd-realm-ranks-and-polity.md— Realm.PolityRank is one of many named-state inputs for Act gates../gd-state-axes-and-thresholds.md— Site / Territory discovery and content state ladders that gate predicates may read../gd-canon.md— canon authority for the Act-vs-PolityRank ladder split.../ux/gd-current-plan.md— UI readback for current Act.../ux/gd-core-screen.md— UI readback for saga-position surface.../../../../../.claude/rules/secs-concepts.md— scope, channel, contract, system, event primitives; phase declarations.../../../../../.claude/rules/behavior-vocabulary.md—on_actionallowlist.../../../../../.claude/rules/valenar-contract-backing.md— backing/propagation gate.