END-1: the base can be lost - a losable Engine Core with integrity
Adds CoreIntegrity{[GhostField] Current,Max,OverrunTick} on the GLOBAL
CycleDirector ghost (no new ghost/relevancy). CoreDamageSystem (server,
after EnemyAISystem): a Husk within ~3u of PlotCenter drains + is consumed;
CoreRestoreSystem regenerates only in Calm. The SOFT-loss edge lives inside
CyclePhaseSystem (sole Phase writer): Current<=0 in Siege flips to Calm with
NO goal reward, StorageMath.DrainFraction drains the shared ledger, all Husks
despawn, and OverrunTick is stamped (a transient HUD-flash pulse, not a
latching outcome - the Victory latch is END-2's). EnemyAISystem treats the
Core as a FALLBACK target so an undefended base is overrun instead of idling.
SaveData -> v4 persists CoreCurrent (0 -> born full, the EB-1 HP sentinel);
3 live TuningConfig knobs + a red HUD Core bar. Soft-loss + targeting +
breach-resolution forks operator-locked.
See DR-034.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,10 @@ namespace ProjectM.Client
|
||||
TuningRow("Melee combo len", TuningKnob.MeleeComboLength, 1f, "0");
|
||||
GUILayout.Space(4);
|
||||
TuningRow("Struct aggro w", TuningKnob.StructureAggroWeight, 0.1f, "0.00"); // EB-1: <1 prefers structures
|
||||
TuningRow("Core dmg/husk", TuningKnob.CoreDamagePerHusk, 1f, "0"); // END-1: integrity per breaching Husk
|
||||
TuningRow("Core regen int", TuningKnob.CoreRegenIntervalTicks, 1f, "0"); // END-1: ticks between +1 in Calm
|
||||
TuningRow("Core overrun %", TuningKnob.CoreOverrunDrainPct, 0.05f, "0.00"); // END-1: ledger fraction lost on breach
|
||||
|
||||
}
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace ProjectM.Client
|
||||
static readonly Color OreAmber = new(1f, 0.72f, 0.35f);
|
||||
static readonly Color BioGreen = new(0.55f, 0.85f, 0.45f);
|
||||
static readonly Color ChargeViolet = new(0.80f, 0.45f, 1f);
|
||||
static readonly Color CoreRed = new(1f, 0.40f, 0.32f); // END-1 Engine Core integrity bar
|
||||
|
||||
static readonly Color PanelDark = new(0.08f, 0.11f, 0.15f, 0.90f);
|
||||
static readonly Color PanelWarm = new(0.16f, 0.09f, 0.09f, 0.88f);
|
||||
static readonly Color PipDim = new(0.25f, 0.30f, 0.36f, 0.9f);
|
||||
@@ -56,6 +58,13 @@ namespace ProjectM.Client
|
||||
// macro: banner + location + goal
|
||||
VisualElement _banner, _goalContainer, _goalPipsRow, _goalBar, _goalFill;
|
||||
Label _phaseText, _cycleText, _locationText, _goalText;
|
||||
|
||||
// END-1: Engine Core integrity (losable base-heart) + overrun flash edge-detector
|
||||
VisualElement _coreContainer, _coreBar, _coreFill;
|
||||
Label _coreText;
|
||||
uint _lastOverrunTick;
|
||||
float _overrunFlashLeft;
|
||||
|
||||
readonly List<VisualElement> _pips = new();
|
||||
|
||||
// resources
|
||||
@@ -216,6 +225,33 @@ namespace ProjectM.Client
|
||||
_locationText.style.color = new Color(1f, 0.4f, 0.9f);
|
||||
}
|
||||
|
||||
// ---- Engine Core integrity (END-1): a red base-heart bar; an overrun stamps a transient pulse we flash ----
|
||||
if (SystemAPI.TryGetSingleton<CoreIntegrity>(out var core) && core.Max > 0)
|
||||
{
|
||||
_coreContainer.style.display = DisplayStyle.Flex;
|
||||
float cfrac = Mathf.Clamp01(core.Current / (float)core.Max);
|
||||
HudUi.SetFill(_coreFill, cfrac);
|
||||
_coreText.text = "CORE " + core.Current + " / " + core.Max;
|
||||
_coreText.style.color = Color.Lerp(BlightRed, CoreRed, cfrac); // shifts to danger as it drops
|
||||
if (core.OverrunTick != 0 && core.OverrunTick != _lastOverrunTick)
|
||||
{
|
||||
_lastOverrunTick = core.OverrunTick; // edge-detect the replicated breach pulse
|
||||
_overrunFlashLeft = 3.5f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_coreContainer.style.display = DisplayStyle.None;
|
||||
}
|
||||
// Overrun flash overrides the location line (runs AFTER the EB-2 cue so it wins; at a breach Phase is Calm).
|
||||
if (_overrunFlashLeft > 0f)
|
||||
{
|
||||
_overrunFlashLeft -= dt;
|
||||
_locationText.text = "BASE OVERRUN - resources lost; the Core will recover";
|
||||
_locationText.style.color = new Color(1f, 0.3f, 0.25f);
|
||||
}
|
||||
|
||||
|
||||
// ---- Threat readout (top-right) — hidden entirely at base with zero husks; its reappearance is the cue ----
|
||||
bool showThreat = siege || huskCount > 0;
|
||||
_threatPanel.style.display = showThreat ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
@@ -703,6 +739,23 @@ namespace ProjectM.Client
|
||||
_goalContainer.Add(_goalBar);
|
||||
|
||||
macro.Add(_goalContainer);
|
||||
|
||||
// END-1: Engine Core integrity bar (red) — the losable base-heart meter.
|
||||
_coreContainer = HudUi.Group(Align.Center);
|
||||
_coreContainer.style.marginTop = 6;
|
||||
var coreLine = new VisualElement();
|
||||
coreLine.style.flexDirection = FlexDirection.Row;
|
||||
coreLine.style.alignItems = Align.Center;
|
||||
coreLine.pickingMode = PickingMode.Ignore;
|
||||
_coreBar = HudUi.Bar(360, 14, CoreRed, out _coreFill);
|
||||
coreLine.Add(_coreBar);
|
||||
_coreText = HudUi.Text("CORE 100 / 100", 13, CoreRed, TextAnchor.MiddleLeft);
|
||||
_coreText.style.marginLeft = 10;
|
||||
coreLine.Add(_coreText);
|
||||
_coreContainer.Add(coreLine);
|
||||
_coreContainer.style.display = DisplayStyle.None;
|
||||
macro.Add(_coreContainer);
|
||||
|
||||
root.Add(macro);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, HasData = 1 });
|
||||
em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, HasData = 1 });
|
||||
var buf = em.AddBuffer<PendingSaveLedgerRow>(e);
|
||||
if (data.Ledger != null)
|
||||
foreach (var row in data.Ledger)
|
||||
@@ -138,6 +138,8 @@ namespace ProjectM.Client
|
||||
if (q.IsEmptyIgnoreFilter) return;
|
||||
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 buffer = em.GetBuffer<StorageEntry>(dir, true);
|
||||
var rows = new LedgerRow[buffer.Length];
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
@@ -157,6 +159,8 @@ namespace ProjectM.Client
|
||||
{
|
||||
GoalCharge = goal.Charge,
|
||||
GoalTarget = goal.Target,
|
||||
CoreCurrent = core.Current,
|
||||
|
||||
Ledger = rows,
|
||||
Structures = structures,
|
||||
StructureIo = structureIo,
|
||||
|
||||
Reference in New Issue
Block a user