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:
@@ -20,6 +20,10 @@ namespace ProjectM.Server
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
/// <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
|
||||
@@ -51,26 +55,55 @@ namespace ProjectM.Server
|
||||
return;
|
||||
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;
|
||||
foreach (var region in SystemAPI.Query<RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
if (region.ValueRO.Region == RegionId.Expedition)
|
||||
expeditionPlayers++;
|
||||
if (expeditionPlayers == 0)
|
||||
return; // nobody out there: the field manager owns teardown, we do nothing
|
||||
|
||||
var directorEntity = SystemAPI.GetSingletonEntity<ZoneEnemyDirector>();
|
||||
var dir = SystemAPI.GetComponent<ZoneEnemyDirector>(directorEntity);
|
||||
var zs = SystemAPI.GetComponent<ZoneEnemyState>(directorEntity);
|
||||
var prefabs = SystemAPI.GetBuffer<ZoneEnemyPrefab>(directorEntity);
|
||||
if (prefabs.Length == 0)
|
||||
return;
|
||||
|
||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
||||
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
|
||||
// siege). GruntsPerWave/ChargersPerWave are the Grunt/Charger base counts.
|
||||
var bands = new MixBands
|
||||
@@ -94,8 +127,6 @@ namespace ProjectM.Server
|
||||
zs.NextSpawnTick = TickUtil.NonZero(now); // first slot this tick
|
||||
}
|
||||
|
||||
int aliveZone = m_ZoneEnemies.CalculateEntityCount();
|
||||
|
||||
if (zs.RemainingToSpawn > 0)
|
||||
{
|
||||
// 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 });
|
||||
|
||||
// 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).
|
||||
if (SystemAPI.TryGetSingletonEntity<PendingSave>(out var pendingEntity))
|
||||
{
|
||||
@@ -79,6 +81,7 @@ namespace ProjectM.Server
|
||||
var srcLedger = SystemAPI.GetBuffer<PendingSaveLedgerRow>(pendingEntity);
|
||||
var destLedger = ecb.SetBuffer<StorageEntry>(director);
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
ecb.AddComponent(director, new SaveRequest { Pending = 0 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user