using Unity.Entities; namespace ProjectM.Simulation { /// /// Baked, server-only tuning for the composite ThreatDirector (the data-driven base-attack scheduler). Lives /// on the global CycleDirector entity but is NOT a [GhostField] — the server alone decides when a siege /// fires; clients learn about it only through the replicated flip to Siege. /// Flat scalar fields (never an enum/array — dodges the MCP authoring-drop gotcha + stays Burst-trivial). This /// slice wires only the POST-EXPEDITION source; the Heat/Schedule fields are baked-but-inert so those sources /// drop in later additively with NO re-bake (server-only layout, not a ghost serializer change). /// public struct ThreatConfig : IComponentData { // ---- Post-expedition retaliation (the only source wired this slice) ---- /// 1 = a completed expedition (a player returning to base) can draw a retaliation siege. public byte PostExpeditionEnabled; /// Telegraph/arming delay (server ticks) between the trigger and the siege actually spawning. public uint PostExpeditionDelayTicks; /// Siege size floor (Husk count) for a post-expedition retaliation. public int SizeBase; /// Extra Husks per unit of resources hauled back this run (0 = a flat siege). public int SizePerExpeditionResource; /// How a pending siege starts (see ). public byte StartCondition; /// Max server ticks a Siege may run before it auto-collapses (remaining Husks culled) so an unattended/empty-base siege can never soft-lock. 0 = no cap. public uint SiegeTimeoutTicks; // ---- Reserved, present-but-inert this slice (additive later, no re-bake) ---- public byte HeatEnabled; public float HeatPerTickAtBase; public float HeatPerHarvest; public float HeatThreshold; public byte ScheduleEnabled; public uint ScheduleIntervalTicks; /// Extra Husks per surviving wave for a SCHEDULED base siege (size = SizeBase + this*WaveNumber). 0 = flat SizeBase. public int ScheduleSizePerWave; } /// Start-condition constants for (bytes — never an enum, never in an RPC). public static class ThreatStartCondition { /// DEFAULT: arm via the telegraph countdown () then fire — even at an empty base. public const byte Immediate = 0; /// Hold the pending siege until ≥1 player is in the base region OR the arm tick + a grace window elapses (bounded — never a soft-lock). public const byte RequirePlayerAtBase = 1; } /// /// Server-only runtime state of the ThreatDirector, on the global CycleDirector entity beside /// . NOT replicated. is the single documented entry /// point: any source (post-expedition, dev tools, later Heat/Schedule) sets it; CyclePhaseSystem /// consumes it on the Calm→Siege edge and zeroes it. All stored ticks are wrap-safe (TickUtil.NonZero + /// NetworkTick compares), never raw uint. /// public struct ThreatState : IComponentData { /// Husk count of the armed siege; 0 = none pending. Consumed (zeroed) by CyclePhaseSystem at Siege entry. public int PendingSiegeSize; /// Server tick the pending siege fires (telegraph). 0 = fire as soon as seen. Routed through TickUtil.NonZero. public uint ArmTick; /// Server tick the current Siege began (0 = not under siege). The bounded-resolution timeout measures from here (TickUtil.NonZero) so an unattended/empty-base siege can never soft-lock. public uint SiegeStartTick; /// Count of expeditions completed (a player returned to base). Drives the post-expedition source + stats. public int ExpeditionsCompleted; /// Return events the gate has signalled but the director has not yet consumed (the gate teleports the player out of its radius, so one increment per return — natural de-dup). public int PendingReturns; /// Accumulated heat (inert this slice; the Heat source reads/writes it later). public float Heat; /// Next scheduled-siege tick (inert this slice; the Schedule source uses it later). TickUtil.NonZero when used. public uint NextScheduledTick; } }