using NUnit.Framework; using SepCore.Components; using SepCore.Simulation; using UnityEngine; namespace SepCore.Tests.Editor { public class SimulationWorldTickTests { private GameObject _worldObject; private SimulationWorld _world; [SetUp] public void SetUp() { _worldObject = new GameObject("SimulationWorldTests"); _world = _worldObject.AddComponent(); } [TearDown] public void TearDown() { if (_worldObject != null) { Object.DestroyImmediate(_worldObject); } } [Test] public void MovementComponent_OnUpdate_DoesNotMoveTransformDirectly() { GameObject moverObject = new GameObject("Mover"); try { MovementComponent movement = moverObject.AddComponent(); moverObject.transform.position = new Vector3(1f, 0f, 2f); movement.OnInit(5f, moverObject.transform, null, false, 0.45f, 2); movement.SetMove(true); movement.SetDirection(Vector3.right); movement.OnUpdate(1f, 1f); Assert.That(moverObject.transform.position.x, Is.EqualTo(1f).Within(0.0001f)); Assert.That(moverObject.transform.position.y, Is.EqualTo(0f).Within(0.0001f)); Assert.That(moverObject.transform.position.z, Is.EqualTo(2f).Within(0.0001f)); } finally { Object.DestroyImmediate(moverObject); } } [Test] public void Tick_AdvancesEnemyAndUpdatesNearestTargetQuery() { _world.UpsertEnemy(new EnemySimData { EntityId = 1001, Position = new Vector3(0f, 0f, 0f), Forward = Vector3.forward, Rotation = Quaternion.identity, Speed = 3f, AttackRange = 0.5f, AvoidEnemyOverlap = true, EnemyBodyRadius = 0.45f, SeparationIterations = 2 }); _world.Tick(new SimulationTickContext(1f, new Vector3(10f, 0f, 0f))); Assert.That(_world.EnemyCount, Is.EqualTo(1)); Assert.That(_world.TryGetEnemySnapshot(1001, out EnemySimData enemy), Is.True); Assert.That(enemy.State, Is.EqualTo(1)); Assert.That(enemy.Position.x, Is.EqualTo(3f).Within(0.001f)); Assert.That(_world.TryGetNearestEnemyEntityId(new Vector3(3.2f, 0f, 0f), 1f, out int enemyEntityId), Is.True); Assert.That(enemyEntityId, Is.EqualTo(1001)); } [Test] public void Tick_AccumulatesTimeAcrossTicksBeforeRunningFixedStep() { _world.UpsertEnemy(new EnemySimData { EntityId = 1003, Position = Vector3.zero, Forward = Vector3.forward, Rotation = Quaternion.identity, Speed = 6f, AttackRange = 1f, AvoidEnemyOverlap = false, EnemyBodyRadius = 0.45f, SeparationIterations = 1 }); _world.Tick(new SimulationTickContext(0.01f, new Vector3(10f, 0f, 0f))); Assert.That(_world.TryGetEnemySnapshot(1003, out EnemySimData enemy), Is.True); Assert.That(enemy.Position.x, Is.EqualTo(0f).Within(0.0001f)); _world.Tick(new SimulationTickContext(0.01f, new Vector3(10f, 0f, 0f))); Assert.That(_world.TryGetEnemySnapshot(1003, out enemy), Is.True); Assert.That(enemy.Position.x, Is.EqualTo(0.1f).Within(0.0001f)); } [Test] public void SyncEnemyMovementInput_DisablesEnemyMovement() { _world.UpsertEnemy(new EnemySimData { EntityId = 1002, Position = Vector3.zero, Forward = Vector3.forward, Rotation = Quaternion.identity, Speed = 4f, AttackRange = 0.5f, AvoidEnemyOverlap = true, EnemyBodyRadius = 0.45f, SeparationIterations = 2 }); _world.SyncEnemyMovementInput(1002, false, Vector3.left, 4f, true, 0.45f, 2); _world.Tick(new SimulationTickContext(1f, new Vector3(10f, 0f, 0f))); Assert.That(_world.TryGetEnemySnapshot(1002, out EnemySimData enemy), Is.True); Assert.That(enemy.State, Is.EqualTo(0)); Assert.That(enemy.Position.x, Is.EqualTo(0f).Within(0.0001f)); Assert.That(enemy.Position.y, Is.EqualTo(0f).Within(0.0001f)); Assert.That(enemy.Position.z, Is.EqualTo(0f).Within(0.0001f)); } [Test] public void Tick_ExpiresProjectileThroughSimulationLifetime() { _world.UpsertProjectile(new ProjectileSimData { EntityId = 2001, OwnerEntityId = 1001, Position = Vector3.zero, Forward = Vector3.right, Velocity = Vector3.right * 4f, LifeTime = 0.25f, Age = 0f, Active = true }); _world.Tick(new SimulationTickContext(0.5f, Vector3.zero)); Assert.That(_world.ProjectileCount, Is.EqualTo(0)); } [Test] public void Tick_WithIdenticalInput_ProducesEquivalentSnapshots() { const int tickCount = 30; GameObject firstWorldObject = new GameObject("SimulationWorldDeterminismA"); GameObject secondWorldObject = new GameObject("SimulationWorldDeterminismB"); try { SimulationWorld firstWorld = firstWorldObject.AddComponent(); SimulationWorld secondWorld = secondWorldObject.AddComponent(); SeedDeterminismScene(firstWorld); SeedDeterminismScene(secondWorld); SimulationTickContext tickContext = new SimulationTickContext(1f / 30f, new Vector3(3.5f, 0f, -2.5f)); for (int tick = 0; tick < tickCount; tick++) { firstWorld.Tick(in tickContext); secondWorld.Tick(in tickContext); } Assert.That(firstWorld.EnemyCount, Is.EqualTo(DeterminismEnemyIds.Length)); Assert.That(secondWorld.EnemyCount, Is.EqualTo(firstWorld.EnemyCount)); for (int i = 0; i < DeterminismEnemyIds.Length; i++) { Assert.That(firstWorld.TryGetEnemySnapshot(DeterminismEnemyIds[i], out EnemySimData first), Is.True); Assert.That(secondWorld.TryGetEnemySnapshot(DeterminismEnemyIds[i], out EnemySimData second), Is.True); Assert.That(second.Position.x, Is.EqualTo(first.Position.x).Within(0.01f)); Assert.That(second.Position.z, Is.EqualTo(first.Position.z).Within(0.01f)); Assert.That(second.State, Is.EqualTo(first.State)); } Assert.That(secondWorld.ProjectileCount, Is.EqualTo(firstWorld.ProjectileCount)); for (int i = 0; i < DeterminismProjectileIds.Length; i++) { Assert.That( firstWorld.TryGetProjectileSnapshot(DeterminismProjectileIds[i], out ProjectileSimData first), Is.True); Assert.That( secondWorld.TryGetProjectileSnapshot(DeterminismProjectileIds[i], out ProjectileSimData second), Is.True); Assert.That(second.Position.x, Is.EqualTo(first.Position.x).Within(0.01f)); Assert.That(second.Position.z, Is.EqualTo(first.Position.z).Within(0.01f)); Assert.That(second.Age, Is.EqualTo(first.Age).Within(0.001f)); } } finally { Object.DestroyImmediate(firstWorldObject); Object.DestroyImmediate(secondWorldObject); } } [Test] public void ClearSimulationState_ClearsPublicStateContainers() { _world.UpsertEnemy(new EnemySimData { EntityId = 1, Position = Vector3.zero, Forward = Vector3.forward }); _world.UpsertProjectile(new ProjectileSimData { EntityId = 2, Active = true }); _world.ClearSimulationState(); Assert.That(_world.EnemyCount, Is.EqualTo(0)); Assert.That(_world.ProjectileCount, Is.EqualTo(0)); Assert.That(_world.PickupCount, Is.EqualTo(0)); } [Test] public void Pickup_WhenOutsideAbsorbRange_DoesNotMove() { _world.UpsertPickup(new PickupSimData { EntityId = 7001, Position = new Vector3(10f, 0f, 10f), PickupRadius = 0.35f, State = 0 }); _world.SyncPlayerAbsorbSettings(3f, 5f, 0.5f); _world.Tick(new SimulationTickContext(0.1f, Vector3.zero)); Assert.That(_world.TryGetPickupSnapshot(7001, out PickupSimData snapshot), Is.True); Assert.That(snapshot.Position.x, Is.EqualTo(10f).Within(0.001f)); Assert.That(snapshot.Position.z, Is.EqualTo(10f).Within(0.001f)); Assert.That(snapshot.State, Is.EqualTo(0)); } [Test] public void Pickup_WhenInsideAbsorbRange_MovesTowardsPlayerAndTransitionsState() { _world.UpsertPickup(new PickupSimData { EntityId = 7002, Position = new Vector3(2f, 0f, 0f), PickupRadius = 0.2f, State = 0 }); _world.SyncPlayerAbsorbSettings(5f, 4f, 0.5f); _world.Tick(new SimulationTickContext(0.25f, Vector3.zero)); Assert.That(_world.TryGetPickupSnapshot(7002, out PickupSimData snapshot), Is.True); // Moved from 2.0 towards 0 by 4.0 * 0.25 = 1.0 -> remaining distance is 1.0 Assert.That(snapshot.Position.x, Is.EqualTo(1.0f).Within(0.01f)); Assert.That(snapshot.Position.z, Is.EqualTo(0f).Within(0.01f)); Assert.That(snapshot.State, Is.EqualTo(1)); } [Test] public void Pickup_TriggerFullMapMagnet_ActivatesDistantPickups() { _world.UpsertPickup(new PickupSimData { EntityId = 7003, Position = new Vector3(50f, 0f, 0f), PickupRadius = 0.2f, State = 0 }); _world.SyncPlayerAbsorbSettings(5f, 10f, 0.5f); _world.TriggerFullMapMagnet(); Assert.That(_world.TryGetPickupSnapshot(7003, out PickupSimData snapshotBefore), Is.True); Assert.That(snapshotBefore.State, Is.EqualTo(1)); _world.Tick(new SimulationTickContext(0.5f, Vector3.zero)); Assert.That(_world.TryGetPickupSnapshot(7003, out PickupSimData snapshotAfter), Is.True); // Moved from 50 towards 0 by 10 * 0.5 = 5 -> remaining distance is 45 Assert.That(snapshotAfter.Position.x, Is.EqualTo(45f).Within(0.01f)); } private static readonly int[] DeterminismEnemyIds = { 5001, 5002, 5003, 5004, 5005, 5006 }; private static readonly int[] DeterminismProjectileIds = { 6001, 6002, 6003 }; private static void SeedDeterminismScene(SimulationWorld world) { for (int i = 0; i < DeterminismEnemyIds.Length; i++) { float angle = i * 0.9f; world.UpsertEnemy(new EnemySimData { EntityId = DeterminismEnemyIds[i], Position = new Vector3(Mathf.Cos(angle) * 1.5f, 0f, Mathf.Sin(angle) * 1.5f), Forward = Vector3.forward, Rotation = Quaternion.identity, Speed = 2f + i * 0.25f, AttackRange = 0.5f, AvoidEnemyOverlap = true, EnemyBodyRadius = 0.4f + i * 0.05f, SeparationIterations = 2 }); } for (int i = 0; i < DeterminismProjectileIds.Length; i++) { world.UpsertProjectile(new ProjectileSimData { EntityId = DeterminismProjectileIds[i], OwnerEntityId = DeterminismEnemyIds[i], Position = new Vector3(0f, 1f, 0f), Forward = Vector3.right, Velocity = Vector3.right * (3f + i), LifeTime = 5f, Age = 0f, Active = true }); } } } }