DR-042 Phase C (legibility, part 1): expedition objective HUD, Aether button, cold-start seed, Biomass sink, palette declutter
Scoping/design-gated (wf_7c5a555e-136). Fixes "the base reads as inert after Phase A":
- C7b objective readout: new replicated ExpeditionObjective{[GhostField] byte State, short Remaining} on the
untagged CycleDirector ghost (cross-region safe). Sole writer ZoneEnemyDirectorSystem, written ABOVE its
early-returns (snapshot-above-early-return) so the HUD never freezes stale. Play-verified it replicates
server->client.
- C7a gate prompt + C7b HUD readout: HudSystem shows "GO TO THE EXPEDITION GATE" / "EXPEDITION IN PROGRESS - N
remaining" / "CLEARED - return to claim", below the siege/overrun overrides.
- C6a Aether upgrade button: un-gated BuildSendSystem.UpgradeAbility (was #if UNITY_EDITOR); HudSystem adds a
MenuUi.Button with live affordability tint (the only Aether sink was U-key only).
- C6c cold-start seed: CycleDirectorSpawnSystem seeds Tuning.StartingOre (50) into the ledger on a NEW game only
(born-correct, pre-Playback), killing the silent turret-before-fabricator deadlock. Play-verified seededOre=50.
- C6b Biomass sink: Wall cost Ore->Biomass (the dead currency now has a home). Play-verified WallCostRes=Biomass.
- C6d palette declutter: hide dead Pylon/Harvester/Conveyor from the build palette + trimmed their dev hotkeys
(catalog/prefabs stay baked, code-intact per DR-020).
389/389 EditMode + clean netcode Play smoke (ghost re-hash OK, no exceptions). SaveData stays v5.
C5 (walls block enemies) is the remaining Phase C item, sequenced separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ namespace ProjectM.Authoring
|
|||||||
{
|
{
|
||||||
Type = StructureType.Wall,
|
Type = StructureType.Wall,
|
||||||
Prefab = GetEntity(authoring.WallPrefab, TransformUsageFlags.Dynamic),
|
Prefab = GetEntity(authoring.WallPrefab, TransformUsageFlags.Dynamic),
|
||||||
CostResourceId = ResourceId.Ore,
|
CostResourceId = ResourceId.Biomass, // DR-042 C6b: walls cost Biomass (the dead currency's only sink)
|
||||||
CostAmount = authoring.WallCostOre,
|
CostAmount = authoring.WallCostOre,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ namespace ProjectM.Authoring
|
|||||||
// CycleDirectorSpawnSystem overrides it with a persisted Victory/Loss on Continue.
|
// CycleDirectorSpawnSystem overrides it with a persisted Victory/Loss on Continue.
|
||||||
AddComponent(entity, new RunOutcome { Value = RunOutcomeId.InProgress });
|
AddComponent(entity, new RunOutcome { Value = RunOutcomeId.InProgress });
|
||||||
|
|
||||||
|
// DR-042 C7b: replicated expedition-objective summary (the HUD 'enemies remaining / cleared' readout).
|
||||||
|
// Born Idle; ZoneEnemyDirectorSystem is the sole writer. New [GhostField] component -> re-hashes the
|
||||||
|
// runtime-spawned director ghost (server + client bake the same prefab -> hash matches), like CoreIntegrity.
|
||||||
|
AddComponent(entity, new ExpeditionObjective { State = ExpeditionObjectiveState.Idle, Remaining = 0 });
|
||||||
|
|
||||||
|
|
||||||
AddComponent(entity, new ThreatConfig
|
AddComponent(entity, new ThreatConfig
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace ProjectM.Client
|
|||||||
/// rotates a conveyor's facing. Fire is suppressed while build mode is active (PlayerInputGatherSystem reads
|
/// rotates a conveyor's facing. Fire is suppressed while build mode is active (PlayerInputGatherSystem reads
|
||||||
/// <see cref="BuildPaletteState.Active"/>), so the place-click never also fires. Build mode is suspended while
|
/// <see cref="BuildPaletteState.Active"/>), so the place-click never also fires. Build mode is suspended while
|
||||||
/// the pause overlay is open, and the frame a palette button changes the selection never also places.
|
/// the pause overlay is open, and the frame a palette button changes the selection never also places.
|
||||||
/// (2) keyboard hotkeys (fallback, suppressed in palette mode): B/V/N/H/F/C place at the local player's cell.
|
/// (2) keyboard hotkeys (fallback, suppressed in palette mode): B/V/F place at the local player's cell.
|
||||||
/// Editor-only statics (PlaceStructure / PlaceHarvester / ...) drive the same RPC path from execute_code for
|
/// Editor-only statics (PlaceStructure / PlaceHarvester / ...) drive the same RPC path from execute_code for
|
||||||
/// headless validation. Managed SystemBase; UnityEngine.InputSystem types are fully qualified to avoid the
|
/// headless validation. Managed SystemBase; UnityEngine.InputSystem types are fully qualified to avoid the
|
||||||
/// ProjectM.Simulation.PlayerInput name collision. The server re-validates legality + cost authoritatively.
|
/// ProjectM.Simulation.PlayerInput name collision. The server re-validates legality + cost authoritatively.
|
||||||
@@ -30,10 +30,9 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
(UnityEngine.InputSystem.Key.B, StructureType.Turret),
|
(UnityEngine.InputSystem.Key.B, StructureType.Turret),
|
||||||
(UnityEngine.InputSystem.Key.V, StructureType.Wall),
|
(UnityEngine.InputSystem.Key.V, StructureType.Wall),
|
||||||
(UnityEngine.InputSystem.Key.N, StructureType.Pylon),
|
|
||||||
(UnityEngine.InputSystem.Key.H, StructureType.Harvester),
|
|
||||||
(UnityEngine.InputSystem.Key.F, StructureType.Fabricator),
|
(UnityEngine.InputSystem.Key.F, StructureType.Fabricator),
|
||||||
(UnityEngine.InputSystem.Key.C, StructureType.Conveyor),
|
// DR-042 C6d: Pylon/Harvester/Conveyor are dead (unwired automation) — dropped from the hotkey fallback
|
||||||
|
// to match the hidden build palette; their PlaceStructure execute_code statics remain for dev.
|
||||||
};
|
};
|
||||||
|
|
||||||
UnityEngine.Camera _camera; // cursor -> ground re-raycast for click-to-place (resolved lazily)
|
UnityEngine.Camera _camera; // cursor -> ground re-raycast for click-to-place (resolved lazily)
|
||||||
@@ -41,11 +40,16 @@ namespace ProjectM.Client
|
|||||||
Material _ghostMat;
|
Material _ghostMat;
|
||||||
byte _lastSelected; // skip placing on the frame a palette click changes the selection
|
byte _lastSelected; // skip placing on the frame a palette click changes the selection
|
||||||
|
|
||||||
|
// DR-042 C6a: the ability-upgrade send is RUNTIME (the HUD Aether button calls UpgradeAbility); only the
|
||||||
|
// execute_code PLACE statics stay editor-gated. Mirrors EquipSendSystem's unconditional queue + drain.
|
||||||
|
static int s_PendingUpgrades = 0;
|
||||||
|
/// <summary>Runtime hook (HUD Aether button) + execute_code: queue an ability-damage upgrade.</summary>
|
||||||
|
public static void UpgradeAbility() => s_PendingUpgrades++;
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
struct PendingBuild { public byte Type; public int CellX; public int CellZ; public byte Direction; }
|
struct PendingBuild { public byte Type; public int CellX; public int CellZ; public byte Direction; }
|
||||||
static readonly System.Collections.Generic.Queue<PendingBuild> s_PendingBuild =
|
static readonly System.Collections.Generic.Queue<PendingBuild> s_PendingBuild =
|
||||||
new System.Collections.Generic.Queue<PendingBuild>();
|
new System.Collections.Generic.Queue<PendingBuild>();
|
||||||
static int s_PendingUpgrades = 0;
|
|
||||||
|
|
||||||
/// <summary>EDITOR / execute_code hook: queue a structure placement at a specific cell.</summary>
|
/// <summary>EDITOR / execute_code hook: queue a structure placement at a specific cell.</summary>
|
||||||
public static void PlaceStructure(byte type, int cellX, int cellZ, byte direction = 0) =>
|
public static void PlaceStructure(byte type, int cellX, int cellZ, byte direction = 0) =>
|
||||||
@@ -69,8 +73,6 @@ namespace ProjectM.Client
|
|||||||
/// <summary>EDITOR / execute_code hook: queue a conveyor placement facing direction (0=+X,1=-X,2=+Z,3=-Z).</summary>
|
/// <summary>EDITOR / execute_code hook: queue a conveyor placement facing direction (0=+X,1=-X,2=+Z,3=-Z).</summary>
|
||||||
public static void PlaceConveyor(int cellX, int cellZ, byte direction) => PlaceStructure(StructureType.Conveyor, cellX, cellZ, direction);
|
public static void PlaceConveyor(int cellX, int cellZ, byte direction) => PlaceStructure(StructureType.Conveyor, cellX, cellZ, direction);
|
||||||
|
|
||||||
/// <summary>EDITOR / execute_code hook: queue an ability-damage upgrade.</summary>
|
|
||||||
public static void UpgradeAbility() => s_PendingUpgrades++;
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
protected override void OnCreate()
|
protected override void OnCreate()
|
||||||
@@ -115,17 +117,19 @@ namespace ProjectM.Client
|
|||||||
SendUpgrade(connection);
|
SendUpgrade(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DR-042 C6a: the ability-upgrade drain runs at RUNTIME (the HUD Aether button enqueues via UpgradeAbility);
|
||||||
|
// only the execute_code PLACE drain stays editor-gated.
|
||||||
|
while (s_PendingUpgrades > 0)
|
||||||
|
{
|
||||||
|
s_PendingUpgrades--;
|
||||||
|
SendUpgrade(connection);
|
||||||
|
}
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
while (s_PendingBuild.Count > 0)
|
while (s_PendingBuild.Count > 0)
|
||||||
{
|
{
|
||||||
var b = s_PendingBuild.Dequeue();
|
var b = s_PendingBuild.Dequeue();
|
||||||
SendBuild(connection, b.Type, b.CellX, b.CellZ, b.Direction);
|
SendBuild(connection, b.Type, b.CellX, b.CellZ, b.Direction);
|
||||||
}
|
}
|
||||||
while (s_PendingUpgrades > 0)
|
|
||||||
{
|
|
||||||
s_PendingUpgrades--;
|
|
||||||
SendUpgrade(connection);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ namespace ProjectM.Client
|
|||||||
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
|
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
|
||||||
VisualElement _runBanner;
|
VisualElement _runBanner;
|
||||||
Label _runBannerText, _runBannerSub;
|
Label _runBannerText, _runBannerSub;
|
||||||
|
// DR-042 C6a: the Aether ability-upgrade button (was U-key only) + its live affordability tint.
|
||||||
|
Button _upgradeBtn;
|
||||||
|
|
||||||
|
|
||||||
readonly List<VisualElement> _pips = new();
|
readonly List<VisualElement> _pips = new();
|
||||||
@@ -181,6 +183,28 @@ namespace ProjectM.Client
|
|||||||
_locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f)
|
_locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f)
|
||||||
: finalSiege ? new Color(1f, 0.3f, 0.25f)
|
: finalSiege ? new Color(1f, 0.3f, 0.25f)
|
||||||
: siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
|
: siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
|
||||||
|
// DR-042 C7 (gate prompt) + C7b (objective readout): the expedition is the win-driver, so signpost it.
|
||||||
|
// Reads the REPLICATED ExpeditionObjective summary (cross-region safe). Lower priority than the siege /
|
||||||
|
// cold-turret / overrun overrides below, which still win.
|
||||||
|
if (SystemAPI.TryGetSingleton<ExpeditionObjective>(out var obj))
|
||||||
|
{
|
||||||
|
if (onExpedition)
|
||||||
|
{
|
||||||
|
if (obj.State == ExpeditionObjectiveState.Cleared)
|
||||||
|
{ _locationText.text = "ZONE CLEARED - return to base to claim"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
|
||||||
|
else if (obj.State == ExpeditionObjectiveState.Active)
|
||||||
|
{ _locationText.text = "CLEAR THE ZONE - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
|
||||||
|
}
|
||||||
|
else if (!siege)
|
||||||
|
{
|
||||||
|
if (obj.State == ExpeditionObjectiveState.Cleared)
|
||||||
|
{ _locationText.text = "EXPEDITION CLEARED - return to claim your reward"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
|
||||||
|
else if (obj.State == ExpeditionObjectiveState.Active)
|
||||||
|
{ _locationText.text = "EXPEDITION IN PROGRESS - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
|
||||||
|
else
|
||||||
|
{ _locationText.text = "GO TO THE EXPEDITION GATE - clear a sortie to advance the Engine"; _locationText.style.color = new Color(0.55f, 0.85f, 1f); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Goal (hex-pip meter, or a continuous bar for large targets) ----
|
// ---- Goal (hex-pip meter, or a continuous bar for large targets) ----
|
||||||
if (SystemAPI.TryGetSingleton<GoalProgress>(out var goal))
|
if (SystemAPI.TryGetSingleton<GoalProgress>(out var goal))
|
||||||
@@ -230,6 +254,9 @@ namespace ProjectM.Client
|
|||||||
_oreNum.text = ore.ToString();
|
_oreNum.text = ore.ToString();
|
||||||
_bioNum.text = bio.ToString();
|
_bioNum.text = bio.ToString();
|
||||||
_chargeNum.text = charge.ToString();
|
_chargeNum.text = charge.ToString();
|
||||||
|
// DR-042 C6a: dim the Aether upgrade button when it isn't affordable (cost is a compile-time const).
|
||||||
|
if (_upgradeBtn != null)
|
||||||
|
_upgradeBtn.style.opacity = aether >= Tuning.AbilityUpgradeCostAmount ? 1f : 0.5f;
|
||||||
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
|
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
|
||||||
// broken turret): a dry base during a siege tells the player to build a Fabricator.
|
// broken turret): a dry base during a siege tells the player to build a Fabricator.
|
||||||
if (siege && charge == 0 && !onExpedition)
|
if (siege && charge == 0 && !onExpedition)
|
||||||
@@ -446,13 +473,19 @@ namespace ProjectM.Client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DR-042 C6d: Harvester/Conveyor/Pylon are dead (unwired automation) -> hidden from the build palette
|
||||||
|
// (catalog + prefabs stay baked, code-intact per DR-020). Only Turret/Wall/Fabricator are buildable in the UI.
|
||||||
|
static bool IsPaletteType(byte type) =>
|
||||||
|
type != StructureType.Pylon && type != StructureType.Harvester && type != StructureType.Conveyor;
|
||||||
|
|
||||||
void UpdatePalette(int aether, int ore, int bio, bool onExpedition)
|
void UpdatePalette(int aether, int ore, int bio, bool onExpedition)
|
||||||
{
|
{
|
||||||
if (!_paletteBuilt && SystemAPI.TryGetSingletonEntity<StructureCatalog>(out var catE))
|
if (!_paletteBuilt && SystemAPI.TryGetSingletonEntity<StructureCatalog>(out var catE))
|
||||||
{
|
{
|
||||||
var cat = SystemAPI.GetBuffer<StructureCatalogEntry>(catE);
|
var cat = SystemAPI.GetBuffer<StructureCatalogEntry>(catE);
|
||||||
for (int i = 0; i < cat.Length; i++)
|
for (int i = 0; i < cat.Length; i++)
|
||||||
AddPaletteItem(cat[i].Type, cat[i].CostAmount, cat[i].CostResourceId);
|
if (IsPaletteType(cat[i].Type))
|
||||||
|
AddPaletteItem(cat[i].Type, cat[i].CostAmount, cat[i].CostResourceId);
|
||||||
_paletteBuilt = true;
|
_paletteBuilt = true;
|
||||||
}
|
}
|
||||||
if (!_paletteBuilt) { _paletteRow.style.display = DisplayStyle.None; return; }
|
if (!_paletteBuilt) { _paletteRow.style.display = DisplayStyle.None; return; }
|
||||||
@@ -809,6 +842,11 @@ namespace ProjectM.Client
|
|||||||
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
|
strip.Add(ResourceChip(theme != null ? theme.OreIcon : null, OreAmber, "0", out _oreNum, 30, 22));
|
||||||
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
|
strip.Add(ResourceChip(theme != null ? theme.BioIcon : null, BioGreen, "0", out _bioNum, 26, 20));
|
||||||
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
|
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
|
||||||
|
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
|
||||||
|
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
|
||||||
|
_upgradeBtn = MenuUi.Button("UPGRADE DMG (" + Tuning.AbilityUpgradeCostAmount + " AETHER)", BuildSendSystem.UpgradeAbility);
|
||||||
|
_upgradeBtn.style.marginLeft = 18;
|
||||||
|
strip.Add(_upgradeBtn);
|
||||||
|
|
||||||
root.Add(strip);
|
root.Add(strip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ namespace ProjectM.Server
|
|||||||
/// wave is fully spawned and every zone enemy is dead, it marks <see cref="CycleRuntime.ClearedThisEpoch"/> once —
|
/// wave is fully spawned and every zone enemy is dead, it marks <see cref="CycleRuntime.ClearedThisEpoch"/> once —
|
||||||
/// the gate's once-per-epoch Ore reward reads that on the player's return.
|
/// the gate's once-per-epoch Ore reward reads that on the player's return.
|
||||||
///
|
///
|
||||||
|
/// DR-042 C7b: it ALSO writes the replicated <see cref="ExpeditionObjective"/> summary every tick, ABOVE the
|
||||||
|
/// presence early-return (snapshot-above-early-return), so the client HUD's "enemies remaining / cleared" readout
|
||||||
|
/// never freezes stale even when nobody is out.
|
||||||
|
///
|
||||||
/// Ordering: <c>[UpdateAfter(ExpeditionFieldSystem)]</c> ONLY. ExpeditionFieldSystem is itself
|
/// Ordering: <c>[UpdateAfter(ExpeditionFieldSystem)]</c> ONLY. ExpeditionFieldSystem is itself
|
||||||
/// <c>[UpdateAfter(CyclePhaseSystem)]</c>, so ALSO declaring <c>[UpdateBefore(CyclePhaseSystem)]</c> here (as the
|
/// <c>[UpdateAfter(CyclePhaseSystem)]</c>, so ALSO declaring <c>[UpdateBefore(CyclePhaseSystem)]</c> here (as the
|
||||||
/// v1 plan first sketched) would close a CyclePhase->Field->Zone->CyclePhase sort cycle that throws at Play
|
/// v1 plan first sketched) would close a CyclePhase->Field->Zone->CyclePhase sort cycle that throws at Play
|
||||||
@@ -51,26 +55,55 @@ namespace ProjectM.Server
|
|||||||
return;
|
return;
|
||||||
uint now = serverTick.TickIndexForValidTick;
|
uint now = serverTick.TickIndexForValidTick;
|
||||||
|
|
||||||
// Per-player presence: only run while someone is OUT in the expedition (mirrors ExpeditionFieldSystem).
|
// Per-player presence: the SPAWNER only runs while someone is OUT in the expedition (mirrors
|
||||||
|
// ExpeditionFieldSystem). The objective readout below is written FIRST, every tick, even when nobody's out.
|
||||||
int expeditionPlayers = 0;
|
int expeditionPlayers = 0;
|
||||||
foreach (var region in SystemAPI.Query<RefRO<RegionTag>>().WithAll<PlayerTag>())
|
foreach (var region in SystemAPI.Query<RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||||
if (region.ValueRO.Region == RegionId.Expedition)
|
if (region.ValueRO.Region == RegionId.Expedition)
|
||||||
expeditionPlayers++;
|
expeditionPlayers++;
|
||||||
if (expeditionPlayers == 0)
|
|
||||||
return; // nobody out there: the field manager owns teardown, we do nothing
|
|
||||||
|
|
||||||
var directorEntity = SystemAPI.GetSingletonEntity<ZoneEnemyDirector>();
|
var directorEntity = SystemAPI.GetSingletonEntity<ZoneEnemyDirector>();
|
||||||
var dir = SystemAPI.GetComponent<ZoneEnemyDirector>(directorEntity);
|
var dir = SystemAPI.GetComponent<ZoneEnemyDirector>(directorEntity);
|
||||||
var zs = SystemAPI.GetComponent<ZoneEnemyState>(directorEntity);
|
var zs = SystemAPI.GetComponent<ZoneEnemyState>(directorEntity);
|
||||||
var prefabs = SystemAPI.GetBuffer<ZoneEnemyPrefab>(directorEntity);
|
|
||||||
if (prefabs.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||||
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
||||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
||||||
int epoch = runtime.ExpeditionEpoch;
|
int epoch = runtime.ExpeditionEpoch;
|
||||||
|
|
||||||
|
int aliveZone = m_ZoneEnemies.CalculateEntityCount();
|
||||||
|
|
||||||
|
// DR-042 C7b: write the REPLICATED objective summary FIRST, above the early-returns (snapshot-above-
|
||||||
|
// early-return) so the HUD never freezes stale. Rides the untagged CycleDirector ghost (cross-region safe).
|
||||||
|
if (SystemAPI.HasComponent<ExpeditionObjective>(cycleEntity))
|
||||||
|
{
|
||||||
|
byte objState;
|
||||||
|
short objRemaining;
|
||||||
|
if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch)
|
||||||
|
{
|
||||||
|
objState = ExpeditionObjectiveState.Cleared; // cleared but not yet claimed -> "return to claim"
|
||||||
|
objRemaining = 0;
|
||||||
|
}
|
||||||
|
else if (expeditionPlayers > 0 && (aliveZone > 0 || zs.RemainingToSpawn > 0))
|
||||||
|
{
|
||||||
|
objState = ExpeditionObjectiveState.Active;
|
||||||
|
objRemaining = (short)math.min(aliveZone + zs.RemainingToSpawn, short.MaxValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
objState = ExpeditionObjectiveState.Idle;
|
||||||
|
objRemaining = 0;
|
||||||
|
}
|
||||||
|
SystemAPI.SetComponent(cycleEntity, new ExpeditionObjective { State = objState, Remaining = objRemaining });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expeditionPlayers == 0)
|
||||||
|
return; // nobody out there: the field manager owns teardown, the spawner does nothing
|
||||||
|
|
||||||
|
var prefabs = SystemAPI.GetBuffer<ZoneEnemyPrefab>(directorEntity);
|
||||||
|
if (prefabs.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// MC-2: build the 4-type weighted mix band from the director's baked weights (shared math with the base
|
// MC-2: build the 4-type weighted mix band from the director's baked weights (shared math with the base
|
||||||
// siege). GruntsPerWave/ChargersPerWave are the Grunt/Charger base counts.
|
// siege). GruntsPerWave/ChargersPerWave are the Grunt/Charger base counts.
|
||||||
var bands = new MixBands
|
var bands = new MixBands
|
||||||
@@ -94,8 +127,6 @@ namespace ProjectM.Server
|
|||||||
zs.NextSpawnTick = TickUtil.NonZero(now); // first slot this tick
|
zs.NextSpawnTick = TickUtil.NonZero(now); // first slot this tick
|
||||||
}
|
}
|
||||||
|
|
||||||
int aliveZone = m_ZoneEnemies.CalculateEntityCount();
|
|
||||||
|
|
||||||
if (zs.RemainingToSpawn > 0)
|
if (zs.RemainingToSpawn > 0)
|
||||||
{
|
{
|
||||||
// Spawn only in Calm (a base Siege pauses the expedition wave), one SLOT per cadence, under the cap.
|
// Spawn only in Calm (a base Siege pauses the expedition wave), one SLOT per cadence, under the cap.
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ namespace ProjectM.Server
|
|||||||
ecb.AddComponent(director, new RunPhase { Value = RunPhaseId.Normal });
|
ecb.AddComponent(director, new RunPhase { Value = RunPhaseId.Normal });
|
||||||
|
|
||||||
// Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director
|
// Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director
|
||||||
|
// DR-042 C6c: a NEW game seeds starting Ore below; a restored save (Continue) keeps its ledger.
|
||||||
|
bool restoredLedger = false;
|
||||||
// ghost never serializes a default GoalProgress / empty ledger to clients (no replication flicker).
|
// ghost never serializes a default GoalProgress / empty ledger to clients (no replication flicker).
|
||||||
if (SystemAPI.TryGetSingletonEntity<PendingSave>(out var pendingEntity))
|
if (SystemAPI.TryGetSingletonEntity<PendingSave>(out var pendingEntity))
|
||||||
{
|
{
|
||||||
@@ -79,6 +81,7 @@ namespace ProjectM.Server
|
|||||||
var srcLedger = SystemAPI.GetBuffer<PendingSaveLedgerRow>(pendingEntity);
|
var srcLedger = SystemAPI.GetBuffer<PendingSaveLedgerRow>(pendingEntity);
|
||||||
var destLedger = ecb.SetBuffer<StorageEntry>(director);
|
var destLedger = ecb.SetBuffer<StorageEntry>(director);
|
||||||
SaveApply.WriteLedger(srcLedger, destLedger);
|
SaveApply.WriteLedger(srcLedger, destLedger);
|
||||||
|
restoredLedger = true; // a save restored the ledger -> do NOT seed starting Ore (C6c)
|
||||||
|
|
||||||
// END-1: born-correct the Engine Core. Max comes from the BAKED prefab (never the save); a
|
// END-1: born-correct the Engine Core. Max comes from the BAKED prefab (never the save); a
|
||||||
// persisted wounded Current (>0) restores clamped to Max, else (0 = pre-v4 save) born full.
|
// persisted wounded Current (>0) restores clamped to Max, else (0 = pre-v4 save) born full.
|
||||||
@@ -99,6 +102,12 @@ namespace ProjectM.Server
|
|||||||
ecb.DestroyEntity(pendingEntity);
|
ecb.DestroyEntity(pendingEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DR-042 C6c: NEW game only (no restored ledger) -> seed a little Ore so the build loop isn't a cold
|
||||||
|
// deadlock (a turret needs Charge from a Fabricator that costs Ore you haven't mined yet). Appended
|
||||||
|
// BEFORE Playback so the ghost first-serializes WITH the seed (no empty-ledger replication flicker).
|
||||||
|
if (!restoredLedger)
|
||||||
|
ecb.AppendToBuffer(director, new StorageEntry { ItemId = ResourceId.Ore, Count = Tuning.StartingOre });
|
||||||
|
|
||||||
// Host-only autosave flag; SaveWriteSystem consumes it on the Siege->Calm checkpoint.
|
// Host-only autosave flag; SaveWriteSystem consumes it on the Siege->Calm checkpoint.
|
||||||
ecb.AddComponent(director, new SaveRequest { Pending = 0 });
|
ecb.AddComponent(director, new SaveRequest { Pending = 0 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ namespace ProjectM.Simulation
|
|||||||
/// Ore. Operator feel-fork: keep generous so turrets stay fed while you keep mining.</summary>
|
/// Ore. Operator feel-fork: keep generous so turrets stay fed while you keep mining.</summary>
|
||||||
public const int TurretChargeCostPerShot = 1;
|
public const int TurretChargeCostPerShot = 1;
|
||||||
|
|
||||||
|
// ---- Cold start (CycleDirectorSpawnSystem seeds the shared ledger on a NEW game) ----
|
||||||
|
|
||||||
|
/// <summary>DR-042 C6c: Ore deposited into the shared ledger at spawn on a NEW game ONLY (a restored save keeps
|
||||||
|
/// its persisted ledger). Bootstraps the Fabricator(30)->Charge->Turret(10) chain so a turret placed before any
|
||||||
|
/// mining isn't a silent cold deadlock. Ore-only so the 'build a Fabricator to arm turrets' lesson survives.</summary>
|
||||||
|
public const int StartingOre = 50;
|
||||||
|
|
||||||
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
|
// ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
|
||||||
|
|
||||||
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
|
/// <summary>Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.</summary>
|
||||||
|
|||||||
@@ -83,4 +83,29 @@ namespace ProjectM.Simulation
|
|||||||
/// <summary>1 once the current epoch's expedition wave has FULLY spawned and been cleared to zero live zone enemies; reset to 0 on the empty->occupied epoch bump. The reward fires only on a REAL clear.</summary>
|
/// <summary>1 once the current epoch's expedition wave has FULLY spawned and been cleared to zero live zone enemies; reset to 0 on the empty->occupied epoch bump. The reward fires only on a REAL clear.</summary>
|
||||||
public byte ClearedThisEpoch;
|
public byte ClearedThisEpoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DR-042 C7b — a SMALL replicated summary of the current expedition objective so the client HUD can show an
|
||||||
|
/// "enemies remaining / cleared — return to claim" readout. Rides the GLOBAL UNTAGGED CycleDirector ghost
|
||||||
|
/// (alongside <see cref="CycleState"/> / GoalProgress) so GhostRelevancy.SetIsIrrelevant never hides it
|
||||||
|
/// cross-region — a base teammate can't see the expedition's own (region-tagged, relevancy-hidden) enemy
|
||||||
|
/// ghosts. SOLE writer: ZoneEnemyDirectorSystem (server, plain group), written ABOVE its early-returns
|
||||||
|
/// (snapshot-above-early-return) so the readout never freezes stale. byte/short, never enum (writer is [BurstCompile]).
|
||||||
|
/// </summary>
|
||||||
|
public struct ExpeditionObjective : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>0 = Idle (no sortie active), 1 = Active (wave in progress), 2 = Cleared (return to claim).</summary>
|
||||||
|
[GhostField] public byte State;
|
||||||
|
|
||||||
|
/// <summary>Live zone enemies remaining (alive + not-yet-spawned) while Active; 0 when Idle/Cleared.</summary>
|
||||||
|
[GhostField] public short Remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>State constants for <see cref="ExpeditionObjective.State"/> (byte, not enum — Burst/serialization).</summary>
|
||||||
|
public static class ExpeditionObjectiveState
|
||||||
|
{
|
||||||
|
public const byte Idle = 0;
|
||||||
|
public const byte Active = 1;
|
||||||
|
public const byte Cleared = 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user