using FluentAssertions; using SpaceGame.Api.Definitions; using SpaceGame.Api.Industry.Planning; using SpaceGame.Api.Shared.Runtime; using SpaceGame.Api.Ships.AI; using SpaceGame.Api.Ships.Runtime; using SpaceGame.Api.Universe.Contracts; using SpaceGame.Api.Universe.Runtime; using SpaceGame.Api.Universe.Simulation; using Xunit; namespace SpaceGame.Api.Tests; public sealed class ShipAiServiceExecutionTests { [Fact] public void MoveOrder_CompletesAndIsRemovedAfterArrival() { var ship = new ShipRuntime { Id = "ship-1", SystemId = "sys-1", Definition = CreateShipDefinition(), FactionId = "player", Position = Vector3.Zero, TargetPosition = Vector3.Zero, SpatialState = new ShipSpatialStateRuntime { CurrentSystemId = "sys-1", CurrentAnchorId = null, LocalPosition = Vector3.Zero, SystemPosition = Vector3.Zero, }, DefaultBehavior = new DefaultBehaviorRuntime { Kind = ShipBehaviorKinds.Idle, }, Skills = new ShipSkillProfileRuntime { Navigation = 3, Trade = 3, Mining = 3, Combat = 3, Construction = 3, }, Health = 100f, }; ship.OrderQueue.EnqueuePlayerOrder(new ShipOrderRuntime { Id = "move-1", Kind = ShipOrderKinds.Move, SourceKind = ShipOrderSourceKind.Player, SourceId = "test", Label = "Fly to point", TargetSystemId = "sys-1", TargetPosition = new Vector3(5f, 0f, 0f), }); var world = CreateWorld(ship); var service = new ShipAiService(new TestBalanceService()); var events = new List(); service.UpdateShip(world, ship, 1f, events); service.UpdateShip(world, ship, 0.01f, events); ship.Position.Should().Be(new Vector3(5f, 0f, 0f)); ship.OrderQueue.Should().BeEmpty(); ship.ActiveOrderId.Should().BeNull(); } private static SimulationWorld CreateWorld(ShipRuntime ship) { return new SimulationWorld { Label = "test", Seed = 1, Systems = [ new SystemRuntime { Definition = new SolarSystemDefinition { Id = "sys-1", Label = "System 1", Position = [0f, 0f, 0f], Stars = [], AsteroidField = new AsteroidFieldDefinition(), ResourceNodes = [], Planets = [], }, Position = Vector3.Zero, } ], Anchors = [], Nodes = [], Celestials = [], Wrecks = [], Stations = [], Ships = [ship], Factions = [], Commanders = [], Claims = [], ConstructionSites = [], MarketOrders = [], Policies = [], ShipDefinitions = new Dictionary(StringComparer.Ordinal), ItemDefinitions = new Dictionary(StringComparer.Ordinal), ModuleDefinitions = new Dictionary(StringComparer.Ordinal), ModuleRecipes = new Dictionary(StringComparer.Ordinal), Recipes = new Dictionary(StringComparer.Ordinal), ProductionGraph = new ProductionGraph { Commodities = new Dictionary(StringComparer.Ordinal), Processes = new Dictionary(StringComparer.Ordinal), ProcessesByOutputId = new Dictionary>(StringComparer.Ordinal), ProcessesByInputId = new Dictionary>(StringComparer.Ordinal), OutputsByModuleId = new Dictionary>(StringComparer.Ordinal), }, GeneratedAtUtc = DateTimeOffset.UtcNow, }; } private static ShipDefinition CreateShipDefinition() { return new ShipDefinition { Id = "ship-def", Name = "Test Ship", Size = "small", Hull = 100f, Purpose = ShipPurpose.Trade, Type = ShipType.Courier, }; } private sealed class TestBalanceService : IBalanceService { public float SimulationSpeedMultiplier => 1f; public float YPlane => 0f; public float ArrivalThreshold => 1f; public float MiningRate => 10f; public float MiningCycleSeconds => 1f; public float TransferRate => 10f; public float DockingDuration => 0.1f; public float UndockingDuration => 0.1f; public float UndockDistance => 10f; public BalanceOptions GetCurrent() => new(); public BalanceOptions Update(BalanceOptions candidate) => candidate; } }