END-2: final siege + latching win/lose (SL-3)

At GoalProgress.Charge>=Target a new server-only GoalReachedSystem arms a larger final siege (x live FinalSiegeMultiplier) and flips RunPhase=FinalDefense; CyclePhaseSystem latches a REPLICATED RunOutcome (Victory on clear / Loss on Core breach) and halts the director. RunOutcome is a [GhostField] byte on the global CycleDirector ghost (the client banner observes it); RunPhase stays server-only. ThreatDirector/CoreRestore/CoreDamage halt once decided; SiegeTimeout is off during the final siege. SaveData v5 persists the outcome so a won/lost run loads finished. GoalProgress.Target 10->4. Completes Path A's spine. See DR-036.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:38:21 -07:00
parent 33c85c4f9a
commit 4f0b4e8087
16 changed files with 313 additions and 33 deletions
@@ -56,6 +56,11 @@ namespace ProjectM.Simulation
public float CoreRegenIntervalTicks; // ticks between +1 regen in Calm (18 -> +1/0.3s -> ~full over one short Calm)
public float CoreOverrunDrainPct; // fraction (0..1) of the shared ledger lost on a breach (soft-loss penalty)
// END-2 final siege: the would-be-next normal siege size is multiplied by this for the FINAL siege so the
// climax reads visibly larger. Floors at 1 (the default ClampKnob bucket) — a final siege is never smaller
// than a normal one; GoalReachedSystem also math.max(1, ...) at the use-site.
public float FinalSiegeMultiplier;
/// <summary>The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path.</summary>
public static TuningConfig Defaults() => new TuningConfig
{
@@ -82,6 +87,7 @@ namespace ProjectM.Simulation
CoreDamagePerHusk = 10f, // END-1: 10 breaching Husks = full loss; ~5 = a serious dent
CoreRegenIntervalTicks = 18f, // END-1: +1 integrity / 0.3s in Calm (~30s to refill 100 from 0)
CoreOverrunDrainPct = 0.5f, // END-1: a breach costs half the shared ledger (soft-loss penalty)
FinalSiegeMultiplier = 2.5f, // END-2: the final siege is ~2.5x the would-be-next normal siege
};
/// <summary>Clamp a knob to its safe floor: tick knobs &gt;= 1, value knobs &gt;= 0. Used by every write path
@@ -104,7 +110,8 @@ namespace ProjectM.Simulation
case TuningKnob.CoreDamagePerHusk:
case TuningKnob.CoreOverrunDrainPct:
return math.max(0f, value);
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem)
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem).
// FinalSiegeMultiplier also lands here on purpose — a final siege should never be < 1x a normal one.
default:
return math.max(1f, value);
}
@@ -139,6 +146,7 @@ namespace ProjectM.Simulation
case TuningKnob.CoreDamagePerHusk: c.CoreDamagePerHusk = value; break;
case TuningKnob.CoreRegenIntervalTicks: c.CoreRegenIntervalTicks = value; break;
case TuningKnob.CoreOverrunDrainPct: c.CoreOverrunDrainPct = value; break;
case TuningKnob.FinalSiegeMultiplier: c.FinalSiegeMultiplier = value; break;
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
}
}
@@ -171,6 +179,7 @@ namespace ProjectM.Simulation
case TuningKnob.CoreDamagePerHusk: return c.CoreDamagePerHusk;
case TuningKnob.CoreRegenIntervalTicks: return c.CoreRegenIntervalTicks;
case TuningKnob.CoreOverrunDrainPct: return c.CoreOverrunDrainPct;
case TuningKnob.FinalSiegeMultiplier: return c.FinalSiegeMultiplier;
default: return 0f;
}
}
@@ -201,6 +210,7 @@ namespace ProjectM.Simulation
CoreDamagePerHusk = c.CoreDamagePerHusk,
CoreRegenIntervalTicks = c.CoreRegenIntervalTicks,
CoreOverrunDrainPct = c.CoreOverrunDrainPct,
FinalSiegeMultiplier = c.FinalSiegeMultiplier,
};
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
@@ -229,6 +239,7 @@ namespace ProjectM.Simulation
CoreDamagePerHusk = r.CoreDamagePerHusk,
CoreRegenIntervalTicks = r.CoreRegenIntervalTicks,
CoreOverrunDrainPct = r.CoreOverrunDrainPct,
FinalSiegeMultiplier = r.FinalSiegeMultiplier,
};
}
@@ -258,9 +269,10 @@ namespace ProjectM.Simulation
public const byte CoreDamagePerHusk = 20;
public const byte CoreRegenIntervalTicks = 21;
public const byte CoreOverrunDrainPct = 22;
public const byte FinalSiegeMultiplier = 23;
/// <summary>Knob count (overlay iteration bound).</summary>
public const byte Count = 23;
public const byte Count = 24;
}
/// <summary>
@@ -294,5 +306,6 @@ namespace ProjectM.Simulation
public float CoreDamagePerHusk;
public float CoreRegenIntervalTicks;
public float CoreOverrunDrainPct;
public float FinalSiegeMultiplier;
}
}