3109b86d71
Reactivate the dormant Expedition region as a procedural combat venue.
v1 loop: walk the gate -> fight an epoch-seeded enemy wave in the
expedition -> clear -> return -> flat Ore reward (once per epoch) ->
escalated retaliation base siege.
- New sim types: ZoneEnemyTag, ZoneEnemyDirector (+ ZoneEnemyPrefab
buffer), ZoneEnemyState, ZoneEnemyMath (grunt->charger composition
by epoch). ZoneEnemyDirectorSystem (server, Burst): drip-spawns the
wave at a deterministic ring under a MaxAlive cap while a player is
out and the base is Calm; marks ClearedThisEpoch on a real clear.
[UpdateAfter(ExpeditionFieldSystem)] only (avoids a sort cycle).
- BLOCKER 1: EnemyAISystem region-filters target selection (player +
structure snapshots gain parallel region lists; no base structures /
no Core fallback for expedition husks).
- BLOCKER 3: WaveSystem, ThreatDirectorSystem timeout cull, and
CyclePhaseSystem DefendCleared + Core-breach cull all count/cull
RegionTag{Base} husks only (the breach cull was caught region-blind
by the post-impl review: a base breach wiped the live expedition
wave and spuriously paid the reward).
- BLOCKER 4: reward de-duped via CycleRuntime.LastRewardedEpoch +
ClearedThisEpoch; ExpeditionGateSystem deposits RewardOre once/epoch.
- ExpeditionFieldSystem teardown also culls zone enemies + region-
guards the clutter loop. Subscene wired with the director + roster.
368/368 EditMode green + clean netcode Play smoke. Docs: DR-040 ->
built, session log, CLAUDE.md cross-region tag-reaudit rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
5.0 KiB
C#
87 lines
5.0 KiB
C#
using Unity.Entities;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// Macro-loop state for "The Aether Cycle": which phase the run is in, the cycle number, and the server
|
|
/// tick the current (timed) phase ends. Server-authoritative, maintained by CyclePhaseSystem. Currently a
|
|
/// server-side singleton; the [GhostField]s below are inert until it is moved onto the runtime-spawned
|
|
/// CycleDirector ghost (when the client HUD is wired), at which point the same struct replicates unchanged.
|
|
/// The Defend phase is NOT timed — it ends when the base-defense wave is cleared — so PhaseEndTick is only
|
|
/// meaningful in Expedition/Build (0 during Defend).
|
|
/// </summary>
|
|
public struct CycleState : IComponentData
|
|
{
|
|
/// <summary>Current phase (see <see cref="CyclePhase"/>).</summary>
|
|
[GhostField] public byte Phase;
|
|
|
|
/// <summary>1-based cycle counter (increments when a new Expedition begins).</summary>
|
|
[GhostField] public int CycleNumber;
|
|
|
|
/// <summary>Server tick the current timed phase ends (Expedition/Build only; 0 in Defend).</summary>
|
|
[GhostField] public uint PhaseEndTick;
|
|
|
|
/// <summary>Live Husk wave number during Defend, synced from the server-only WaveState by CyclePhaseSystem so the replicated-state-only HUD can show it (holds the last wave number outside Defend; the HUD gates the display to the Defend phase).</summary>
|
|
[GhostField] public int WaveNumber;
|
|
}
|
|
|
|
/// <summary>Phase constants for <see cref="CycleState.Phase"/> — the GLOBAL shared posture (a byte, not an enum, for trivial Burst/serialization). Being out on an expedition is per-player presence (server-only RegionTag), NOT a global phase.</summary>
|
|
public static class CyclePhase
|
|
{
|
|
// Re-meaned IN PLACE — the byte VALUES are unchanged from the old Expedition/Defend/Build, so the
|
|
// [GhostField] serializer layout is identical (the const re-mean alone forces no re-bake). slot 0 (was
|
|
// Expedition) -> Calm; slot 1 (was Defend) -> Siege; slot 2 (was Build) -> retired.
|
|
|
|
/// <summary>The persistent, unhurried home base — the DEFAULT posture. No countdown; build/prep at your pace.</summary>
|
|
public const byte Calm = 0;
|
|
|
|
/// <summary>The base is under assault by a Husk wave (event-triggered; ends when the wave is cleared).</summary>
|
|
public const byte Siege = 1;
|
|
|
|
// ---- Deprecated aliases (kept so HUD/audio/tests keep compiling through the cut-over; cleaned up later). ----
|
|
|
|
/// <summary>DEPRECATED alias of <see cref="Calm"/>.</summary>
|
|
public const byte Expedition = 0;
|
|
|
|
/// <summary>DEPRECATED alias of <see cref="Siege"/>.</summary>
|
|
public const byte Defend = 1;
|
|
|
|
/// <summary>DEPRECATED, unreachable — the timed Build phase is retired.</summary>
|
|
public const byte Build = 2;
|
|
|
|
/// <summary>DEPRECATED — the forced Expedition timer is retired (the loop is player-driven). Kept so existing crefs resolve.</summary>
|
|
public const uint ExpeditionTicks = 3600;
|
|
|
|
/// <summary>DEPRECATED — the forced Build timer is retired. Kept so existing crefs resolve.</summary>
|
|
public const uint BuildTicks = 1200;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-only bookkeeping for the run-state machine that must NOT replicate (kept separate from the
|
|
/// replicated <see cref="CycleState"/>). Records the wave number captured when the current Siege began plus
|
|
/// the procedural-expedition-field session epoch (bumped when the expedition region goes empty->occupied so
|
|
/// the field reseeds per sortie).
|
|
/// </summary>
|
|
public struct CycleRuntime : IComponentData
|
|
{
|
|
/// <summary>WaveState.WaveNumber captured the moment the current Siege started (DefendCleared tests > this).</summary>
|
|
public int DefendStartWave;
|
|
|
|
/// <summary>Monotonic expedition-field session counter; bumped on the expedition region's empty->occupied edge so each sortie reseeds. RNG seed (never tick math; never 0, via max(1, ...)).</summary>
|
|
public int ExpeditionEpoch;
|
|
|
|
/// <summary>The <see cref="ExpeditionEpoch"/> the field was last seeded for (compared by int equality).</summary>
|
|
public int LastSpawnedEpoch;
|
|
|
|
/// <summary>Previous-tick expedition occupancy (1 = at least one player out), for the empty<->occupied edge.</summary>
|
|
public byte PrevExpeditionOccupied;
|
|
|
|
/// <summary>The <see cref="ExpeditionEpoch"/> a zone-clear Ore reward was last banked for — gates the once-per-epoch reward so two same-tick co-op returners pay once and gate re-entry can't farm (int equality, never tick math).</summary>
|
|
public int LastRewardedEpoch;
|
|
|
|
/// <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;
|
|
}
|
|
}
|