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>
43 lines
2.3 KiB
C#
43 lines
2.3 KiB
C#
using Unity.Mathematics;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// Pure, deterministic composition math for the expedition zone-enemy wave — no RNG state, no wall-clock — so the
|
|
/// per-epoch wave is reproducible across restarts/saves and EditMode-unit-testable without an ECS world (mirrors
|
|
/// <see cref="EnemyAIMath"/> / <c>ProductionMath</c>). The highest-leverage Slice-3 variety lever: the encounter
|
|
/// COMPOSITION shifts grunt-heavy -> charger-heavy as the expedition <c>epoch</c> climbs (grunt count stays
|
|
/// fixed; the per-epoch growth is all chargers).
|
|
/// </summary>
|
|
public static class ZoneEnemyMath
|
|
{
|
|
/// <summary>
|
|
/// Total enemies in this epoch's wave: the baked <paramref name="gruntsPerWave"/> + <paramref name="chargersPerWave"/>
|
|
/// baseline plus one extra per epoch beyond the first (a gentle ramp). Lower-bounded at 1 so an occupied
|
|
/// expedition always has a fight. <paramref name="epoch"/> is the monotonic sortie counter (>=1 in practice).
|
|
/// </summary>
|
|
public static int WaveSize(int epoch, int gruntsPerWave, int chargersPerWave)
|
|
{
|
|
int e = math.max(1, epoch);
|
|
int baseCount = math.max(0, gruntsPerWave) + math.max(0, chargersPerWave);
|
|
return math.max(1, baseCount + (e - 1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deterministic grunt/charger pick for spawn <paramref name="slot"/> of this epoch's wave. The charger
|
|
/// count is <paramref name="chargersPerWave"/> + (epoch - 1), clamped to the wave size, assigned to the LAST
|
|
/// slots; everything earlier is a Grunt. So the grunt count stays fixed at <paramref name="gruntsPerWave"/>
|
|
/// and the wave skews charger-heavy as the epoch climbs. Returns true for a Charger slot. Stable per
|
|
/// (epoch, slot) — a replayed wave is identical. Pure integer math (Burst-safe; no enum, no RNG).
|
|
/// </summary>
|
|
public static bool IsChargerSlot(int epoch, int slot, int gruntsPerWave, int chargersPerWave)
|
|
{
|
|
int e = math.max(1, epoch);
|
|
int size = WaveSize(epoch, gruntsPerWave, chargersPerWave);
|
|
int chargers = math.clamp(math.max(0, chargersPerWave) + (e - 1), 0, size);
|
|
int s = ((slot % size) + size) % size;
|
|
return s >= size - chargers;
|
|
}
|
|
}
|
|
}
|