using NUnit.Framework;
using ProjectM.Server;
using ProjectM.Simulation;
using Unity.Core;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Tests
{
///
/// Plain-Entities EditMode tests for (Geyser_Build_Spec; design-review
/// wf_900e9965-8f0). Pins the review-hardened contract: an ELAPSED geyser radius-damages LIVING players AND
/// enemies (friendly fire) with SourceNetworkId = -1, RESCHEDULES NextEruptTick = now + period (never +=, never
/// the 0 sentinel), and is NEVER destroyed (opposite of the barrel). The load-bearing HIGH regression: a geyser
/// that ships an UNSTAMPED NextEruptTick (0) must NEVER erupt — the inverted guard lazy-stamps it born-correct
/// instead of the party-wiping per-tick barrage that copying the barrel's invalid-guard verbatim would cause.
///
public class GeyserEruptSystemTests
{
const uint T0 = 1000;
static (World world, SimulationSystemGroup group) MakeWorld(string name, uint serverTick = T0)
{
var world = new World(name);
var group = world.GetOrCreateSystemManaged();
group.AddSystemToUpdateList(world.GetOrCreateSystem());
group.SortSystems();
world.SetTime(new TimeData(elapsedTime: 0f, deltaTime: 1f / 60f));
var nt = world.EntityManager.CreateEntity(typeof(NetworkTime));
world.EntityManager.SetComponentData(nt, new NetworkTime { ServerTick = new NetworkTick(serverTick) });
return (world, group);
}
static Entity MakeGeyser(EntityManager em, float3 pos, uint nextEruptTick)
{
var e = em.CreateEntity();
em.AddComponentData(e, LocalTransform.FromPosition(pos));
em.AddComponentData(e, new Geyser { NextEruptTick = nextEruptTick });
return e;
}
static Entity MakePlayer(EntityManager em, float3 pos, float health, byte region)
{
var e = em.CreateEntity();
em.AddComponentData(e, LocalTransform.FromPosition(pos));
em.AddComponentData(e, new PlayerTag());
em.AddComponentData(e, new Health { Current = health, Max = 130f });
em.AddComponentData(e, new RegionTag { Region = region });
em.AddBuffer(e);
return e;
}
static Entity MakeEnemy(EntityManager em, float3 pos, float health)
{
var e = em.CreateEntity();
em.AddComponentData(e, LocalTransform.FromPosition(pos));
em.AddComponentData(e, new EnemyTag());
em.AddComponentData(e, new Health { Current = health, Max = 30f });
em.AddBuffer(e);
return e;
}
[Test]
public void Elapsed_Geyser_Damages_Living_Both_Sides_Reschedules_And_Survives()
{
var (world, group) = MakeWorld("EruptBoth");
using (world)
{
var em = world.EntityManager;
var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0); // due exactly now
var player = MakePlayer(em, new float3(11, 0, 10), health: 100f, region: RegionId.Expedition);
var enemy = MakeEnemy(em, new float3(10, 0, 12), health: 30f);
group.Update();
Assert.IsTrue(em.Exists(geyser), "A geyser is PERMANENT — it is never destroyed on erupt.");
var pd = em.GetBuffer(player);
Assert.AreEqual(1, pd.Length, "Friendly fire: the living expedition player in radius is hit.");
Assert.AreEqual(Tuning.GeyserEruptDamage, pd[0].Amount);
Assert.AreEqual(-1, pd[0].SourceNetworkId, "Environment convention: never a player id.");
Assert.AreNotEqual(0u, pd[0].SourceTick, "SourceTick stamped at NOW (the authored-tick contract).");
Assert.AreEqual(1, em.GetBuffer(enemy).Length, "The living enemy in radius is hit too (bait mechanic).");
Assert.AreEqual(TickUtil.NonZero(T0 + Tuning.GeyserPeriodTicks), em.GetComponentData(geyser).NextEruptTick,
"Reschedule = now + period (never += ; never the 0 sentinel).");
}
}
[Test]
public void Unstamped_Geyser_Never_Erupts_And_Is_Lazy_Stamped()
{
// The HIGH regression: a baked-0 NextEruptTick at a high serverTick must NOT storm (the inverted guard).
var (world, group) = MakeWorld("UnstampedNoStorm", serverTick: 5000);
using (world)
{
var em = world.EntityManager;
var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: 0u); // unstamped
var player = MakePlayer(em, new float3(10, 0, 10), health: 130f, region: RegionId.Expedition);
group.Update();
Assert.AreEqual(0, em.GetBuffer(player).Length,
"An unstamped (0) geyser must NEVER erupt — no per-tick barrage.");
Assert.AreEqual(TickUtil.NonZero(5000u + Tuning.GeyserPeriodTicks), em.GetComponentData(geyser).NextEruptTick,
"0 is lazy-stamped born-correct (now + period) instead of erupting.");
Assert.IsTrue(em.Exists(geyser));
}
}
[Test]
public void Dead_Player_And_Out_Of_Radius_Get_Nothing()
{
var (world, group) = MakeWorld("EruptFilters");
using (world)
{
var em = world.EntityManager;
MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0);
var deadPlayer = MakePlayer(em, new float3(10.5f, 0, 10), health: 0f, region: RegionId.Expedition);
var basePlayer = MakePlayer(em, new float3(10.5f, 0, 10), health: 100f, region: RegionId.Base);
var farEnemy = MakeEnemy(em, new float3(10 + Tuning.GeyserEruptRadius + 1f, 0, 10), health: 30f);
group.Update();
Assert.AreEqual(0, em.GetBuffer(deadPlayer).Length, "A dead player in radius gets NOTHING.");
Assert.AreEqual(0, em.GetBuffer(basePlayer).Length, "A non-expedition player gets NOTHING.");
Assert.AreEqual(0, em.GetBuffer(farEnemy).Length, "Out of radius gets nothing.");
}
}
[Test]
public void Unelapsed_Geyser_Does_Nothing_And_Keeps_Counting_Down()
{
var (world, group) = MakeWorld("EruptWait");
using (world)
{
var em = world.EntityManager;
var geyser = MakeGeyser(em, new float3(10, 0, 10), nextEruptTick: T0 + 120); // still counting down
var player = MakePlayer(em, new float3(10, 0, 10), health: 100f, region: RegionId.Expedition);
group.Update();
Assert.AreEqual(0, em.GetBuffer(player).Length, "No damage before the eruption tick.");
Assert.AreEqual(T0 + 120, em.GetComponentData(geyser).NextEruptTick, "An un-elapsed geyser is not rescheduled.");
}
}
}
}