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,109 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Server;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Core;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Plain-Entities EditMode tests for the once-per-epoch zone-clear reward folded into
|
||||
/// <see cref="ExpeditionGateSystem"/> (DR-040 BLOCKER 4). A returning player banks flat Ore to the shared ledger
|
||||
/// IFF this epoch's expedition wave was actually cleared and not yet rewarded — and never twice for the same
|
||||
/// epoch (the co-op same-tick / gate-re-entry de-dup).
|
||||
/// </summary>
|
||||
public class ExpeditionGateRewardTests
|
||||
{
|
||||
static (World world, SimulationSystemGroup group, Entity cycle) MakeWorld(string name,
|
||||
int epoch, byte clearedThisEpoch, int lastRewardedEpoch)
|
||||
{
|
||||
var world = new World(name);
|
||||
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
||||
group.AddSystemToUpdateList(world.GetOrCreateSystem<ExpeditionGateSystem>());
|
||||
group.SortSystems();
|
||||
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
|
||||
var em = world.EntityManager;
|
||||
|
||||
// CycleDirector-like entity: cycle state/runtime + the shared resource ledger + threat state.
|
||||
var cyc = em.CreateEntity(typeof(CycleState), typeof(CycleRuntime), typeof(ResourceLedger), typeof(ThreatState));
|
||||
em.SetComponentData(cyc, new CycleState { Phase = CyclePhase.Calm });
|
||||
em.SetComponentData(cyc, new CycleRuntime
|
||||
{
|
||||
ExpeditionEpoch = epoch, ClearedThisEpoch = clearedThisEpoch, LastRewardedEpoch = lastRewardedEpoch,
|
||||
});
|
||||
em.AddBuffer<StorageEntry>(cyc);
|
||||
|
||||
// Zone-enemy director singleton (only RewardOre matters to the reward fold).
|
||||
var dir = em.CreateEntity(typeof(ZoneEnemyDirector));
|
||||
em.SetComponentData(dir, new ZoneEnemyDirector { RewardOre = 25 });
|
||||
|
||||
// A gate Expedition->Base sitting at the expedition origin.
|
||||
var gate = em.CreateEntity(typeof(ExpeditionGate), typeof(LocalTransform));
|
||||
em.SetComponentData(gate, new ExpeditionGate
|
||||
{
|
||||
FromRegion = RegionId.Expedition, ToRegion = RegionId.Base, Radius = 3f, ArrivalPos = new float3(0, 1, 0),
|
||||
});
|
||||
em.SetComponentData(gate, LocalTransform.FromPosition(new float3(1000, 1, 0)));
|
||||
|
||||
return (world, group, cyc);
|
||||
}
|
||||
|
||||
static Entity MakeExpeditionPlayerAtGate(EntityManager em)
|
||||
{
|
||||
var e = em.CreateEntity();
|
||||
em.AddComponentData(e, new RegionTag { Region = RegionId.Expedition });
|
||||
em.AddComponentData(e, LocalTransform.FromPosition(new float3(1000, 1, 0)));
|
||||
em.AddComponent<PlayerTag>(e);
|
||||
return e;
|
||||
}
|
||||
|
||||
static int OreInLedger(EntityManager em, Entity cyc)
|
||||
{
|
||||
var buf = em.GetBuffer<StorageEntry>(cyc);
|
||||
for (int i = 0; i < buf.Length; i++)
|
||||
if (buf[i].ItemId == (ushort)ResourceId.Ore) return buf[i].Count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Cleared_Return_Banks_Ore_Once()
|
||||
{
|
||||
var (world, group, cyc) = MakeWorld("GateRewardOnce", epoch: 1, clearedThisEpoch: 1, lastRewardedEpoch: 0);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
var player = MakeExpeditionPlayerAtGate(em);
|
||||
|
||||
group.Update(); // player walks the gate back to base -> reward
|
||||
|
||||
Assert.AreEqual(25, OreInLedger(em, cyc), "a cleared return banks RewardOre to the shared ledger");
|
||||
Assert.AreEqual(1, em.GetComponentData<CycleRuntime>(cyc).LastRewardedEpoch, "the epoch is marked rewarded");
|
||||
|
||||
// Force a second same-epoch return (the player is back in the expedition at the gate).
|
||||
em.SetComponentData(player, new RegionTag { Region = RegionId.Expedition });
|
||||
em.SetComponentData(player, LocalTransform.FromPosition(new float3(1000, 1, 0)));
|
||||
|
||||
group.Update(); // returns again, but the epoch was already rewarded
|
||||
|
||||
Assert.AreEqual(25, OreInLedger(em, cyc), "the same epoch never pays twice (co-op / re-entry de-dup)");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Uncleared_Return_Banks_Nothing()
|
||||
{
|
||||
var (world, group, cyc) = MakeWorld("GateRewardUncleared", epoch: 1, clearedThisEpoch: 0, lastRewardedEpoch: 0);
|
||||
using (world)
|
||||
{
|
||||
var em = world.EntityManager;
|
||||
MakeExpeditionPlayerAtGate(em);
|
||||
|
||||
group.Update();
|
||||
|
||||
Assert.AreEqual(0, OreInLedger(em, cyc), "returning without clearing the wave banks nothing (no farming)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user