DR-042 Phase A: expedition-driven win — move win-driver off base-siege survival, kill the AFK path

Design-review-gated (wf_ebef4e81-dba, GREEN-WITH-CHANGES). The win-driver moves
from "survive N base sieges" to "clear N expeditions". The review overturned the
literal plan: credit on RETURN, not at the clear edge (clear-edge crediting arms
the undefended final base siege -> uncontestable terminal Loss).

- ExpeditionGateSystem: now the sole production writer of GoalProgress.Charge —
  a clamped +1 per cleared expedition folded into the existing once-per-epoch
  reward block, reusing the LastRewardedEpoch latch (Ore + Charge share fate) +
  a SaveRequest checkpoint. No new latch, no new GhostField, no ordering change.
- CyclePhaseSystem: deleted the survived-siege +1 (the AFK win path). Victory
  latch unchanged; GoalReached still arms the final base siege at cap.
- CycleDirectorAuthoring + CycleDirector.prefab: ScheduleEnabled baked OFF
  (retaliation-only). A serialized prefab bool ignores the C# field initializer,
  so the value is flipped in the prefab, not just the code default.
- Tests: re-pointed CyclePhaseSystemTests + EndgameWinLoseTests survived-siege
  assertions; extended ExpeditionGateRewardTests (+1, no-double-credit, clamp).
  389/389 EditMode green; clean netcode Play smoke (no sort-cycle, Schedule=0).

SaveData stays v5. Docs: DR-042 build record + forks resolved, CLAUDE.md
base-loop line, Backlog (A done).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 20:49:27 -07:00
parent 03f778085b
commit ed65770cc9
11 changed files with 111 additions and 54 deletions
@@ -83,19 +83,35 @@ namespace ProjectM.Server
SystemAPI.SetComponent(threatEntity, threat);
}
// Once-per-epoch zone-clear reward: a returner banks flat Ore IFF this epoch's expedition wave was
// actually cleared and not yet rewarded. Resolved ONCE here (not per-returner) so two same-tick co-op
// returns pay exactly once (DR-040 BLOCKER 4) and gate re-entry before a clear can't farm (MINOR 2).
if (SystemAPI.HasSingleton<CycleState>()
&& SystemAPI.TryGetSingleton<ZoneEnemyDirector>(out var zoneDir)
&& SystemAPI.HasSingleton<ResourceLedger>())
// Once-per-epoch zone-clear reward: a returner BANKS flat Ore to the shared ledger AND advances the
// long-arc win meter (DR-042 — EXPEDITION CLEARS, not survived base sieges, are the win-driver:
// CyclePhaseSystem no longer credits Charge, so this is the sole PRODUCTION writer of GoalProgress.Charge).
// Resolved ONCE here (not per-returner) so two same-tick co-op returns pay exactly once (DR-040 BLOCKER 4)
// and gate re-entry before a clear can't farm (MINOR 2). Ore + Charge share the SAME LastRewardedEpoch
// latch so they always share fate (never one without the other). The Charge credit is guarded
// independently of the ledger so it still lands in ledger-less worlds.
if (SystemAPI.HasSingleton<CycleState>())
{
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch)
{
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
StorageMath.Deposit(ledger, (ushort)ResourceId.Ore, zoneDir.RewardOre);
if (SystemAPI.TryGetSingleton<ZoneEnemyDirector>(out var zoneDir)
&& SystemAPI.HasSingleton<ResourceLedger>())
{
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
StorageMath.Deposit(ledger, (ushort)ResourceId.Ore, zoneDir.RewardOre);
}
if (SystemAPI.HasComponent<GoalProgress>(cycleEntity))
{
// +1 toward the goal per cleared expedition, CLAMPED to Target (single production writer).
var goal = SystemAPI.GetComponent<GoalProgress>(cycleEntity);
goal.Charge = math.min(goal.Charge + 1, goal.Target);
SystemAPI.SetComponent(cycleEntity, goal);
}
// Checkpoint the hard-won clear (replaces the deleted survived-siege autosave in CyclePhaseSystem).
if (SystemAPI.HasComponent<SaveRequest>(cycleEntity))
SystemAPI.SetComponent(cycleEntity, new SaveRequest { Pending = 1 });
runtime.LastRewardedEpoch = runtime.ExpeditionEpoch;
SystemAPI.SetComponent(cycleEntity, runtime);
}