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:
@@ -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 >= 1, value knobs >= 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace ProjectM.Simulation
|
||||
/// <summary>END-1: Engine Core integrity to restore (0 = pre-v4 save / New Game -> born full at baked Max).</summary>
|
||||
public int CoreCurrent;
|
||||
|
||||
/// <summary>END-2: terminal run outcome to restore (0 = InProgress / pre-v5 save / New Game; 1 = Victory,
|
||||
/// 2 = Loss -> the run loads finished + halted, no re-arm). Born-correct at director spawn.</summary>
|
||||
public byte RunOutcome;
|
||||
|
||||
/// <summary>0 = nothing staged (New Game); non-zero = apply the staged slice at director spawn.</summary>
|
||||
public byte HasData;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace ProjectM.Simulation
|
||||
[Serializable]
|
||||
public class SaveData
|
||||
{
|
||||
public const int CurrentVersion = 4; // END-1: v4 adds CoreCurrent (a wounded base persists)
|
||||
public const int CurrentVersion = 5; // END-2: v5 adds RunOutcome (a won/lost run loads finished); v4 added CoreCurrent
|
||||
|
||||
/// <summary>Oldest save schema the loader accepts (additive); a v2 save loads with structures at full HP.</summary>
|
||||
public const int MinLoadableVersion = 2;
|
||||
@@ -60,6 +60,7 @@ namespace ProjectM.Simulation
|
||||
public int GoalCharge;
|
||||
public int GoalTarget;
|
||||
public int CoreCurrent; // END-1: Engine Core integrity at save time (0 from a pre-v4 save -> restored to baked Max)
|
||||
public int RunOutcome; // END-2: 0=InProgress (also any pre-v5 save) / 1=Victory / 2=Loss -> a finished run loads finished
|
||||
|
||||
public LedgerRow[] Ledger = Array.Empty<LedgerRow>();
|
||||
public StructureSave[] Structures = Array.Empty<StructureSave>();
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// END-2 — server-only marker of which run-phase the macro loop is in. Lives on the GLOBAL CycleDirector
|
||||
/// entity beside <see cref="CycleState"/>/<see cref="CycleRuntime"/>/<see cref="ThreatState"/>; NOT replicated
|
||||
/// (the client never needs to distinguish "the final siege is armed" — the larger wave + telegraph already read
|
||||
/// as escalation; the client shows the terminal banner from the replicated <see cref="RunOutcome"/> instead).
|
||||
/// SINGLE writer: <c>GoalReachedSystem</c> flips <see cref="RunPhaseId.Normal"/> ->
|
||||
/// <see cref="RunPhaseId.FinalDefense"/> exactly once when <see cref="GoalProgress.Charge"/> reaches Target.
|
||||
/// Added at spawn by <c>CycleDirectorSpawnSystem</c> (like CycleRuntime/ThreatState), so it is server-world-only
|
||||
/// and never on the ghost serializer (no re-hash). A <c>byte</c> (never an enum) so a Bursted reader can't trip
|
||||
/// the cross-assembly-enum Burst ICE.
|
||||
/// </summary>
|
||||
public struct RunPhase : IComponentData
|
||||
{
|
||||
public byte Value;
|
||||
}
|
||||
|
||||
/// <summary>Phase constants for <see cref="RunPhase.Value"/> (bytes — never an enum on a Bursted path).</summary>
|
||||
public static class RunPhaseId
|
||||
{
|
||||
/// <summary>Normal play: scheduled / post-expedition sieges arm; the goal meter climbs +1 per survived siege.</summary>
|
||||
public const byte Normal = 0;
|
||||
|
||||
/// <summary>The goal cap was reached: the larger FINAL siege is armed/running. No further sieges arm.</summary>
|
||||
public const byte FinalDefense = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// END-2 — the terminal run outcome: the LATCHING win/lose state, REPLICATED so the client HUD shows the
|
||||
/// victory/loss banner by simply observing it (no fragile client-side reconstruction). Rides the GLOBAL
|
||||
/// untagged CycleDirector ghost (relevant to every connection in every region — it must NEVER be region-tagged;
|
||||
/// the shared-global-state rule), one <c>[GhostField] byte</c> alongside <see cref="CoreIntegrity"/>/
|
||||
/// <see cref="GoalProgress"/>. SINGLE writer: <c>CyclePhaseSystem</c> latches <see cref="RunOutcomeId.Victory"/>
|
||||
/// (final siege cleared) or <see cref="RunOutcomeId.Loss"/> (Core breached during the final siege). Once it is
|
||||
/// non-<see cref="RunOutcomeId.InProgress"/> the run HALTS (GoalReachedSystem + ThreatDirectorSystem stop arming;
|
||||
/// CoreRestoreSystem stops regen). Baked onto the prefab so it is part of the ghost (adding this <c>[GhostField]</c>
|
||||
/// re-hashes the CycleDirector ghost -> one re-bake); born-correct at spawn (InProgress for a New Game, or the
|
||||
/// persisted value on Continue — SaveData v5).
|
||||
/// </summary>
|
||||
public struct RunOutcome : IComponentData
|
||||
{
|
||||
[GhostField] public byte Value;
|
||||
}
|
||||
|
||||
/// <summary>Outcome constants for <see cref="RunOutcome.Value"/> (bytes — never an enum on a Bursted/serialized path).</summary>
|
||||
public static class RunOutcomeId
|
||||
{
|
||||
/// <summary>The run is live (no terminal result yet).</summary>
|
||||
public const byte InProgress = 0;
|
||||
|
||||
/// <summary>The final siege was survived — the Engine holds. Terminal; the run halts.</summary>
|
||||
public const byte Victory = 1;
|
||||
|
||||
/// <summary>The Core was breached during the final siege — overrun. Terminal; the run halts.</summary>
|
||||
public const byte Loss = 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ce481dc9a135834fa6d59882895b0f5
|
||||
Reference in New Issue
Block a user