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
@@ -64,6 +64,10 @@ namespace ProjectM.Client
Label _coreText;
uint _lastOverrunTick;
float _overrunFlashLeft;
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
VisualElement _runBanner;
Label _runBannerText, _runBannerSub;
readonly List<VisualElement> _pips = new();
@@ -250,6 +254,21 @@ namespace ProjectM.Client
_locationText.text = "BASE OVERRUN - resources lost; the Core will recover";
_locationText.style.color = new Color(1f, 0.3f, 0.25f);
}
// ---- END-2: terminal run banner (Victory / Loss), observed from the replicated RunOutcome ----
if (SystemAPI.TryGetSingleton<RunOutcome>(out var runOutcome) && runOutcome.Value != RunOutcomeId.InProgress)
{
bool win = runOutcome.Value == RunOutcomeId.Victory;
_runBanner.style.display = DisplayStyle.Flex;
_runBannerText.text = win ? "THE ENGINE HOLDS" : "OVERRUN";
_runBannerText.style.color = win ? new Color(0.45f, 0.95f, 1f) : new Color(1f, 0.35f, 0.3f);
_runBannerSub.text = win ? "VICTORY - the final siege is broken" : "THE FINAL STAND FELL";
_runBannerSub.style.color = win ? new Color(0.7f, 0.95f, 1f) : new Color(1f, 0.6f, 0.5f);
}
else
{
_runBanner.style.display = DisplayStyle.None;
}
// ---- Threat readout (top-right) — hidden entirely at base with zero husks; its reappearance is the cue ----
@@ -565,6 +584,7 @@ namespace ProjectM.Client
BuildHintBar(root);
BuildDowned(root);
BuildInventory(root);
BuildRunBanner(root);
}
void BuildVignette(VisualElement root)
@@ -839,6 +859,29 @@ namespace ProjectM.Client
_downed.style.display = DisplayStyle.None;
root.Add(_downed);
}
void BuildRunBanner(VisualElement root)
{
_runBanner = new VisualElement();
_runBanner.style.position = Position.Absolute;
_runBanner.style.left = 0; _runBanner.style.right = 0; _runBanner.style.top = 0; _runBanner.style.bottom = 0;
_runBanner.style.alignItems = Align.Center;
_runBanner.style.justifyContent = Justify.Center;
_runBanner.pickingMode = PickingMode.Ignore;
_runBanner.style.backgroundColor = new Color(0.02f, 0.03f, 0.05f, 0.55f);
var col = HudUi.Group(Align.Center);
_runBannerText = HudUi.Display("", 72, Color.white, TextAnchor.MiddleCenter);
col.Add(_runBannerText);
_runBannerSub = HudUi.Text("", 22, MenuUi.SubCol, TextAnchor.MiddleCenter);
_runBannerSub.style.marginTop = 8;
col.Add(_runBannerSub);
var hint = HudUi.Text("Esc - menu", 15, MenuUi.SubCol, TextAnchor.MiddleCenter);
hint.style.marginTop = 24;
col.Add(hint);
_runBanner.Add(col);
_runBanner.style.display = DisplayStyle.None;
root.Add(_runBanner);
}
void BuildInventory(VisualElement root)
{
@@ -107,7 +107,7 @@ namespace ProjectM.Client
if (data == null) return;
var em = server.EntityManager;
var e = em.CreateEntity();
em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, HasData = 1 });
em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, RunOutcome = (byte)data.RunOutcome, HasData = 1 });
var buf = em.AddBuffer<PendingSaveLedgerRow>(e);
if (data.Ledger != null)
foreach (var row in data.Ledger)
@@ -139,6 +139,7 @@ namespace ProjectM.Client
var dir = q.GetSingletonEntity();
var goal = em.HasComponent<GoalProgress>(dir) ? em.GetComponentData<GoalProgress>(dir) : default;
var core = em.HasComponent<CoreIntegrity>(dir) ? em.GetComponentData<CoreIntegrity>(dir) : default; // END-1
var outcome = em.HasComponent<RunOutcome>(dir) ? em.GetComponentData<RunOutcome>(dir) : default; // END-2
var buffer = em.GetBuffer<StorageEntry>(dir, true);
var rows = new LedgerRow[buffer.Length];
@@ -160,6 +161,7 @@ namespace ProjectM.Client
GoalCharge = goal.Charge,
GoalTarget = goal.Target,
CoreCurrent = core.Current,
RunOutcome = outcome.Value,
Ledger = rows,
Structures = structures,