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:
@@ -13,7 +13,7 @@ namespace ProjectM.Tests
|
||||
/// <see cref="ThreatDirectorSystem"/> SiegeTimeout guard. A bare world is seeded with a NetworkTime singleton and a
|
||||
/// CycleDirector entity carrying the full run-state set (CycleState/CycleRuntime/ThreatState/ThreatConfig/
|
||||
/// GoalProgress/CoreIntegrity/RunPhase/RunOutcome/SaveRequest + a ledger). These pin: the goal cap arms a bigger
|
||||
/// final siege EXACTLY once and CyclePhaseSystem enters it once; Charge clamps to Target; a survived final siege
|
||||
/// final siege EXACTLY once and CyclePhaseSystem enters it once; a survived NORMAL siege no longer charges the goal (DR-042); a survived final siege
|
||||
/// latches Victory (no extra charge); a Core breach during the final siege latches Loss with NONE of the END-1
|
||||
/// soft-loss side effects (no ledger drain, no OverrunTick); a NORMAL-phase overrun STILL takes the END-1 soft
|
||||
/// path (the key regression); a restored Victory does not re-arm; and the SiegeTimeout cull is disabled during the
|
||||
@@ -127,21 +127,25 @@ namespace ProjectM.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Charge_Clamps_To_Target_On_Survived_Siege()
|
||||
public void Survived_Normal_Siege_Neither_Charges_Goal_Nor_Arms_Final()
|
||||
{
|
||||
var (world, group) = MakeWorld("End2Clamp", serverTick: 200);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
// Charge already AT Target, a NORMAL (non-final) siege is survived -> Charge must not exceed Target.
|
||||
var dir = MakeDirector(em, CyclePhase.Siege, defendStartWave: 5, charge: 4, target: 4,
|
||||
// DR-042: surviving a NORMAL siege one short of the cap must neither charge the goal nor arm the final.
|
||||
var dir = MakeDirector(em, CyclePhase.Siege, defendStartWave: 5, charge: 3, target: 4,
|
||||
core: 100, RunPhaseId.Normal, RunOutcomeId.InProgress);
|
||||
MakeWave(em, waveNumber: 6, phase: WavePhase.Spawning, remaining: 0); // DefendCleared
|
||||
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(4, em.GetComponentData<GoalProgress>(dir).Charge,
|
||||
"Charge clamps at Target on a survived siege (min(Charge+1, Target)); it never runs away.");
|
||||
Assert.AreEqual(3, em.GetComponentData<GoalProgress>(dir).Charge,
|
||||
"a survived normal siege does NOT charge the goal (DR-042: base-siege survival is not win-progress).");
|
||||
Assert.AreEqual(RunPhaseId.Normal, em.GetComponentData<RunPhase>(dir).Value,
|
||||
"the final siege is NOT armed by a survived siege near the cap.");
|
||||
Assert.AreEqual(0, em.GetComponentData<ThreatState>(dir).PendingSiegeSize,
|
||||
"nothing is armed (the cap is only crossed by an expedition clear).");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,13 +318,13 @@ namespace ProjectM.Tests
|
||||
public void Final_Siege_Arms_On_Goal_Edge_Through_Pipeline_Not_Stomped_By_Scheduler()
|
||||
{
|
||||
// M-3 + N-4: drive the REAL cross-system handoff (ThreatDirector -> CyclePhase -> GoalReached) over the
|
||||
// Charge edge (3 -> 4 via a survived siege), then prove a DUE scheduled source can't stomp the armed final
|
||||
// Charge edge (now crossed by an EXPEDITION CLEAR in production; PRE-SEEDED at Target here), then prove a DUE scheduled source can't stomp the armed final
|
||||
// siege (the FinalDefense + PendingSiegeSize!=0 guards) and the FINAL size flows through to the wave.
|
||||
var (world, group) = MakeFullWorld("End2Pipeline", serverTick: 200);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
var dir = MakeDirector(em, CyclePhase.Siege, defendStartWave: 5, charge: 3, target: 4,
|
||||
var dir = MakeDirector(em, CyclePhase.Siege, defendStartWave: 5, charge: 4, target: 4,
|
||||
core: 100, RunPhaseId.Normal, RunOutcomeId.InProgress);
|
||||
var cfg = Cfg(); cfg.ScheduleEnabled = 1; cfg.ScheduleIntervalTicks = 100;
|
||||
em.SetComponentData(dir, cfg);
|
||||
@@ -328,9 +332,10 @@ namespace ProjectM.Tests
|
||||
var wave = MakeWave(em, waveNumber: 6, phase: WavePhase.Spawning, remaining: 0); // DefendCleared this tick
|
||||
int expected = ExpectedFinalSize(5, 1, 6); // (5 + 6) * 2.5 = 27
|
||||
|
||||
// Tick 1: ThreatDirector (Siege -> no arm) -> CyclePhase (survive -> Charge 3->4, Calm) -> GoalReached (arm).
|
||||
// Tick 1: ThreatDirector (Siege -> no arm) -> CyclePhase (survive -> Calm, Charge stays at cap) -> GoalReached (arm).
|
||||
group.Update();
|
||||
Assert.AreEqual(4, em.GetComponentData<GoalProgress>(dir).Charge, "the survived siege reaches the cap.");
|
||||
Assert.AreEqual(4, em.GetComponentData<GoalProgress>(dir).Charge,
|
||||
"Charge sits at the cap (crossed by an expedition clear in production; survived sieges no longer credit — DR-042).");
|
||||
Assert.AreEqual(RunPhaseId.FinalDefense, em.GetComponentData<RunPhase>(dir).Value,
|
||||
"GoalReached flips FinalDefense the same tick the Charge edge is crossed.");
|
||||
Assert.AreEqual(expected, em.GetComponentData<ThreatState>(dir).PendingSiegeSize,
|
||||
|
||||
Reference in New Issue
Block a user