Slice 3: Expedition Combat Spine — epoch-seeded zone waves (DR-040)
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>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the expedition zone-enemy director (singleton). Place ONE on a GameObject in the gameplay
|
||||
/// subscene; the server-only <c>ZoneEnemyDirectorSystem</c> reads it. Bakes a <see cref="ZoneEnemyDirector"/>
|
||||
/// config + a <see cref="ZoneEnemyPrefab"/> pool (index 0 = Grunt variant, index 1 = Charger variant — the
|
||||
/// epoch-seeded composition picks between them) + the initial <see cref="ZoneEnemyState"/>. The entity carries no
|
||||
/// transform; only the referenced prefabs need a runtime transform. The prefabs ALIAS the existing Husk ghost
|
||||
/// prefabs, so zone enemies reuse the whole combat/readability stack with no new ghost type.
|
||||
/// </summary>
|
||||
public class ZoneEnemyDirectorAuthoring : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Zone-enemy variant prefabs. Index 0 = Grunt, index 1 = Charger. Each must carry EnemyAuthoring + an interpolated GhostAuthoringComponent.")]
|
||||
public GameObject[] EnemyPrefabs;
|
||||
|
||||
[Min(1), Tooltip("Max concurrent live zone enemies (the v1 ghost-relevancy budget).")] public int MaxAlive = 12;
|
||||
[Min(0f)] public float RingRadius = 14f;
|
||||
[Min(1)] public int RingSlots = 10;
|
||||
[Min(1), Tooltip("Ticks between individual spawns within a wave (~60/sec).")] public int SpawnIntervalTicks = 30;
|
||||
[Min(0), Tooltip("Grunts in the epoch-1 wave (held roughly constant as the epoch climbs).")] public int GruntsPerWave = 4;
|
||||
[Min(0), Tooltip("Chargers in the epoch-1 wave (grows ~1 per epoch -> charger-heavy).")] public int ChargersPerWave = 1;
|
||||
[Min(0), Tooltip("Flat Ore banked to the shared ledger on a real clear, once per sortie.")] public int RewardOre = 25;
|
||||
|
||||
private class ZoneEnemyDirectorBaker : Baker<ZoneEnemyDirectorAuthoring>
|
||||
{
|
||||
public override void Bake(ZoneEnemyDirectorAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
AddComponent(entity, new ZoneEnemyDirector
|
||||
{
|
||||
MaxAlive = authoring.MaxAlive,
|
||||
RingRadius = authoring.RingRadius,
|
||||
RingSlots = authoring.RingSlots,
|
||||
SpawnIntervalTicks = authoring.SpawnIntervalTicks,
|
||||
GruntsPerWave = authoring.GruntsPerWave,
|
||||
ChargersPerWave = authoring.ChargersPerWave,
|
||||
RewardOre = authoring.RewardOre,
|
||||
});
|
||||
|
||||
var buffer = AddBuffer<ZoneEnemyPrefab>(entity);
|
||||
if (authoring.EnemyPrefabs != null)
|
||||
{
|
||||
foreach (var prefab in authoring.EnemyPrefabs)
|
||||
{
|
||||
if (prefab != null)
|
||||
buffer.Add(new ZoneEnemyPrefab { Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic) });
|
||||
}
|
||||
}
|
||||
|
||||
AddComponent(entity, new ZoneEnemyState
|
||||
{
|
||||
SpawnCounter = 0,
|
||||
RemainingToSpawn = 0,
|
||||
NextSpawnTick = 0,
|
||||
SeededEpoch = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5918787bb2ebb7747afe473a7937ed53
|
||||
Reference in New Issue
Block a user