From 3234b628ea4c6e1ebc7e23bc838e53103265e858 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Tue, 17 Mar 2026 03:32:37 -0400 Subject: [PATCH] feat: rework modules, items and fuel --- .../WorldContracts.Infrastructure.cs | 15 +- .../backend/Contracts/WorldContracts.Ships.cs | 2 - apps/backend/Data/WorldDefinitions.cs | 282 +-- .../Simulation/AI/ShipBehaviorStateMachine.cs | 2 - .../Simulation/AI/ShipBehaviorStates.cs | 231 +-- .../Simulation/Model/ShipRuntimeModels.cs | 93 +- .../Simulation/Model/SimulationKinds.cs | 352 ++-- .../Simulation/Model/SimulationWorld.cs | 1 + .../Simulation/Model/StationRuntimeModels.cs | 71 +- .../Simulation/ScenarioLoader.Generation.cs | 44 - .../Simulation/ScenarioLoader.Seeding.cs | 25 +- apps/backend/Simulation/ScenarioLoader.cs | 154 +- .../SimulationEngine.MovementSystem.cs | 504 +++--- .../SimulationEngine.OrbitalSystem.cs | 2 +- ...mulationEngine.PowerAndInventorySystems.cs | 664 ++----- .../SimulationEngine.Replication.cs | 1556 +++++++++-------- ...Engine.ResourceAndInfrastructureSystems.cs | 506 +++--- .../SimulationEngine.ShipActionSystem.cs | 1068 +++++------ .../SimulationEngine.ShipControl.cs | 1123 ++++++------ .../SimulationEngine.StationSystems.cs | 901 +++++----- apps/backend/Simulation/SimulationEngine.cs | 156 +- apps/viewer/src/contractsInfrastructure.ts | 17 +- apps/viewer/src/contractsShips.ts | 3 +- apps/viewer/src/viewerFactionStrip.ts | 3 +- apps/viewer/src/viewerPanels.ts | 107 +- apps/viewer/src/viewerSelection.ts | 2 - docs/COMMANDERS.md | 2 +- docs/DATA-MODEL.md | 13 - docs/DESIGN.md | 1 - docs/ECONOMY.md | 10 +- docs/EVENTS.md | 1 - docs/ITEMS.md | 43 +- docs/MODULES.md | 6 - docs/PRODUCTION.md | 13 - docs/STATIONS.md | 23 +- docs/TASKS.md | 5 +- docs/WORKFORCE.md | 5 +- shared/data/balance.json | 9 +- shared/data/constructibles.json | 87 - shared/data/items.json | 772 ++++++-- shared/data/module-recipes.json | 68 - shared/data/modules.json | 247 +++ shared/data/recipes.json | 1304 -------------- shared/data/scenario.json | 20 +- shared/data/ships.json | 421 ++++- 45 files changed, 4882 insertions(+), 6052 deletions(-) delete mode 100644 shared/data/constructibles.json delete mode 100644 shared/data/module-recipes.json create mode 100644 shared/data/modules.json delete mode 100644 shared/data/recipes.json diff --git a/apps/backend/Contracts/WorldContracts.Infrastructure.cs b/apps/backend/Contracts/WorldContracts.Infrastructure.cs index 18d0597..08f87bc 100644 --- a/apps/backend/Contracts/WorldContracts.Infrastructure.cs +++ b/apps/backend/Contracts/WorldContracts.Infrastructure.cs @@ -17,10 +17,6 @@ public sealed record StationSnapshot( int DockedShips, IReadOnlyList DockedShipIds, int DockingPads, - float FuelStored, - float FuelCapacity, - float EnergyStored, - float EnergyCapacity, IReadOnlyList CurrentProcesses, IReadOnlyList Inventory, string FactionId, @@ -30,6 +26,7 @@ public sealed record StationSnapshot( float PopulationCapacity, float WorkforceRequired, float WorkforceEffectiveRatio, + IReadOnlyList StorageUsage, IReadOnlyList InstalledModules, IReadOnlyList MarketOrderIds); @@ -46,10 +43,6 @@ public sealed record StationDelta( int DockedShips, IReadOnlyList DockedShipIds, int DockingPads, - float FuelStored, - float FuelCapacity, - float EnergyStored, - float EnergyCapacity, IReadOnlyList CurrentProcesses, IReadOnlyList Inventory, string FactionId, @@ -59,6 +52,7 @@ public sealed record StationDelta( float PopulationCapacity, float WorkforceRequired, float WorkforceEffectiveRatio, + IReadOnlyList StorageUsage, IReadOnlyList InstalledModules, IReadOnlyList MarketOrderIds); @@ -67,6 +61,11 @@ public sealed record StationActionProgressSnapshot( string Label, float Progress); +public sealed record StationStorageUsageSnapshot( + string StorageClass, + float Used, + float Capacity); + public sealed record ClaimSnapshot( string Id, string FactionId, diff --git a/apps/backend/Contracts/WorldContracts.Ships.cs b/apps/backend/Contracts/WorldContracts.Ships.cs index 81cd359..e213de9 100644 --- a/apps/backend/Contracts/WorldContracts.Ships.cs +++ b/apps/backend/Contracts/WorldContracts.Ships.cs @@ -21,7 +21,6 @@ public sealed record ShipSnapshot( float CargoCapacity, string? CargoItemId, float WorkerPopulation, - float EnergyStored, float TravelSpeed, string TravelSpeedUnit, IReadOnlyList Inventory, @@ -52,7 +51,6 @@ public sealed record ShipDelta( float CargoCapacity, string? CargoItemId, float WorkerPopulation, - float EnergyStored, float TravelSpeed, string TravelSpeedUnit, IReadOnlyList Inventory, diff --git a/apps/backend/Data/WorldDefinitions.cs b/apps/backend/Data/WorldDefinitions.cs index c35c8a4..33cb9fb 100644 --- a/apps/backend/Data/WorldDefinitions.cs +++ b/apps/backend/Data/WorldDefinitions.cs @@ -1,190 +1,206 @@ namespace SpaceGame.Simulation.Api.Data; +public sealed class ConstructionDefinition +{ + public string? RecipeId { get; set; } + public string FacilityCategory { get; set; } = "station"; + public List RequiredModules { get; set; } = []; + public List Requirements { get; set; } = []; + public float CycleTime { get; set; } + public float BatchSize { get; set; } = 1f; + public float ProductsPerHour { get; set; } + public float MaxEfficiency { get; set; } = 1f; + public int Priority { get; set; } +} + public sealed class BalanceDefinition { - public float YPlane { get; set; } - public float ArrivalThreshold { get; set; } - public float MiningRate { get; set; } - public float MiningCycleSeconds { get; set; } - public float TransferRate { get; set; } - public float DockingDuration { get; set; } - public float UndockingDuration { get; set; } - public float UndockDistance { get; set; } - public EnergyBalanceDefinition Energy { get; set; } = new(); -} - -public sealed class EnergyBalanceDefinition -{ - public float IdleDrain { get; set; } - public float MoveDrain { get; set; } - public float WarpDrain { get; set; } - public float ShipRechargeRate { get; set; } - public float StationSolarCharge { get; set; } + public float YPlane { get; set; } + public float ArrivalThreshold { get; set; } + public float MiningRate { get; set; } + public float MiningCycleSeconds { get; set; } + public float TransferRate { get; set; } + public float DockingDuration { get; set; } + public float UndockingDuration { get; set; } + public float UndockDistance { get; set; } } public sealed class SolarSystemDefinition { - public required string Id { get; set; } - public required string Label { get; set; } - public required float[] Position { get; set; } - public string StarKind { get; set; } = "main-sequence"; - public int StarCount { get; set; } = 1; - public required string StarColor { get; set; } - public required string StarGlow { get; set; } - public float StarSize { get; set; } - public float GravityWellRadius { get; set; } - public required AsteroidFieldDefinition AsteroidField { get; set; } - public required List ResourceNodes { get; set; } - public required List Planets { get; set; } + public required string Id { get; set; } + public required string Label { get; set; } + public required float[] Position { get; set; } + public string StarKind { get; set; } = "main-sequence"; + public int StarCount { get; set; } = 1; + public required string StarColor { get; set; } + public required string StarGlow { get; set; } + public float StarSize { get; set; } + public float GravityWellRadius { get; set; } + public required AsteroidFieldDefinition AsteroidField { get; set; } + public required List ResourceNodes { get; set; } + public required List Planets { get; set; } } public sealed class AsteroidFieldDefinition { - public int DecorationCount { get; set; } - public float RadiusOffset { get; set; } - public float RadiusVariance { get; set; } - public float HeightVariance { get; set; } + public int DecorationCount { get; set; } + public float RadiusOffset { get; set; } + public float RadiusVariance { get; set; } + public float HeightVariance { get; set; } } public sealed class ResourceNodeDefinition { - public string SourceKind { get; set; } = "asteroid-belt"; - public float Angle { get; set; } - public float RadiusOffset { get; set; } - public float InclinationDegrees { get; set; } - public int? AnchorPlanetIndex { get; set; } - public int? AnchorMoonIndex { get; set; } - public float OreAmount { get; set; } - public required string ItemId { get; set; } - public int ShardCount { get; set; } + public string SourceKind { get; set; } = "asteroid-belt"; + public float Angle { get; set; } + public float RadiusOffset { get; set; } + public float InclinationDegrees { get; set; } + public int? AnchorPlanetIndex { get; set; } + public int? AnchorMoonIndex { get; set; } + public float OreAmount { get; set; } + public required string ItemId { get; set; } + public int ShardCount { get; set; } } public sealed class ItemDefinition { - public required string Id { get; set; } - public required string Label { get; set; } - public required string Storage { get; set; } - public string Summary { get; set; } = string.Empty; -} - -public sealed class RecipeInputDefinition -{ - public required string ItemId { get; set; } - public float Amount { get; set; } -} - -public sealed class ModuleRecipeDefinition -{ - public required string ModuleId { get; set; } - public float Duration { get; set; } - public required List Inputs { get; set; } + public required string Id { get; set; } + public required string Name { get; set; } + public string Description { get; set; } = string.Empty; + public string Type { get; set; } = "material"; + public required string CargoKind { get; set; } + public float Volume { get; set; } = 1f; + public ConstructionDefinition? Construction { get; set; } } public sealed class RecipeOutputDefinition { - public required string ItemId { get; set; } - public float Amount { get; set; } + public required string ItemId { get; set; } + public float Amount { get; set; } +} + +public sealed class RecipeInputDefinition +{ + public required string ItemId { get; set; } + public float Amount { get; set; } +} + +public sealed class ModuleConstructionDefinition +{ + public required List Requirements { get; set; } + public float ProductionTime { get; set; } +} + +public sealed class ModuleDefinition +{ + public required string Id { get; set; } + public required string Name { get; set; } + public string Description { get; set; } = string.Empty; + public required string Type { get; set; } + public string? Product { get; set; } + public float Radius { get; set; } = 12f; + public float Hull { get; set; } = 100f; + public float WorkforceNeeded { get; set; } + public ModuleConstructionDefinition? Construction { get; set; } +} + +public sealed class ModuleRecipeDefinition +{ + public required string ModuleId { get; set; } + public float Duration { get; set; } + public required List Inputs { get; set; } } public sealed class RecipeDefinition { - public required string Id { get; set; } - public required string Label { get; set; } - public required string FacilityCategory { get; set; } - public float Duration { get; set; } - public int Priority { get; set; } - public List RequiredModules { get; set; } = []; - public List Inputs { get; set; } = []; - public List Outputs { get; set; } = []; - public string? ShipOutputId { get; set; } + public required string Id { get; set; } + public required string Label { get; set; } + public required string FacilityCategory { get; set; } + public float Duration { get; set; } + public int Priority { get; set; } + public List RequiredModules { get; set; } = []; + public List Inputs { get; set; } = []; + public List Outputs { get; set; } = []; + public string? ShipOutputId { get; set; } } public sealed class PlanetDefinition { - public required string Label { get; set; } - public string PlanetType { get; set; } = "terrestrial"; - public string Shape { get; set; } = "sphere"; - public int MoonCount { get; set; } - public float OrbitRadius { get; set; } - public float OrbitSpeed { get; set; } - public float OrbitEccentricity { get; set; } - public float OrbitInclination { get; set; } - public float OrbitLongitudeOfAscendingNode { get; set; } - public float OrbitArgumentOfPeriapsis { get; set; } - public float OrbitPhaseAtEpoch { get; set; } - public float Size { get; set; } - public required string Color { get; set; } - public float Tilt { get; set; } - public bool HasRing { get; set; } + public required string Label { get; set; } + public string PlanetType { get; set; } = "terrestrial"; + public string Shape { get; set; } = "sphere"; + public int MoonCount { get; set; } + public float OrbitRadius { get; set; } + public float OrbitSpeed { get; set; } + public float OrbitEccentricity { get; set; } + public float OrbitInclination { get; set; } + public float OrbitLongitudeOfAscendingNode { get; set; } + public float OrbitArgumentOfPeriapsis { get; set; } + public float OrbitPhaseAtEpoch { get; set; } + public float Size { get; set; } + public required string Color { get; set; } + public float Tilt { get; set; } + public bool HasRing { get; set; } } public sealed class ShipDefinition { - public required string Id { get; set; } - public required string Label { get; set; } - public required string Role { get; set; } - public required string ShipClass { get; set; } - public float Speed { get; set; } - public float WarpSpeed { get; set; } - public float FtlSpeed { get; set; } - public float SpoolTime { get; set; } - public float CargoCapacity { get; set; } - public string? CargoKind { get; set; } - public string? CargoItemId { get; set; } - public required string Color { get; set; } - public required string HullColor { get; set; } - public float Size { get; set; } - public float MaxHealth { get; set; } - public List Modules { get; set; } = []; -} - -public sealed class ConstructibleDefinition -{ - public required string Id { get; set; } - public required string Label { get; set; } - public required string Category { get; set; } - public required string Color { get; set; } - public float Radius { get; set; } - public int DockingCapacity { get; set; } - public Dictionary Storage { get; set; } = new(StringComparer.Ordinal); - public List Modules { get; set; } = []; + public required string Id { get; set; } + public required string Label { get; set; } + public required string Role { get; set; } + public required string ShipClass { get; set; } + public float Speed { get; set; } + public float WarpSpeed { get; set; } + public float FtlSpeed { get; set; } + public float SpoolTime { get; set; } + public float CargoCapacity { get; set; } + public string? CargoKind { get; set; } + public string? CargoItemId { get; set; } + public required string Color { get; set; } + public required string HullColor { get; set; } + public float Size { get; set; } + public float MaxHealth { get; set; } + public List Modules { get; set; } = []; + public ConstructionDefinition? Construction { get; set; } } public sealed class ScenarioDefinition { - public required List InitialStations { get; set; } - public required List ShipFormations { get; set; } - public required List PatrolRoutes { get; set; } - public required MiningDefaultsDefinition MiningDefaults { get; set; } + public required List InitialStations { get; set; } + public required List ShipFormations { get; set; } + public required List PatrolRoutes { get; set; } + public required MiningDefaultsDefinition MiningDefaults { get; set; } } public sealed class InitialStationDefinition { - public required string ConstructibleId { get; set; } - public required string SystemId { get; set; } - public string? FactionId { get; set; } - public int? PlanetIndex { get; set; } - public int? LagrangeSide { get; set; } - public float[]? Position { get; set; } + public required string SystemId { get; set; } + public string Label { get; set; } = "Orbital Station"; + public string Color { get; set; } = "#8df0d2"; + public List StartingModules { get; set; } = []; + public string? FactionId { get; set; } + public int? PlanetIndex { get; set; } + public int? LagrangeSide { get; set; } + public float[]? Position { get; set; } } public sealed class ShipFormationDefinition { - public required string ShipId { get; set; } - public int Count { get; set; } - public required float[] Center { get; set; } - public required string SystemId { get; set; } - public string? FactionId { get; set; } + public required string ShipId { get; set; } + public int Count { get; set; } + public required float[] Center { get; set; } + public required string SystemId { get; set; } + public string? FactionId { get; set; } } public sealed class PatrolRouteDefinition { - public required string SystemId { get; set; } - public required List Points { get; set; } + public required string SystemId { get; set; } + public required List Points { get; set; } } public sealed class MiningDefaultsDefinition { - public required string NodeSystemId { get; set; } - public required string RefinerySystemId { get; set; } + public required string NodeSystemId { get; set; } + public required string RefinerySystemId { get; set; } } diff --git a/apps/backend/Simulation/AI/ShipBehaviorStateMachine.cs b/apps/backend/Simulation/AI/ShipBehaviorStateMachine.cs index 761a1d3..5ec5c38 100644 --- a/apps/backend/Simulation/AI/ShipBehaviorStateMachine.cs +++ b/apps/backend/Simulation/AI/ShipBehaviorStateMachine.cs @@ -19,8 +19,6 @@ internal sealed class ShipBehaviorStateMachine idleState, new PatrolShipBehaviorState(), new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining-turret"), - new ResourceHarvestShipBehaviorState("auto-harvest-gas", "gas", "gas-extractor"), - new EnergySupplyShipBehaviorState(), new ConstructStationShipBehaviorState(), }; diff --git a/apps/backend/Simulation/AI/ShipBehaviorStates.cs b/apps/backend/Simulation/AI/ShipBehaviorStates.cs index 3b38d1e..c266c63 100644 --- a/apps/backend/Simulation/AI/ShipBehaviorStates.cs +++ b/apps/backend/Simulation/AI/ShipBehaviorStates.cs @@ -2,169 +2,126 @@ namespace SpaceGame.Simulation.Api.Simulation; internal sealed class IdleShipBehaviorState : IShipBehaviorState { - public string Kind => "idle"; + public string Kind => "idle"; - public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) + public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) + { + ship.ControllerTask = new ControllerTaskRuntime { - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Idle, - Threshold = world.Balance.ArrivalThreshold, - Status = WorkStatus.Pending, - }; - } + Kind = ControllerTaskKind.Idle, + Threshold = world.Balance.ArrivalThreshold, + Status = WorkStatus.Pending, + }; + } - public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) - { - } + public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + { + } } internal sealed class PatrolShipBehaviorState : IShipBehaviorState { - public string Kind => "patrol"; + public string Kind => "patrol"; - public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) + public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) + { + if (ship.DefaultBehavior.PatrolPoints.Count == 0) { - if (ship.DefaultBehavior.PatrolPoints.Count == 0) - { - ship.DefaultBehavior.Kind = "idle"; - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Idle, - Threshold = world.Balance.ArrivalThreshold, - Status = WorkStatus.Pending, - }; - return; - } - - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex], - TargetSystemId = ship.SystemId, - Threshold = 18f, - }; + ship.DefaultBehavior.Kind = "idle"; + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Idle, + Threshold = world.Balance.ArrivalThreshold, + Status = WorkStatus.Pending, + }; + return; } - public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + ship.ControllerTask = new ControllerTaskRuntime { - if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0) - { - ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count; - } + Kind = ControllerTaskKind.Travel, + TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex], + TargetSystemId = ship.SystemId, + Threshold = 18f, + }; + } + + public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + { + if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0) + { + ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count; } + } } internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState { - private readonly string resourceItemId; - private readonly string requiredModule; + private readonly string resourceItemId; + private readonly string requiredModule; - public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule) + public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule) + { + Kind = kind; + this.resourceItemId = resourceItemId; + this.requiredModule = requiredModule; + } + + public string Kind { get; } + + public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => + engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule); + + public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + { + switch (ship.DefaultBehavior.Phase, controllerEvent) { - Kind = kind; - this.resourceItemId = resourceItemId; - this.requiredModule = requiredModule; - } - - public string Kind { get; } - - public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => - engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule); - - public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) - { - switch (ship.DefaultBehavior.Phase, controllerEvent) - { - case ("travel-to-node", "arrived"): - ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract"; - break; - case ("extract", "cargo-full"): - ship.DefaultBehavior.Phase = "travel-to-station"; - break; - case ("extract", "node-depleted"): - ship.DefaultBehavior.Phase = "travel-to-node"; - ship.DefaultBehavior.NodeId = null; - break; - case ("travel-to-station", "arrived"): - ship.DefaultBehavior.Phase = "dock"; - break; - case ("dock", "docked"): - ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "refuel"; - break; - case ("unload", "unloaded"): - ship.DefaultBehavior.Phase = "refuel"; - break; - case ("refuel", "refueled"): - ship.DefaultBehavior.Phase = "undock"; - break; - case ("undock", "undocked"): - ship.DefaultBehavior.Phase = "travel-to-node"; - ship.DefaultBehavior.NodeId = null; - break; - } + case ("travel-to-node", "arrived"): + ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract"; + break; + case ("extract", "cargo-full"): + ship.DefaultBehavior.Phase = "travel-to-station"; + break; + case ("extract", "node-depleted"): + ship.DefaultBehavior.Phase = "travel-to-node"; + ship.DefaultBehavior.NodeId = null; + break; + case ("travel-to-station", "arrived"): + ship.DefaultBehavior.Phase = "dock"; + break; + case ("dock", "docked"): + ship.DefaultBehavior.Phase = "unload"; + break; + case ("undock", "undocked"): + ship.DefaultBehavior.Phase = "travel-to-node"; + ship.DefaultBehavior.NodeId = null; + break; } + } } internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState { - public string Kind => "construct-station"; + public string Kind => "construct-station"; - public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => - engine.PlanStationConstruction(ship, world); + public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => + engine.PlanStationConstruction(ship, world); - public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) + { + switch (ship.DefaultBehavior.Phase, controllerEvent) { - switch (ship.DefaultBehavior.Phase, controllerEvent) - { - case ("travel-to-station", "arrived"): - ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "deliver-to-site"; - break; - case ("refuel", "refueled"): - ship.DefaultBehavior.Phase = "deliver-to-site"; - break; - case ("deliver-to-site", "construction-delivered"): - ship.DefaultBehavior.Phase = "build-site"; - break; - case ("construct-module", "module-constructed"): - case ("build-site", "site-constructed"): - ship.DefaultBehavior.Phase = "travel-to-station"; - ship.DefaultBehavior.ModuleId = null; - break; - } - } -} - -internal sealed class EnergySupplyShipBehaviorState : IShipBehaviorState -{ - public string Kind => "auto-supply-energy"; - - public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => - engine.PlanEnergySupply(ship, world); - - public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) - { - switch (ship.DefaultBehavior.Phase, controllerEvent) - { - case ("travel-to-source", "arrived"): - case ("travel-to-destination", "arrived"): - ship.DefaultBehavior.Phase = "dock"; - break; - case ("dock", "docked"): - ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "load"; - break; - case ("load", "loaded"): - ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock"; - break; - case ("unload", "unloaded"): - ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock"; - break; - case ("refuel", "refueled"): - ship.DefaultBehavior.Phase = "undock"; - break; - case ("undock", "undocked"): - ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "travel-to-destination" : "travel-to-source"; - break; - } + case ("travel-to-station", "arrived"): + ship.DefaultBehavior.Phase = "deliver-to-site"; + break; + case ("deliver-to-site", "construction-delivered"): + ship.DefaultBehavior.Phase = "build-site"; + break; + case ("construct-module", "module-constructed"): + case ("build-site", "site-constructed"): + ship.DefaultBehavior.Phase = "travel-to-station"; + ship.DefaultBehavior.ModuleId = null; + break; } + } } diff --git a/apps/backend/Simulation/Model/ShipRuntimeModels.cs b/apps/backend/Simulation/Model/ShipRuntimeModels.cs index 91adb86..a389f39 100644 --- a/apps/backend/Simulation/Model/ShipRuntimeModels.cs +++ b/apps/backend/Simulation/Model/ShipRuntimeModels.cs @@ -4,63 +4,62 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed class ShipRuntime { - public required string Id { get; init; } - public required string SystemId { get; set; } - public required ShipDefinition Definition { get; init; } - public required string FactionId { get; init; } - public required Vector3 Position { get; set; } - public required Vector3 TargetPosition { get; set; } - public required ShipSpatialStateRuntime SpatialState { get; set; } - public Vector3 Velocity { get; set; } = Vector3.Zero; - public ShipState State { get; set; } = ShipState.Idle; - public ShipOrderRuntime? Order { get; set; } - public required DefaultBehaviorRuntime DefaultBehavior { get; set; } - public required ControllerTaskRuntime ControllerTask { get; set; } - public float ActionTimer { get; set; } - public Dictionary Inventory { get; } = new(StringComparer.Ordinal); - public float WorkerPopulation { get; set; } - public float EnergyStored { get; set; } - public string? DockedStationId { get; set; } - public int? AssignedDockingPadIndex { get; set; } - public string? CommanderId { get; set; } - public string? PolicySetId { get; set; } - public float Health { get; set; } - public string? TrackedActionKey { get; set; } - public float TrackedActionTotal { get; set; } - public List History { get; } = []; - public string LastSignature { get; set; } = string.Empty; - public string LastDeltaSignature { get; set; } = string.Empty; + public required string Id { get; init; } + public required string SystemId { get; set; } + public required ShipDefinition Definition { get; init; } + public required string FactionId { get; init; } + public required Vector3 Position { get; set; } + public required Vector3 TargetPosition { get; set; } + public required ShipSpatialStateRuntime SpatialState { get; set; } + public Vector3 Velocity { get; set; } = Vector3.Zero; + public ShipState State { get; set; } = ShipState.Idle; + public ShipOrderRuntime? Order { get; set; } + public required DefaultBehaviorRuntime DefaultBehavior { get; set; } + public required ControllerTaskRuntime ControllerTask { get; set; } + public float ActionTimer { get; set; } + public Dictionary Inventory { get; } = new(StringComparer.Ordinal); + public float WorkerPopulation { get; set; } + public string DockedStationId { get; set; } + public int? AssignedDockingPadIndex { get; set; } + public string? CommanderId { get; set; } + public string? PolicySetId { get; set; } + public float Health { get; set; } + public string? TrackedActionKey { get; set; } + public float TrackedActionTotal { get; set; } + public List History { get; } = []; + public string LastSignature { get; set; } = string.Empty; + public string LastDeltaSignature { get; set; } = string.Empty; } public sealed class ShipOrderRuntime { - public required string Kind { get; init; } - public OrderStatus Status { get; set; } = OrderStatus.Accepted; - public required string DestinationSystemId { get; init; } - public required Vector3 DestinationPosition { get; init; } + public required string Kind { get; init; } + public OrderStatus Status { get; set; } = OrderStatus.Accepted; + public required string DestinationSystemId { get; init; } + public required Vector3 DestinationPosition { get; init; } } public sealed class DefaultBehaviorRuntime { - public required string Kind { get; set; } - public string? AreaSystemId { get; set; } - public string? StationId { get; set; } - public string? RefineryId { get; set; } - public string? NodeId { get; set; } - public string? ModuleId { get; set; } - public string? Phase { get; set; } - public List PatrolPoints { get; set; } = []; - public int PatrolIndex { get; set; } + public required string Kind { get; set; } + public string? AreaSystemId { get; set; } + public string? StationId { get; set; } + public string? RefineryId { get; set; } + public string? NodeId { get; set; } + public string? ModuleId { get; set; } + public string? Phase { get; set; } + public List PatrolPoints { get; set; } = []; + public int PatrolIndex { get; set; } } public sealed class ControllerTaskRuntime { - public required ControllerTaskKind Kind { get; set; } - public WorkStatus Status { get; set; } = WorkStatus.Pending; - public string? CommanderId { get; set; } - public string? TargetEntityId { get; set; } - public string? TargetSystemId { get; set; } - public string? TargetNodeId { get; set; } - public Vector3? TargetPosition { get; set; } - public float Threshold { get; set; } + public required ControllerTaskKind Kind { get; set; } + public WorkStatus Status { get; set; } = WorkStatus.Pending; + public string? CommanderId { get; set; } + public string? TargetEntityId { get; set; } + public string? TargetSystemId { get; set; } + public string? TargetNodeId { get; set; } + public Vector3? TargetPosition { get; set; } + public float Threshold { get; set; } } diff --git a/apps/backend/Simulation/Model/SimulationKinds.cs b/apps/backend/Simulation/Model/SimulationKinds.cs index bac4e3c..2b4561a 100644 --- a/apps/backend/Simulation/Model/SimulationKinds.cs +++ b/apps/backend/Simulation/Model/SimulationKinds.cs @@ -2,243 +2,237 @@ namespace SpaceGame.Simulation.Api.Simulation; public enum SpatialNodeKind { - Star, - Planet, - Moon, - LagrangePoint, - Station, - ResourceSite, + Star, + Planet, + Moon, + LagrangePoint, + Station, + ResourceSite, } public enum WorkStatus { - Pending, - Active, - Completed, + Pending, + Active, + Completed, } public enum OrderStatus { - Queued, - Accepted, - Completed, + Queued, + Accepted, + Completed, } public enum ShipState { - Idle, - Arriving, - CapacitorStarved, - LocalFlight, - SpoolingWarp, - Warping, - SpoolingFtl, - Ftl, - CargoFull, - MiningApproach, - Mining, - NodeDepleted, - AwaitingDock, - DockingApproach, - Docking, - Docked, - Transferring, - Loading, - Unloading, - Refueling, - WaitingMaterials, - ConstructionBlocked, - Constructing, - DeliveringConstruction, - Blocked, - Undocking, + Idle, + Arriving, + LocalFlight, + SpoolingWarp, + Warping, + SpoolingFtl, + Ftl, + CargoFull, + MiningApproach, + Mining, + NodeDepleted, + AwaitingDock, + DockingApproach, + Docking, + Docked, + Transferring, + Loading, + Unloading, + WaitingMaterials, + ConstructionBlocked, + Constructing, + DeliveringConstruction, + Blocked, + Undocking, } public enum ControllerTaskKind { - Idle, - Travel, - Extract, - Dock, - Load, - Unload, - Refuel, - DeliverConstruction, - BuildConstructionSite, - LoadWorkers, - UnloadWorkers, - ConstructModule, - Undock, + Idle, + Travel, + Extract, + Dock, + Load, + Unload, + DeliverConstruction, + BuildConstructionSite, + LoadWorkers, + UnloadWorkers, + ConstructModule, + Undock, } public static class SpaceLayerKinds { - public const string UniverseSpace = "universe-space"; - public const string GalaxySpace = "galaxy-space"; - public const string SystemSpace = "system-space"; - public const string LocalSpace = "local-space"; + public const string UniverseSpace = "universe-space"; + public const string GalaxySpace = "galaxy-space"; + public const string SystemSpace = "system-space"; + public const string LocalSpace = "local-space"; } public static class MovementRegimeKinds { - public const string LocalFlight = "local-flight"; - public const string Warp = "warp"; - public const string StargateTransit = "stargate-transit"; - public const string FtlTransit = "ftl-transit"; + public const string LocalFlight = "local-flight"; + public const string Warp = "warp"; + public const string StargateTransit = "stargate-transit"; + public const string FtlTransit = "ftl-transit"; } public static class CommanderKind { - public const string Faction = "faction"; - public const string Station = "station"; - public const string Ship = "ship"; - public const string Fleet = "fleet"; - public const string Sector = "sector"; - public const string TaskGroup = "task-group"; + public const string Faction = "faction"; + public const string Station = "station"; + public const string Ship = "ship"; + public const string Fleet = "fleet"; + public const string Sector = "sector"; + public const string TaskGroup = "task-group"; } public static class ShipTaskKinds { - public const string Idle = "idle"; - public const string LocalMove = "local-move"; - public const string WarpToNode = "warp-to-node"; - public const string UseStargate = "use-stargate"; - public const string UseFtl = "use-ftl"; - public const string Dock = "dock"; - public const string Undock = "undock"; - public const string LoadCargo = "load-cargo"; - public const string UnloadCargo = "unload-cargo"; - public const string LoadWorkers = "load-workers"; - public const string UnloadWorkers = "unload-workers"; - public const string MineNode = "mine-node"; - public const string HarvestGas = "harvest-gas"; - public const string DeliverToStation = "deliver-to-station"; - public const string ClaimLagrangePoint = "claim-lagrange-point"; - public const string BuildConstructionSite = "build-construction-site"; - public const string EscortTarget = "escort-target"; - public const string AttackTarget = "attack-target"; - public const string DefendBubble = "defend-bubble"; - public const string Retreat = "retreat"; - public const string HoldPosition = "hold-position"; + public const string Idle = "idle"; + public const string LocalMove = "local-move"; + public const string WarpToNode = "warp-to-node"; + public const string UseStargate = "use-stargate"; + public const string UseFtl = "use-ftl"; + public const string Dock = "dock"; + public const string Undock = "undock"; + public const string LoadCargo = "load-cargo"; + public const string UnloadCargo = "unload-cargo"; + public const string LoadWorkers = "load-workers"; + public const string UnloadWorkers = "unload-workers"; + public const string MineNode = "mine-node"; + public const string HarvestGas = "harvest-gas"; + public const string DeliverToStation = "deliver-to-station"; + public const string ClaimLagrangePoint = "claim-lagrange-point"; + public const string BuildConstructionSite = "build-construction-site"; + public const string EscortTarget = "escort-target"; + public const string AttackTarget = "attack-target"; + public const string DefendBubble = "defend-bubble"; + public const string Retreat = "retreat"; + public const string HoldPosition = "hold-position"; } public static class ShipOrderKinds { - public const string DirectMove = "direct-move"; - public const string TravelToNode = "travel-to-node"; - public const string DockAtStation = "dock-at-station"; - public const string DeliverCargo = "deliver-cargo"; - public const string BuildAtSite = "build-at-site"; - public const string AttackTarget = "attack-target"; - public const string HoldPosition = "hold-position"; + public const string DirectMove = "direct-move"; + public const string TravelToNode = "travel-to-node"; + public const string DockAtStation = "dock-at-station"; + public const string DeliverCargo = "deliver-cargo"; + public const string BuildAtSite = "build-at-site"; + public const string AttackTarget = "attack-target"; + public const string HoldPosition = "hold-position"; } public static class ClaimStateKinds { - public const string Placed = "placed"; - public const string Activating = "activating"; - public const string Active = "active"; - public const string Destroyed = "destroyed"; + public const string Placed = "placed"; + public const string Activating = "activating"; + public const string Active = "active"; + public const string Destroyed = "destroyed"; } public static class ConstructionSiteStateKinds { - public const string Planned = "planned"; - public const string Active = "active"; - public const string Paused = "paused"; - public const string Completed = "completed"; - public const string Destroyed = "destroyed"; + public const string Planned = "planned"; + public const string Active = "active"; + public const string Paused = "paused"; + public const string Completed = "completed"; + public const string Destroyed = "destroyed"; } public static class MarketOrderKinds { - public const string Buy = "buy"; - public const string Sell = "sell"; + public const string Buy = "buy"; + public const string Sell = "sell"; } public static class MarketOrderStateKinds { - public const string Open = "open"; - public const string PartiallyFilled = "partially-filled"; - public const string Filled = "filled"; - public const string Cancelled = "cancelled"; + public const string Open = "open"; + public const string PartiallyFilled = "partially-filled"; + public const string Filled = "filled"; + public const string Cancelled = "cancelled"; } public static class SimulationEnumMappings { - public static string ToContractValue(this SpatialNodeKind kind) => kind switch - { - SpatialNodeKind.Star => "star", - SpatialNodeKind.Planet => "planet", - SpatialNodeKind.Moon => "moon", - SpatialNodeKind.LagrangePoint => "lagrange-point", - SpatialNodeKind.Station => "station", - SpatialNodeKind.ResourceSite => "resource-site", - _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), - }; + public static string ToContractValue(this SpatialNodeKind kind) => kind switch + { + SpatialNodeKind.Star => "star", + SpatialNodeKind.Planet => "planet", + SpatialNodeKind.Moon => "moon", + SpatialNodeKind.LagrangePoint => "lagrange-point", + SpatialNodeKind.Station => "station", + SpatialNodeKind.ResourceSite => "resource-site", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; - public static string ToContractValue(this WorkStatus status) => status switch - { - WorkStatus.Pending => "pending", - WorkStatus.Active => "active", - WorkStatus.Completed => "completed", - _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), - }; + public static string ToContractValue(this WorkStatus status) => status switch + { + WorkStatus.Pending => "pending", + WorkStatus.Active => "active", + WorkStatus.Completed => "completed", + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), + }; - public static string ToContractValue(this OrderStatus status) => status switch - { - OrderStatus.Queued => "queued", - OrderStatus.Accepted => "accepted", - OrderStatus.Completed => "completed", - _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), - }; + public static string ToContractValue(this OrderStatus status) => status switch + { + OrderStatus.Queued => "queued", + OrderStatus.Accepted => "accepted", + OrderStatus.Completed => "completed", + _ => throw new ArgumentOutOfRangeException(nameof(status), status, null), + }; - public static string ToContractValue(this ShipState state) => state switch - { - ShipState.Idle => "idle", - ShipState.Arriving => "arriving", - ShipState.CapacitorStarved => "capacitor-starved", - ShipState.LocalFlight => "local-flight", - ShipState.SpoolingWarp => "spooling-warp", - ShipState.Warping => "warping", - ShipState.SpoolingFtl => "spooling-ftl", - ShipState.Ftl => "ftl", - ShipState.CargoFull => "cargo-full", - ShipState.MiningApproach => "mining-approach", - ShipState.Mining => "mining", - ShipState.NodeDepleted => "node-depleted", - ShipState.AwaitingDock => "awaiting-dock", - ShipState.DockingApproach => "docking-approach", - ShipState.Docking => "docking", - ShipState.Docked => "docked", - ShipState.Transferring => "transferring", - ShipState.Loading => "loading", - ShipState.Unloading => "unloading", - ShipState.Refueling => "refueling", - ShipState.WaitingMaterials => "waiting-materials", - ShipState.ConstructionBlocked => "construction-blocked", - ShipState.Constructing => "constructing", - ShipState.DeliveringConstruction => "delivering-construction", - ShipState.Blocked => "blocked", - ShipState.Undocking => "undocking", - _ => throw new ArgumentOutOfRangeException(nameof(state), state, null), - }; + public static string ToContractValue(this ShipState state) => state switch + { + ShipState.Idle => "idle", + ShipState.Arriving => "arriving", + ShipState.LocalFlight => "local-flight", + ShipState.SpoolingWarp => "spooling-warp", + ShipState.Warping => "warping", + ShipState.SpoolingFtl => "spooling-ftl", + ShipState.Ftl => "ftl", + ShipState.CargoFull => "cargo-full", + ShipState.MiningApproach => "mining-approach", + ShipState.Mining => "mining", + ShipState.NodeDepleted => "node-depleted", + ShipState.AwaitingDock => "awaiting-dock", + ShipState.DockingApproach => "docking-approach", + ShipState.Docking => "docking", + ShipState.Docked => "docked", + ShipState.Transferring => "transferring", + ShipState.Loading => "loading", + ShipState.Unloading => "unloading", + ShipState.WaitingMaterials => "waiting-materials", + ShipState.ConstructionBlocked => "construction-blocked", + ShipState.Constructing => "constructing", + ShipState.DeliveringConstruction => "delivering-construction", + ShipState.Blocked => "blocked", + ShipState.Undocking => "undocking", + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null), + }; - public static string ToContractValue(this ControllerTaskKind kind) => kind switch - { - ControllerTaskKind.Idle => "idle", - ControllerTaskKind.Travel => "travel", - ControllerTaskKind.Extract => "extract", - ControllerTaskKind.Dock => "dock", - ControllerTaskKind.Load => "load", - ControllerTaskKind.Unload => "unload", - ControllerTaskKind.Refuel => "refuel", - ControllerTaskKind.DeliverConstruction => "deliver-construction", - ControllerTaskKind.BuildConstructionSite => "build-construction-site", - ControllerTaskKind.LoadWorkers => "load-workers", - ControllerTaskKind.UnloadWorkers => "unload-workers", - ControllerTaskKind.ConstructModule => "construct-module", - ControllerTaskKind.Undock => "undock", - _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), - }; + public static string ToContractValue(this ControllerTaskKind kind) => kind switch + { + ControllerTaskKind.Idle => "idle", + ControllerTaskKind.Travel => "travel", + ControllerTaskKind.Extract => "extract", + ControllerTaskKind.Dock => "dock", + ControllerTaskKind.Load => "load", + ControllerTaskKind.Unload => "unload", + ControllerTaskKind.DeliverConstruction => "deliver-construction", + ControllerTaskKind.BuildConstructionSite => "build-construction-site", + ControllerTaskKind.LoadWorkers => "load-workers", + ControllerTaskKind.UnloadWorkers => "unload-workers", + ControllerTaskKind.ConstructModule => "construct-module", + ControllerTaskKind.Undock => "undock", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; } diff --git a/apps/backend/Simulation/Model/SimulationWorld.cs b/apps/backend/Simulation/Model/SimulationWorld.cs index b2c1159..0b7ebf0 100644 --- a/apps/backend/Simulation/Model/SimulationWorld.cs +++ b/apps/backend/Simulation/Model/SimulationWorld.cs @@ -21,6 +21,7 @@ public sealed class SimulationWorld public required List Policies { get; init; } public required Dictionary ShipDefinitions { get; init; } public required Dictionary ItemDefinitions { get; init; } + public required Dictionary ModuleDefinitions { get; init; } public required Dictionary ModuleRecipes { get; init; } public required Dictionary Recipes { get; init; } public int TickIntervalMs { get; init; } = 200; diff --git a/apps/backend/Simulation/Model/StationRuntimeModels.cs b/apps/backend/Simulation/Model/StationRuntimeModels.cs index 12a836b..402f1c9 100644 --- a/apps/backend/Simulation/Model/StationRuntimeModels.cs +++ b/apps/backend/Simulation/Model/StationRuntimeModels.cs @@ -1,40 +1,49 @@ -using SpaceGame.Simulation.Api.Data; - namespace SpaceGame.Simulation.Api.Simulation; public sealed class StationRuntime { - public required string Id { get; init; } - public required string SystemId { get; init; } - public required ConstructibleDefinition Definition { get; init; } - public required Vector3 Position { get; set; } - public required string FactionId { get; init; } - public string? NodeId { get; set; } - public string? BubbleId { get; set; } - public string? AnchorNodeId { get; set; } - public string? CommanderId { get; set; } - public string? PolicySetId { get; set; } - public List InstalledModules { get; } = []; - public Dictionary Inventory { get; } = new(StringComparer.Ordinal); - public Dictionary ProductionLaneTimers { get; } = new(StringComparer.Ordinal); - public Dictionary DockingPadAssignments { get; } = new(); - public HashSet MarketOrderIds { get; } = new(StringComparer.Ordinal); - public float EnergyStored { get; set; } - public float Population { get; set; } - public float PopulationCapacity { get; set; } - public float WorkforceRequired { get; set; } - public float WorkforceEffectiveRatio { get; set; } = 0.1f; - public float PopulationGrowthProgress { get; set; } - public float ShipProductionProgressSeconds { get; set; } - public HashSet DockedShipIds { get; } = []; - public ModuleConstructionRuntime? ActiveConstruction { get; set; } - public string LastDeltaSignature { get; set; } = string.Empty; + public required string Id { get; init; } + public required string SystemId { get; init; } + public required string Label { get; set; } + public string Category { get; set; } = "station"; + public string Color { get; set; } = "#8df0d2"; + public required Vector3 Position { get; set; } + public float Radius { get; set; } = 24f; + public required string FactionId { get; init; } + public string? NodeId { get; set; } + public string? BubbleId { get; set; } + public string? AnchorNodeId { get; set; } + public string? CommanderId { get; set; } + public string? PolicySetId { get; set; } + public List Modules { get; } = []; + public IEnumerable InstalledModules => Modules.Select((module) => module.ModuleId); + public Dictionary Inventory { get; } = new(StringComparer.Ordinal); + public Dictionary ProductionLaneTimers { get; } = new(StringComparer.Ordinal); + public Dictionary DockingPadAssignments { get; } = new(); + public HashSet MarketOrderIds { get; } = new(StringComparer.Ordinal); + public float Population { get; set; } + public float PopulationCapacity { get; set; } + public float WorkforceRequired { get; set; } + public float WorkforceEffectiveRatio { get; set; } = 0.1f; + public float PopulationGrowthProgress { get; set; } + public float ShipProductionProgressSeconds { get; set; } + public HashSet DockedShipIds { get; } = []; + public ModuleConstructionRuntime? ActiveConstruction { get; set; } + public string LastDeltaSignature { get; set; } = string.Empty; +} + +public sealed class StationModuleRuntime +{ + public required string Id { get; init; } + public required string ModuleId { get; init; } + public float Health { get; set; } + public float MaxHealth { get; set; } } public sealed class ModuleConstructionRuntime { - public required string ModuleId { get; init; } - public float ProgressSeconds { get; set; } - public float RequiredSeconds { get; init; } - public string AssignedConstructorShipId { get; set; } = string.Empty; + public required string ModuleId { get; init; } + public float ProgressSeconds { get; set; } + public float RequiredSeconds { get; init; } + public string AssignedConstructorShipId { get; set; } = string.Empty; } diff --git a/apps/backend/Simulation/ScenarioLoader.Generation.cs b/apps/backend/Simulation/ScenarioLoader.Generation.cs index ffb7a4a..70590de 100644 --- a/apps/backend/Simulation/ScenarioLoader.Generation.cs +++ b/apps/backend/Simulation/ScenarioLoader.Generation.cs @@ -254,7 +254,6 @@ public sealed partial class ScenarioLoader } nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets)); - nodes.AddRange(BuildGasCloudNodes(generatedIndex, planets)); return nodes; } @@ -344,46 +343,6 @@ public sealed partial class ScenarioLoader } } - private static IEnumerable BuildGasCloudNodes(int generatedIndex, IReadOnlyList planets) - { - var gasAnchor = planets - .Where((planet) => planet.PlanetType is "gas-giant" or "ice-giant") - .OrderByDescending((planet) => planet.OrbitRadius) - .FirstOrDefault(); - - if (gasAnchor is null) - { - yield break; - } - - var gasAnchorIndex = 0; - for (var index = 0; index < planets.Count; index += 1) - { - if (ReferenceEquals(planets[index], gasAnchor)) - { - gasAnchorIndex = index; - break; - } - } - - var nodeCount = 2 + (generatedIndex % 3); - var gasAmount = 1000f; - for (var index = 0; index < nodeCount; index += 1) - { - yield return new ResourceNodeDefinition - { - SourceKind = "gas-cloud", - Angle = gasAnchor.OrbitPhaseAtEpoch * (MathF.PI / 180f) + (((MathF.PI * 2f) / nodeCount) * index) + Jitter(generatedIndex, 240 + index, 0.18f), - RadiusOffset = 170000f + Jitter(generatedIndex, 260 + index, 44000f), - InclinationDegrees = Jitter(generatedIndex, 320 + index, 10f), - AnchorPlanetIndex = gasAnchorIndex, - OreAmount = gasAmount, - ItemId = "gas", - ShardCount = 10 + index, - }; - } - } - private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList planets) { if (planets.Count == 0) @@ -566,9 +525,6 @@ public sealed partial class ScenarioLoader new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, - new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 0.9f, RadiusOffset = 210000f, InclinationDegrees = 3f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 }, - new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 2.7f, RadiusOffset = 228000f, InclinationDegrees = -4f, AnchorPlanetIndex = 5, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 }, - new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 186000f, InclinationDegrees = 6f, AnchorPlanetIndex = 6, OreAmount = 1000f, ItemId = "gas", ShardCount = 10 }, ], Planets = [ diff --git a/apps/backend/Simulation/ScenarioLoader.Seeding.cs b/apps/backend/Simulation/ScenarioLoader.Seeding.cs index de98a6d..974aeab 100644 --- a/apps/backend/Simulation/ScenarioLoader.Seeding.cs +++ b/apps/backend/Simulation/ScenarioLoader.Seeding.cs @@ -70,7 +70,7 @@ public sealed partial class ScenarioLoader .ToList(); var refineries = ownedStations - .Where((station) => HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank")) + .Where((station) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank")) .ToList(); if (refineries.Count > 0) @@ -86,7 +86,7 @@ public sealed partial class ScenarioLoader } } - foreach (var shipyard in ownedStations.Where((station) => station.Definition.Category == "shipyard")) + foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory"))) { shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock); } @@ -171,7 +171,7 @@ public sealed partial class ScenarioLoader NodeId = anchorNode.Id, BubbleId = anchorNode.BubbleId, TargetKind = "station-module", - TargetDefinitionId = station.Definition.Id, + TargetDefinitionId = "station", BlueprintId = moduleId, ClaimId = claim.Id, StationId = station.Id, @@ -213,8 +213,6 @@ public sealed partial class ScenarioLoader { foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[] { - ("gas-tank", 1), - ("fuel-processor", 1), ("refinery-stack", 1), ("container-bay", 1), ("fabricator-array", 2), @@ -238,7 +236,7 @@ public sealed partial class ScenarioLoader { var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); station.PopulationCapacity = 40f + (habitatModules * 220f); - station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f); + station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.Population = habitatModules > 0 ? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f) : MathF.Min(28f, station.PopulationCapacity); @@ -391,21 +389,6 @@ public sealed partial class ScenarioLoader }; } - if (HasModules(definition, "reactor-core", "capacitor-bank", "gas-extractor") && refinery is not null) - { - return CreateResourceHarvestBehavior("auto-harvest-gas", scenario.MiningDefaults.NodeSystemId, refinery.Id); - } - - if (string.Equals(definition.Role, "transport", StringComparison.Ordinal) && refinery is not null) - { - return new DefaultBehaviorRuntime - { - Kind = "auto-supply-energy", - StationId = refinery.Id, - Phase = "travel-to-source", - }; - } - if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null) { return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id); diff --git a/apps/backend/Simulation/ScenarioLoader.cs b/apps/backend/Simulation/ScenarioLoader.cs index d948afe..1477e85 100644 --- a/apps/backend/Simulation/ScenarioLoader.cs +++ b/apps/backend/Simulation/ScenarioLoader.cs @@ -97,15 +97,15 @@ public sealed partial class ScenarioLoader var scenario = NormalizeScenarioToAvailableSystems( Read("scenario.json"), systems.Select((system) => system.Id).ToList()); + var modules = Read>("modules.json"); var ships = Read>("ships.json"); - var constructibles = Read>("constructibles.json"); var items = Read>("items.json"); - var recipes = Read>("recipes.json"); - var moduleRecipes = Read>("module-recipes.json"); var balance = Read("balance.json"); + var recipes = BuildRecipes(items, ships); + var moduleRecipes = BuildModuleRecipes(modules); + var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); - var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal); @@ -178,7 +178,7 @@ public sealed partial class ScenarioLoader var stationIdCounter = 0; foreach (var plan in scenario.InitialStations) { - if (!constructibleDefinitions.TryGetValue(plan.ConstructibleId, out var definition) || !systemsById.TryGetValue(plan.SystemId, out var system)) + if (!systemsById.TryGetValue(plan.SystemId, out var system)) { continue; } @@ -188,7 +188,8 @@ public sealed partial class ScenarioLoader { Id = $"station-{++stationIdCounter}", SystemId = system.Definition.Id, - Definition = definition, + Label = plan.Label, + Color = plan.Color, Position = placement.Position, FactionId = plan.FactionId ?? DefaultFactionId, }; @@ -214,21 +215,23 @@ public sealed partial class ScenarioLoader Id = stationBubbleId, NodeId = stationNodeId, SystemId = station.SystemId, - Radius = MathF.Max(160f, definition.Radius + 60f), + Radius = MathF.Max(160f, GetStationRadius(moduleDefinitions, station) + 60f), }); localBubbles[^1].OccupantStationIds.Add(station.Id); placement.AnchorNode.OccupyingStructureId = station.Id; - foreach (var moduleId in definition.Modules) + var startingModules = plan.StartingModules.Count > 0 + ? plan.StartingModules + : ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"]; + foreach (var moduleId in startingModules) { - stations[^1].InstalledModules.Add(moduleId); + AddStationModule(stations[^1], moduleDefinitions, moduleId); } } foreach (var station in stations) { InitializeStationPopulation(station); - station.Inventory["fuel"] = 240f; station.Inventory["refined-metals"] = 120f; if (station.Population > 0f) { @@ -277,19 +280,6 @@ public sealed partial class ScenarioLoader ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending }, Health = definition.MaxHealth, }); - - shipsRuntime[^1].Inventory["gas"] = definition.Id switch - { - _ => 0f, - }; - shipsRuntime[^1].Inventory.Remove("gas"); - shipsRuntime[^1].Inventory["fuel"] = definition.Id switch - { - "constructor" => 90f, - "miner" => 90f, - "gas-miner" => 90f, - _ => 120f, - }; } } @@ -320,6 +310,7 @@ public sealed partial class ScenarioLoader Policies = policies, ShipDefinitions = shipDefinitions, ItemDefinitions = itemDefinitions, + ModuleDefinitions = moduleDefinitions, ModuleRecipes = moduleRecipeDefinitions, Recipes = recipeDefinitions, OrbitalTimeSeconds = WorldSeed * 97d, @@ -356,8 +347,10 @@ public sealed partial class ScenarioLoader InitialStations = scenario.InitialStations .Select((station) => new InitialStationDefinition { - ConstructibleId = station.ConstructibleId, SystemId = ResolveSystemId(station.SystemId), + Label = station.Label, + Color = station.Color, + StartingModules = station.StartingModules.ToList(), FactionId = station.FactionId, PlanetIndex = station.PlanetIndex, LagrangeSide = station.LagrangeSide, @@ -404,15 +397,37 @@ public sealed partial class ScenarioLoader : raw; } - private static bool HasModules(ConstructibleDefinition definition, params string[] modules) => - modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); - private static bool HasInstalledModules(StationRuntime station, params string[] modules) => - modules.All((moduleId) => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); + modules.All((moduleId) => station.Modules.Any((candidate) => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); private static bool HasModules(ShipDefinition definition, params string[] modules) => modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); + private static void AddStationModule(StationRuntime station, IReadOnlyDictionary moduleDefinitions, string moduleId) + { + if (!moduleDefinitions.TryGetValue(moduleId, out var definition)) + { + return; + } + + station.Modules.Add(new StationModuleRuntime + { + Id = $"{station.Id}-module-{station.Modules.Count + 1}", + ModuleId = moduleId, + Health = definition.Hull, + MaxHealth = definition.Hull, + }); + station.Radius = GetStationRadius(moduleDefinitions, station); + } + + private static float GetStationRadius(IReadOnlyDictionary moduleDefinitions, StationRuntime station) + { + var totalArea = station.Modules + .Select((module) => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) + .Sum(); + return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); + } + private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z); private static int CountModules(IEnumerable modules, string moduleId) => @@ -429,6 +444,89 @@ public sealed partial class ScenarioLoader return 0.1f + (0.9f * staffedRatio); } + private static List BuildModuleRecipes(IEnumerable modules) => + modules + .Where((module) => module.Construction is not null) + .Select((module) => new ModuleRecipeDefinition + { + ModuleId = module.Id, + Duration = module.Construction!.ProductionTime, + Inputs = module.Construction.Requirements + .Select((input) => new RecipeInputDefinition + { + ItemId = input.ItemId, + Amount = input.Amount, + }) + .ToList(), + }) + .ToList(); + + private static List BuildRecipes(IEnumerable items, IEnumerable ships) + { + var recipes = new List(); + + foreach (var item in items) + { + if (item.Construction is null) + { + continue; + } + + recipes.Add(new RecipeDefinition + { + Id = item.Construction.RecipeId ?? $"{item.Id}-production", + Label = item.Name, + FacilityCategory = item.Construction.FacilityCategory, + Duration = item.Construction.CycleTime, + Priority = item.Construction.Priority, + RequiredModules = item.Construction.RequiredModules.ToList(), + Inputs = item.Construction.Requirements + .Select((input) => new RecipeInputDefinition + { + ItemId = input.ItemId, + Amount = input.Amount, + }) + .ToList(), + Outputs = + [ + new RecipeOutputDefinition + { + ItemId = item.Id, + Amount = item.Construction.BatchSize, + }, + ], + }); + } + + foreach (var ship in ships) + { + if (ship.Construction is null) + { + continue; + } + + recipes.Add(new RecipeDefinition + { + Id = ship.Construction.RecipeId ?? $"{ship.Id}-construction", + Label = $"{ship.Label} Construction", + FacilityCategory = ship.Construction.FacilityCategory, + Duration = ship.Construction.CycleTime, + Priority = ship.Construction.Priority, + RequiredModules = ship.Construction.RequiredModules.ToList(), + Inputs = ship.Construction.Requirements + .Select((input) => new RecipeInputDefinition + { + ItemId = input.ItemId, + Amount = input.Amount, + }) + .ToList(), + ShipOutputId = ship.Id, + }); + } + + return recipes; + } + private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale); private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f); diff --git a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs b/apps/backend/Simulation/SimulationEngine.MovementSystem.cs index b6f1f2e..b9bd9d4 100644 --- a/apps/backend/Simulation/SimulationEngine.MovementSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.MovementSystem.cs @@ -2,312 +2,274 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private const float WarpEngageDistanceKilometers = 250_000f; + private const float WarpEngageDistanceKilometers = 250_000f; - private static float GetLocalTravelSpeed(ShipRuntime ship) => - SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed); + private static float GetLocalTravelSpeed(ShipRuntime ship) => + SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed); - private static float GetWarpTravelSpeed(ShipRuntime ship) => - SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed); + private static float GetWarpTravelSpeed(ShipRuntime ship) => + SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed); - private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => - world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position - ?? Vector3.Zero; + private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => + world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position + ?? Vector3.Zero; - private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var task = ship.ControllerTask; + return task.Kind switch { - var task = ship.ControllerTask; - return task.Kind switch - { - ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds), - ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds), - ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds), - ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds), - ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds), - ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds), - ControllerTaskKind.Refuel => UpdateRefuel(ship, world, deltaSeconds), - ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds), - ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds), - ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds), - ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds), - ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds), - ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds), - _ => UpdateIdle(ship, world, deltaSeconds), - }; + ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds), + ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds), + ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds), + ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds), + ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds), + ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds), + ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds), + ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds), + ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds), + ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds), + ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds), + ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds), + _ => UpdateIdle(ship, world, deltaSeconds), + }; + } + + private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var task = ship.ControllerTask; + if (task.TargetPosition is null || task.TargetSystemId is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; } - private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var targetPosition = task.TargetPosition.Value; + var targetNode = ResolveTravelTargetNode(world, task, targetPosition); + ship.TargetPosition = targetPosition; + + if (ship.SystemId != task.TargetSystemId) { - TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds); - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; + var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId); + var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero; + return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode); } - private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var currentNode = ResolveCurrentNode(world, ship); + if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal)) { - var task = ship.ControllerTask; - if (task.TargetPosition is null || task.TargetSystemId is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var targetPosition = task.TargetPosition.Value; - var targetNode = ResolveTravelTargetNode(world, task, targetPosition); - ship.TargetPosition = targetPosition; - - if (ship.SystemId != task.TargetSystemId) - { - var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId); - var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero; - return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode); - } - - var currentNode = ResolveCurrentNode(world, ship); - if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal)) - { - return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode); - } - - return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold); + return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode); } - private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition) + return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold); + } + + private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition) + { + if (!string.IsNullOrWhiteSpace(task.TargetEntityId)) { - if (!string.IsNullOrWhiteSpace(task.TargetEntityId)) - { - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); - if (station?.NodeId is not null) - { - return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId); - } + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); + if (station?.NodeId is not null) + { + return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId); + } - var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); - if (node is not null) - { - return node; - } - } - - return world.SpatialNodes - .Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId) - .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) - .FirstOrDefault(); + var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); + if (node is not null) + { + return node; + } } - private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship) - { - if (ship.SpatialState.CurrentNodeId is not null) - { - return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId); - } + return world.SpatialNodes + .Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId) + .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) + .FirstOrDefault(); + } - return world.SpatialNodes - .Where(candidate => candidate.SystemId == ship.SystemId) - .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) - .FirstOrDefault(); + private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship) + { + if (ship.SpatialState.CurrentNodeId is not null) + { + return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId); } - private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) => - world.SpatialNodes.FirstOrDefault(candidate => - candidate.SystemId == systemId && - candidate.Kind == SpatialNodeKind.Star); + return world.SpatialNodes + .Where(candidate => candidate.SystemId == ship.SystemId) + .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) + .FirstOrDefault(); + } - private string UpdateLocalTravel( - ShipRuntime ship, - SimulationWorld world, - float deltaSeconds, - string targetSystemId, - Vector3 targetPosition, - NodeRuntime? targetNode, - float threshold) + private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) => + world.SpatialNodes.FirstOrDefault(candidate => + candidate.SystemId == systemId && + candidate.Kind == SpatialNodeKind.Star); + + private string UpdateLocalTravel( + ShipRuntime ship, + SimulationWorld world, + float deltaSeconds, + string targetSystemId, + Vector3 targetPosition, + NodeRuntime? targetNode, + float threshold) + { + var distance = ship.Position.DistanceTo(targetPosition); + ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.Transit = null; + ship.SpatialState.DestinationNodeId = targetNode?.Id; + + if (distance <= threshold) { - var distance = ship.Position.DistanceTo(targetPosition); - ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; - ship.SpatialState.Transit = null; - ship.SpatialState.DestinationNodeId = targetNode?.Id; + ship.ActionTimer = 0f; + ship.Position = targetPosition; + ship.TargetPosition = ship.Position; + ship.SystemId = targetSystemId; + ship.SpatialState.CurrentNodeId = targetNode?.Id; + ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; + ship.State = ShipState.Arriving; + return "arrived"; + } - if (distance <= threshold) - { - ship.ActionTimer = 0f; - TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds); - ship.Position = targetPosition; - ship.TargetPosition = ship.Position; - ship.SystemId = targetSystemId; - ship.SpatialState.CurrentNodeId = targetNode?.Id; - ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; - ship.State = ShipState.Arriving; - return "arrived"; - } + ship.ActionTimer = 0f; + ship.State = ShipState.LocalFlight; + ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; + } - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } + private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode) + { + var transit = ship.SpatialState.Transit; + if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id) + { + transit = new ShipTransitRuntime + { + Regime = MovementRegimeKinds.Warp, + OriginNodeId = ship.SpatialState.CurrentNodeId, + DestinationNodeId = targetNode.Id, + StartedAtUtc = world.GeneratedAtUtc, + }; + ship.SpatialState.Transit = transit; + } + ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace; + ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp; + ship.SpatialState.CurrentNodeId = null; + ship.SpatialState.CurrentBubbleId = null; + ship.SpatialState.DestinationNodeId = targetNode.Id; + + var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); + if (ship.State != ShipState.Warping) + { + if (ship.State != ShipState.SpoolingWarp) + { ship.ActionTimer = 0f; - ship.State = ShipState.LocalFlight; - ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + } + + ship.State = ShipState.SpoolingWarp; + if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration)) + { return "none"; + } + + ship.State = ShipState.Warping; } - private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode) + var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null + ? ship.Position.DistanceTo(targetPosition) + : (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); + ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds); + transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); + return ship.Position.DistanceTo(targetPosition) <= 18f + ? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode) + : "none"; + } + + private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) + { + var destinationNodeId = targetNode?.Id; + var transit = ship.SpatialState.Transit; + if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId) { - var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id) - { - transit = new ShipTransitRuntime - { - Regime = MovementRegimeKinds.Warp, - OriginNodeId = ship.SpatialState.CurrentNodeId, - DestinationNodeId = targetNode.Id, - StartedAtUtc = world.GeneratedAtUtc, - }; - ship.SpatialState.Transit = transit; - } - - ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp; - ship.SpatialState.CurrentNodeId = null; - ship.SpatialState.CurrentBubbleId = null; - ship.SpatialState.DestinationNodeId = targetNode.Id; - - var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); - if (ship.State != ShipState.Warping) - { - if (ship.State != ShipState.SpoolingWarp) - { - ship.ActionTimer = 0f; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.SpoolingWarp; - if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration)) - { - return "none"; - } - - ship.State = ShipState.Warping; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null - ? ship.Position.DistanceTo(targetPosition) - : (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); - ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds); - transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); - return ship.Position.DistanceTo(targetPosition) <= 18f - ? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode) - : "none"; + transit = new ShipTransitRuntime + { + Regime = MovementRegimeKinds.FtlTransit, + OriginNodeId = ship.SpatialState.CurrentNodeId, + DestinationNodeId = destinationNodeId, + StartedAtUtc = world.GeneratedAtUtc, + }; + ship.SpatialState.Transit = transit; } - private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) - { - var destinationNodeId = targetNode?.Id; - var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId) - { - transit = new ShipTransitRuntime - { - Regime = MovementRegimeKinds.FtlTransit, - OriginNodeId = ship.SpatialState.CurrentNodeId, - DestinationNodeId = destinationNodeId, - StartedAtUtc = world.GeneratedAtUtc, - }; - ship.SpatialState.Transit = transit; - } - - ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit; - ship.SpatialState.CurrentNodeId = null; - ship.SpatialState.CurrentBubbleId = null; - ship.SpatialState.DestinationNodeId = destinationNodeId; - - if (ship.State != ShipState.Ftl) - { - if (ship.State != ShipState.SpoolingFtl) - { - ship.ActionTimer = 0f; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.SpoolingFtl; - if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime)) - { - return "none"; - } - - ship.State = ShipState.Ftl; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId); - var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); - var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); - transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance)); - return transit.Progress >= 0.999f - ? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode) - : "none"; - } - - private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) + ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace; + ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit; + ship.SpatialState.CurrentNodeId = null; + ship.SpatialState.CurrentBubbleId = null; + ship.SpatialState.DestinationNodeId = destinationNodeId; + + if (ship.State != ShipState.Ftl) { + if (ship.State != ShipState.SpoolingFtl) + { ship.ActionTimer = 0f; - ship.Position = targetPosition; - ship.TargetPosition = targetPosition; - ship.SystemId = targetSystemId; - ship.SpatialState.Transit = null; - ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; - ship.SpatialState.CurrentNodeId = targetNode?.Id; - ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; - ship.SpatialState.DestinationNodeId = targetNode?.Id; - ship.State = ShipState.Arriving; - return "arrived"; - } + } - private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) - { - ship.ActionTimer = 0f; - ship.Position = targetPosition; - ship.TargetPosition = targetPosition; - ship.SystemId = targetSystemId; - ship.SpatialState.Transit = null; - ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; - ship.SpatialState.CurrentNodeId = targetNode?.Id; - ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; - ship.SpatialState.DestinationNodeId = targetNode?.Id; - ship.State = ShipState.Arriving; + ship.State = ShipState.SpoolingFtl; + if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime)) + { return "none"; + } + + ship.State = ShipState.Ftl; } + + var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId); + var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); + var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); + transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance)); + return transit.Progress >= 0.999f + ? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode) + : "none"; + } + + private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) + { + ship.ActionTimer = 0f; + ship.Position = targetPosition; + ship.TargetPosition = targetPosition; + ship.SystemId = targetSystemId; + ship.SpatialState.Transit = null; + ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.CurrentNodeId = targetNode?.Id; + ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; + ship.SpatialState.DestinationNodeId = targetNode?.Id; + ship.State = ShipState.Arriving; + return "arrived"; + } + + private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) + { + ship.ActionTimer = 0f; + ship.Position = targetPosition; + ship.TargetPosition = targetPosition; + ship.SystemId = targetSystemId; + ship.SpatialState.Transit = null; + ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.CurrentNodeId = targetNode?.Id; + ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId; + ship.SpatialState.DestinationNodeId = targetNode?.Id; + ship.State = ShipState.Arriving; + return "none"; + } } diff --git a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs b/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs index 97dffbd..8e7f0d0 100644 --- a/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.OrbitalSystem.cs @@ -57,7 +57,7 @@ public sealed partial class SimulationEngine private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node) { - var baseSpeed = node.SourceKind == "gas-cloud" ? 0.16f : 0.24f; + var baseSpeed = 0.24f; return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f)); } diff --git a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs b/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs index 6264e98..0ecf7d0 100644 --- a/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs +++ b/apps/backend/Simulation/SimulationEngine.PowerAndInventorySystems.cs @@ -5,560 +5,184 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private const float StationEnergyCellToEnergyRatio = 1f; + private static bool HasShipModules(ShipDefinition definition, params string[] modules) => + modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); - private static bool HasShipModules(ShipDefinition definition, params string[] modules) => - modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); + private static bool CanTransportWorkers(ShipRuntime ship) => + CountModules(ship.Definition.Modules, "habitat-ring") > 0; - private static bool CanTransportWorkers(ShipRuntime ship) => - CountModules(ship.Definition.Modules, "habitat-ring") > 0; + private static float GetWorkerTransportCapacity(ShipRuntime ship) => + CountModules(ship.Definition.Modules, "habitat-ring") * 120f; - private static float GetWorkerTransportCapacity(ShipRuntime ship) => - CountModules(ship.Definition.Modules, "habitat-ring") * 120f; + private static int CountStationModules(StationRuntime station, string moduleId) => + station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal)); - private static void UpdateStationPower(SimulationWorld world, float deltaSeconds, ICollection events) + private static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) + { + if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition)) { - foreach (var station in world.Stations) - { - var previousEnergy = station.EnergyStored; - GenerateStationEnergy(station, world, deltaSeconds); - - if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "fuel") <= 0.01f) - { - events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow)); - } - } + return; } - private static void UpdateShipPower(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ICollection events) + station.Modules.Add(new StationModuleRuntime { - var previousEnergy = ship.EnergyStored; - GenerateShipEnergy(ship, world, deltaSeconds); + Id = $"{station.Id}-module-{station.Modules.Count + 1}", + ModuleId = moduleId, + Health = definition.Hull, + MaxHealth = definition.Hull, + }); + station.Radius = GetStationRadius(world, station); + } - if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "fuel") <= 0.01f) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow)); - } + private static float GetStationRadius(SimulationWorld world, StationRuntime station) + { + var totalArea = station.Modules + .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) + .Sum(); + return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); + } + + private static float GetStationStorageCapacity(StationRuntime station, string storageClass) + { + var baseCapacity = storageClass switch + { + "manufactured" => 400f, + _ => 0f, + }; + + var bulkBays = CountStationModules(station, "bulk-bay"); + var liquidTanks = CountStationModules(station, "liquid-tank"); + var containerBays = CountStationModules(station, "container-bay"); + + var moduleCapacity = storageClass switch + { + "bulk-solid" => bulkBays * 1000f, + "bulk-liquid" => liquidTanks * 500f, + "container" => containerBays * 800f, + "manufactured" => containerBays * 200f, + _ => 0f, + }; + + return baseCapacity + moduleCapacity; + } + + private static int CountModules(IEnumerable modules, string moduleId) => + modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); + + private static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => + inventory.TryGetValue(itemId, out var amount) ? amount : 0f; + + private static void AddInventory(IDictionary inventory, string itemId, float amount) + { + if (amount <= 0f) + { + return; } - private static void GenerateStationEnergy(StationRuntime station, SimulationWorld world, float deltaSeconds) + inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId) + amount; + } + + private static float RemoveInventory(IDictionary inventory, string itemId, float amount) + { + var current = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId); + var removed = MathF.Min(current, amount); + var remaining = current - removed; + if (remaining <= 0.001f) { - var powerCores = CountModules(station.InstalledModules, "power-core"); - var tanks = CountModules(station.InstalledModules, "liquid-tank"); - if (powerCores <= 0 || tanks <= 0) - { - station.EnergyStored = 0f; - station.Inventory.Remove("fuel"); - return; - } - - var energyCapacity = powerCores * StationEnergyPerPowerCore; - var fuelStored = GetInventoryAmount(station.Inventory, "fuel"); - var desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored); - if (desiredEnergy <= 0.01f) - { - station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity); - station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank); - return; - } - - var solarGenerated = MathF.Min(desiredEnergy, GetStationSolarGeneration(station, world) * deltaSeconds); - if (solarGenerated > 0.01f) - { - station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + solarGenerated); - desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored); - } - - if (desiredEnergy > 0.01f && fuelStored <= 0.01f) - { - var energyCells = GetInventoryAmount(station.Inventory, "energy-cell"); - if (energyCells > 0.01f) - { - var consumedCells = MathF.Min(energyCells, desiredEnergy / StationEnergyCellToEnergyRatio); - RemoveInventory(station.Inventory, "energy-cell", consumedCells); - station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + (consumedCells * StationEnergyCellToEnergyRatio)); - desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored); - } - } - - if (desiredEnergy <= 0.01f || fuelStored <= 0.01f) - { - station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity); - station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank); - return; - } - - var generated = MathF.Min(desiredEnergy, powerCores * 24f * deltaSeconds); - var requiredFuel = generated / StationFuelToEnergyRatio; - var consumedFuel = MathF.Min(requiredFuel, fuelStored); - var actualGenerated = consumedFuel * StationFuelToEnergyRatio; - - RemoveInventory(station.Inventory, "fuel", consumedFuel); - station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + actualGenerated); + inventory.Remove(itemId); + } + else + { + inventory[itemId] = remaining; } - private static float GetStationFuelCapacity(StationRuntime station) => - CountModules(station.InstalledModules, "liquid-tank") * StationFuelPerTank; + return removed; + } - private static float GetStationEnergyCapacity(StationRuntime station) => - CountModules(station.InstalledModules, "power-core") * StationEnergyPerPowerCore; + private static bool HasStationModules(StationRuntime station, params string[] modules) => + modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); - private static float GetStationSolarGeneration(StationRuntime station, SimulationWorld world) => - world.Balance.Energy.StationSolarCharge * (1f + CountModules(station.InstalledModules, "solar-array")); + private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) => + node.ItemId switch + { + "ore" => HasShipModules(ship.Definition, "mining-turret"), + _ => false, + }; - private static float GetStationStorageCapacity(StationRuntime station, string storageClass) + private static bool CanBuildClaimBeacon(ShipRuntime ship) => + string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal); + + private static float ComputeWorkforceRatio(float population, float workforceRequired) + { + if (workforceRequired <= 0.01f) { - var baseCapacity = station.Definition.Storage.TryGetValue(storageClass, out var capacity) - ? capacity - : 0f; - - var extraBulkBays = Math.Max(0, CountModules(station.InstalledModules, "bulk-bay") - CountModules(station.Definition.Modules, "bulk-bay")); - var extraLiquidTanks = Math.Max(0, CountModules(station.InstalledModules, "liquid-tank") - CountModules(station.Definition.Modules, "liquid-tank")); - var extraGasTanks = Math.Max(0, CountModules(station.InstalledModules, "gas-tank") - CountModules(station.Definition.Modules, "gas-tank")); - var extraContainerBays = Math.Max(0, CountModules(station.InstalledModules, "container-bay") - CountModules(station.Definition.Modules, "container-bay")); - var moduleBonus = storageClass switch - { - "bulk-solid" => extraBulkBays * 1000f, - "bulk-liquid" => extraLiquidTanks * 500f, - "bulk-gas" => extraGasTanks * 500f, - "container" => extraContainerBays * 800f, - _ => 0f, - }; - - return baseCapacity + moduleBonus; + return 1f; } - private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var staffedRatio = MathF.Min(1f, population / workforceRequired); + return 0.1f + (0.9f * staffedRatio); + } + + private static string? GetStorageRequirement(string storageClass) => + storageClass switch + { + "bulk-solid" => "bulk-bay", + "bulk-liquid" => "liquid-tank", + _ => null, + }; + + private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) + { + if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) { - var reactors = CountModules(ship.Definition.Modules, "reactor-core"); - var capacitors = CountModules(ship.Definition.Modules, "capacitor-bank"); - if (reactors <= 0 || capacitors <= 0) - { - ship.EnergyStored = 0f; - ship.Inventory.Remove("fuel"); - return; - } - - var energyCapacity = capacitors * CapacitorEnergyPerModule; - var fuelCapacity = reactors * ShipFuelPerReactor; - var fuelStored = GetInventoryAmount(ship.Inventory, "fuel"); - var desiredEnergy = MathF.Max(0f, energyCapacity - ship.EnergyStored); - if (desiredEnergy <= 0.01f || fuelStored <= 0.01f) - { - ship.EnergyStored = MathF.Min(ship.EnergyStored, energyCapacity); - ship.Inventory["fuel"] = MathF.Min(fuelStored, fuelCapacity); - return; - } - - var generated = MathF.Min(desiredEnergy, world.Balance.Energy.ShipRechargeRate * reactors * deltaSeconds); - var requiredFuel = generated / ShipFuelToEnergyRatio; - var consumedFuel = MathF.Min(requiredFuel, fuelStored); - var actualGenerated = consumedFuel * ShipFuelToEnergyRatio; - - RemoveInventory(ship.Inventory, "fuel", consumedFuel); - ship.EnergyStored = MathF.Min(energyCapacity, ship.EnergyStored + actualGenerated); + return 0f; } - private static bool TryConsumeShipEnergy(ShipRuntime ship, float amount) + var storageClass = itemDefinition.CargoKind; + var requiredModule = GetStorageRequirement(storageClass); + if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) { - if (ship.EnergyStored + 0.0001f < amount) - { - return false; - } - - ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount); - return true; + return 0f; } - private static bool TryConsumeStationEnergy(StationRuntime station, float amount) + var capacity = GetStationStorageCapacity(station, storageClass); + if (capacity <= 0.01f) { - if (station.EnergyStored + 0.0001f < amount) - { - return false; - } - - station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount); - return true; + return 0f; } - private static int CountModules(IEnumerable modules, string moduleId) => - modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); - - private static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => - inventory.TryGetValue(itemId, out var amount) ? amount : 0f; - - private static void AddInventory(IDictionary inventory, string itemId, float amount) + var used = station.Inventory + .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass) + .Sum(entry => entry.Value); + var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); + if (accepted <= 0.01f) { - if (amount <= 0f) - { - return; - } - - inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId) + amount; + return 0f; } - private static float RemoveInventory(IDictionary inventory, string itemId, float amount) - { - var current = GetInventoryAmount((IReadOnlyDictionary)inventory, itemId); - var removed = MathF.Min(current, amount); - var remaining = current - removed; - if (remaining <= 0.001f) - { - inventory.Remove(itemId); - } - else - { - inventory[itemId] = remaining; - } + AddInventory(station.Inventory, itemId, accepted); + return accepted; + } - return removed; + private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => + recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount); + + private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) => + world.ConstructionSites.FirstOrDefault(site => + string.Equals(site.StationId, stationId, StringComparison.Ordinal) + && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed); + + private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId) + { + if (site.StationId is not null + && world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station) + { + return GetInventoryAmount(station.Inventory, itemId); } - private static bool HasStationModules(StationRuntime station, params string[] modules) => - modules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); + return GetInventoryAmount(site.DeliveredItems, itemId); + } - private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) => - node.ItemId switch - { - "ore" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "mining-turret"), - "gas" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "gas-extractor"), - _ => false, - }; - - private static bool CanBuildClaimBeacon(ShipRuntime ship) => - string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal); - - private static float GetShipFuelCapacity(ShipRuntime ship) => - CountModules(ship.Definition.Modules, "reactor-core") * ShipFuelPerReactor; - - private static float GetShipAvailableEnergyBudget(ShipRuntime ship) => - ship.EnergyStored + (GetInventoryAmount(ship.Inventory, "fuel") * ShipFuelToEnergyRatio); - - private static float GetShipFuelReserve(ShipRuntime ship, float plannedFuel) - { - var capacity = GetShipFuelCapacity(ship); - var reserveRatio = ship.Definition.CargoItemId == "gas" ? 0.4f : 0.3f; - var reserve = MathF.Max(16f, MathF.Max(capacity * 0.18f, plannedFuel * reserveRatio)); - return MathF.Min(capacity, reserve); - } - - private static float EstimateFuelForEnergyDemand(ShipRuntime ship, float energyDemand) => - MathF.Max(0f, energyDemand - ship.EnergyStored) / ShipFuelToEnergyRatio; - - private static float EstimateTimedEnergyUse(SimulationWorld world, float durationSeconds, float drainPerSecond) => - MathF.Max(0f, durationSeconds) * drainPerSecond; - - private static float EstimateTravelEnergy( - ShipRuntime ship, - SimulationWorld world, - Vector3 fromPosition, - string fromSystemId, - Vector3 toPosition, - string toSystemId) - { - if (!string.Equals(fromSystemId, toSystemId, StringComparison.Ordinal)) - { - var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId); - var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition; - var originSystemPosition = ResolveSystemGalaxyPosition(world, fromSystemId); - var destinationSystemPosition = ResolveSystemGalaxyPosition(world, toSystemId); - var ftlDistance = originSystemPosition.DistanceTo(destinationSystemPosition); - var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f); - return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain) - + EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain) - + EstimateInSystemTravelEnergy(ship, world, destinationEntryPosition, toPosition); - } - - return EstimateInSystemTravelEnergy(ship, world, fromPosition, toPosition); - } - - private static float EstimateInSystemTravelEnergy(ShipRuntime ship, SimulationWorld world, Vector3 fromPosition, Vector3 toPosition) - { - var distance = fromPosition.DistanceTo(toPosition); - if (distance <= world.Balance.ArrivalThreshold) - { - return 0f; - } - - if (distance <= WarpEngageDistanceKilometers) - { - var localDuration = distance / MathF.Max(GetLocalTravelSpeed(ship), 0.01f); - return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain); - } - - var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); - var warpDuration = distance / MathF.Max(GetWarpTravelSpeed(ship), 0.01f); - return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain) - + EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain); - } - - private static float EstimateDockingEnergy(SimulationWorld world) => - EstimateTimedEnergyUse(world, world.Balance.DockingDuration, world.Balance.Energy.MoveDrain) - + EstimateTimedEnergyUse(world, 6f, world.Balance.Energy.IdleDrain); - - private static float EstimateUndockingEnergy(SimulationWorld world) => - EstimateTimedEnergyUse(world, world.Balance.UndockingDuration, world.Balance.Energy.MoveDrain) - + EstimateTimedEnergyUse(world, 4f, world.Balance.Energy.IdleDrain); - - private static float EstimateExtractionEnergy(ShipRuntime ship, SimulationWorld world) - { - var remainingCargo = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); - if (remainingCargo <= 0.01f) - { - return 0f; - } - - var cycles = MathF.Ceiling(remainingCargo / MathF.Max(world.Balance.MiningRate, 0.01f)); - return EstimateTimedEnergyUse(world, cycles * world.Balance.MiningCycleSeconds, world.Balance.Energy.MoveDrain) - + EstimateTimedEnergyUse(world, cycles * 1.5f, world.Balance.Energy.IdleDrain); - } - - private static float EstimateConstructionEnergy(ShipRuntime ship, SimulationWorld world, StationRuntime station) - { - var holdPosition = GetConstructionHoldPosition(station, ship.Id); - var travelEnergy = EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, holdPosition, station.SystemId); - var site = GetConstructionSiteForStation(world, station.Id); - if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site)) - { - if (world.ModuleRecipes.TryGetValue(site.BlueprintId ?? string.Empty, out var siteRecipe)) - { - return travelEnergy + EstimateTimedEnergyUse(world, siteRecipe.Duration, world.Balance.Energy.IdleDrain); - } - - return travelEnergy; - } - - var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world); - if (moduleId is not null - && world.ModuleRecipes.TryGetValue(moduleId, out var recipe) - && CanStartModuleConstruction(station, recipe)) - { - return travelEnergy + EstimateTimedEnergyUse(world, recipe.Duration, world.Balance.Energy.IdleDrain); - } - - return travelEnergy; - } - - private static float EstimateResourceHarvestEnergy(ShipRuntime ship, SimulationWorld world) - { - var cargoItemId = ship.Definition.CargoItemId; - if (cargoItemId is null) - { - return 0f; - } - - var requiredModule = cargoItemId == "gas" ? "gas-extractor" : "mining-turret"; - var behavior = ship.DefaultBehavior; - var refinery = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId); - var node = behavior.NodeId is null - ? world.Nodes - .Where(candidate => - (behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) && - candidate.ItemId == cargoItemId && - candidate.OreRemaining > 0.01f) - .OrderByDescending(candidate => candidate.OreRemaining) - .FirstOrDefault() - : world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f); - if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule)) - { - return 0f; - } - - var currentPosition = ship.Position; - var currentSystemId = ship.SystemId; - var energy = 0f; - var cargoAmount = GetShipCargoAmount(ship); - if (ship.DockedStationId == refinery.Id) - { - currentPosition = GetUndockTargetPosition(refinery, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); - currentSystemId = refinery.SystemId; - energy += EstimateUndockingEnergy(world); - } - - if (cargoAmount > 0.01f) - { - energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId); - return energy + EstimateDockingEnergy(world); - } - - var holdPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); - energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, holdPosition, node.SystemId); - energy += EstimateExtractionEnergy(ship, world); - energy += EstimateTravelEnergy(ship, world, holdPosition, node.SystemId, refinery.Position, refinery.SystemId); - energy += EstimateDockingEnergy(world); - return energy; - } - - private static float EstimateResourceReturnEnergy(ShipRuntime ship, SimulationWorld world) - { - var cargoItemId = ship.Definition.CargoItemId; - if (cargoItemId is null) - { - return 0f; - } - - var refinery = SelectBestBuyStation(world, ship, cargoItemId, ship.DefaultBehavior.StationId); - if (refinery is null) - { - return 0f; - } - - var currentPosition = ship.Position; - var currentSystemId = ship.SystemId; - return EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId) - + EstimateDockingEnergy(world); - } - - private static float EstimateTransportEnergy(ShipRuntime ship, SimulationWorld world) - { - var cargoItemId = ship.Definition.CargoItemId; - if (cargoItemId is null) - { - return 0f; - } - - var behavior = ship.DefaultBehavior; - var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId); - var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId); - if (source is null && destination is null) - { - return 0f; - } - - var cargoAmount = GetShipCargoAmount(ship); - var currentPosition = ship.Position; - var currentSystemId = ship.SystemId; - if (ship.DockedStationId is not null) - { - var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (dockedStation is not null) - { - currentPosition = GetUndockTargetPosition(dockedStation, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); - currentSystemId = dockedStation.SystemId; - } - } - - var targetStation = cargoAmount > 0.01f ? destination : source; - if (targetStation is null) - { - return ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f; - } - - var energy = ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f; - energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, targetStation.Position, targetStation.SystemId); - return energy + EstimateDockingEnergy(world); - } - - private static float EstimateShipMissionEnergyDemand(ShipRuntime ship, SimulationWorld world) => - ship.DefaultBehavior.Kind switch - { - "auto-mine" or "auto-harvest-gas" => EstimateResourceHarvestEnergy(ship, world), - "auto-supply-energy" => EstimateTransportEnergy(ship, world), - "construct-station" when ship.DefaultBehavior.StationId is not null - => world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) is { } station - ? EstimateConstructionEnergy(ship, world, station) - : 0f, - _ when ship.ControllerTask.TargetPosition is { } targetPosition && ship.ControllerTask.TargetSystemId is { } targetSystemId - => EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, targetPosition, targetSystemId), - _ => 0f, - }; - - private static float GetShipRefuelTarget(ShipRuntime ship, SimulationWorld world) - { - var capacity = GetShipFuelCapacity(ship); - var missionFuel = EstimateFuelForEnergyDemand(ship, EstimateShipMissionEnergyDemand(ship, world)); - var reserveFuel = GetShipFuelReserve(ship, missionFuel); - return MathF.Min(capacity, missionFuel + reserveFuel); - } - - internal static bool NeedsRefuel(ShipRuntime ship, SimulationWorld world) => - GetInventoryAmount(ship.Inventory, "fuel") + 0.01f < GetShipRefuelTarget(ship, world); - - internal static bool NeedsEmergencyReturn(ShipRuntime ship, SimulationWorld world) - { - if (ship.DefaultBehavior.Kind is not "auto-mine" and not "auto-harvest-gas") - { - return false; - } - - var returnEnergy = EstimateResourceReturnEnergy(ship, world); - var reserveFuel = GetShipFuelReserve(ship, EstimateFuelForEnergyDemand(ship, returnEnergy)); - var requiredBudget = returnEnergy + (reserveFuel * ShipFuelToEnergyRatio); - return GetShipAvailableEnergyBudget(ship) + 0.01f < requiredBudget; - } - - private static float ComputeWorkforceRatio(float population, float workforceRequired) - { - if (workforceRequired <= 0.01f) - { - return 1f; - } - - var staffedRatio = MathF.Min(1f, population / workforceRequired); - return 0.1f + (0.9f * staffedRatio); - } - - private static string? GetStorageRequirement(string storageClass) => - storageClass switch - { - "bulk-solid" => "bulk-bay", - "bulk-liquid" => "liquid-tank", - "bulk-gas" => "gas-tank", - _ => null, - }; - - private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) - { - if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) - { - return 0f; - } - - var storageClass = itemDefinition.Storage; - var requiredModule = GetStorageRequirement(storageClass); - if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) - { - return 0f; - } - - var capacity = GetStationStorageCapacity(station, storageClass); - if (capacity <= 0.01f) - { - return 0f; - } - - var used = station.Inventory - .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == storageClass) - .Sum(entry => entry.Value); - var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); - if (accepted <= 0.01f) - { - return 0f; - } - - AddInventory(station.Inventory, itemId, accepted); - return accepted; - } - - private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => - recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount); - - private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) => - world.ConstructionSites.FirstOrDefault(site => - string.Equals(site.StationId, stationId, StringComparison.Ordinal) - && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed); - - private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId) - { - if (site.StationId is not null - && world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station) - { - return GetInventoryAmount(station.Inventory, itemId); - } - - return GetInventoryAmount(site.DeliveredItems, itemId); - } - - private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) => - site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value); + private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) => + site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value); } diff --git a/apps/backend/Simulation/SimulationEngine.Replication.cs b/apps/backend/Simulation/SimulationEngine.Replication.cs index 7892253..699f603 100644 --- a/apps/backend/Simulation/SimulationEngine.Replication.cs +++ b/apps/backend/Simulation/SimulationEngine.Replication.cs @@ -4,808 +4,816 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) - { - PrimeDeltaBaseline(world); + public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) + { + PrimeDeltaBaseline(world); - return new WorldSnapshot( - world.Label, - world.Seed, - sequence, - world.TickIntervalMs, - world.OrbitalTimeSeconds, - new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond), - world.GeneratedAtUtc, - world.Systems.Select(system => new SystemSnapshot( - system.Definition.Id, - system.Definition.Label, - ToDto(system.Position), - system.Definition.StarKind, - system.Definition.StarCount, - system.Definition.StarColor, - system.Definition.StarSize, - system.Definition.Planets.Select(planet => new PlanetSnapshot( - planet.Label, - planet.PlanetType, - planet.Shape, - planet.MoonCount, - planet.OrbitRadius, - planet.OrbitSpeed, - planet.OrbitEccentricity, - planet.OrbitInclination, - planet.OrbitLongitudeOfAscendingNode, - planet.OrbitArgumentOfPeriapsis, - planet.OrbitPhaseAtEpoch, - planet.Size, - planet.Color, - planet.HasRing)).ToList())).ToList(), - world.SpatialNodes.Select(ToSpatialNodeDelta).Select(node => new SpatialNodeSnapshot( - node.Id, - node.SystemId, - node.Kind, - node.LocalPosition, - node.BubbleId, - node.ParentNodeId, - node.OccupyingStructureId, - node.OrbitReferenceId)).ToList(), - world.LocalBubbles.Select(ToLocalBubbleDelta).Select(bubble => new LocalBubbleSnapshot( - bubble.Id, - bubble.NodeId, - bubble.SystemId, - bubble.Radius, - bubble.OccupantShipIds, - bubble.OccupantStationIds, - bubble.OccupantClaimIds, - bubble.OccupantConstructionSiteIds)).ToList(), - world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot( - node.Id, - node.SystemId, - node.LocalPosition, - node.AnchorNodeId, - node.SourceKind, - node.OreRemaining, - node.MaxOre, - node.ItemId)).ToList(), - world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot( - station.Id, - station.Label, - station.Category, - station.SystemId, - station.LocalPosition, - station.NodeId, - station.BubbleId, - station.AnchorNodeId, - station.Color, - station.DockedShips, - station.DockedShipIds, - station.DockingPads, - station.FuelStored, - station.FuelCapacity, - station.EnergyStored, - station.EnergyCapacity, - station.CurrentProcesses, - station.Inventory, - station.FactionId, - station.CommanderId, - station.PolicySetId, - station.Population, - station.PopulationCapacity, - station.WorkforceRequired, - station.WorkforceEffectiveRatio, - station.InstalledModules, - station.MarketOrderIds)).ToList(), - world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot( - claim.Id, - claim.FactionId, - claim.SystemId, - claim.NodeId, - claim.BubbleId, - claim.State, - claim.Health, - claim.PlacedAtUtc, - claim.ActivatesAtUtc)).ToList(), - world.ConstructionSites.Select(ToConstructionSiteDelta).Select(site => new ConstructionSiteSnapshot( - site.Id, - site.FactionId, - site.SystemId, - site.NodeId, - site.BubbleId, - site.TargetKind, - site.TargetDefinitionId, - site.BlueprintId, - site.ClaimId, - site.StationId, - site.State, - site.Progress, - site.Inventory, - site.RequiredItems, - site.DeliveredItems, - site.AssignedConstructorShipIds, - site.MarketOrderIds)).ToList(), - world.MarketOrders.Select(ToMarketOrderDelta).Select(order => new MarketOrderSnapshot( - order.Id, - order.FactionId, - order.StationId, - order.ConstructionSiteId, - order.Kind, - order.ItemId, - order.Amount, - order.RemainingAmount, - order.Valuation, - order.ReserveThreshold, - order.PolicySetId, - order.State)).ToList(), - world.Policies.Select(ToPolicySetDelta).Select(policy => new PolicySetSnapshot( - policy.Id, - policy.OwnerKind, - policy.OwnerId, - policy.TradeAccessPolicy, - policy.DockingAccessPolicy, - policy.ConstructionAccessPolicy, - policy.OperationalRangePolicy)).ToList(), - world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot( - ship.Id, - ship.Label, - ship.Role, - ship.ShipClass, - ship.SystemId, - ship.LocalPosition, - ship.LocalVelocity, - ship.TargetLocalPosition, - ship.State, - ship.OrderKind, - ship.DefaultBehaviorKind, - ship.ControllerTaskKind, - ship.NodeId, - ship.BubbleId, - ship.DockedStationId, - ship.CommanderId, - ship.PolicySetId, - ship.CargoCapacity, - ship.CargoItemId, - ship.WorkerPopulation, - ship.EnergyStored, - ship.TravelSpeed, - ship.TravelSpeedUnit, - ship.Inventory, - ship.FactionId, - ship.Health, - ship.History, - ship.CurrentAction, - ship.SpatialState)).ToList(), - world.Factions.Select(ToFactionDelta).Select(faction => new FactionSnapshot( - faction.Id, - faction.Label, - faction.Color, - faction.Credits, - faction.PopulationTotal, - faction.OreMined, - faction.GoodsProduced, - faction.ShipsBuilt, - faction.ShipsLost, - faction.DefaultPolicySetId)).ToList()); - } - - public void PrimeDeltaBaseline(SimulationWorld world) - { - foreach (var node in world.Nodes) - { - node.LastDeltaSignature = BuildNodeSignature(node); - } - - foreach (var node in world.SpatialNodes) - { - node.LastDeltaSignature = BuildSpatialNodeSignature(node); - } - - foreach (var bubble in world.LocalBubbles) - { - bubble.LastDeltaSignature = BuildLocalBubbleSignature(bubble); - } - - foreach (var station in world.Stations) - { - station.LastDeltaSignature = BuildStationSignature(world, station); - } - - foreach (var claim in world.Claims) - { - claim.LastDeltaSignature = BuildClaimSignature(claim); - } - - foreach (var site in world.ConstructionSites) - { - site.LastDeltaSignature = BuildConstructionSiteSignature(site); - } - - foreach (var order in world.MarketOrders) - { - order.LastDeltaSignature = BuildMarketOrderSignature(order); - } - - foreach (var policy in world.Policies) - { - policy.LastDeltaSignature = BuildPolicySignature(policy); - } - - foreach (var ship in world.Ships) - { - ship.LastDeltaSignature = BuildShipSignature(world, ship); - } - - foreach (var faction in world.Factions) - { - faction.LastDeltaSignature = BuildFactionSignature(faction); - } - } - - private static IReadOnlyList BuildNodeDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var node in world.Nodes) - { - var signature = BuildNodeSignature(node); - if (signature == node.LastDeltaSignature) - { - continue; - } - - node.LastDeltaSignature = signature; - deltas.Add(ToNodeDelta(node)); - } - - return deltas; - } - - private static IReadOnlyList BuildSpatialNodeDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var node in world.SpatialNodes) - { - var signature = BuildSpatialNodeSignature(node); - if (signature == node.LastDeltaSignature) - { - continue; - } - - node.LastDeltaSignature = signature; - deltas.Add(ToSpatialNodeDelta(node)); - } - - return deltas; - } - - private static IReadOnlyList BuildLocalBubbleDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var bubble in world.LocalBubbles) - { - var signature = BuildLocalBubbleSignature(bubble); - if (signature == bubble.LastDeltaSignature) - { - continue; - } - - bubble.LastDeltaSignature = signature; - deltas.Add(ToLocalBubbleDelta(bubble)); - } - - return deltas; - } - - private static IReadOnlyList BuildStationDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var station in world.Stations) - { - var signature = BuildStationSignature(world, station); - if (signature == station.LastDeltaSignature) - { - continue; - } - - station.LastDeltaSignature = signature; - deltas.Add(ToStationDelta(world, station)); - } - - return deltas; - } - - private static IReadOnlyList BuildClaimDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var claim in world.Claims) - { - var signature = BuildClaimSignature(claim); - if (signature == claim.LastDeltaSignature) - { - continue; - } - - claim.LastDeltaSignature = signature; - deltas.Add(ToClaimDelta(claim)); - } - - return deltas; - } - - private static IReadOnlyList BuildConstructionSiteDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var site in world.ConstructionSites) - { - var signature = BuildConstructionSiteSignature(site); - if (signature == site.LastDeltaSignature) - { - continue; - } - - site.LastDeltaSignature = signature; - deltas.Add(ToConstructionSiteDelta(site)); - } - - return deltas; - } - - private static IReadOnlyList BuildMarketOrderDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var order in world.MarketOrders) - { - var signature = BuildMarketOrderSignature(order); - if (signature == order.LastDeltaSignature) - { - continue; - } - - order.LastDeltaSignature = signature; - deltas.Add(ToMarketOrderDelta(order)); - } - - return deltas; - } - - private static IReadOnlyList BuildPolicyDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var policy in world.Policies) - { - var signature = BuildPolicySignature(policy); - if (signature == policy.LastDeltaSignature) - { - continue; - } - - policy.LastDeltaSignature = signature; - deltas.Add(ToPolicySetDelta(policy)); - } - - return deltas; - } - - private IReadOnlyList BuildShipDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var ship in world.Ships) - { - var signature = BuildShipSignature(world, ship); - if (signature == ship.LastDeltaSignature) - { - continue; - } - - ship.LastDeltaSignature = signature; - deltas.Add(ToShipDelta(world, ship)); - } - - return deltas; - } - - private static IReadOnlyList BuildFactionDeltas(SimulationWorld world) - { - var deltas = new List(); - foreach (var faction in world.Factions) - { - var signature = BuildFactionSignature(faction); - if (signature == faction.LastDeltaSignature) - { - continue; - } - - faction.LastDeltaSignature = signature; - deltas.Add(ToFactionDelta(faction)); - } - - return deltas; - } - - private static string BuildNodeSignature(ResourceNodeRuntime node) => - $"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}"; - - private static string BuildSpatialNodeSignature(NodeRuntime node) => - $"{node.SystemId}|{node.Kind.ToContractValue()}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.BubbleId}|{node.ParentNodeId}|{node.OccupyingStructureId}|{node.OrbitReferenceId}"; - - private static string BuildLocalBubbleSignature(LocalBubbleRuntime bubble) => - $"{bubble.SystemId}|{bubble.NodeId}|{bubble.Radius:0.###}|{string.Join(",", bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal))}"; - - private static string BuildStationSignature(SimulationWorld world, StationRuntime station) - { - var processes = ToStationActionProgressSnapshots(world, station); - return string.Join("|", + return new WorldSnapshot( + world.Label, + world.Seed, + sequence, + world.TickIntervalMs, + world.OrbitalTimeSeconds, + new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond), + world.GeneratedAtUtc, + world.Systems.Select(system => new SystemSnapshot( + system.Definition.Id, + system.Definition.Label, + ToDto(system.Position), + system.Definition.StarKind, + system.Definition.StarCount, + system.Definition.StarColor, + system.Definition.StarSize, + system.Definition.Planets.Select(planet => new PlanetSnapshot( + planet.Label, + planet.PlanetType, + planet.Shape, + planet.MoonCount, + planet.OrbitRadius, + planet.OrbitSpeed, + planet.OrbitEccentricity, + planet.OrbitInclination, + planet.OrbitLongitudeOfAscendingNode, + planet.OrbitArgumentOfPeriapsis, + planet.OrbitPhaseAtEpoch, + planet.Size, + planet.Color, + planet.HasRing)).ToList())).ToList(), + world.SpatialNodes.Select(ToSpatialNodeDelta).Select(node => new SpatialNodeSnapshot( + node.Id, + node.SystemId, + node.Kind, + node.LocalPosition, + node.BubbleId, + node.ParentNodeId, + node.OccupyingStructureId, + node.OrbitReferenceId)).ToList(), + world.LocalBubbles.Select(ToLocalBubbleDelta).Select(bubble => new LocalBubbleSnapshot( + bubble.Id, + bubble.NodeId, + bubble.SystemId, + bubble.Radius, + bubble.OccupantShipIds, + bubble.OccupantStationIds, + bubble.OccupantClaimIds, + bubble.OccupantConstructionSiteIds)).ToList(), + world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot( + node.Id, + node.SystemId, + node.LocalPosition, + node.AnchorNodeId, + node.SourceKind, + node.OreRemaining, + node.MaxOre, + node.ItemId)).ToList(), + world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot( + station.Id, + station.Label, + station.Category, station.SystemId, - station.NodeId ?? "none", - station.BubbleId ?? "none", - station.AnchorNodeId ?? "none", - station.CommanderId ?? "none", - station.PolicySetId ?? "none", - BuildInventorySignature(station.Inventory), - GetInventoryAmount(station.Inventory, "fuel").ToString("0.###"), - GetStationFuelCapacity(station).ToString("0.###"), - station.EnergyStored.ToString("0.###"), - GetStationEnergyCapacity(station).ToString("0.###"), - string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")), - string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)), - station.DockingPadAssignments.Count.ToString(), - station.Population.ToString("0.###"), - station.PopulationCapacity.ToString("0.###"), - station.WorkforceRequired.ToString("0.###"), - station.WorkforceEffectiveRatio.ToString("0.###"), - string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal)), - string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal)), - station.ActiveConstruction?.ModuleId ?? "none", - station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0", - string.Join(",", station.ProductionLaneTimers.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => $"{entry.Key}:{entry.Value:0.###}"))); - } - - private static string BuildClaimSignature(ClaimRuntime claim) => - $"{claim.FactionId}|{claim.SystemId}|{claim.NodeId}|{claim.BubbleId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}"; - - private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) => - $"{site.FactionId}|{site.SystemId}|{site.NodeId}|{site.BubbleId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}"; - - private static string BuildMarketOrderSignature(MarketOrderRuntime order) => - $"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}"; - - private static string BuildPolicySignature(PolicySetRuntime policy) => - $"{policy.OwnerKind}|{policy.OwnerId}|{policy.TradeAccessPolicy}|{policy.DockingAccessPolicy}|{policy.ConstructionAccessPolicy}|{policy.OperationalRangePolicy}"; - - private static string BuildShipSignature(SimulationWorld world, ShipRuntime ship) => - string.Join("|", + station.LocalPosition, + station.NodeId, + station.BubbleId, + station.AnchorNodeId, + station.Color, + station.DockedShips, + station.DockedShipIds, + station.DockingPads, + station.CurrentProcesses, + station.Inventory, + station.FactionId, + station.CommanderId, + station.PolicySetId, + station.Population, + station.PopulationCapacity, + station.WorkforceRequired, + station.WorkforceEffectiveRatio, + station.StorageUsage, + station.InstalledModules, + station.MarketOrderIds)).ToList(), + world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot( + claim.Id, + claim.FactionId, + claim.SystemId, + claim.NodeId, + claim.BubbleId, + claim.State, + claim.Health, + claim.PlacedAtUtc, + claim.ActivatesAtUtc)).ToList(), + world.ConstructionSites.Select(site => ToConstructionSiteDelta(world, site)).Select(site => new ConstructionSiteSnapshot( + site.Id, + site.FactionId, + site.SystemId, + site.NodeId, + site.BubbleId, + site.TargetKind, + site.TargetDefinitionId, + site.BlueprintId, + site.ClaimId, + site.StationId, + site.State, + site.Progress, + site.Inventory, + site.RequiredItems, + site.DeliveredItems, + site.AssignedConstructorShipIds, + site.MarketOrderIds)).ToList(), + world.MarketOrders.Select(ToMarketOrderDelta).Select(order => new MarketOrderSnapshot( + order.Id, + order.FactionId, + order.StationId, + order.ConstructionSiteId, + order.Kind, + order.ItemId, + order.Amount, + order.RemainingAmount, + order.Valuation, + order.ReserveThreshold, + order.PolicySetId, + order.State)).ToList(), + world.Policies.Select(ToPolicySetDelta).Select(policy => new PolicySetSnapshot( + policy.Id, + policy.OwnerKind, + policy.OwnerId, + policy.TradeAccessPolicy, + policy.DockingAccessPolicy, + policy.ConstructionAccessPolicy, + policy.OperationalRangePolicy)).ToList(), + world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot( + ship.Id, + ship.Label, + ship.Role, + ship.ShipClass, ship.SystemId, - ship.Position.X.ToString("0.###"), - ship.Position.Y.ToString("0.###"), - ship.Position.Z.ToString("0.###"), - ship.Velocity.X.ToString("0.###"), - ship.Velocity.Y.ToString("0.###"), - ship.Velocity.Z.ToString("0.###"), - ship.TargetPosition.X.ToString("0.###"), - ship.TargetPosition.Y.ToString("0.###"), - ship.TargetPosition.Z.ToString("0.###"), - ship.State.ToContractValue(), - ship.Order?.Kind ?? "none", - ship.DefaultBehavior.Kind, - ship.ControllerTask.Kind.ToContractValue(), - ship.SpatialState.CurrentNodeId ?? "none", - ship.SpatialState.CurrentBubbleId ?? "none", - ship.DockedStationId ?? "none", - ship.CommanderId ?? "none", - ship.PolicySetId ?? "none", - ship.WorkerPopulation.ToString("0.###"), - ship.SpatialState.SpaceLayer, - ship.SpatialState.CurrentNodeId ?? "none", - ship.SpatialState.CurrentBubbleId ?? "none", - ship.SpatialState.MovementRegime, - ship.SpatialState.DestinationNodeId ?? "none", - ship.SpatialState.Transit?.Regime ?? "none", - ship.SpatialState.Transit?.OriginNodeId ?? "none", - ship.SpatialState.Transit?.DestinationNodeId ?? "none", - ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", - GetShipCargoAmount(ship).ToString("0.###"), - GetInventoryAmount(ship.Inventory, "fuel").ToString("0.###"), - ship.TrackedActionKey ?? "none", - ship.TrackedActionTotal.ToString("0.###"), - ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site - ? GetRemainingConstructionDelivery(world, site).ToString("0.###") - : "0", - ship.EnergyStored.ToString("0.###"), - ship.Health.ToString("0.###"), - ship.ActionTimer.ToString("0.###")); + ship.LocalPosition, + ship.LocalVelocity, + ship.TargetLocalPosition, + ship.State, + ship.OrderKind, + ship.DefaultBehaviorKind, + ship.ControllerTaskKind, + ship.NodeId, + ship.BubbleId, + ship.DockedStationId, + ship.CommanderId, + ship.PolicySetId, + ship.CargoCapacity, + ship.CargoItemId, + ship.WorkerPopulation, + ship.TravelSpeed, + ship.TravelSpeedUnit, + ship.Inventory, + ship.FactionId, + ship.Health, + ship.History, + ship.CurrentAction, + ship.SpatialState)).ToList(), + world.Factions.Select(ToFactionDelta).Select(faction => new FactionSnapshot( + faction.Id, + faction.Label, + faction.Color, + faction.Credits, + faction.PopulationTotal, + faction.OreMined, + faction.GoodsProduced, + faction.ShipsBuilt, + faction.ShipsLost, + faction.DefaultPolicySetId)).ToList()); + } - private static string BuildInventorySignature(IReadOnlyDictionary inventory) => - string.Join(",", - inventory - .Where(entry => entry.Value > 0.001f) - .OrderBy(entry => entry.Key, StringComparer.Ordinal) - .Select(entry => $"{entry.Key}:{entry.Value:0.###}")); + public void PrimeDeltaBaseline(SimulationWorld world) + { + foreach (var node in world.Nodes) + { + node.LastDeltaSignature = BuildNodeSignature(node); + } - private static string BuildFactionSignature(FactionRuntime faction) => - $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}"; + foreach (var node in world.SpatialNodes) + { + node.LastDeltaSignature = BuildSpatialNodeSignature(node); + } - private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new( - node.Id, - node.SystemId, - ToDto(node.Position), - node.AnchorNodeId, - node.SourceKind, - node.OreRemaining, - node.MaxOre, - node.ItemId); + foreach (var bubble in world.LocalBubbles) + { + bubble.LastDeltaSignature = BuildLocalBubbleSignature(bubble); + } - private static SpatialNodeDelta ToSpatialNodeDelta(NodeRuntime node) => new( - node.Id, - node.SystemId, - node.Kind.ToContractValue(), - ToDto(node.Position), - node.BubbleId, - node.ParentNodeId, - node.OccupyingStructureId, - node.OrbitReferenceId); + foreach (var station in world.Stations) + { + station.LastDeltaSignature = BuildStationSignature(world, station); + } - private static LocalBubbleDelta ToLocalBubbleDelta(LocalBubbleRuntime bubble) => new( - bubble.Id, - bubble.NodeId, - bubble.SystemId, - bubble.Radius, - bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); + foreach (var claim in world.Claims) + { + claim.LastDeltaSignature = BuildClaimSignature(claim); + } - private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new( - station.Id, - station.Definition.Label, - station.Definition.Category, + foreach (var site in world.ConstructionSites) + { + site.LastDeltaSignature = BuildConstructionSiteSignature(site); + } + + foreach (var order in world.MarketOrders) + { + order.LastDeltaSignature = BuildMarketOrderSignature(order); + } + + foreach (var policy in world.Policies) + { + policy.LastDeltaSignature = BuildPolicySignature(policy); + } + + foreach (var ship in world.Ships) + { + ship.LastDeltaSignature = BuildShipSignature(world, ship); + } + + foreach (var faction in world.Factions) + { + faction.LastDeltaSignature = BuildFactionSignature(faction); + } + } + + private static IReadOnlyList BuildNodeDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var node in world.Nodes) + { + var signature = BuildNodeSignature(node); + if (signature == node.LastDeltaSignature) + { + continue; + } + + node.LastDeltaSignature = signature; + deltas.Add(ToNodeDelta(node)); + } + + return deltas; + } + + private static IReadOnlyList BuildSpatialNodeDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var node in world.SpatialNodes) + { + var signature = BuildSpatialNodeSignature(node); + if (signature == node.LastDeltaSignature) + { + continue; + } + + node.LastDeltaSignature = signature; + deltas.Add(ToSpatialNodeDelta(node)); + } + + return deltas; + } + + private static IReadOnlyList BuildLocalBubbleDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var bubble in world.LocalBubbles) + { + var signature = BuildLocalBubbleSignature(bubble); + if (signature == bubble.LastDeltaSignature) + { + continue; + } + + bubble.LastDeltaSignature = signature; + deltas.Add(ToLocalBubbleDelta(bubble)); + } + + return deltas; + } + + private static IReadOnlyList BuildStationDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var station in world.Stations) + { + var signature = BuildStationSignature(world, station); + if (signature == station.LastDeltaSignature) + { + continue; + } + + station.LastDeltaSignature = signature; + deltas.Add(ToStationDelta(world, station)); + } + + return deltas; + } + + private static IReadOnlyList BuildClaimDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var claim in world.Claims) + { + var signature = BuildClaimSignature(claim); + if (signature == claim.LastDeltaSignature) + { + continue; + } + + claim.LastDeltaSignature = signature; + deltas.Add(ToClaimDelta(claim)); + } + + return deltas; + } + + private static IReadOnlyList BuildConstructionSiteDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var site in world.ConstructionSites) + { + var signature = BuildConstructionSiteSignature(site); + if (signature == site.LastDeltaSignature) + { + continue; + } + + site.LastDeltaSignature = signature; + deltas.Add(ToConstructionSiteDelta(world, site)); + } + + return deltas; + } + + private static IReadOnlyList BuildMarketOrderDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var order in world.MarketOrders) + { + var signature = BuildMarketOrderSignature(order); + if (signature == order.LastDeltaSignature) + { + continue; + } + + order.LastDeltaSignature = signature; + deltas.Add(ToMarketOrderDelta(order)); + } + + return deltas; + } + + private static IReadOnlyList BuildPolicyDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var policy in world.Policies) + { + var signature = BuildPolicySignature(policy); + if (signature == policy.LastDeltaSignature) + { + continue; + } + + policy.LastDeltaSignature = signature; + deltas.Add(ToPolicySetDelta(policy)); + } + + return deltas; + } + + private IReadOnlyList BuildShipDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var ship in world.Ships) + { + var signature = BuildShipSignature(world, ship); + if (signature == ship.LastDeltaSignature) + { + continue; + } + + ship.LastDeltaSignature = signature; + deltas.Add(ToShipDelta(world, ship)); + } + + return deltas; + } + + private static IReadOnlyList BuildFactionDeltas(SimulationWorld world) + { + var deltas = new List(); + foreach (var faction in world.Factions) + { + var signature = BuildFactionSignature(faction); + if (signature == faction.LastDeltaSignature) + { + continue; + } + + faction.LastDeltaSignature = signature; + deltas.Add(ToFactionDelta(faction)); + } + + return deltas; + } + + private static string BuildNodeSignature(ResourceNodeRuntime node) => + $"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}"; + + private static string BuildSpatialNodeSignature(NodeRuntime node) => + $"{node.SystemId}|{node.Kind.ToContractValue()}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.BubbleId}|{node.ParentNodeId}|{node.OccupyingStructureId}|{node.OrbitReferenceId}"; + + private static string BuildLocalBubbleSignature(LocalBubbleRuntime bubble) => + $"{bubble.SystemId}|{bubble.NodeId}|{bubble.Radius:0.###}|{string.Join(",", bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal))}"; + + private static string BuildStationSignature(SimulationWorld world, StationRuntime station) + { + var processes = ToStationActionProgressSnapshots(world, station); + return string.Join("|", station.SystemId, - ToDto(station.Position), - station.NodeId, - station.BubbleId, - station.AnchorNodeId, - station.Definition.Color, - station.DockedShipIds.Count, - station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - GetDockingPadCount(station), - GetInventoryAmount(station.Inventory, "fuel"), - GetStationFuelCapacity(station), - station.EnergyStored, - GetStationEnergyCapacity(station), - ToStationActionProgressSnapshots(world, station), - ToInventoryEntries(station.Inventory), - station.FactionId, - station.CommanderId, - station.PolicySetId, - station.Population, - station.PopulationCapacity, - station.WorkforceRequired, - station.WorkforceEffectiveRatio, - station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(), - station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList()); + station.NodeId ?? "none", + station.BubbleId ?? "none", + station.AnchorNodeId ?? "none", + station.CommanderId ?? "none", + station.PolicySetId ?? "none", + BuildInventorySignature(station.Inventory), + string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")), + string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)), + station.DockingPadAssignments.Count.ToString(), + station.Population.ToString("0.###"), + station.PopulationCapacity.ToString("0.###"), + station.WorkforceRequired.ToString("0.###"), + station.WorkforceEffectiveRatio.ToString("0.###"), + string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal)), + string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal)), + station.ActiveConstruction?.ModuleId ?? "none", + station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0", + string.Join(",", station.ProductionLaneTimers.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => $"{entry.Key}:{entry.Value:0.###}"))); + } - private static IReadOnlyList ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) => - GetStationProductionLanes(station) - .Select(laneKey => - { - var recipe = SelectProductionRecipe(world, station, laneKey); - var timer = GetStationProductionTimer(station, laneKey); - return recipe is null || station.EnergyStored <= 0.01f || timer <= 0.01f - ? null - : new StationActionProgressSnapshot( - laneKey, - recipe.Label, - Math.Clamp(timer / MathF.Max(recipe.Duration, 0.1f), 0f, 1f)); - }) - .Where(snapshot => snapshot is not null) - .Cast() - .ToList(); + private static string BuildClaimSignature(ClaimRuntime claim) => + $"{claim.FactionId}|{claim.SystemId}|{claim.NodeId}|{claim.BubbleId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}"; - private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new( - claim.Id, - claim.FactionId, - claim.SystemId, - claim.NodeId, - claim.BubbleId, - claim.State, - claim.Health, - claim.PlacedAtUtc, - claim.ActivatesAtUtc); + private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) => + $"{site.FactionId}|{site.SystemId}|{site.NodeId}|{site.BubbleId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}"; - private static ConstructionSiteDelta ToConstructionSiteDelta(ConstructionSiteRuntime site) => new( - site.Id, - site.FactionId, - site.SystemId, - site.NodeId, - site.BubbleId, - site.TargetKind, - site.TargetDefinitionId, - site.BlueprintId, - site.ClaimId, - site.StationId, - site.State, - site.Progress, - ToInventoryEntries(site.Inventory), - ToInventoryEntries(site.RequiredItems), - ToInventoryEntries(site.DeliveredItems), - site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); + private static string BuildMarketOrderSignature(MarketOrderRuntime order) => + $"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}"; - private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new( - order.Id, - order.FactionId, - order.StationId, - order.ConstructionSiteId, - order.Kind, - order.ItemId, - order.Amount, - order.RemainingAmount, - order.Valuation, - order.ReserveThreshold, - order.PolicySetId, - order.State); + private static string BuildPolicySignature(PolicySetRuntime policy) => + $"{policy.OwnerKind}|{policy.OwnerId}|{policy.TradeAccessPolicy}|{policy.DockingAccessPolicy}|{policy.ConstructionAccessPolicy}|{policy.OperationalRangePolicy}"; - private static PolicySetDelta ToPolicySetDelta(PolicySetRuntime policy) => new( - policy.Id, - policy.OwnerKind, - policy.OwnerId, - policy.TradeAccessPolicy, - policy.DockingAccessPolicy, - policy.ConstructionAccessPolicy, - policy.OperationalRangePolicy); + private static string BuildShipSignature(SimulationWorld world, ShipRuntime ship) => + string.Join("|", + ship.SystemId, + ship.Position.X.ToString("0.###"), + ship.Position.Y.ToString("0.###"), + ship.Position.Z.ToString("0.###"), + ship.Velocity.X.ToString("0.###"), + ship.Velocity.Y.ToString("0.###"), + ship.Velocity.Z.ToString("0.###"), + ship.TargetPosition.X.ToString("0.###"), + ship.TargetPosition.Y.ToString("0.###"), + ship.TargetPosition.Z.ToString("0.###"), + ship.State.ToContractValue(), + ship.Order?.Kind ?? "none", + ship.DefaultBehavior.Kind, + ship.ControllerTask.Kind.ToContractValue(), + ship.SpatialState.CurrentNodeId ?? "none", + ship.SpatialState.CurrentBubbleId ?? "none", + ship.DockedStationId ?? "none", + ship.CommanderId ?? "none", + ship.PolicySetId ?? "none", + ship.WorkerPopulation.ToString("0.###"), + ship.SpatialState.SpaceLayer, + ship.SpatialState.CurrentNodeId ?? "none", + ship.SpatialState.CurrentBubbleId ?? "none", + ship.SpatialState.MovementRegime, + ship.SpatialState.DestinationNodeId ?? "none", + ship.SpatialState.Transit?.Regime ?? "none", + ship.SpatialState.Transit?.OriginNodeId ?? "none", + ship.SpatialState.Transit?.DestinationNodeId ?? "none", + ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", + GetShipCargoAmount(ship).ToString("0.###"), + ship.TrackedActionKey ?? "none", + ship.TrackedActionTotal.ToString("0.###"), + ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site + ? GetRemainingConstructionDelivery(world, site).ToString("0.###") + : "0", + ship.Health.ToString("0.###"), + ship.ActionTimer.ToString("0.###")); - private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship) => new( - ship.Id, - ship.Definition.Label, - ship.Definition.Role, - ship.Definition.ShipClass, - ship.SystemId, - ToDto(ship.Position), - ToDto(ship.Velocity), - ToDto(ship.TargetPosition), - ship.State.ToContractValue(), - ship.Order?.Kind, - ship.DefaultBehavior.Kind, - ship.ControllerTask.Kind.ToContractValue(), - ship.SpatialState.CurrentNodeId, - ship.SpatialState.CurrentBubbleId, - ship.DockedStationId, - ship.CommanderId, - ship.PolicySetId, - ship.Definition.CargoCapacity, - ship.Definition.CargoItemId, - ship.WorkerPopulation, - ship.EnergyStored, - ToShipTravelSpeed(ship).Speed, - ToShipTravelSpeed(ship).Unit, - ToInventoryEntries(ship.Inventory), - ship.FactionId, - ship.Health, - ship.History.ToList(), - ToShipActionProgressSnapshot(world, ship), - ToShipSpatialStateSnapshot(ship.SpatialState)); + private static string BuildInventorySignature(IReadOnlyDictionary inventory) => + string.Join(",", + inventory + .Where(entry => entry.Value > 0.001f) + .OrderBy(entry => entry.Key, StringComparer.Ordinal) + .Select(entry => $"{entry.Key}:{entry.Value:0.###}")); - private static ShipActionProgressSnapshot? ToShipActionProgressSnapshot(SimulationWorld world, ShipRuntime ship) + private static string BuildFactionSignature(FactionRuntime faction) => + $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}"; + + private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new( + node.Id, + node.SystemId, + ToDto(node.Position), + node.AnchorNodeId, + node.SourceKind, + node.OreRemaining, + node.MaxOre, + node.ItemId); + + private static SpatialNodeDelta ToSpatialNodeDelta(NodeRuntime node) => new( + node.Id, + node.SystemId, + node.Kind.ToContractValue(), + ToDto(node.Position), + node.BubbleId, + node.ParentNodeId, + node.OccupyingStructureId, + node.OrbitReferenceId); + + private static LocalBubbleDelta ToLocalBubbleDelta(LocalBubbleRuntime bubble) => new( + bubble.Id, + bubble.NodeId, + bubble.SystemId, + bubble.Radius, + bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); + + private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new( + station.Id, + station.Label, + station.Category, + station.SystemId, + ToDto(station.Position), + station.NodeId, + station.BubbleId, + station.AnchorNodeId, + station.Color, + station.DockedShipIds.Count, + station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + GetDockingPadCount(station), + ToStationActionProgressSnapshots(world, station), + ToInventoryEntries(station.Inventory), + station.FactionId, + station.CommanderId, + station.PolicySetId, + station.Population, + station.PopulationCapacity, + station.WorkforceRequired, + station.WorkforceEffectiveRatio, + ToStationStorageUsageSnapshots(world, station), + station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(), + station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList()); + + private static IReadOnlyList ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) => + GetStationProductionLanes(station) + .Select(laneKey => + { + var recipe = SelectProductionRecipe(world, station, laneKey); + var timer = GetStationProductionTimer(station, laneKey); + return recipe is null || timer <= 0.01f + ? null + : new StationActionProgressSnapshot( + laneKey, + recipe.Label, + Math.Clamp(timer / MathF.Max(recipe.Duration, 0.1f), 0f, 1f)); + }) + .Where(snapshot => snapshot is not null) + .Cast() + .ToList(); + + private static IReadOnlyList ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station) + { + string[] storageClasses = ["bulk-solid", "bulk-liquid", "container", "manufactured"]; + return storageClasses + .Select(storageClass => new StationStorageUsageSnapshot( + storageClass, + station.Inventory + .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass) + .Sum(entry => entry.Value), + GetStationStorageCapacity(station, storageClass))) + .Where(snapshot => snapshot.Capacity > 0.01f) + .ToList(); + } + + private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new( + claim.Id, + claim.FactionId, + claim.SystemId, + claim.NodeId, + claim.BubbleId, + claim.State, + claim.Health, + claim.PlacedAtUtc, + claim.ActivatesAtUtc); + + private static ConstructionSiteDelta ToConstructionSiteDelta(SimulationWorld world, ConstructionSiteRuntime site) => new( + site.Id, + site.FactionId, + site.SystemId, + site.NodeId, + site.BubbleId, + site.TargetKind, + site.TargetDefinitionId, + site.BlueprintId, + site.ClaimId, + site.StationId, + site.State, + GetConstructionSiteProgress(world, site), + ToInventoryEntries(site.Inventory), + ToInventoryEntries(site.RequiredItems), + ToInventoryEntries(site.DeliveredItems), + site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); + + private static float GetConstructionSiteProgress(SimulationWorld world, ConstructionSiteRuntime site) + { + if (site.BlueprintId is not null + && world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe) + && recipe.Duration > 0.01f) { - var progress = ship.State switch - { - ShipState.SpoolingFtl => CreateShipActionProgress("FTL spool", ship.ActionTimer, MathF.Max(ship.Definition.SpoolTime, 0.1f)), - ShipState.Ftl => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("FTL", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)), - ShipState.SpoolingWarp => CreateShipActionProgress("Warp spool", ship.ActionTimer, MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f)), - ShipState.Warping => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("Warp", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)), - ShipState.Mining => CreateShipActionProgress("Mining", ship.ActionTimer, MathF.Max(world.Balance.MiningCycleSeconds, 0.1f)), - ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)), - ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)), - ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)), - ShipState.Refueling => CreateShipRemainingActionProgress( - "Refuel", - ship.TrackedActionTotal, - MathF.Max(0f, GetShipRefuelTarget(ship, world) - GetInventoryAmount(ship.Inventory, "fuel"))), - ShipState.Loading => CreateShipRemainingActionProgress( - "Load workers", - ship.TrackedActionTotal, - MathF.Max(0f, ship.TrackedActionTotal - ship.WorkerPopulation)), - ShipState.Unloading => CreateShipRemainingActionProgress( - "Unload workers", - ship.TrackedActionTotal, - ship.WorkerPopulation), - ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null - ? null - : world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site - ? null - : CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, GetRemainingConstructionDelivery(world, site)), - _ => null, - }; - - return progress; + return Math.Clamp(site.Progress / recipe.Duration, 0f, 1f); } - private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship) + return Math.Clamp(site.Progress, 0f, 1f); + } + + private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new( + order.Id, + order.FactionId, + order.StationId, + order.ConstructionSiteId, + order.Kind, + order.ItemId, + order.Amount, + order.RemainingAmount, + order.Valuation, + order.ReserveThreshold, + order.PolicySetId, + order.State); + + private static PolicySetDelta ToPolicySetDelta(PolicySetRuntime policy) => new( + policy.Id, + policy.OwnerKind, + policy.OwnerId, + policy.TradeAccessPolicy, + policy.DockingAccessPolicy, + policy.ConstructionAccessPolicy, + policy.OperationalRangePolicy); + + private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship) => new( + ship.Id, + ship.Definition.Label, + ship.Definition.Role, + ship.Definition.ShipClass, + ship.SystemId, + ToDto(ship.Position), + ToDto(ship.Velocity), + ToDto(ship.TargetPosition), + ship.State.ToContractValue(), + ship.Order?.Kind, + ship.DefaultBehavior.Kind, + ship.ControllerTask.Kind.ToContractValue(), + ship.SpatialState.CurrentNodeId, + ship.SpatialState.CurrentBubbleId, + ship.DockedStationId, + ship.CommanderId, + ship.PolicySetId, + ship.Definition.CargoCapacity, + ship.Definition.CargoItemId, + ship.WorkerPopulation, + ToShipTravelSpeed(ship).Speed, + ToShipTravelSpeed(ship).Unit, + ToInventoryEntries(ship.Inventory), + ship.FactionId, + ship.Health, + ship.History.ToList(), + ToShipActionProgressSnapshot(world, ship), + ToShipSpatialStateSnapshot(ship.SpatialState)); + + private static ShipActionProgressSnapshot? ToShipActionProgressSnapshot(SimulationWorld world, ShipRuntime ship) + { + var progress = ship.State switch { - return ship.SpatialState.MovementRegime switch - { - MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"), - MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"), - _ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"), - }; + ShipState.SpoolingFtl => CreateShipActionProgress("FTL spool", ship.ActionTimer, MathF.Max(ship.Definition.SpoolTime, 0.1f)), + ShipState.Ftl => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("FTL", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)), + ShipState.SpoolingWarp => CreateShipActionProgress("Warp spool", ship.ActionTimer, MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f)), + ShipState.Warping => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("Warp", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)), + ShipState.Mining => CreateShipActionProgress("Mining", ship.ActionTimer, MathF.Max(world.Balance.MiningCycleSeconds, 0.1f)), + ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)), + ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)), + ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)), + ShipState.Loading => CreateShipRemainingActionProgress( + "Load workers", + ship.TrackedActionTotal, + MathF.Max(0f, ship.TrackedActionTotal - ship.WorkerPopulation)), + ShipState.Unloading => CreateShipRemainingActionProgress( + "Unload workers", + ship.TrackedActionTotal, + ship.WorkerPopulation), + ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null + ? null + : world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site + ? null + : CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, GetRemainingConstructionDelivery(world, site)), + _ => null, + }; + + return progress; + } + + private static (float Speed, string Unit) ToShipTravelSpeed(ShipRuntime ship) + { + return ship.SpatialState.MovementRegime switch + { + MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"), + MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"), + _ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"), + }; + } + + private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) => + new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f)); + + private static ShipActionProgressSnapshot? CreateShipRemainingActionProgress(string label, float totalAmount, float remainingAmount) + { + if (totalAmount <= 0.01f) + { + return null; } - private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) => - new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f)); + var progress = 1f - Math.Clamp(remainingAmount / totalAmount, 0f, 1f); + return new ShipActionProgressSnapshot(label, progress); + } - private static ShipActionProgressSnapshot? CreateShipRemainingActionProgress(string label, float totalAmount, float remainingAmount) + private static IReadOnlyList ToInventoryEntries(IReadOnlyDictionary inventory) => + inventory + .Where(entry => entry.Value > 0.001f) + .OrderBy(entry => entry.Key, StringComparer.Ordinal) + .Select(entry => new InventoryEntry(entry.Key, entry.Value)) + .ToList(); + + private static FactionDelta ToFactionDelta(FactionRuntime faction) => new( + faction.Id, + faction.Label, + faction.Color, + faction.Credits, + faction.PopulationTotal, + faction.OreMined, + faction.GoodsProduced, + faction.ShipsBuilt, + faction.ShipsLost, + faction.DefaultPolicySetId); + + private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new( + state.SpaceLayer, + state.CurrentSystemId, + state.CurrentNodeId, + state.CurrentBubbleId, + state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value), + state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value), + state.MovementRegime, + state.DestinationNodeId, + state.Transit is null ? null : new ShipTransitSnapshot( + state.Transit.Regime, + state.Transit.OriginNodeId, + state.Transit.DestinationNodeId, + state.Transit.StartedAtUtc, + state.Transit.ArrivalDueAtUtc, + state.Transit.Progress)); + + private static void EmitShipStateEvents( + ShipRuntime ship, + ShipState previousState, + string previousBehavior, + ControllerTaskKind previousTask, + string controllerEvent, + ICollection events) + { + var occurredAtUtc = DateTimeOffset.UtcNow; + + if (previousState != ship.State) { - if (totalAmount <= 0.01f) - { - return null; - } - - var progress = 1f - Math.Clamp(remainingAmount / totalAmount, 0f, 1f); - return new ShipActionProgressSnapshot(label, progress); + events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc)); } - private static IReadOnlyList ToInventoryEntries(IReadOnlyDictionary inventory) => - inventory - .Where(entry => entry.Value > 0.001f) - .OrderBy(entry => entry.Key, StringComparer.Ordinal) - .Select(entry => new InventoryEntry(entry.Key, entry.Value)) - .ToList(); - - private static FactionDelta ToFactionDelta(FactionRuntime faction) => new( - faction.Id, - faction.Label, - faction.Color, - faction.Credits, - faction.PopulationTotal, - faction.OreMined, - faction.GoodsProduced, - faction.ShipsBuilt, - faction.ShipsLost, - faction.DefaultPolicySetId); - - private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new( - state.SpaceLayer, - state.CurrentSystemId, - state.CurrentNodeId, - state.CurrentBubbleId, - state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value), - state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value), - state.MovementRegime, - state.DestinationNodeId, - state.Transit is null ? null : new ShipTransitSnapshot( - state.Transit.Regime, - state.Transit.OriginNodeId, - state.Transit.DestinationNodeId, - state.Transit.StartedAtUtc, - state.Transit.ArrivalDueAtUtc, - state.Transit.Progress)); - - private static void EmitShipStateEvents( - ShipRuntime ship, - ShipState previousState, - string previousBehavior, - ControllerTaskKind previousTask, - string controllerEvent, - ICollection events) + if (previousBehavior != ship.DefaultBehavior.Kind) { - var occurredAtUtc = DateTimeOffset.UtcNow; - - if (previousState != ship.State) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc)); - } - - if (previousBehavior != ship.DefaultBehavior.Kind) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc)); - } - - if (previousTask != ship.ControllerTask.Kind) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc)); - } - - if (controllerEvent != "none") - { - events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc)); - } + events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc)); } - private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z); + if (previousTask != ship.ControllerTask.Kind) + { + events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc)); + } + + if (controllerEvent != "none") + { + events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc)); + } + } + + private static Vector3Dto ToDto(Vector3 value) => new(value.X, value.Y, value.Z); } diff --git a/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs b/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs index f5d52ee..3f46807 100644 --- a/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs +++ b/apps/backend/Simulation/SimulationEngine.ResourceAndInfrastructureSystems.cs @@ -5,303 +5,299 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private static void UpdateClaims(SimulationWorld world, ICollection events) + private static void UpdateClaims(SimulationWorld world, ICollection events) + { + foreach (var claim in world.Claims) { - foreach (var claim in world.Claims) + if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f) + { + if (claim.State != ClaimStateKinds.Destroyed) { - if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f) - { - if (claim.State != ClaimStateKinds.Destroyed) - { - claim.State = ClaimStateKinds.Destroyed; - events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc)); - } - - foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id)) - { - site.State = ConstructionSiteStateKinds.Destroyed; - } - - continue; - } - - if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc) - { - claim.State = ClaimStateKinds.Active; - events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc)); - } + claim.State = ClaimStateKinds.Destroyed; + events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc)); } + + foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id)) + { + site.State = ConstructionSiteStateKinds.Destroyed; + } + + continue; + } + + if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc) + { + claim.State = ClaimStateKinds.Active; + events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc)); + } + } + } + + private static void UpdateConstructionSites(SimulationWorld world, ICollection events) + { + foreach (var site in world.ConstructionSites) + { + if (site.State == ConstructionSiteStateKinds.Destroyed) + { + continue; + } + + var claim = site.ClaimId is null + ? null + : world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId); + if (claim?.State == ClaimStateKinds.Destroyed) + { + site.State = ConstructionSiteStateKinds.Destroyed; + continue; + } + + if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned) + { + site.State = ConstructionSiteStateKinds.Active; + events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc)); + } + + foreach (var orderId in site.MarketOrderIds) + { + var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); + if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required)) + { + continue; + } + + var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId)); + order.RemainingAmount = remaining; + order.State = remaining <= 0.01f + ? MarketOrderStateKinds.Filled + : remaining < order.Amount + ? MarketOrderStateKinds.PartiallyFilled + : MarketOrderStateKinds.Open; + } + } + } + + private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) + { + if (station.ActiveConstruction is not null) + { + return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal) + && string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal); } - private static void UpdateConstructionSites(SimulationWorld world, ICollection events) + if (!CanStartModuleConstruction(station, recipe)) { - foreach (var site in world.ConstructionSites) - { - if (site.State == ConstructionSiteStateKinds.Destroyed) - { - continue; - } - - var claim = site.ClaimId is null - ? null - : world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId); - if (claim?.State == ClaimStateKinds.Destroyed) - { - site.State = ConstructionSiteStateKinds.Destroyed; - continue; - } - - if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned) - { - site.State = ConstructionSiteStateKinds.Active; - events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc)); - } - - foreach (var orderId in site.MarketOrderIds) - { - var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); - if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required)) - { - continue; - } - - var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId)); - order.RemainingAmount = remaining; - order.State = remaining <= 0.01f - ? MarketOrderStateKinds.Filled - : remaining < order.Amount - ? MarketOrderStateKinds.PartiallyFilled - : MarketOrderStateKinds.Open; - } - } + return false; } - private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) + foreach (var input in recipe.Inputs) { - if (station.ActiveConstruction is not null) - { - return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal) - && string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal); - } + RemoveInventory(station.Inventory, input.ItemId, input.Amount); + } - if (!CanStartModuleConstruction(station, recipe)) - { - return false; - } + station.ActiveConstruction = new ModuleConstructionRuntime + { + ModuleId = recipe.ModuleId, + RequiredSeconds = recipe.Duration, + AssignedConstructorShipId = shipId, + }; - foreach (var input in recipe.Inputs) - { - RemoveInventory(station.Inventory, input.ItemId, input.Amount); - } + return true; + } - station.ActiveConstruction = new ModuleConstructionRuntime + private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world) + { + var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f + ? new (string ModuleId, int TargetCount)[] { - ModuleId = recipe.ModuleId, - RequiredSeconds = recipe.Duration, - AssignedConstructorShipId = shipId, + ("refinery-stack", 1), + ("container-bay", 1), + ("fabricator-array", 2), + ("component-factory", 1), + ("ship-factory", 1), + ("dock-bay-small", 2), + ("solar-array", 2), + } + : new (string ModuleId, int TargetCount)[] + { + ("refinery-stack", 1), + ("container-bay", 1), + ("fabricator-array", 2), + ("component-factory", 1), + ("ship-factory", 1), + ("solar-array", 2), + ("dock-bay-small", 2), }; - return true; - } - - private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world) + foreach (var (moduleId, targetCount) in priorities) { - var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f - ? new (string ModuleId, int TargetCount)[] - { - ("gas-tank", 1), - ("fuel-processor", 1), - ("refinery-stack", 1), - ("container-bay", 1), - ("fabricator-array", 2), - ("component-factory", 1), - ("ship-factory", 1), - ("dock-bay-small", 2), - ("solar-array", 2), - } - : new (string ModuleId, int TargetCount)[] - { - ("gas-tank", 1), - ("fuel-processor", 1), - ("refinery-stack", 1), - ("container-bay", 1), - ("fabricator-array", 2), - ("component-factory", 1), - ("ship-factory", 1), - ("solar-array", 2), - ("dock-bay-small", 2), - }; - - foreach (var (moduleId, targetCount) in priorities) - { - if (CountModules(station.InstalledModules, moduleId) < targetCount - && world.ModuleRecipes.ContainsKey(moduleId)) - { - return moduleId; - } - } - - return null; + if (CountModules(station.InstalledModules, moduleId) < targetCount + && world.ModuleRecipes.ContainsKey(moduleId)) + { + return moduleId; + } } - private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) + return null; + } + + private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) + { + var nextModuleId = GetNextStationModuleToBuild(station, world); + foreach (var orderId in site.MarketOrderIds) { - var nextModuleId = GetNextStationModuleToBuild(station, world); - foreach (var orderId in site.MarketOrderIds) - { - var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); - if (order is not null) - { - order.State = MarketOrderStateKinds.Cancelled; - order.RemainingAmount = 0f; - world.MarketOrders.Remove(order); - } + var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); + if (order is not null) + { + order.State = MarketOrderStateKinds.Cancelled; + order.RemainingAmount = 0f; + world.MarketOrders.Remove(order); + } - station.MarketOrderIds.Remove(orderId); - } - - site.MarketOrderIds.Clear(); - site.Inventory.Clear(); - site.DeliveredItems.Clear(); - site.RequiredItems.Clear(); - site.AssignedConstructorShipIds.Clear(); - site.Progress = 0f; - - if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe)) - { - site.State = ConstructionSiteStateKinds.Completed; - site.BlueprintId = null; - return; - } - - site.BlueprintId = nextModuleId; - site.State = ConstructionSiteStateKinds.Active; - foreach (var input in recipe.Inputs) - { - site.RequiredItems[input.ItemId] = input.Amount; - site.DeliveredItems[input.ItemId] = 0f; - var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}"; - site.MarketOrderIds.Add(orderId); - station.MarketOrderIds.Add(orderId); - world.MarketOrders.Add(new MarketOrderRuntime - { - Id = orderId, - FactionId = station.FactionId, - StationId = station.Id, - ConstructionSiteId = site.Id, - Kind = MarketOrderKinds.Buy, - ItemId = input.ItemId, - Amount = input.Amount, - RemainingAmount = input.Amount, - Valuation = 1f, - State = MarketOrderStateKinds.Open, - }); - } + station.MarketOrderIds.Remove(orderId); } - private static int GetDockingPadCount(StationRuntime station) => - CountModules(station.InstalledModules, "dock-bay-small") * 2; + site.MarketOrderIds.Clear(); + site.Inventory.Clear(); + site.DeliveredItems.Clear(); + site.RequiredItems.Clear(); + site.AssignedConstructorShipIds.Clear(); + site.Progress = 0f; - private static int? ReserveDockingPad(StationRuntime station, string shipId) + if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe)) { - if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing - && !string.IsNullOrEmpty(existing.Value)) - { - return existing.Key; - } - - var padCount = GetDockingPadCount(station); - for (var padIndex = 0; padIndex < padCount; padIndex += 1) - { - if (station.DockingPadAssignments.ContainsKey(padIndex)) - { - continue; - } - - station.DockingPadAssignments[padIndex] = shipId; - return padIndex; - } - - return null; + site.State = ConstructionSiteStateKinds.Completed; + site.BlueprintId = null; + return; } - private static void ReleaseDockingPad(StationRuntime station, string shipId) + site.BlueprintId = nextModuleId; + site.State = ConstructionSiteStateKinds.Active; + foreach (var input in recipe.Inputs) { - var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)); - if (!string.IsNullOrEmpty(assignment.Value)) - { - station.DockingPadAssignments.Remove(assignment.Key); - } + site.RequiredItems[input.ItemId] = input.Amount; + site.DeliveredItems[input.ItemId] = 0f; + var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}"; + site.MarketOrderIds.Add(orderId); + station.MarketOrderIds.Add(orderId); + world.MarketOrders.Add(new MarketOrderRuntime + { + Id = orderId, + FactionId = station.FactionId, + StationId = station.Id, + ConstructionSiteId = site.Id, + Kind = MarketOrderKinds.Buy, + ItemId = input.ItemId, + Amount = input.Amount, + RemainingAmount = input.Amount, + Valuation = 1f, + State = MarketOrderStateKinds.Open, + }); } + } - private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) + private static int GetDockingPadCount(StationRuntime station) => + CountModules(station.InstalledModules, "dock-bay-small") * 2; + + private static int? ReserveDockingPad(StationRuntime station, string shipId) + { + if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing + && !string.IsNullOrEmpty(existing.Value)) { - var padCount = Math.Max(1, GetDockingPadCount(station)); - var angle = ((MathF.PI * 2f) / padCount) * padIndex; - var radius = station.Definition.Radius + 18f; - return new Vector3( - station.Position.X + (MathF.Cos(angle) * radius), - station.Position.Y, - station.Position.Z + (MathF.Sin(angle) * radius)); + return existing.Key; } - private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) + var padCount = GetDockingPadCount(station); + for (var padIndex = 0; padIndex < padCount; padIndex += 1) { - var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); - var angle = (hash % 360) * (MathF.PI / 180f); - var radius = station.Definition.Radius + 24f; - return new Vector3( - station.Position.X + (MathF.Cos(angle) * radius), - station.Position.Y, - station.Position.Z + (MathF.Sin(angle) * radius)); + if (station.DockingPadAssignments.ContainsKey(padIndex)) + { + continue; + } + + station.DockingPadAssignments[padIndex] = shipId; + return padIndex; } - private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) + return null; + } + + private static void ReleaseDockingPad(StationRuntime station, string shipId) + { + var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)); + if (!string.IsNullOrEmpty(assignment.Value)) { - if (padIndex is null) - { - return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); - } - - var pad = GetDockingPadPosition(station, padIndex.Value); - var dx = pad.X - station.Position.X; - var dz = pad.Z - station.Position.Z; - var length = MathF.Sqrt((dx * dx) + (dz * dz)); - if (length <= 0.001f) - { - return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); - } - - var scale = distance / length; - return new Vector3( - pad.X + (dx * scale), - station.Position.Y, - pad.Z + (dz * scale)); + station.DockingPadAssignments.Remove(assignment.Key); } + } - private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => - ship.AssignedDockingPadIndex is int padIndex - ? GetDockingPadPosition(station, padIndex) - : station.Position; + private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) + { + var padCount = Math.Max(1, GetDockingPadCount(station)); + var angle = ((MathF.PI * 2f) / padCount) * padIndex; + var radius = station.Radius + 18f; + return new Vector3( + station.Position.X + (MathF.Cos(angle) * radius), + station.Position.Y, + station.Position.Z + (MathF.Sin(angle) * radius)); + } - private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) + private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) + { + var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); + var angle = (hash % 360) * (MathF.PI / 180f); + var radius = station.Radius + 24f; + return new Vector3( + station.Position.X + (MathF.Cos(angle) * radius), + station.Position.Y, + station.Position.Z + (MathF.Sin(angle) * radius)); + } + + private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) + { + if (padIndex is null) { - var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); - var angle = (hash % 360) * (MathF.PI / 180f); - var radius = station.Definition.Radius + 78f; - return new Vector3( - station.Position.X + (MathF.Cos(angle) * radius), - station.Position.Y, - station.Position.Z + (MathF.Sin(angle) * radius)); + return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); } - private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) + var pad = GetDockingPadPosition(station, padIndex.Value); + var dx = pad.X - station.Position.X; + var dz = pad.Z - station.Position.Z; + var length = MathF.Sqrt((dx * dx) + (dz * dz)); + if (length <= 0.001f) { - var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); - var angle = (hash % 360) * (MathF.PI / 180f); - return new Vector3( - nodePosition.X + (MathF.Cos(angle) * radius), - nodePosition.Y, - nodePosition.Z + (MathF.Sin(angle) * radius)); + return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z); } + + var scale = distance / length; + return new Vector3( + pad.X + (dx * scale), + station.Position.Y, + pad.Z + (dz * scale)); + } + + private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => + ship.AssignedDockingPadIndex is int padIndex + ? GetDockingPadPosition(station, padIndex) + : station.Position; + + private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) + { + var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); + var angle = (hash % 360) * (MathF.PI / 180f); + var radius = station.Radius + 78f; + return new Vector3( + station.Position.X + (MathF.Cos(angle) * radius), + station.Position.Y, + station.Position.Z + (MathF.Sin(angle) * radius)); + } + + private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) + { + var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); + var angle = (hash % 360) * (MathF.PI / 180f); + return new Vector3( + nodePosition.X + (MathF.Cos(angle) * radius), + nodePosition.Y, + nodePosition.Z + (MathF.Sin(angle) * radius)); + } } diff --git a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs b/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs index e38fd55..d4b143b 100644 --- a/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs +++ b/apps/backend/Simulation/SimulationEngine.ShipActionSystem.cs @@ -2,647 +2,519 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds) + private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds) + { + ship.ActionTimer += deltaSeconds; + if (ship.ActionTimer < requiredSeconds) { - ship.ActionTimer += deltaSeconds; - if (ship.ActionTimer < requiredSeconds) - { - return false; - } - - ship.ActionTimer = 0f; - return true; + return false; } - private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total) - { - if (ship.TrackedActionKey == actionKey) - { - return; - } + ship.ActionTimer = 0f; + return true; + } - ship.TrackedActionKey = actionKey; - ship.TrackedActionTotal = MathF.Max(total, 0.01f); + private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total) + { + if (ship.TrackedActionKey == actionKey) + { + return; } - internal static float GetShipCargoAmount(ShipRuntime ship) + ship.TrackedActionKey = actionKey; + ship.TrackedActionTotal = MathF.Max(total, 0.01f); + } + + internal static float GetShipCargoAmount(ShipRuntime ship) + { + var cargoItemId = ship.Definition.CargoItemId; + return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId); + } + + private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var task = ship.ControllerTask; + var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); + if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node)) { - var cargoItemId = ship.Definition.CargoItemId; - return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId); + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; } - private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var cargoAmount = GetShipCargoAmount(ship); + if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f) { - var task = ship.ControllerTask; - var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); - if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node)) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var cargoAmount = GetShipCargoAmount(ship); - if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f) - { - ship.ActionTimer = 0f; - ship.State = ShipState.CargoFull; - ship.TargetPosition = ship.Position; - return "cargo-full"; - } - - ship.TargetPosition = task.TargetPosition.Value; - var distance = ship.Position.DistanceTo(task.TargetPosition.Value); - if (distance > task.Threshold) - { - ship.ActionTimer = 0f; - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.MiningApproach; - ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.Mining; - if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds)) - { - return "none"; - } - - var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount); - var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity); - mined = MathF.Min(mined, node.OreRemaining); - if (mined <= 0.01f) - { - ship.ActionTimer = 0f; - ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull; - ship.TargetPosition = ship.Position; - return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full"; - } - - if (ship.Definition.CargoItemId is not null) - { - AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined); - } - - node.OreRemaining -= mined; - node.OreRemaining = MathF.Max(0f, node.OreRemaining); - - return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none"; + ship.ActionTimer = 0f; + ship.State = ShipState.CargoFull; + ship.TargetPosition = ship.Position; + return "cargo-full"; } - private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + ship.TargetPosition = task.TargetPosition.Value; + var distance = ship.Position.DistanceTo(task.TargetPosition.Value); + if (distance > task.Threshold) { - var task = ship.ControllerTask; - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); - if (station is null || task.TargetPosition is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } + ship.ActionTimer = 0f; - var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); - if (padIndex is null) - { - ship.ActionTimer = 0f; - ship.State = ShipState.AwaitingDock; - ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); - var waitDistance = ship.Position.DistanceTo(ship.TargetPosition); - if (waitDistance > 4f && TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - } - - return "none"; - } - - ship.AssignedDockingPadIndex = padIndex; - var padPosition = GetDockingPadPosition(station, padIndex.Value); - ship.TargetPosition = padPosition; - var distance = ship.Position.DistanceTo(padPosition); - if (distance > 4f) - { - ship.ActionTimer = 0f; - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.DockingApproach; - ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.Docking; - if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration)) - { - return "none"; - } - - ship.State = ShipState.Docked; - ship.DockedStationId = station.Id; - station.DockedShipIds.Add(ship.Id); - ship.Position = padPosition; - ship.TargetPosition = padPosition; - return "docked"; + ship.State = ShipState.MiningApproach; + ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; } - private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + ship.State = ShipState.Mining; + if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds)) { - if (ship.DockedStationId is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (station is null) - { - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.TargetPosition = GetShipDockedPosition(ship, station); - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.Transferring; - BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship)); - var cargoItemId = ship.Definition.CargoItemId; - var moved = cargoItemId is null ? 0f : MathF.Min(GetInventoryAmount(ship.Inventory, cargoItemId), world.Balance.TransferRate * deltaSeconds); - if (cargoItemId is not null) - { - var accepted = TryAddStationInventory(world, station, cargoItemId, moved); - RemoveInventory(ship.Inventory, cargoItemId, accepted); - moved = accepted; - } - - var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId); - if (faction is not null && cargoItemId == "ore") - { - faction.OreMined += moved; - faction.Credits += moved * 0.4f; - } - - return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none"; + return "none"; } - private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount); + var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity); + mined = MathF.Min(mined, node.OreRemaining); + if (mined <= 0.01f) { - if (ship.DockedStationId is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (station is null) - { - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.TargetPosition = GetShipDockedPosition(ship, station); - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.Loading; - BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship))); - var cargoItemId = ship.Definition.CargoItemId; - var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); - var moved = cargoItemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, cargoItemId)); - if (cargoItemId is not null && moved > 0.01f) - { - RemoveInventory(station.Inventory, cargoItemId, moved); - AddInventory(ship.Inventory, cargoItemId, moved); - } - - return cargoItemId is null - || GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f - || GetInventoryAmount(station.Inventory, cargoItemId) <= 0.01f - ? "loaded" - : "none"; + ship.ActionTimer = 0f; + ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull; + ship.TargetPosition = ship.Position; + return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full"; } - private string UpdateRefuel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + if (ship.Definition.CargoItemId is not null) { - var station = ResolveShipSupportStation(ship, world); - if (station is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var supportPosition = ResolveShipSupportPosition(ship, station); - if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) - || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.TargetPosition = supportPosition; - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.Refueling; - var refuelTarget = GetShipRefuelTarget(ship, world); - BeginTrackedAction(ship, "refueling", MathF.Max(0f, refuelTarget - GetInventoryAmount(ship.Inventory, "fuel"))); - var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, refuelTarget - GetInventoryAmount(ship.Inventory, "fuel")); - var moved = MathF.Min(transfer, GetInventoryAmount(station.Inventory, "fuel")); - if (moved > 0.01f) - { - RemoveInventory(station.Inventory, "fuel", moved); - AddInventory(ship.Inventory, "fuel", moved); - } - - return !NeedsRefuel(ship, world) ? "refueled" : "none"; + AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined); } - private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + node.OreRemaining -= mined; + node.OreRemaining = MathF.Max(0f, node.OreRemaining); + + return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none"; + } + + private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var task = ship.ControllerTask; + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); + if (station is null || task.TargetPosition is null) { - var station = ResolveShipSupportStation(ship, world); - if (station is null || ship.DefaultBehavior.ModuleId is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe)) - { - ship.AssignedDockingPadIndex = null; - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var supportPosition = ResolveShipSupportPosition(ship, station); - if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) - || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id)) - { - ship.ActionTimer = 0f; - ship.State = ShipState.WaitingMaterials; - ship.TargetPosition = supportPosition; - return "none"; - } - - if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id) - { - ship.State = ShipState.ConstructionBlocked; - ship.TargetPosition = supportPosition; - return "none"; - } - - ship.TargetPosition = supportPosition; - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.Constructing; - station.ActiveConstruction.ProgressSeconds += deltaSeconds; - if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds) - { - return "none"; - } - - station.InstalledModules.Add(station.ActiveConstruction.ModuleId); - station.ActiveConstruction = null; - return "module-constructed"; + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; } - private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); + if (padIndex is null) { - var station = ResolveShipSupportStation(ship, world); - if (station is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } + ship.ActionTimer = 0f; + ship.State = ShipState.AwaitingDock; + ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); + var waitDistance = ship.Position.DistanceTo(ship.TargetPosition); + if (waitDistance > 4f) + { + ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + } - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); - if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var supportPosition = ResolveShipSupportPosition(ship, station); - if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) - || !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.TargetPosition = supportPosition; - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.DeliveringConstruction; - BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site)); - - if (site.StationId is not null) - { - return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; - } - - foreach (var required in site.RequiredItems) - { - var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); - var remaining = MathF.Max(0f, required.Value - delivered); - if (remaining <= 0.01f) - { - continue; - } - - var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds); - moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key)); - if (moved <= 0.01f) - { - continue; - } - - RemoveInventory(station.Inventory, required.Key, moved); - AddInventory(site.Inventory, required.Key, moved); - AddInventory(site.DeliveredItems, required.Key, moved); - return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; - } - - return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; + return "none"; } - private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + ship.AssignedDockingPadIndex = padIndex; + var padPosition = GetDockingPadPosition(station, padIndex.Value); + ship.TargetPosition = padPosition; + var distance = ship.Position.DistanceTo(padPosition); + if (distance > 4f) { - var station = ResolveShipSupportStation(ship, world); - if (station is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } + ship.ActionTimer = 0f; - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); - if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var supportPosition = ResolveShipSupportPosition(ship, station); - if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return "none"; - } - - if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) - { - ship.State = ShipState.WaitingMaterials; - ship.TargetPosition = supportPosition; - return "none"; - } - - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds) - || !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.TargetPosition = supportPosition; - ship.Position = ship.TargetPosition; - ship.ActionTimer = 0f; - ship.State = ShipState.Constructing; - site.AssignedConstructorShipIds.Add(ship.Id); - site.Progress += deltaSeconds; - if (site.Progress < recipe.Duration) - { - return "none"; - } - - station.InstalledModules.Add(site.BlueprintId); - PrepareNextConstructionSiteStep(world, station, site); - return "site-constructed"; + ship.State = ShipState.DockingApproach; + ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; } - private StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) => - ship.DockedStationId is not null - ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId) - : ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null - ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) - : null; - - private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station) => - ship.DockedStationId is not null - ? GetShipDockedPosition(ship, station) - : GetConstructionHoldPosition(station, ship.Id); - - private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => - ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); - - private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + ship.State = ShipState.Docking; + if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration)) { - if (ship.DockedStationId is null || !CanTransportWorkers(ship)) - { - ship.State = ShipState.Blocked; - return "failed"; - } - - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (station is null || station.Population <= 0.01f) - { - ship.State = ShipState.Idle; - return "none"; - } - - var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); - var totalTransfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); - transfer = MathF.Min(transfer, 4f * deltaSeconds); - if (transfer <= 0.01f) - { - return "none"; - } - - station.Population = MathF.Max(0f, station.Population - transfer); - ship.WorkerPopulation += transfer; - ship.State = ShipState.Loading; - BeginTrackedAction(ship, "loading", totalTransfer); - return ship.WorkerPopulation >= GetWorkerTransportCapacity(ship) - 0.01f ? "workers-loaded" : "none"; + return "none"; } - private string UpdateUnloadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + ship.State = ShipState.Docked; + ship.DockedStationId = station.Id; + station.DockedShipIds.Add(ship.Id); + ship.Position = padPosition; + ship.TargetPosition = padPosition; + return "docked"; + } + + private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + if (ship.DockedStationId is null) { - if (ship.DockedStationId is null || !CanTransportWorkers(ship)) - { - ship.State = ShipState.Blocked; - return "failed"; - } - - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (station is null || ship.WorkerPopulation <= 0.01f) - { - ship.State = ShipState.Idle; - return "none"; - } - - var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population)); - var totalTransfer = transfer; - transfer = MathF.Min(transfer, 4f * deltaSeconds); - if (transfer <= 0.01f) - { - return "none"; - } - - ship.WorkerPopulation = MathF.Max(0f, ship.WorkerPopulation - transfer); - station.Population = MathF.Min(station.PopulationCapacity, station.Population + transfer); - ship.State = ShipState.Unloading; - BeginTrackedAction(ship, "unloading", totalTransfer); - return ship.WorkerPopulation <= 0.01f ? "workers-unloaded" : "none"; + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; } - private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + if (station is null) { - var task = ship.ControllerTask; - if (ship.DockedStationId is null || task.TargetPosition is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return "none"; - } - - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - var undockTarget = station is null - ? task.TargetPosition.Value - : GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); - ship.TargetPosition = undockTarget; - if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - if (station is not null && !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds)) - { - ship.State = ShipState.CapacitorStarved; - ship.TargetPosition = ship.Position; - return "none"; - } - - ship.State = ShipState.Undocking; - if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration)) - { - if (station is not null) - { - ship.Position = GetShipDockedPosition(ship, station); - } - - return "none"; - } - - ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance); - if (ship.Position.DistanceTo(undockTarget) > task.Threshold) - { - return "none"; - } - - if (station is not null) - { - station.DockedShipIds.Remove(ship.Id); - ReleaseDockingPad(station, ship.Id); - } - - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - return "undocked"; + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; } - private static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => - site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key))); + ship.TargetPosition = GetShipDockedPosition(ship, station); + ship.Position = ship.TargetPosition; + ship.ActionTimer = 0f; + ship.State = ShipState.Transferring; + BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship)); + var cargoItemId = ship.Definition.CargoItemId; + var moved = cargoItemId is null ? 0f : MathF.Min(GetInventoryAmount(ship.Inventory, cargoItemId), world.Balance.TransferRate * deltaSeconds); + if (cargoItemId is not null) + { + var accepted = TryAddStationInventory(world, station, cargoItemId, moved); + RemoveInventory(ship.Inventory, cargoItemId, accepted); + moved = accepted; + } + + var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId); + if (faction is not null && cargoItemId == "ore") + { + faction.OreMined += moved; + faction.Credits += moved * 0.4f; + } + + return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none"; + } + + private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + if (ship.DockedStationId is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + if (station is null) + { + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + ship.TargetPosition = GetShipDockedPosition(ship, station); + ship.Position = ship.TargetPosition; + ship.ActionTimer = 0f; + ship.State = ShipState.Loading; + BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship))); + var cargoItemId = ship.Definition.CargoItemId; + var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); + var moved = cargoItemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, cargoItemId)); + if (cargoItemId is not null && moved > 0.01f) + { + RemoveInventory(station.Inventory, cargoItemId, moved); + AddInventory(ship.Inventory, cargoItemId, moved); + } + + return cargoItemId is null + || GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f + || GetInventoryAmount(station.Inventory, cargoItemId) <= 0.01f + ? "loaded" + : "none"; + } + + private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var station = ResolveShipSupportStation(ship, world); + if (station is null || ship.DefaultBehavior.ModuleId is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe)) + { + ship.AssignedDockingPadIndex = null; + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var supportPosition = ResolveShipSupportPosition(ship, station); + if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) + { + ship.State = ShipState.LocalFlight; + ship.TargetPosition = supportPosition; + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; + } + + if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id)) + { + ship.ActionTimer = 0f; + ship.State = ShipState.WaitingMaterials; + ship.TargetPosition = supportPosition; + return "none"; + } + + if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id) + { + ship.State = ShipState.ConstructionBlocked; + ship.TargetPosition = supportPosition; + return "none"; + } + + ship.TargetPosition = supportPosition; + ship.Position = ship.TargetPosition; + ship.ActionTimer = 0f; + ship.State = ShipState.Constructing; + station.ActiveConstruction.ProgressSeconds += deltaSeconds; + if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds) + { + return "none"; + } + + AddStationModule(world, station, station.ActiveConstruction.ModuleId); + station.ActiveConstruction = null; + return "module-constructed"; + } + + private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var station = ResolveShipSupportStation(ship, world); + if (station is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); + if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var supportPosition = ResolveShipSupportPosition(ship, station); + if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) + { + ship.State = ShipState.LocalFlight; + ship.TargetPosition = supportPosition; + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; + } + + ship.TargetPosition = supportPosition; + ship.Position = ship.TargetPosition; + ship.ActionTimer = 0f; + ship.State = ShipState.DeliveringConstruction; + BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site)); + + if (site.StationId is not null) + { + return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; + } + + foreach (var required in site.RequiredItems) + { + var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); + var remaining = MathF.Max(0f, required.Value - delivered); + if (remaining <= 0.01f) + { + continue; + } + + var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds); + moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key)); + if (moved <= 0.01f) + { + continue; + } + + RemoveInventory(station.Inventory, required.Key, moved); + AddInventory(site.Inventory, required.Key, moved); + AddInventory(site.DeliveredItems, required.Key, moved); + return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; + } + + return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none"; + } + + private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var station = ResolveShipSupportStation(ship, world); + if (station is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId); + if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var supportPosition = ResolveShipSupportPosition(ship, station); + if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold)) + { + ship.State = ShipState.LocalFlight; + ship.TargetPosition = supportPosition; + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return "none"; + } + + if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) + { + ship.State = ShipState.WaitingMaterials; + ship.TargetPosition = supportPosition; + return "none"; + } + + ship.TargetPosition = supportPosition; + ship.Position = ship.TargetPosition; + ship.ActionTimer = 0f; + ship.State = ShipState.Constructing; + site.AssignedConstructorShipIds.Add(ship.Id); + site.Progress += deltaSeconds; + if (site.Progress < recipe.Duration) + { + return "none"; + } + + AddStationModule(world, station, site.BlueprintId); + PrepareNextConstructionSiteStep(world, station, site); + return "site-constructed"; + } + + private StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) => + ship.DockedStationId is not null + ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId) + : ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null + ? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) + : null; + + private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station) => + ship.DockedStationId is not null + ? GetShipDockedPosition(ship, station) + : GetConstructionHoldPosition(station, ship.Id); + + private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => + ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); + + private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + if (ship.DockedStationId is null || !CanTransportWorkers(ship)) + { + ship.State = ShipState.Blocked; + return "failed"; + } + + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + if (station is null || station.Population <= 0.01f) + { + ship.State = ShipState.Idle; + return "none"; + } + + var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); + var totalTransfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation); + transfer = MathF.Min(transfer, 4f * deltaSeconds); + if (transfer <= 0.01f) + { + return "none"; + } + + station.Population = MathF.Max(0f, station.Population - transfer); + ship.WorkerPopulation += transfer; + ship.State = ShipState.Loading; + BeginTrackedAction(ship, "loading", totalTransfer); + return ship.WorkerPopulation >= GetWorkerTransportCapacity(ship) - 0.01f ? "workers-loaded" : "none"; + } + + private string UpdateUnloadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + if (ship.DockedStationId is null || !CanTransportWorkers(ship)) + { + ship.State = ShipState.Blocked; + return "failed"; + } + + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + if (station is null || ship.WorkerPopulation <= 0.01f) + { + ship.State = ShipState.Idle; + return "none"; + } + + var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population)); + var totalTransfer = transfer; + transfer = MathF.Min(transfer, 4f * deltaSeconds); + if (transfer <= 0.01f) + { + return "none"; + } + + ship.WorkerPopulation = MathF.Max(0f, ship.WorkerPopulation - transfer); + station.Population = MathF.Min(station.PopulationCapacity, station.Population + transfer); + ship.State = ShipState.Unloading; + BeginTrackedAction(ship, "unloading", totalTransfer); + return ship.WorkerPopulation <= 0.01f ? "workers-unloaded" : "none"; + } + + private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds) + { + var task = ship.ControllerTask; + if (ship.DockedStationId is null || task.TargetPosition is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return "none"; + } + + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + var undockTarget = station is null + ? task.TargetPosition.Value + : GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); + ship.TargetPosition = undockTarget; + + ship.State = ShipState.Undocking; + if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration)) + { + if (station is not null) + { + ship.Position = GetShipDockedPosition(ship, station); + } + + return "none"; + } + + ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance); + if (ship.Position.DistanceTo(undockTarget) > task.Threshold) + { + return "none"; + } + + if (station is not null) + { + station.DockedShipIds.Remove(ship.Id); + ReleaseDockingPad(station, ship.Id); + } + + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + return "undocked"; + } + + private static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) => + site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key))); } diff --git a/apps/backend/Simulation/SimulationEngine.ShipControl.cs b/apps/backend/Simulation/SimulationEngine.ShipControl.cs index f84a826..9dfee6d 100644 --- a/apps/backend/Simulation/SimulationEngine.ShipControl.cs +++ b/apps/backend/Simulation/SimulationEngine.ShipControl.cs @@ -4,677 +4,530 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) => - ship.CommanderId is null - ? null - : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId && candidate.Kind == CommanderKind.Ship); + private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) => + ship.CommanderId is null + ? null + : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId && candidate.Kind == CommanderKind.Ship); - private static void SyncCommanderToShip(ShipRuntime ship, CommanderRuntime commander) + private static void SyncCommanderToShip(ShipRuntime ship, CommanderRuntime commander) + { + if (commander.ActiveBehavior is not null) { - if (commander.ActiveBehavior is not null) - { - ship.DefaultBehavior.Kind = commander.ActiveBehavior.Kind; - ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior.AreaSystemId; - ship.DefaultBehavior.ModuleId = commander.ActiveBehavior.ModuleId; - ship.DefaultBehavior.NodeId = commander.ActiveBehavior.NodeId; - ship.DefaultBehavior.Phase = commander.ActiveBehavior.Phase; - ship.DefaultBehavior.PatrolIndex = commander.ActiveBehavior.PatrolIndex; - ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId; - } - - if (commander.ActiveOrder is null) - { - ship.Order = null; - } - else - { - ship.Order = new ShipOrderRuntime - { - Kind = commander.ActiveOrder.Kind, - Status = commander.ActiveOrder.Status, - DestinationSystemId = commander.ActiveOrder.DestinationSystemId, - DestinationPosition = commander.ActiveOrder.DestinationPosition, - }; - } - - if (commander.ActiveTask is not null) - { - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ParseControllerTaskKind(commander.ActiveTask.Kind), - Status = commander.ActiveTask.Status, - CommanderId = commander.Id, - TargetEntityId = commander.ActiveTask.TargetEntityId, - TargetNodeId = commander.ActiveTask.TargetNodeId, - TargetPosition = commander.ActiveTask.TargetPosition, - TargetSystemId = commander.ActiveTask.TargetSystemId, - Threshold = commander.ActiveTask.Threshold, - }; - } + ship.DefaultBehavior.Kind = commander.ActiveBehavior.Kind; + ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior.AreaSystemId; + ship.DefaultBehavior.ModuleId = commander.ActiveBehavior.ModuleId; + ship.DefaultBehavior.NodeId = commander.ActiveBehavior.NodeId; + ship.DefaultBehavior.Phase = commander.ActiveBehavior.Phase; + ship.DefaultBehavior.PatrolIndex = commander.ActiveBehavior.PatrolIndex; + ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId; } - private static void SyncShipToCommander(ShipRuntime ship, CommanderRuntime commander) + if (commander.ActiveOrder is null) { - commander.ActiveBehavior ??= new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind }; - commander.ActiveBehavior.Kind = ship.DefaultBehavior.Kind; - commander.ActiveBehavior.AreaSystemId = ship.DefaultBehavior.AreaSystemId; - commander.ActiveBehavior.ModuleId = ship.DefaultBehavior.ModuleId; - commander.ActiveBehavior.NodeId = ship.DefaultBehavior.NodeId; - commander.ActiveBehavior.Phase = ship.DefaultBehavior.Phase; - commander.ActiveBehavior.PatrolIndex = ship.DefaultBehavior.PatrolIndex; - commander.ActiveBehavior.StationId = ship.DefaultBehavior.StationId; - - if (ship.Order is null) - { - commander.ActiveOrder = null; - } - else - { - commander.ActiveOrder ??= new CommanderOrderRuntime - { - Kind = ship.Order.Kind, - DestinationSystemId = ship.Order.DestinationSystemId, - DestinationPosition = ship.Order.DestinationPosition, - }; - commander.ActiveOrder.Status = ship.Order.Status; - commander.ActiveOrder.TargetEntityId = ship.ControllerTask.TargetEntityId; - commander.ActiveOrder.DestinationNodeId = ship.ControllerTask.TargetNodeId ?? ship.SpatialState.DestinationNodeId; - } - - commander.ActiveTask ??= new CommanderTaskRuntime { Kind = ship.ControllerTask.Kind.ToContractValue() }; - commander.ActiveTask.Kind = ship.ControllerTask.Kind.ToContractValue(); - commander.ActiveTask.Status = ship.ControllerTask.Status; - commander.ActiveTask.TargetEntityId = ship.ControllerTask.TargetEntityId; - commander.ActiveTask.TargetNodeId = ship.ControllerTask.TargetNodeId; - commander.ActiveTask.TargetPosition = ship.ControllerTask.TargetPosition; - commander.ActiveTask.TargetSystemId = ship.ControllerTask.TargetSystemId; - commander.ActiveTask.Threshold = ship.ControllerTask.Threshold; + ship.Order = null; + } + else + { + ship.Order = new ShipOrderRuntime + { + Kind = commander.ActiveOrder.Kind, + Status = commander.ActiveOrder.Status, + DestinationSystemId = commander.ActiveOrder.DestinationSystemId, + DestinationPosition = commander.ActiveOrder.DestinationPosition, + }; } - private void RefreshControlLayers(ShipRuntime ship, SimulationWorld world) + if (commander.ActiveTask is not null) { - var commander = GetShipCommander(world, ship); - if (commander is not null) - { - SyncCommanderToShip(ship, commander); - } + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ParseControllerTaskKind(commander.ActiveTask.Kind), + Status = commander.ActiveTask.Status, + CommanderId = commander.Id, + TargetEntityId = commander.ActiveTask.TargetEntityId, + TargetNodeId = commander.ActiveTask.TargetNodeId, + TargetPosition = commander.ActiveTask.TargetPosition, + TargetSystemId = commander.ActiveTask.TargetSystemId, + Threshold = commander.ActiveTask.Threshold, + }; + } + } - if (ship.Order is not null && ship.Order.Status == OrderStatus.Queued) - { - ship.Order.Status = OrderStatus.Accepted; - if (commander?.ActiveOrder is not null) - { - commander.ActiveOrder.Status = ship.Order.Status; - } - } + private static void SyncShipToCommander(ShipRuntime ship, CommanderRuntime commander) + { + commander.ActiveBehavior ??= new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind }; + commander.ActiveBehavior.Kind = ship.DefaultBehavior.Kind; + commander.ActiveBehavior.AreaSystemId = ship.DefaultBehavior.AreaSystemId; + commander.ActiveBehavior.ModuleId = ship.DefaultBehavior.ModuleId; + commander.ActiveBehavior.NodeId = ship.DefaultBehavior.NodeId; + commander.ActiveBehavior.Phase = ship.DefaultBehavior.Phase; + commander.ActiveBehavior.PatrolIndex = ship.DefaultBehavior.PatrolIndex; + commander.ActiveBehavior.StationId = ship.DefaultBehavior.StationId; - if (commander is not null) - { - SyncShipToCommander(ship, commander); - } + if (ship.Order is null) + { + commander.ActiveOrder = null; + } + else + { + commander.ActiveOrder ??= new CommanderOrderRuntime + { + Kind = ship.Order.Kind, + DestinationSystemId = ship.Order.DestinationSystemId, + DestinationPosition = ship.Order.DestinationPosition, + }; + commander.ActiveOrder.Status = ship.Order.Status; + commander.ActiveOrder.TargetEntityId = ship.ControllerTask.TargetEntityId; + commander.ActiveOrder.DestinationNodeId = ship.ControllerTask.TargetNodeId ?? ship.SpatialState.DestinationNodeId; } - private void PlanControllerTask(ShipRuntime ship, SimulationWorld world) - { - var commander = GetShipCommander(world, ship); - if (ship.Order is not null) - { - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - Status = WorkStatus.Active, - CommanderId = commander?.Id, - TargetSystemId = ship.Order.DestinationSystemId, - TargetNodeId = ship.SpatialState.DestinationNodeId, - TargetPosition = ship.Order.DestinationPosition, - Threshold = world.Balance.ArrivalThreshold, - }; - SyncCommanderTask(commander, ship.ControllerTask); - return; - } + commander.ActiveTask ??= new CommanderTaskRuntime { Kind = ship.ControllerTask.Kind.ToContractValue() }; + commander.ActiveTask.Kind = ship.ControllerTask.Kind.ToContractValue(); + commander.ActiveTask.Status = ship.ControllerTask.Status; + commander.ActiveTask.TargetEntityId = ship.ControllerTask.TargetEntityId; + commander.ActiveTask.TargetNodeId = ship.ControllerTask.TargetNodeId; + commander.ActiveTask.TargetPosition = ship.ControllerTask.TargetPosition; + commander.ActiveTask.TargetSystemId = ship.ControllerTask.TargetSystemId; + commander.ActiveTask.Threshold = ship.ControllerTask.Threshold; + } - _shipBehaviorStateMachine.Plan(this, ship, world); - SyncCommanderTask(commander, ship.ControllerTask); + private void RefreshControlLayers(ShipRuntime ship, SimulationWorld world) + { + var commander = GetShipCommander(world, ship); + if (commander is not null) + { + SyncCommanderToShip(ship, commander); } - internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) + if (ship.Order is not null && ship.Order.Status == OrderStatus.Queued) { - var behavior = ship.DefaultBehavior; - var refinery = SelectBestBuyStation(world, ship, resourceItemId, behavior.StationId); - behavior.StationId = refinery?.Id; - var node = behavior.NodeId is null - ? world.Nodes - .Where(candidate => - (behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) && - candidate.ItemId == resourceItemId && - candidate.OreRemaining > 0.01f) - .OrderByDescending(candidate => candidate.OreRemaining) - .FirstOrDefault() - : world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f); - - if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule)) - { - behavior.Kind = "idle"; - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - behavior.NodeId ??= node.Id; - if (NeedsEmergencyReturn(ship, world) && behavior.Phase is "travel-to-node" or "extract") - { - behavior.Phase = "travel-to-station"; - } - - if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f - && behavior.Phase is "travel-to-node" or "extract") - { - behavior.Phase = "travel-to-station"; - } - - if (ship.DockedStationId == refinery.Id) - { - if (GetShipCargoAmount(ship) > 0.01f) - { - behavior.Phase = "unload"; - } - else if (behavior.Phase == "undock") - { - // Keep the post-refuel departure decision stable for the current dock cycle. - behavior.Phase = "undock"; - } - else if (NeedsRefuel(ship, world)) - { - behavior.Phase = "refuel"; - } - else if (behavior.Phase is "dock" or "unload" or "refuel") - { - behavior.Phase = "undock"; - } - } - else if (NeedsRefuel(ship, world) && behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract") - { - behavior.Phase = "travel-to-station"; - } - - switch (behavior.Phase) - { - case "extract": - var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Extract, - TargetEntityId = node.Id, - TargetSystemId = node.SystemId, - TargetPosition = extractionPosition, - Threshold = 5f, - }; - break; - case "travel-to-station": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetEntityId = refinery.Id, - TargetSystemId = refinery.SystemId, - TargetPosition = refinery.Position, - Threshold = refinery.Definition.Radius + 8f, - }; - break; - case "dock": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Dock, - TargetEntityId = refinery.Id, - TargetSystemId = refinery.SystemId, - TargetPosition = refinery.Position, - Threshold = refinery.Definition.Radius + 4f, - }; - break; - case "unload": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Unload, - TargetEntityId = refinery.Id, - TargetSystemId = refinery.SystemId, - TargetPosition = refinery.Position, - Threshold = 0f, - }; - break; - case "refuel": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Refuel, - TargetEntityId = refinery.Id, - TargetSystemId = refinery.SystemId, - TargetPosition = refinery.Position, - Threshold = 0f, - }; - break; - case "undock": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Undock, - TargetEntityId = refinery.Id, - TargetSystemId = refinery.SystemId, - TargetPosition = new Vector3(refinery.Position.X + world.Balance.UndockDistance, refinery.Position.Y, refinery.Position.Z), - Threshold = 8f, - }; - break; - default: - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetEntityId = node.Id, - TargetSystemId = node.SystemId, - TargetPosition = node.Position, - Threshold = 18f, - }; - behavior.Phase = "travel-to-node"; - break; - } + ship.Order.Status = OrderStatus.Accepted; + if (commander?.ActiveOrder is not null) + { + commander.ActiveOrder.Status = ship.Order.Status; + } } - internal static StationRuntime? SelectBestBuyStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId) + if (commander is not null) { - var preferred = preferredStationId is null - ? null - : world.Stations.FirstOrDefault(station => station.Id == preferredStationId); + SyncShipToCommander(ship, commander); + } + } - var bestOrder = world.MarketOrders - .Where(order => - order.Kind == MarketOrderKinds.Buy && - order.ConstructionSiteId is null && - order.State != MarketOrderStateKinds.Cancelled && - order.ItemId == itemId && - order.RemainingAmount > 0.01f) - .Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId))) - .Where(entry => entry.Station is not null) - .OrderByDescending(entry => - { - var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f; - return entry.Order.Valuation - distancePenalty; - }) - .FirstOrDefault(); - - return bestOrder.Station ?? preferred; + private void PlanControllerTask(ShipRuntime ship, SimulationWorld world) + { + var commander = GetShipCommander(world, ship); + if (ship.Order is not null) + { + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Travel, + Status = WorkStatus.Active, + CommanderId = commander?.Id, + TargetSystemId = ship.Order.DestinationSystemId, + TargetNodeId = ship.SpatialState.DestinationNodeId, + TargetPosition = ship.Order.DestinationPosition, + Threshold = world.Balance.ArrivalThreshold, + }; + SyncCommanderTask(commander, ship.ControllerTask); + return; } - internal static StationRuntime? SelectBestSellStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId) + _shipBehaviorStateMachine.Plan(this, ship, world); + SyncCommanderTask(commander, ship.ControllerTask); + } + + internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) + { + var behavior = ship.DefaultBehavior; + var refinery = SelectBestBuyStation(world, ship, resourceItemId, behavior.StationId); + behavior.StationId = refinery?.Id; + var node = behavior.NodeId is null + ? world.Nodes + .Where(candidate => + (behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) && + candidate.ItemId == resourceItemId && + candidate.OreRemaining > 0.01f) + .OrderByDescending(candidate => candidate.OreRemaining) + .FirstOrDefault() + : world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f); + + if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule)) { - var preferred = preferredStationId is null - ? null - : world.Stations.FirstOrDefault(station => station.Id == preferredStationId); - - var bestOrder = world.MarketOrders - .Where(order => - order.Kind == MarketOrderKinds.Sell && - order.ConstructionSiteId is null && - order.State != MarketOrderStateKinds.Cancelled && - order.ItemId == itemId && - order.RemainingAmount > 0.01f) - .Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId))) - .Where(entry => entry.Station is not null && GetInventoryAmount(entry.Station!.Inventory, itemId) > 0.01f) - .OrderByDescending(entry => - { - var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f; - return entry.Order.Valuation - distancePenalty; - }) - .FirstOrDefault(); - - return bestOrder.Station ?? preferred; + behavior.Kind = "idle"; + ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); + return; } - internal void PlanEnergySupply(ShipRuntime ship, SimulationWorld world) + behavior.NodeId ??= node.Id; + + if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f + && behavior.Phase is "travel-to-node" or "extract") { - var behavior = ship.DefaultBehavior; - var cargoItemId = ship.Definition.CargoItemId; - if (cargoItemId is null) - { - behavior.Kind = "idle"; - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - var cargoAmount = GetShipCargoAmount(ship); - if (cargoAmount > 0.01f) - { - var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId); - if (destination is null) - { - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - behavior.StationId = destination.Id; - switch (behavior.Phase) - { - case "dock": - case "unload": - case "refuel": - case "undock": - ship.ControllerTask = CreateStationSupportTask(world, ship, destination, behavior.Phase); - break; - default: - behavior.Phase = "travel-to-destination"; - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetEntityId = destination.Id, - TargetSystemId = destination.SystemId, - TargetPosition = destination.Position, - Threshold = 18f, - }; - break; - } - - return; - } - - var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId); - if (source is null) - { - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - behavior.StationId = source.Id; - switch (behavior.Phase) - { - case "dock": - case "load": - case "refuel": - case "undock": - ship.ControllerTask = CreateStationSupportTask(world, ship, source, behavior.Phase); - break; - default: - behavior.Phase = "travel-to-source"; - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetEntityId = source.Id, - TargetSystemId = source.SystemId, - TargetPosition = source.Position, - Threshold = 18f, - }; - break; - } + behavior.Phase = "travel-to-station"; } - private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) => - phase switch + if (ship.DockedStationId == refinery.Id) + { + if (GetShipCargoAmount(ship) > 0.01f) + { + behavior.Phase = "unload"; + } + else if (behavior.Phase is "dock" or "unload") + { + behavior.Phase = "undock"; + } + } + else if (behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract") + { + behavior.Phase = "travel-to-station"; + } + + switch (behavior.Phase) + { + case "extract": + var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); + ship.ControllerTask = new ControllerTaskRuntime { - "dock" => new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Dock, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = station.Position, - Threshold = 8f, - }, - "load" => new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Load, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = station.Position, - Threshold = 8f, - }, - "unload" => new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Unload, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = station.Position, - Threshold = 8f, - }, - "refuel" => new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Refuel, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = station.Position, - Threshold = 12f, - }, - "undock" => new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Undock, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = new Vector3(station.Position.X + world.Balance.UndockDistance, station.Position.Y, station.Position.Z), - Threshold = 8f, - }, - _ => CreateIdleTask(world.Balance.ArrivalThreshold), + Kind = ControllerTaskKind.Extract, + TargetEntityId = node.Id, + TargetSystemId = node.SystemId, + TargetPosition = extractionPosition, + Threshold = 5f, }; - - internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) - { - var behavior = ship.DefaultBehavior; - var station = world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId); - var site = station is null ? null : GetConstructionSiteForStation(world, station.Id); - if (station is null) + break; + case "travel-to-station": + ship.ControllerTask = new ControllerTaskRuntime { - behavior.Kind = "idle"; - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world); - behavior.ModuleId = moduleId; - if (moduleId is null) - { - ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); - return; - } - - if (ship.DockedStationId is not null) - { - var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); - if (dockedStation is not null) - { - dockedStation.DockedShipIds.Remove(ship.Id); - ReleaseDockingPad(dockedStation, ship.Id); - } - - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - ship.Position = GetConstructionHoldPosition(station, ship.Id); - ship.TargetPosition = ship.Position; - } - - var constructionHoldPosition = GetConstructionHoldPosition(station, ship.Id); - var isAtConstructionHold = ship.SystemId == station.SystemId - && ship.Position.DistanceTo(constructionHoldPosition) <= 10f; - - if (isAtConstructionHold) - { - if (NeedsRefuel(ship, world)) - { - behavior.Phase = "refuel"; - } - else if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site)) - { - behavior.Phase = "deliver-to-site"; - } - else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site)) - { - behavior.Phase = "build-site"; - } - else if (site is not null) - { - behavior.Phase = "wait-for-materials"; - } - else if (CanStartModuleConstruction(station, world.ModuleRecipes[moduleId])) - { - behavior.Phase = "construct-module"; - } - else - { - behavior.Phase = "wait-for-materials"; - } - } - else if (behavior.Phase != "travel-to-station") - { - behavior.Phase = "travel-to-station"; - } - - switch (behavior.Phase) - { - case "refuel": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Refuel, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 10f, - }; - break; - case "construct-module": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.ConstructModule, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 10f, - }; - break; - case "deliver-to-site": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.DeliverConstruction, - TargetEntityId = site?.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 10f, - }; - break; - case "build-site": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.BuildConstructionSite, - TargetEntityId = site?.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 10f, - }; - break; - case "wait-for-materials": - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Idle, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 0f, - }; - break; - default: - ship.ControllerTask = new ControllerTaskRuntime - { - Kind = ControllerTaskKind.Travel, - TargetEntityId = station.Id, - TargetSystemId = station.SystemId, - TargetPosition = constructionHoldPosition, - Threshold = 10f, - }; - behavior.Phase = "travel-to-station"; - break; - } - } - - private void AdvanceControlState(ShipRuntime ship, SimulationWorld world, string controllerEvent) - { - var commander = GetShipCommander(world, ship); - if (ship.Order is not null && controllerEvent == "arrived") - { - ship.Order = null; - ship.ControllerTask.Kind = ControllerTaskKind.Idle; - if (commander is not null) - { - commander.ActiveOrder = null; - commander.ActiveTask = new CommanderTaskRuntime - { - Kind = ShipTaskKinds.Idle, - Status = WorkStatus.Completed, - TargetSystemId = ship.SystemId, - Threshold = 0f, - }; - } - - return; - } - - _shipBehaviorStateMachine.ApplyEvent(this, ship, world, controllerEvent); - if (commander is not null) - { - SyncShipToCommander(ship, commander); - if (commander.ActiveTask is not null) - { - commander.ActiveTask.Status = controllerEvent == "none" ? WorkStatus.Active : WorkStatus.Completed; - } - } - } - - private static void TrackHistory(ShipRuntime ship, string controllerEvent) - { - var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}"; - if (signature == ship.LastSignature) - { - return; - } - - ship.LastSignature = signature; - var target = ship.ControllerTask.TargetEntityId - ?? ship.ControllerTask.TargetSystemId - ?? "none"; - var eventSummary = controllerEvent == "none" ? string.Empty : $" event={controllerEvent}"; - ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind.ToContractValue()} target={target} cargo={GetShipCargoAmount(ship):0.#}{eventSummary}"); - if (ship.History.Count > 18) - { - ship.History.RemoveAt(0); - } - } - - private static ControllerTaskRuntime CreateIdleTask(float threshold) => - new() - { - Kind = ControllerTaskKind.Idle, - Threshold = threshold, + Kind = ControllerTaskKind.Travel, + TargetEntityId = refinery.Id, + TargetSystemId = refinery.SystemId, + TargetPosition = refinery.Position, + Threshold = refinery.Radius + 8f, }; - - private static ControllerTaskKind ParseControllerTaskKind(string kind) => kind switch - { - "travel" => ControllerTaskKind.Travel, - "extract" => ControllerTaskKind.Extract, - "dock" => ControllerTaskKind.Dock, - "load" => ControllerTaskKind.Load, - "unload" => ControllerTaskKind.Unload, - "refuel" => ControllerTaskKind.Refuel, - "deliver-construction" => ControllerTaskKind.DeliverConstruction, - "build-construction-site" => ControllerTaskKind.BuildConstructionSite, - "load-workers" => ControllerTaskKind.LoadWorkers, - "unload-workers" => ControllerTaskKind.UnloadWorkers, - "construct-module" => ControllerTaskKind.ConstructModule, - "undock" => ControllerTaskKind.Undock, - _ => ControllerTaskKind.Idle, - }; - - private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task) - { - if (commander is null) + break; + case "dock": + ship.ControllerTask = new ControllerTaskRuntime { - return; - } + Kind = ControllerTaskKind.Dock, + TargetEntityId = refinery.Id, + TargetSystemId = refinery.SystemId, + TargetPosition = refinery.Position, + Threshold = refinery.Radius + 4f, + }; + break; + case "unload": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Unload, + TargetEntityId = refinery.Id, + TargetSystemId = refinery.SystemId, + TargetPosition = refinery.Position, + Threshold = 0f, + }; + break; + case "undock": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Undock, + TargetEntityId = refinery.Id, + TargetSystemId = refinery.SystemId, + TargetPosition = new Vector3(refinery.Position.X + world.Balance.UndockDistance, refinery.Position.Y, refinery.Position.Z), + Threshold = 8f, + }; + break; + default: + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Travel, + TargetEntityId = node.Id, + TargetSystemId = node.SystemId, + TargetPosition = node.Position, + Threshold = 18f, + }; + behavior.Phase = "travel-to-node"; + break; + } + } + internal static StationRuntime? SelectBestBuyStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId) + { + var preferred = preferredStationId is null + ? null + : world.Stations.FirstOrDefault(station => station.Id == preferredStationId); + + var bestOrder = world.MarketOrders + .Where(order => + order.Kind == MarketOrderKinds.Buy && + order.ConstructionSiteId is null && + order.State != MarketOrderStateKinds.Cancelled && + order.ItemId == itemId && + order.RemainingAmount > 0.01f) + .Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId))) + .Where(entry => entry.Station is not null) + .OrderByDescending(entry => + { + var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f; + return entry.Order.Valuation - distancePenalty; + }) + .FirstOrDefault(); + + return bestOrder.Station ?? preferred; + } + + private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) => + phase switch + { + "dock" => new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Dock, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = station.Position, + Threshold = 8f, + }, + "load" => new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Load, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = station.Position, + Threshold = 8f, + }, + "unload" => new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Unload, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = station.Position, + Threshold = 8f, + }, + "undock" => new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Undock, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = new Vector3(station.Position.X + world.Balance.UndockDistance, station.Position.Y, station.Position.Z), + Threshold = 8f, + }, + _ => CreateIdleTask(world.Balance.ArrivalThreshold), + }; + + internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) + { + var behavior = ship.DefaultBehavior; + var station = world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId); + var site = station is null ? null : GetConstructionSiteForStation(world, station.Id); + if (station is null) + { + behavior.Kind = "idle"; + ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); + return; + } + + var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world); + behavior.ModuleId = moduleId; + if (moduleId is null) + { + ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); + return; + } + + if (ship.DockedStationId is not null) + { + var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); + if (dockedStation is not null) + { + dockedStation.DockedShipIds.Remove(ship.Id); + ReleaseDockingPad(dockedStation, ship.Id); + } + + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + ship.Position = GetConstructionHoldPosition(station, ship.Id); + ship.TargetPosition = ship.Position; + } + + var constructionHoldPosition = GetConstructionHoldPosition(station, ship.Id); + var isAtConstructionHold = ship.SystemId == station.SystemId + && ship.Position.DistanceTo(constructionHoldPosition) <= 10f; + + if (isAtConstructionHold) + { + if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site)) + { + behavior.Phase = "deliver-to-site"; + } + else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site)) + { + behavior.Phase = "build-site"; + } + else if (site is not null) + { + behavior.Phase = "wait-for-materials"; + } + else if (CanStartModuleConstruction(station, world.ModuleRecipes[moduleId])) + { + behavior.Phase = "construct-module"; + } + else + { + behavior.Phase = "wait-for-materials"; + } + } + else if (behavior.Phase != "travel-to-station") + { + behavior.Phase = "travel-to-station"; + } + + switch (behavior.Phase) + { + case "construct-module": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.ConstructModule, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = constructionHoldPosition, + Threshold = 10f, + }; + break; + case "deliver-to-site": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.DeliverConstruction, + TargetEntityId = site?.Id, + TargetSystemId = station.SystemId, + TargetPosition = constructionHoldPosition, + Threshold = 10f, + }; + break; + case "build-site": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.BuildConstructionSite, + TargetEntityId = site?.Id, + TargetSystemId = station.SystemId, + TargetPosition = constructionHoldPosition, + Threshold = 10f, + }; + break; + case "wait-for-materials": + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Idle, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = constructionHoldPosition, + Threshold = 0f, + }; + break; + default: + ship.ControllerTask = new ControllerTaskRuntime + { + Kind = ControllerTaskKind.Travel, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + TargetPosition = constructionHoldPosition, + Threshold = 10f, + }; + behavior.Phase = "travel-to-station"; + break; + } + } + + private void AdvanceControlState(ShipRuntime ship, SimulationWorld world, string controllerEvent) + { + var commander = GetShipCommander(world, ship); + if (ship.Order is not null && controllerEvent == "arrived") + { + ship.Order = null; + ship.ControllerTask.Kind = ControllerTaskKind.Idle; + if (commander is not null) + { + commander.ActiveOrder = null; commander.ActiveTask = new CommanderTaskRuntime { - Kind = task.Kind.ToContractValue(), - Status = task.Status, - TargetEntityId = task.TargetEntityId, - TargetNodeId = task.TargetNodeId, - TargetPosition = task.TargetPosition, - TargetSystemId = task.TargetSystemId, - Threshold = task.Threshold, + Kind = ShipTaskKinds.Idle, + Status = WorkStatus.Completed, + TargetSystemId = ship.SystemId, + Threshold = 0f, }; + } + + return; } + + _shipBehaviorStateMachine.ApplyEvent(this, ship, world, controllerEvent); + if (commander is not null) + { + SyncShipToCommander(ship, commander); + if (commander.ActiveTask is not null) + { + commander.ActiveTask.Status = controllerEvent == "none" ? WorkStatus.Active : WorkStatus.Completed; + } + } + } + + private static void TrackHistory(ShipRuntime ship, string controllerEvent) + { + var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}"; + if (signature == ship.LastSignature) + { + return; + } + + ship.LastSignature = signature; + var target = ship.ControllerTask.TargetEntityId + ?? ship.ControllerTask.TargetSystemId + ?? "none"; + var eventSummary = controllerEvent == "none" ? string.Empty : $" event={controllerEvent}"; + ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind.ToContractValue()} target={target} cargo={GetShipCargoAmount(ship):0.#}{eventSummary}"); + if (ship.History.Count > 18) + { + ship.History.RemoveAt(0); + } + } + + private static ControllerTaskRuntime CreateIdleTask(float threshold) => + new() + { + Kind = ControllerTaskKind.Idle, + Threshold = threshold, + }; + + private static ControllerTaskKind ParseControllerTaskKind(string kind) => kind switch + { + "travel" => ControllerTaskKind.Travel, + "extract" => ControllerTaskKind.Extract, + "dock" => ControllerTaskKind.Dock, + "load" => ControllerTaskKind.Load, + "unload" => ControllerTaskKind.Unload, + "deliver-construction" => ControllerTaskKind.DeliverConstruction, + "build-construction-site" => ControllerTaskKind.BuildConstructionSite, + "load-workers" => ControllerTaskKind.LoadWorkers, + "unload-workers" => ControllerTaskKind.UnloadWorkers, + "construct-module" => ControllerTaskKind.ConstructModule, + "undock" => ControllerTaskKind.Undock, + _ => ControllerTaskKind.Idle, + }; + + private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task) + { + if (commander is null) + { + return; + } + + commander.ActiveTask = new CommanderTaskRuntime + { + Kind = task.Kind.ToContractValue(), + Status = task.Status, + TargetEntityId = task.TargetEntityId, + TargetNodeId = task.TargetNodeId, + TargetPosition = task.TargetPosition, + TargetSystemId = task.TargetSystemId, + Threshold = task.Threshold, + }; + } } diff --git a/apps/backend/Simulation/SimulationEngine.StationSystems.cs b/apps/backend/Simulation/SimulationEngine.StationSystems.cs index 6a612dd..15d7f38 100644 --- a/apps/backend/Simulation/SimulationEngine.StationSystems.cs +++ b/apps/backend/Simulation/SimulationEngine.StationSystems.cs @@ -5,536 +5,495 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private const int StrategicControlTargetSystems = 5; + private const int StrategicControlTargetSystems = 5; - private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) + private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) + { + var factionPopulation = new Dictionary(StringComparer.Ordinal); + foreach (var station in world.Stations) { - var factionPopulation = new Dictionary(StringComparer.Ordinal); - foreach (var station in world.Stations) - { - UpdateStationPopulation(station, deltaSeconds, events); - ReviewStationMarketOrders(world, station); - RunStationProduction(world, station, deltaSeconds, events); - factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population; - } - - foreach (var faction in world.Factions) - { - faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id); - } + UpdateStationPopulation(station, deltaSeconds, events); + ReviewStationMarketOrders(world, station); + RunStationProduction(world, station, deltaSeconds, events); + factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population; } - private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection events) + foreach (var faction in world.Factions) { - station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f); + faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id); + } + } - var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; - var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); - var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; - var hasPower = station.EnergyStored > 0.01f; - var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); - station.PopulationCapacity = 40f + (habitatModules * 220f); + private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection events) + { + station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); - if (waterSatisfied && hasPower) - { - if (habitatModules > 0 && station.Population < station.PopulationCapacity) - { - station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds)); - } - } - else if (station.Population > 0f) - { - var previous = station.Population; - station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds)); - if (MathF.Floor(previous) > MathF.Floor(station.Population)) - { - events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Definition.Label} lost population due to support shortages.", DateTimeOffset.UtcNow)); - } - } + var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; + var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); + var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; + var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); + station.PopulationCapacity = 40f + (habitatModules * 220f); - station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); + if (waterSatisfied) + { + if (habitatModules > 0 && station.Population < station.PopulationCapacity) + { + station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds)); + } + } + else if (station.Population > 0f) + { + var previous = station.Population; + station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds)); + if (MathF.Floor(previous) > MathF.Floor(station.Population)) + { + events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow)); + } } - private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) + station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); + } + + private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) + { + if (station.CommanderId is null) { - if (station.CommanderId is null) - { - return; - } - - var desiredOrders = new List(); - var fuelReserve = MathF.Max(80f, CountModules(station.InstalledModules, "power-core") * 140f); - var energyCellReserve = HasStationModules(station, "power-core", "liquid-tank") ? MathF.Max(20f, CountModules(station.InstalledModules, "power-core") * 40f) : 0f; - var waterReserve = MathF.Max(30f, station.Population * 3f); - var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f; - var oreReserve = HasRefineryCapability(station) ? 180f : 0f; - var gasReserve = CanProcessFuel(station) ? 120f : 0f; - var shipPartsReserve = HasStationModules(station, "fabricator-array") - && !HasStationModules(station, "component-factory", "ship-factory") - && FactionNeedsMoreWarships(world, station.FactionId) - ? 90f - : 0f; - - AddDemandOrder(desiredOrders, station, "fuel", fuelReserve, valuationBase: 1.2f); - AddDemandOrder(desiredOrders, station, "energy-cell", energyCellReserve, valuationBase: 1.1f); - AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f); - AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f); - AddDemandOrder(desiredOrders, station, "gas", gasReserve, valuationBase: 0.95f); - AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f); - AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f); - - AddSupplyOrder(desiredOrders, station, "fuel", fuelReserve * 1.5f, reserveFloor: fuelReserve, valuationBase: 0.8f); - AddSupplyOrder(desiredOrders, station, "energy-cell", energyCellReserve * 1.8f, reserveFloor: energyCellReserve, valuationBase: 0.82f); - AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f); - AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f); - AddSupplyOrder(desiredOrders, station, "gas", gasReserve * 1.4f, reserveFloor: gasReserve, valuationBase: 0.72f); - AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f); - - ReconcileStationMarketOrders(world, station, desiredOrders); + return; } - private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) + var desiredOrders = new List(); + var waterReserve = MathF.Max(30f, station.Population * 3f); + var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f; + var oreReserve = HasRefineryCapability(station) ? 180f : 0f; + var shipPartsReserve = HasStationModules(station, "fabricator-array") + && !HasStationModules(station, "component-factory", "ship-factory") + && FactionNeedsMoreWarships(world, station.FactionId) + ? 90f + : 0f; + + AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f); + AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f); + AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f); + AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f); + + AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f); + AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f); + AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f); + + ReconcileStationMarketOrders(world, station, desiredOrders); + } + + private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) + { + var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); + foreach (var laneKey in GetStationProductionLanes(station)) { - var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); - foreach (var laneKey in GetStationProductionLanes(station)) + var recipe = SelectProductionRecipe(world, station, laneKey); + if (recipe is null) + { + station.ProductionLaneTimers[laneKey] = 0f; + continue; + } + + var throughput = GetStationProductionThroughput(station, recipe); + + var produced = 0f; + station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput); + while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe)) + { + station.ProductionLaneTimers[laneKey] -= recipe.Duration; + foreach (var input in recipe.Inputs) { - var recipe = SelectProductionRecipe(world, station, laneKey); - if (recipe is null || station.EnergyStored <= 0.01f) - { - station.ProductionLaneTimers[laneKey] = 0f; - continue; - } - - var throughput = GetStationProductionThroughput(station, recipe); - if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds * throughput)) - { - station.ProductionLaneTimers[laneKey] = 0f; - continue; - } - - var produced = 0f; - station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput); - while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe)) - { - station.ProductionLaneTimers[laneKey] -= recipe.Duration; - foreach (var input in recipe.Inputs) - { - RemoveInventory(station.Inventory, input.ItemId, input.Amount); - } - - if (recipe.ShipOutputId is not null) - { - produced += CompleteShipRecipe(world, station, recipe, events); - continue; - } - - foreach (var output in recipe.Outputs) - { - produced += TryAddStationInventory(world, station, output.ItemId, output.Amount); - } - } - - if (produced <= 0.01f) - { - continue; - } - - events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Definition.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow)); - if (faction is not null) - { - faction.GoodsProduced += produced; - } - } - } - - private static IEnumerable GetStationProductionLanes(StationRuntime station) - { - if (CountModules(station.InstalledModules, "refinery-stack") > 0) - { - yield return "refinery"; + RemoveInventory(station.Inventory, input.ItemId, input.Amount); } - if (CountModules(station.InstalledModules, "fuel-processor") > 0) - { - yield return "fuel"; - } - - if (CountModules(station.InstalledModules, "fabricator-array") > 0) - { - yield return "fabrication"; - } - - if (CountModules(station.InstalledModules, "component-factory") > 0) - { - yield return "components"; - } - - if (CountModules(station.InstalledModules, "ship-factory") > 0) - { - yield return "shipyard"; - } - } - - private static float GetStationProductionTimer(StationRuntime station, string laneKey) => - station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f; - - private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) => - world.Recipes.Values - .Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal)) - .OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe)) - .FirstOrDefault(recipe => CanRunRecipe(world, station, recipe)); - - private static string? GetStationProductionLaneKey(RecipeDefinition recipe) - { - if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal)) - { - return "fuel"; - } - - if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal)) - { - return "refinery"; - } - - if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal)) - { - return "fabrication"; - } - - if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal)) - { - return "components"; - } - - if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal)) - { - return "shipyard"; - } - - return null; - } - - private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) - { - var priority = (float)recipe.Priority; - var producesFuel = recipe.Outputs.Any(output => string.Equals(output.ItemId, "fuel", StringComparison.Ordinal)); - if (producesFuel) - { - var fuelCapacity = MathF.Max(GetStationFuelCapacity(station), 1f); - var fuelRatio = GetInventoryAmount(station.Inventory, "fuel") / fuelCapacity; - priority += (1f - Math.Clamp(fuelRatio, 0f, 1f)) * 200f; - } - - var expansionPressure = GetFactionExpansionPressure(world, station.FactionId); - var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f; - priority += recipe.Id switch - { - "ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory") - ? -140f * MathF.Max(expansionPressure, fleetPressure) - : 280f * MathF.Max(expansionPressure, fleetPressure), - "hull-fabrication" => 180f * expansionPressure, - "equipment-assembly" => 170f * expansionPressure, - "gun-assembly" => 160f * expansionPressure, - "command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly" - => 220f * MathF.Max(expansionPressure, fleetPressure), - "frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure), - "destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure), - "cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure), - "ammo-fabrication" => -80f * expansionPressure, - "trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly" - => -120f * expansionPressure, - _ => 0f, - }; - - return priority; - } - - private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe) - { - var categoryMatch = string.Equals(station.Definition.Category, recipe.FacilityCategory, StringComparison.Ordinal) - || (string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal) - && station.Definition.Category is "station" or "shipyard" or "defense" or "gate"); - return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); - } - - private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) - { if (recipe.ShipOutputId is not null) { - if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition) - || !CanLaunchShipFromStation(station)) - { - return false; - } - - if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal) - || !FactionNeedsMoreWarships(world, station.FactionId)) - { - return false; - } + produced += CompleteShipRecipe(world, station, recipe, events); + continue; } - if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount)) + foreach (var output in recipe.Outputs) { - return false; + produced += TryAddStationInventory(world, station, output.ItemId, output.Amount); } + } - return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount)); + if (produced <= 0.01f) + { + continue; + } + + events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow)); + if (faction is not null) + { + faction.GoodsProduced += produced; + } + } + } + + private static IEnumerable GetStationProductionLanes(StationRuntime station) + { + if (CountModules(station.InstalledModules, "refinery-stack") > 0) + { + yield return "refinery"; } - private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) + if (CountModules(station.InstalledModules, "fabricator-array") > 0) { - if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) - { - return false; - } - - var requiredModule = GetStorageRequirement(itemDefinition.Storage); - if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) - { - return false; - } - - var capacity = GetStationStorageCapacity(station, itemDefinition.Storage); - if (capacity <= 0.01f) - { - return false; - } - - var used = station.Inventory - .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == itemDefinition.Storage) - .Sum(entry => entry.Value); - return used + amount <= capacity + 0.001f; + yield return "fabrication"; } - private static void AddDemandOrder(ICollection desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase) + if (CountModules(station.InstalledModules, "component-factory") > 0) { - var current = GetInventoryAmount(station.Inventory, itemId); - if (current >= targetAmount - 0.01f) - { - return; - } - - var deficit = targetAmount - current; - var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount); - desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null)); + yield return "components"; } - private static void AddSupplyOrder(ICollection desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase) + if (CountModules(station.InstalledModules, "ship-factory") > 0) { - var current = GetInventoryAmount(station.Inventory, itemId); - if (current <= triggerAmount + 0.01f) - { - return; - } + yield return "shipyard"; + } + } - var surplus = current - reserveFloor; - if (surplus <= 0.01f) - { - return; - } + private static float GetStationProductionTimer(StationRuntime station, string laneKey) => + station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f; - desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor)); + private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) => + world.Recipes.Values + .Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal)) + .OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe)) + .FirstOrDefault(recipe => CanRunRecipe(world, station, recipe)); + + private static string? GetStationProductionLaneKey(RecipeDefinition recipe) + { + if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal)) + { + return "refinery"; } - private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection desiredOrders) + if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal)) { - var existingOrders = world.MarketOrders - .Where(order => order.StationId == station.Id && order.ConstructionSiteId is null) - .ToList(); - - foreach (var desired in desiredOrders) - { - var order = existingOrders.FirstOrDefault(candidate => - candidate.Kind == desired.Kind && - candidate.ItemId == desired.ItemId && - candidate.ConstructionSiteId is null); - - if (order is null) - { - order = new MarketOrderRuntime - { - Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}", - FactionId = station.FactionId, - StationId = station.Id, - Kind = desired.Kind, - ItemId = desired.ItemId, - Amount = desired.Amount, - RemainingAmount = desired.Amount, - Valuation = desired.Valuation, - ReserveThreshold = desired.ReserveThreshold, - State = MarketOrderStateKinds.Open, - }; - world.MarketOrders.Add(order); - station.MarketOrderIds.Add(order.Id); - existingOrders.Add(order); - continue; - } - - order.RemainingAmount = desired.Amount; - order.Valuation = desired.Valuation; - order.ReserveThreshold = desired.ReserveThreshold; - order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open; - } - - foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId))) - { - order.RemainingAmount = 0f; - order.State = MarketOrderStateKinds.Cancelled; - } + return "fabrication"; } - private static bool HasRefineryCapability(StationRuntime station) => - HasStationModules(station, "refinery-stack", "power-core", "bulk-bay"); - - private static bool CanProcessFuel(StationRuntime station) => - HasStationModules(station, "fuel-processor", "power-core", "gas-tank", "liquid-tank"); - - private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection events) + if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal)) { - if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition)) - { - return 0f; - } - - var spawnPosition = new Vector3(station.Position.X + station.Definition.Radius + 32f, station.Position.Y, station.Position.Z); - var ship = new ShipRuntime - { - Id = $"ship-{world.Ships.Count + 1}", - SystemId = station.SystemId, - Definition = definition, - FactionId = station.FactionId, - Position = spawnPosition, - TargetPosition = spawnPosition, - SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition), - DefaultBehavior = CreateSpawnedShipBehavior(definition, station), - ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold), - Health = definition.MaxHealth, - }; - - ship.Inventory["fuel"] = 120f; - world.Ships.Add(ship); - if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction) - { - faction.ShipsBuilt += 1; - } - - events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Definition.Label} launched {definition.Label}.", DateTimeOffset.UtcNow)); - return 1f; + return "components"; } - private static bool CanLaunchShipFromStation(StationRuntime station) => - HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small"); - - private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId) + if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal)) { - var militaryShipCount = world.Ships.Count(ship => - ship.FactionId == factionId - && string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal)); - var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); - var controlledSystems = GetFactionControlledSystemsCount(world, factionId); - var expansionDeficit = Math.Max(0, targetSystems - controlledSystems); - var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3)); - return militaryShipCount < targetWarships; + return "shipyard"; } - private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) - { - return world.Claims - .Where(claim => claim.State != ClaimStateKinds.Destroyed) - .Select(claim => claim.SystemId) - .Distinct(StringComparer.Ordinal) - .Count(systemId => FactionControlsSystem(world, factionId, systemId)); - } + return null; + } - private static float GetFactionExpansionPressure(SimulationWorld world, string factionId) - { - var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); - var controlledSystems = GetFactionControlledSystemsCount(world, factionId); - var deficit = Math.Max(0, targetSystems - controlledSystems); - return Math.Clamp(deficit / (float)targetSystems, 0f, 1f); - } + private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) + { + var priority = (float)recipe.Priority; - private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) + var expansionPressure = GetFactionExpansionPressure(world, station.FactionId); + var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f; + priority += recipe.Id switch { - var buildableLocations = world.Claims - .Where(claim => - claim.SystemId == systemId && - claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active) - .ToList(); - if (buildableLocations.Count == 0) - { - return false; - } - - var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId); - return ownedLocations > (buildableLocations.Count / 2f); - } - - private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new() - { - CurrentSystemId = station.SystemId, - SpaceLayer = SpaceLayerKinds.LocalSpace, - CurrentNodeId = station.NodeId, - CurrentBubbleId = station.BubbleId, - LocalPosition = position, - SystemPosition = position, - MovementRegime = MovementRegimeKinds.LocalFlight, + "ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory") + ? -140f * MathF.Max(expansionPressure, fleetPressure) + : 280f * MathF.Max(expansionPressure, fleetPressure), + "hull-fabrication" => 180f * expansionPressure, + "equipment-assembly" => 170f * expansionPressure, + "gun-assembly" => 160f * expansionPressure, + "command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly" + => 220f * MathF.Max(expansionPressure, fleetPressure), + "frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure), + "destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure), + "cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure), + "ammo-fabrication" => -80f * expansionPressure, + "trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly" + => -120f * expansionPressure, + _ => 0f, }; - private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) - { - if (!string.Equals(definition.Role, "military", StringComparison.Ordinal)) - { - return new DefaultBehaviorRuntime - { - Kind = "idle", - }; - } + return priority; + } - var patrolRadius = station.Definition.Radius + 90f; - return new DefaultBehaviorRuntime + private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe) + { + var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal) + || string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal) + || string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal); + return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); + } + + private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) + { + if (recipe.ShipOutputId is not null) + { + if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition) + || !CanLaunchShipFromStation(station)) + { + return false; + } + + if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal) + || !FactionNeedsMoreWarships(world, station.FactionId)) + { + return false; + } + } + + if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount)) + { + return false; + } + + return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount)); + } + + private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) + { + if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) + { + return false; + } + + var requiredModule = GetStorageRequirement(itemDefinition.CargoKind); + if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) + { + return false; + } + + var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind); + if (capacity <= 0.01f) + { + return false; + } + + var used = station.Inventory + .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind) + .Sum(entry => entry.Value); + return used + amount <= capacity + 0.001f; + } + + private static void AddDemandOrder(ICollection desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase) + { + var current = GetInventoryAmount(station.Inventory, itemId); + if (current >= targetAmount - 0.01f) + { + return; + } + + var deficit = targetAmount - current; + var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount); + desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null)); + } + + private static void AddSupplyOrder(ICollection desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase) + { + var current = GetInventoryAmount(station.Inventory, itemId); + if (current <= triggerAmount + 0.01f) + { + return; + } + + var surplus = current - reserveFloor; + if (surplus <= 0.01f) + { + return; + } + + desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor)); + } + + private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection desiredOrders) + { + var existingOrders = world.MarketOrders + .Where(order => order.StationId == station.Id && order.ConstructionSiteId is null) + .ToList(); + + foreach (var desired in desiredOrders) + { + var order = existingOrders.FirstOrDefault(candidate => + candidate.Kind == desired.Kind && + candidate.ItemId == desired.ItemId && + candidate.ConstructionSiteId is null); + + if (order is null) + { + order = new MarketOrderRuntime { - Kind = "patrol", - PatrolPoints = - [ - new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z), + Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}", + FactionId = station.FactionId, + StationId = station.Id, + Kind = desired.Kind, + ItemId = desired.ItemId, + Amount = desired.Amount, + RemainingAmount = desired.Amount, + Valuation = desired.Valuation, + ReserveThreshold = desired.ReserveThreshold, + State = MarketOrderStateKinds.Open, + }; + world.MarketOrders.Add(order); + station.MarketOrderIds.Add(order.Id); + existingOrders.Add(order); + continue; + } + + order.RemainingAmount = desired.Amount; + order.Valuation = desired.Valuation; + order.ReserveThreshold = desired.ReserveThreshold; + order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open; + } + + foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId))) + { + order.RemainingAmount = 0f; + order.State = MarketOrderStateKinds.Cancelled; + } + } + + private static bool HasRefineryCapability(StationRuntime station) => + HasStationModules(station, "refinery-stack", "power-core", "bulk-bay"); + + private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection events) + { + if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition)) + { + return 0f; + } + + var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z); + var ship = new ShipRuntime + { + Id = $"ship-{world.Ships.Count + 1}", + SystemId = station.SystemId, + Definition = definition, + FactionId = station.FactionId, + Position = spawnPosition, + TargetPosition = spawnPosition, + SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition), + DefaultBehavior = CreateSpawnedShipBehavior(definition, station), + ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold), + Health = definition.MaxHealth, + }; + + world.Ships.Add(ship); + if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction) + { + faction.ShipsBuilt += 1; + } + + events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow)); + return 1f; + } + + private static bool CanLaunchShipFromStation(StationRuntime station) => + HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small"); + + private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId) + { + var militaryShipCount = world.Ships.Count(ship => + ship.FactionId == factionId + && string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal)); + var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); + var controlledSystems = GetFactionControlledSystemsCount(world, factionId); + var expansionDeficit = Math.Max(0, targetSystems - controlledSystems); + var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3)); + return militaryShipCount < targetWarships; + } + + private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) + { + return world.Claims + .Where(claim => claim.State != ClaimStateKinds.Destroyed) + .Select(claim => claim.SystemId) + .Distinct(StringComparer.Ordinal) + .Count(systemId => FactionControlsSystem(world, factionId, systemId)); + } + + private static float GetFactionExpansionPressure(SimulationWorld world, string factionId) + { + var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); + var controlledSystems = GetFactionControlledSystemsCount(world, factionId); + var deficit = Math.Max(0, targetSystems - controlledSystems); + return Math.Clamp(deficit / (float)targetSystems, 0f, 1f); + } + + private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) + { + var buildableLocations = world.Claims + .Where(claim => + claim.SystemId == systemId && + claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active) + .ToList(); + if (buildableLocations.Count == 0) + { + return false; + } + + var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId); + return ownedLocations > (buildableLocations.Count / 2f); + } + + private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new() + { + CurrentSystemId = station.SystemId, + SpaceLayer = SpaceLayerKinds.LocalSpace, + CurrentNodeId = station.NodeId, + CurrentBubbleId = station.BubbleId, + LocalPosition = position, + SystemPosition = position, + MovementRegime = MovementRegimeKinds.LocalFlight, + }; + + private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) + { + if (!string.Equals(definition.Role, "military", StringComparison.Ordinal)) + { + return new DefaultBehaviorRuntime + { + Kind = "idle", + }; + } + + var patrolRadius = station.Radius + 90f; + return new DefaultBehaviorRuntime + { + Kind = "patrol", + PatrolPoints = + [ + new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z), new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius), new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z), new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius), ], - }; - } + }; + } - private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe) + private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe) + { + if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal)) { - if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal)) - { - return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack")); - } - - if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal)) - { - return Math.Max(1, CountModules(station.InstalledModules, "fuel-processor")); - } - - if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal)) - { - return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array")); - } - - if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal)) - { - return Math.Max(1, CountModules(station.InstalledModules, "component-factory")); - } - - if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal)) - { - return Math.Max(1, CountModules(station.InstalledModules, "ship-factory")); - } - - return 1f; + return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack")); } - private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold); + if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal)) + { + return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array")); + } + + if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal)) + { + return Math.Max(1, CountModules(station.InstalledModules, "component-factory")); + } + + if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal)) + { + return Math.Max(1, CountModules(station.InstalledModules, "ship-factory")); + } + + return 1f; + } + + private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold); } diff --git a/apps/backend/Simulation/SimulationEngine.cs b/apps/backend/Simulation/SimulationEngine.cs index 35a74dc..793e42d 100644 --- a/apps/backend/Simulation/SimulationEngine.cs +++ b/apps/backend/Simulation/SimulationEngine.cs @@ -4,106 +4,98 @@ namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { - private readonly OrbitalSimulationOptions _orbitalSimulation; - private const float ShipFuelToEnergyRatio = 12f; - private const float StationFuelToEnergyRatio = 18f; - private const float CapacitorEnergyPerModule = 120f; - private const float StationEnergyPerPowerCore = 480f; - private const float ShipFuelPerReactor = 100f; - private const float StationFuelPerTank = 500f; - private const float WaterConsumptionPerWorkerPerSecond = 0.004f; - private const float PopulationGrowthPerSecond = 0.012f; - private const float PopulationAttritionPerSecond = 0.018f; - private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault(); - private static readonly IReadOnlyList _worldUpdatePipeline = - [ - new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)), + private readonly OrbitalSimulationOptions _orbitalSimulation; + private const float WaterConsumptionPerWorkerPerSecond = 0.004f; + private const float PopulationGrowthPerSecond = 0.012f; + private const float PopulationAttritionPerSecond = 0.018f; + private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault(); + private static readonly IReadOnlyList _worldUpdatePipeline = + [ + new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)), new((engine, world, deltaSeconds, nowUtc, events) => UpdateClaims(world, events)), new((engine, world, deltaSeconds, nowUtc, events) => UpdateConstructionSites(world, events)), - new((engine, world, deltaSeconds, nowUtc, events) => UpdateStationPower(world, deltaSeconds, events)), new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateStations(world, deltaSeconds, events)), ]; - private static readonly IReadOnlyList _shipUpdatePipeline = - [ - new((engine, ship, world, deltaSeconds, events) => UpdateShipPower(ship, world, deltaSeconds, events)), + private static readonly IReadOnlyList _shipUpdatePipeline = + [ new((engine, ship, world, deltaSeconds, events) => engine.RefreshControlLayers(ship, world)), new((engine, ship, world, deltaSeconds, events) => engine.PlanControllerTask(ship, world)), ]; - public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) + public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) + { + _orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions(); + } + + public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) + { + var events = new List(); + var nowUtc = DateTimeOffset.UtcNow; + world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; + + foreach (var step in _worldUpdatePipeline) { - _orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions(); + step.Execute(this, world, deltaSeconds, nowUtc, events); } - public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) + foreach (var ship in world.Ships) { - var events = new List(); - var nowUtc = DateTimeOffset.UtcNow; - world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; + var previousPosition = ship.Position; + var previousState = ship.State; + var previousBehavior = ship.DefaultBehavior.Kind; + var previousTask = ship.ControllerTask.Kind; - foreach (var step in _worldUpdatePipeline) - { - step.Execute(this, world, deltaSeconds, nowUtc, events); - } + foreach (var step in _shipUpdatePipeline) + { + step.Execute(this, ship, world, deltaSeconds, events); + } - foreach (var ship in world.Ships) - { - var previousPosition = ship.Position; - var previousState = ship.State; - var previousBehavior = ship.DefaultBehavior.Kind; - var previousTask = ship.ControllerTask.Kind; + var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds); + AdvanceControlState(ship, world, controllerEvent); + ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds); + TrackHistory(ship, controllerEvent); - foreach (var step in _shipUpdatePipeline) - { - step.Execute(this, ship, world, deltaSeconds, events); - } - - var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds); - AdvanceControlState(ship, world, controllerEvent); - ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds); - TrackHistory(ship, controllerEvent); - - EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events); - } - - SyncSpatialState(world); - world.GeneratedAtUtc = nowUtc; - - return new WorldDelta( - sequence, - world.TickIntervalMs, - world.OrbitalTimeSeconds, - new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond), - world.GeneratedAtUtc, - false, - events, - BuildSpatialNodeDeltas(world), - BuildLocalBubbleDeltas(world), - BuildNodeDeltas(world), - BuildStationDeltas(world), - BuildClaimDeltas(world), - BuildConstructionSiteDeltas(world), - BuildMarketOrderDeltas(world), - BuildPolicyDeltas(world), - BuildShipDeltas(world), - BuildFactionDeltas(world)); + EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events); } - private delegate void WorldUpdateStepAction( - SimulationEngine engine, - SimulationWorld world, - float deltaSeconds, - DateTimeOffset nowUtc, - List events); + SyncSpatialState(world); + world.GeneratedAtUtc = nowUtc; - private delegate void ShipUpdateStepAction( - SimulationEngine engine, - ShipRuntime ship, - SimulationWorld world, - float deltaSeconds, - List events); + return new WorldDelta( + sequence, + world.TickIntervalMs, + world.OrbitalTimeSeconds, + new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond), + world.GeneratedAtUtc, + false, + events, + BuildSpatialNodeDeltas(world), + BuildLocalBubbleDeltas(world), + BuildNodeDeltas(world), + BuildStationDeltas(world), + BuildClaimDeltas(world), + BuildConstructionSiteDeltas(world), + BuildMarketOrderDeltas(world), + BuildPolicyDeltas(world), + BuildShipDeltas(world), + BuildFactionDeltas(world)); + } - private sealed record WorldUpdateStep(WorldUpdateStepAction Execute); + private delegate void WorldUpdateStepAction( + SimulationEngine engine, + SimulationWorld world, + float deltaSeconds, + DateTimeOffset nowUtc, + List events); - private sealed record ShipUpdateStep(ShipUpdateStepAction Execute); + private delegate void ShipUpdateStepAction( + SimulationEngine engine, + ShipRuntime ship, + SimulationWorld world, + float deltaSeconds, + List events); + + private sealed record WorldUpdateStep(WorldUpdateStepAction Execute); + + private sealed record ShipUpdateStep(ShipUpdateStepAction Execute); } diff --git a/apps/viewer/src/contractsInfrastructure.ts b/apps/viewer/src/contractsInfrastructure.ts index 7c05d4d..8e9da09 100644 --- a/apps/viewer/src/contractsInfrastructure.ts +++ b/apps/viewer/src/contractsInfrastructure.ts @@ -6,6 +6,12 @@ export interface StationActionProgressSnapshot { progress: number; } +export interface StationStorageUsageSnapshot { + storageClass: string; + used: number; + capacity: number; +} + export interface StationSnapshot { id: string; label: string; @@ -19,10 +25,6 @@ export interface StationSnapshot { dockedShips: number; dockedShipIds: string[]; dockingPads: number; - fuelStored: number; - fuelCapacity: number; - energyStored: number; - energyCapacity: number; currentProcesses: StationActionProgressSnapshot[]; inventory: InventoryEntry[]; factionId: string; @@ -32,11 +34,12 @@ export interface StationSnapshot { populationCapacity: number; workforceRequired: number; workforceEffectiveRatio: number; + storageUsage: StationStorageUsageSnapshot[]; installedModules: string[]; marketOrderIds: string[]; } -export interface StationDelta extends StationSnapshot {} +export interface StationDelta extends StationSnapshot { } export interface ClaimSnapshot { id: string; @@ -50,7 +53,7 @@ export interface ClaimSnapshot { activatesAtUtc: string; } -export interface ClaimDelta extends ClaimSnapshot {} +export interface ClaimDelta extends ClaimSnapshot { } export interface ConstructionSiteSnapshot { id: string; @@ -72,4 +75,4 @@ export interface ConstructionSiteSnapshot { marketOrderIds: string[]; } -export interface ConstructionSiteDelta extends ConstructionSiteSnapshot {} +export interface ConstructionSiteDelta extends ConstructionSiteSnapshot { } diff --git a/apps/viewer/src/contractsShips.ts b/apps/viewer/src/contractsShips.ts index 883e97c..35eaa35 100644 --- a/apps/viewer/src/contractsShips.ts +++ b/apps/viewer/src/contractsShips.ts @@ -21,7 +21,6 @@ export interface ShipSnapshot { cargoCapacity: number; cargoItemId?: string | null; workerPopulation: number; - energyStored: number; travelSpeed: number; travelSpeedUnit: string; inventory: InventoryEntry[]; @@ -32,7 +31,7 @@ export interface ShipSnapshot { spatialState: ShipSpatialStateSnapshot; } -export interface ShipDelta extends ShipSnapshot {} +export interface ShipDelta extends ShipSnapshot { } export interface ShipActionProgressSnapshot { label: string; diff --git a/apps/viewer/src/viewerFactionStrip.ts b/apps/viewer/src/viewerFactionStrip.ts index 7bed5db..ef6fb02 100644 --- a/apps/viewer/src/viewerFactionStrip.ts +++ b/apps/viewer/src/viewerFactionStrip.ts @@ -26,7 +26,6 @@ export function renderFactionStrip( return ships .map((ship) => { - const fuel = inventoryAmount(ship.inventory, "fuel"); const cargo = ship.cargoItemId ? inventoryAmount(ship.inventory, ship.cargoItemId) : 0; @@ -54,7 +53,7 @@ export function renderFactionStrip(

${shipLocation.system}${shipLocation.local ? `
${shipLocation.local}` : ""}

-

Fuel ${fuel.toFixed(1)} · Cap ${ship.energyStored.toFixed(1)}${ship.cargoCapacity > 0 ? ` · Cargo ${cargo.toFixed(0)}` : ""}

+

Cargo ${cargo.toFixed(0)}

State ${shipState}

${shipAction ? `
diff --git a/apps/viewer/src/viewerPanels.ts b/apps/viewer/src/viewerPanels.ts index db9692e..792adb1 100644 --- a/apps/viewer/src/viewerPanels.ts +++ b/apps/viewer/src/viewerPanels.ts @@ -37,6 +37,94 @@ interface SystemPanelParams { cameraTargetShipId?: string; } +function laneModuleId(lane: string): string | undefined { + switch (lane) { + case "refinery": + return "refinery-stack"; + case "fabrication": + return "fabricator-array"; + case "components": + return "component-factory"; + case "shipyard": + return "ship-factory"; + default: + return undefined; + } +} + +function formatModuleListWithConstruction( + world: WorldState, + stationId: string, + installedModules: string[], + currentProcesses: { lane: string; label: string; progress: number }[], +): string { + const processByModule = new Map(); + for (const process of currentProcesses) { + const moduleId = laneModuleId(process.lane); + if (!moduleId) { + continue; + } + + const existing = processByModule.get(moduleId) ?? []; + existing.push({ label: process.label, progress: process.progress }); + processByModule.set(moduleId, existing); + } + + const renderedProcessCount = new Map(); + const moduleLines = installedModules.map((moduleId) => { + const processIndex = renderedProcessCount.get(moduleId) ?? 0; + const processes = processByModule.get(moduleId) ?? []; + const process = processes[processIndex]; + renderedProcessCount.set(moduleId, processIndex + 1); + if (!process) { + return moduleId; + } + + return `${moduleId} -> ${process.label} (${Math.round(process.progress * 100)}%)`; + }); + const activeSites = [...world.constructionSites.values()] + .filter((site) => site.stationId === stationId && site.state !== "completed") + .sort((left, right) => left.targetDefinitionId.localeCompare(right.targetDefinitionId)); + + for (const site of activeSites) { + const moduleId = site.blueprintId ?? site.targetDefinitionId; + const progress = Math.round(site.progress * 100); + const tooltip = site.requiredItems.length > 0 + ? site.requiredItems + .map((entry) => `${entry.itemId}: ${entry.amount.toFixed(0)} required / ${inventoryAmount(site.stationId ? (world.stations.get(site.stationId)?.inventory ?? []) : site.deliveredItems, entry.itemId).toFixed(0)} available`) + .join("\n") + : "No material requirements"; + const escapedTooltip = tooltip + .replaceAll("&", "&") + .replaceAll("\"", """) + .replaceAll("<", "<") + .replaceAll(">", ">"); + moduleLines.push(`${moduleId} (${progress}% constructing)`); + } + + return moduleLines.length > 0 ? moduleLines.join("
") : "none"; +} + +function formatStorageClassLabel(storageClass: string): string { + return storageClass + .split("-") + .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) + .join(" "); +} + +function formatStorageUsage(storageUsage: { storageClass: string; used: number; capacity: number }[]): string { + if (storageUsage.length === 0) { + return "none"; + } + + return storageUsage + .map((entry) => { + const percentUsed = entry.capacity > 0 ? Math.round((entry.used / entry.capacity) * 100) : 0; + return `${formatStorageClassLabel(entry.storageClass)} ${percentUsed}% used (${entry.used.toFixed(0)} / ${entry.capacity.toFixed(0)})`; + }) + .join("
"); +} + function renderSystemOwnership(world: WorldState, systemId: string): string { const claims = [...world.claims.values()].filter((claim) => claim.systemId === systemId && claim.state !== "destroyed"); @@ -108,7 +196,6 @@ export function updateDetailPanel( return; } const parent = describeSelectionParent(selected); - const fuelStored = inventoryAmount(ship.inventory, "fuel"); const cargoUsed = ship.cargoItemId ? inventoryAmount(ship.inventory, ship.cargoItemId) : 0; @@ -130,7 +217,6 @@ export function updateDetailPanel(
` : ""} -

Fuel ${fuelStored.toFixed(1)}
Capacitor ${ship.energyStored.toFixed(1)}

Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}

Inventory ${formatInventory(ship.inventory)}

Speed ${formatShipSpeed(ship)}

@@ -145,17 +231,12 @@ export function updateDetailPanel( return; } const parent = describeSelectionParent(selected); - const installedModules = station.installedModules.length > 0 - ? station.installedModules.join("
") - : "none"; - const activeConstruction = [...world.constructionSites.values()] - .filter((site) => site.stationId === station.id && site.state !== "completed") - .map((site) => `${site.blueprintId ?? site.targetDefinitionId} (${site.state})`) - .join("
") || "none"; + const moduleList = formatModuleListWithConstruction(world, station.id, station.installedModules, station.currentProcesses); const dockedShipLabels = station.dockedShipIds.length > 0 ? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("
") : "none"; - const stationInventory = station.inventory.filter((entry) => entry.itemId !== "fuel"); + const stationInventory = station.inventory; + const stationStorageUsage = formatStorageUsage(station.storageUsage); const stationProcesses = station.currentProcesses; const stationProcessingHtml = stationProcesses.length > 0 ? stationProcesses.map((process) => ` @@ -175,14 +256,12 @@ export function updateDetailPanel(

${station.category} · ${station.systemId}

Parent ${parent}

${stationProcessingHtml} -

Fuel ${station.fuelStored.toFixed(1)} / ${station.fuelCapacity.toFixed(1)}
Capacitor ${station.energyStored.toFixed(1)} / ${station.energyCapacity.toFixed(1)}

Docked ${station.dockedShips} / ${station.dockingPads}
${dockedShipLabels}

-

Modules ${installedModules}

-

Constructing ${activeConstruction}

+

Modules ${moduleList}

+

Storage ${stationStorageUsage}

Inventory ${formatInventory(stationInventory)}

-

History available in the separate history window.

`; return; } diff --git a/apps/viewer/src/viewerSelection.ts b/apps/viewer/src/viewerSelection.ts index ed4d15b..2b10b3a 100644 --- a/apps/viewer/src/viewerSelection.ts +++ b/apps/viewer/src/viewerSelection.ts @@ -332,8 +332,6 @@ function describeControllerTask(taskKind: string): string { return "docking"; case "unload": return "transfer"; - case "refuel": - return "refuel"; case "deliver-construction": return "material delivery"; case "build-construction-site": diff --git a/docs/COMMANDERS.md b/docs/COMMANDERS.md index bd12d5b..5e8e936 100644 --- a/docs/COMMANDERS.md +++ b/docs/COMMANDERS.md @@ -164,7 +164,7 @@ Typical outputs: - current destination node - local tactical task - retreat decision -- docking/refuel intent +- docking intent - trade or delivery acceptance ## Commander Ownership diff --git a/docs/DATA-MODEL.md b/docs/DATA-MODEL.md index 3fde84f..8e1119d 100644 --- a/docs/DATA-MODEL.md +++ b/docs/DATA-MODEL.md @@ -390,19 +390,6 @@ Suggested station-side workforce fields: Commanders should not be ordinary cargo items even if they are population-derived. -## Power State - -Ships and stations both need explicit operational power state. - -Suggested fields: - -- `fuelInventory` -- `energyStored` -- `powerOperational` -- `powerDeficitReason?` - -This matters because no fuel leads to no power, and no power halts major operations. - ## Inventories Inventories should remain generic item maps, but hosts should also have explicit context. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index ba4e833..bf52027 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -31,7 +31,6 @@ For the implementation migration path from the current codebase to this design s - item categories - life-support goods - construction goods - - fuel-chain goods - population-related units - [WORKFORCE.md](/home/jbourdon/repos/space-game/docs/WORKFORCE.md) diff --git a/docs/ECONOMY.md b/docs/ECONOMY.md index 19a7b99..776cf76 100644 --- a/docs/ECONOMY.md +++ b/docs/ECONOMY.md @@ -84,7 +84,6 @@ A buy order should include, conceptually: Buy orders let a station express: - production input demand -- fuel shortages - construction material shortages - military resupply needs @@ -138,8 +137,6 @@ The station commander should: Without a station commander, a station should not act like a healthy market participant. -If the station has no fuel and therefore no power, it should not continue normal market operation. - However, there is an important exception during founding or emergency intervention: - a higher actor may force transfers toward the station or construction site even without ordinary market behavior @@ -149,7 +146,7 @@ Recommended review loop: 1. inspect current inventory 2. inspect production queues or goals 3. inspect incoming and outgoing reservations -4. inspect fuel, defense, and construction reserves +4. inspect defense, and construction reserves 5. update buy orders 6. update sell orders 7. request logistics or strategic help if necessary @@ -185,7 +182,7 @@ The intended economy should eventually support flows such as: 1. extract raw resources 2. move them to useful stations 3. refine or process them -4. consume them for fuel, production, or expansion +4. consume them for production or expansion 5. produce intermediate and advanced goods 6. sell surpluses or acquire shortages through the market @@ -200,7 +197,6 @@ Logistics should emerge from market demand, not only from hardcoded behavior loo Examples: - a hauler sees a profitable sell-to-buy opportunity -- a station commander requests urgent fuel delivery - a faction commander subsidizes strategic resource movement This is a better long-term basis than one-off scripted “mine and deliver to this exact station” logic. @@ -208,7 +204,6 @@ This is a better long-term basis than one-off scripted “mine and deliver to th Traders should generally prefer the best reachable buy opportunity within their allowed operational range, subject to: - travel time -- fuel cost - risk - behavioral restrictions - territorial or regional limits @@ -236,7 +231,6 @@ The economy will work better if stations can reserve expected inventory changes. Examples: -- incoming fuel is reserved for station power - outbound metals are reserved for a construction project - a hauler claims part of a sell order before pickup diff --git a/docs/EVENTS.md b/docs/EVENTS.md index c888565..e17f9cc 100644 --- a/docs/EVENTS.md +++ b/docs/EVENTS.md @@ -289,7 +289,6 @@ Every event should be capable of producing a concise human-readable summary. Example style: - `Claim at Helios IV L4 destroyed by pirates` -- `Station buy order for fuel opened` - `Miner completed warp to refinery node` This helps reuse the same event model for: diff --git a/docs/ITEMS.md b/docs/ITEMS.md index 5662fb5..1dabb29 100644 --- a/docs/ITEMS.md +++ b/docs/ITEMS.md @@ -33,10 +33,9 @@ The intended categories are: 2. processed industrial goods 3. life-support goods 4. civilian goods -5. fuel and power-chain goods -6. construction goods -7. population-related units -8. special logistics goods later +5. construction goods +6. population-related units +7. special logistics goods later ## Raw Resources @@ -86,20 +85,6 @@ Current important example: These goods should matter for workforce health, quality of life, and possibly future growth modifiers. -## Fuel And Power-Chain Goods - -These are the goods that keep ships and stations running. - -Examples: - -- gas as an energy-chain input -- fuel as a refined operational good - -The exact chain may evolve, but the important distinction is: - -- some goods are energy inputs -- some goods are operational fuels - ## Construction Goods These are the goods used to build stations and possibly ships. @@ -116,7 +101,7 @@ Construction storage at a station site should create demand for these goods thro ## Population-Related Units -Population itself should be treated as a tracked resource, but not as an ordinary trade good in the same sense as metal or fuel. +Population itself should be treated as a tracked resource, but not as an ordinary trade good in the same sense as ore. Important distinctions: @@ -144,12 +129,6 @@ The current design implies at least these roles: - `ore` - raw industrial input -- `gas` - - raw fuel-chain input - -- `fuel` - - operational energy good - - `food` - workforce life-support @@ -173,12 +152,11 @@ Not every item should necessarily fit in every hold type forever. Useful distinctions later may include: -- bulk industrial cargo -- liquid cargo -- gas cargo -- containerized finished goods -- human transport capacity -- livestock capacity +- solid storage +- liquid storage +- container storage +- passengers +- livestock For now, the important rule is simply: @@ -191,7 +169,6 @@ Items should participate in the market according to their role. Examples: - life-support goods generate recurring demand -- fuel goods generate operational demand - construction goods generate burst demand during expansion - industrial goods feed production chains - worker transport supports station staffing @@ -220,7 +197,7 @@ The following rules should remain true unless deliberately revised: - workforce depends on real support goods - station construction depends on real construction goods -- fuel and industrial chains are item-based +- industrial chains are item-based - workers are movable population units - commanders are not ordinary trade cargo - livestock is distinct from workers diff --git a/docs/MODULES.md b/docs/MODULES.md index d0aaf09..c6f7138 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -48,7 +48,6 @@ Examples: - no docking module means no docking service - no habitat module means no population growth or human transport - no refinery module means no refining -- no fuel-processing module means no gas-to-fuel conversion - no storage module means reduced or absent inventory capability - no shipyard-related module means no ship production @@ -92,7 +91,6 @@ Likely station-side categories include: - storage - habitat - refinery -- fuel processing - manufacturing - shipyard or construction support - defense @@ -141,7 +139,6 @@ Examples: - reactor - capacitor - station power core -- fuel systems ### Production Modules @@ -150,7 +147,6 @@ These convert goods into other goods or into built output. Examples: - refinery -- fuel processor - factory - shipyard support @@ -198,7 +194,6 @@ They may require: - build time - power - workforce -- fuel or energy inputs - docking or logistics support This should let stations and ships fail in believable ways when underbuilt or undersupplied. @@ -219,7 +214,6 @@ Modules should define which item flows an entity can participate in. Examples: - a habitat module enables population support -- a fuel-processing module consumes gas and produces fuel - a refinery consumes raw resources and produces processed goods - a storage module determines what volume or class of goods can be held - a livestock module participates in the food chain diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md index 0193927..3778a1e 100644 --- a/docs/PRODUCTION.md +++ b/docs/PRODUCTION.md @@ -49,7 +49,6 @@ A recipe should conceptually define: - cycle time - valid producing module types - optional workforce requirement -- optional power or fuel requirement Recipes should be first-class design objects, not hidden assumptions inside modules. @@ -60,7 +59,6 @@ Recipes are executed by production-capable modules. Examples: - refinery module -- fuel processing module - factory module - food-chain module later - shipyard support module @@ -112,16 +110,6 @@ For now: This keeps the initial system consistent and simple. -## Power Interaction - -Production should also respect power and fuel state. - -Without power: - -- production stops - -This is especially important for stations because no-fuel means no-power, and no-power means no normal operation. - ## Input Shortage Behavior If inputs are missing: @@ -153,7 +141,6 @@ The exact recipes can evolve, but the intended shape includes chains like: 2. refining or processing - ore -> refined goods - - gas -> fuel - food-loop conversions later 3. industrial use diff --git a/docs/STATIONS.md b/docs/STATIONS.md index 4bedebd..767a347 100644 --- a/docs/STATIONS.md +++ b/docs/STATIONS.md @@ -151,23 +151,6 @@ Not: This means friendly or otherwise permitted factions may build stations within the same system, so long as they use different valid locations. -## Failure State - -Without fuel there is no power. - -Without power, station function collapses. - -A powerless station should not continue normal market or industrial behavior. - -At that point, recovery should require outside intervention such as emergency restoration, delivered fuel, or a dedicated support operation. - -This also means: - -- no loading -- no unloading -- no ordinary trade handling -- no ordinary production - ## Services Depending on modules and category, a station may provide: @@ -175,11 +158,9 @@ Depending on modules and category, a station may provide: - docking - storage - refining -- fuel processing - manufacturing -- repair later -- fitting later -- rearm and resupply later +- repair +- fitting, rearm and resupply later - habitats The exact conversion and factory behavior behind these services is described in [PRODUCTION.md](/home/jbourdon/repos/space-game/docs/PRODUCTION.md). diff --git a/docs/TASKS.md b/docs/TASKS.md index 2dc689a..3e532dd 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -41,7 +41,6 @@ Goals are high-level commander intentions. Examples: - expand into this system -- keep this station fueled - defend this claim - protect trade in this region - supply this station with workers @@ -60,7 +59,6 @@ Examples: - dock at station - claim Lagrange point - build station here -- deliver fuel - escort this ship - defend this bubble @@ -223,7 +221,6 @@ Examples: - deny dock request - transfer goods - request defense -- request emergency fuel support These may be implemented as station jobs, station operations, or station-side tasks. @@ -237,7 +234,7 @@ Examples: - flee to nearest allowed station - hold position if no valid route exists - suspend trade when no legal destination exists -- wait for fuel, escort, or dock access +- wait for escort, or dock access This prevents autonomous loops from becoming self-destructive. diff --git a/docs/WORKFORCE.md b/docs/WORKFORCE.md index 7bb3b5a..4926b5b 100644 --- a/docs/WORKFORCE.md +++ b/docs/WORKFORCE.md @@ -52,7 +52,6 @@ Workers consume, per worker: - food - water -- energy - consumer goods These should be understood using the item roles defined in [ITEMS.md](/home/jbourdon/repos/space-game/docs/ITEMS.md). @@ -146,7 +145,6 @@ A newly founded station may begin with: It can still exist and operate at baseline efficiency, but it remains weak until supplied with: -- fuel - workers - support goods - eventually a station commander @@ -159,7 +157,6 @@ Relevant shortages include: - food shortage - water shortage -- energy shortage - consumer goods shortage This gives logistics failure lasting demographic consequences. @@ -178,7 +175,7 @@ The following rules should remain true unless deliberately revised: - population grows only at stations for now - habitat modules are required for growth -- workers consume food, water, energy, and consumer goods +- workers consume food, water and consumer goods - workforce affects station efficiency - stations retain a small baseline efficiency at zero workforce - population can be transported between stations diff --git a/shared/data/balance.json b/shared/data/balance.json index 1aeecf6..9d635a7 100644 --- a/shared/data/balance.json +++ b/shared/data/balance.json @@ -6,12 +6,5 @@ "transferRate": 56, "dockingDuration": 1.2, "undockingDuration": 1.2, - "undockDistance": 42, - "energy": { - "idleDrain": 0.7, - "moveDrain": 1.8, - "warpDrain": 7, - "shipRechargeRate": 10, - "stationSolarCharge": 5 - } + "undockDistance": 42 } diff --git a/shared/data/constructibles.json b/shared/data/constructibles.json deleted file mode 100644 index cfb0fcc..0000000 --- a/shared/data/constructibles.json +++ /dev/null @@ -1,87 +0,0 @@ -[ - { - "id": "station-core", - "label": "Orbital Station", - "category": "station", - "color": "#8df0d2", - "radius": 24, - "dockingCapacity": 4, - "storage": { - "bulk-solid": 2000, - "manufactured": 1200, - "bulk-liquid": 600, - "bulk-gas": 600 - }, - "modules": ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"] - }, - { - "id": "trade-hub", - "label": "Trade Hub", - "category": "station", - "color": "#8bd3ff", - "radius": 20, - "dockingCapacity": 4, - "storage": { "container": 1200, "manufactured": 800 }, - "modules": ["habitat-ring", "container-bay"] - }, - { - "id": "refinery", - "label": "Refining Station", - "category": "station", - "color": "#ffb86c", - "radius": 24, - "dockingCapacity": 3, - "storage": { "bulk-solid": 2000, "manufactured": 1000, "bulk-liquid": 400, "bulk-gas": 400 }, - "modules": ["power-core", "bulk-bay", "liquid-tank", "gas-tank", "refinery-stack", "fuel-processor"] - }, - { - "id": "farm-ring", - "label": "Farm Station", - "category": "farm", - "color": "#92ef8a", - "radius": 22, - "dockingCapacity": 2, - "storage": { "bulk-liquid": 600, "container": 400 }, - "modules": ["habitat-ring", "fabricator-array", "container-bay"] - }, - { - "id": "manufactory", - "label": "Orbital Manufactory", - "category": "station", - "color": "#8df0d2", - "radius": 24, - "dockingCapacity": 3, - "storage": { "manufactured": 2200, "container": 1600 }, - "modules": ["fabricator-array", "fabricator-array", "container-bay"] - }, - { - "id": "shipyard", - "label": "Orbital Shipyard", - "category": "shipyard", - "color": "#d0a2ff", - "radius": 28, - "dockingCapacity": 5, - "storage": { "manufactured": 1800, "container": 1200 }, - "modules": ["component-factory", "ship-factory", "container-bay", "dock-bay-small", "power-core"] - }, - { - "id": "defense-grid", - "label": "Defense Platform", - "category": "defense", - "color": "#ff7a95", - "radius": 18, - "dockingCapacity": 1, - "storage": { "manufactured": 300 }, - "modules": ["turret-grid", "command-bridge"] - }, - { - "id": "stargate", - "label": "Stargate", - "category": "gate", - "color": "#76f0ff", - "radius": 34, - "dockingCapacity": 0, - "storage": { "manufactured": 2400, "container": 800 }, - "modules": ["ftl-core", "fabricator-array"] - } -] diff --git a/shared/data/items.json b/shared/data/items.json index 5595622..79e84de 100644 --- a/shared/data/items.json +++ b/shared/data/items.json @@ -1,206 +1,630 @@ [ { "id": "ore", - "label": "Raw Ore", - "storage": "bulk-solid", - "summary": "Unprocessed asteroid ore used as the main industrial feedstock." - }, - { - "id": "refined-metals", - "label": "Refined Metals", - "storage": "manufactured", - "summary": "Processed structural metals used by stations and shipyards." - }, - { - "id": "hull-sections", - "label": "Hull Sections", - "storage": "manufactured", - "summary": "Prefabricated structural assemblies for ships and stations." - }, - { - "id": "ammo-crates", - "label": "Ammo Crates", - "storage": "container", - "summary": "Containerized magazines for turrets, launchers, and point defense." - }, - { - "id": "naval-guns", - "label": "Naval Guns", - "storage": "manufactured", - "summary": "Shipboard turret and cannon assemblies." - }, - { - "id": "ship-equipment", - "label": "Ship Equipment", - "storage": "container", - "summary": "Shield emitters, avionics, cooling loops, and service kits." - }, - { - "id": "ship-parts", - "label": "Ship Parts", - "storage": "manufactured", - "summary": "High-value integration kits for hull fitting and final assembly." - }, - { - "id": "command-bridge-module", - "label": "Command Bridge Module", - "storage": "container", - "summary": "Packaged bridge and combat-information-center assembly for final ship integration." - }, - { - "id": "reactor-core-module", - "label": "Reactor Core Module", - "storage": "container", - "summary": "Contained ship reactor package ready for installation into a hull." - }, - { - "id": "capacitor-bank-module", - "label": "Capacitor Bank Module", - "storage": "container", - "summary": "Buffered capacitor section for propulsion, weapons, and industrial loads." - }, - { - "id": "ion-drive-module", - "label": "Ion Drive Module", - "storage": "container", - "summary": "Preassembled sublight engine unit." - }, - { - "id": "ftl-core-module", - "label": "FTL Core Module", - "storage": "container", - "summary": "Integrated FTL drive package for inter-system transit." - }, - { - "id": "gun-turret-module", - "label": "Gun Turret Module", - "storage": "container", - "summary": "Shipboard turret mount and fire-control package." - }, - { - "id": "carrier-bay-module", - "label": "Carrier Bay Module", - "storage": "container", - "summary": "Hangar and launch-recovery assembly for capital ship integration." - }, - { - "id": "habitat-ring-module", - "label": "Habitat Ring Module", - "storage": "container", - "summary": "Crew habitat section packaged for large ship installation." - }, - { - "id": "bulk-bay-module", - "label": "Bulk Bay Module", - "storage": "container", - "summary": "Industrial cargo hold segment for raw-solid hauling ships." - }, - { - "id": "container-bay-module", - "label": "Container Bay Module", - "storage": "container", - "summary": "Freight rack segment for manufactured and palletized cargo." - }, - { - "id": "liquid-tank-module", - "label": "Liquid Tank Module", - "storage": "container", - "summary": "Pressurized liquid storage segment for fuel and energy logistics." - }, - { - "id": "gas-tank-module", - "label": "Gas Tank Module", - "storage": "container", - "summary": "Pressurized gas storage segment for volatile cargo hauling." - }, - { - "id": "mining-turret-module", - "label": "Mining Turret Module", - "storage": "container", - "summary": "Ship-mounted hard-rock extraction head." - }, - { - "id": "gas-extractor-module", - "label": "Gas Extractor Module", - "storage": "container", - "summary": "Cryogenic intake and compression package for gas harvesting ships." - }, - { - "id": "fabricator-array-module", - "label": "Fabricator Array Module", - "storage": "container", - "summary": "Mobile industrial fabrication block for constructors." - }, - { - "id": "gas", - "label": "Volatile Gas", - "storage": "bulk-gas", - "summary": "Compressed gas reserves for future chemical and fuel chains." - }, - { - "id": "fuel", - "label": "Reactor Fuel", - "storage": "bulk-liquid", - "summary": "Processed liquid fuel consumed by ships and station power systems." - }, - { - "id": "energy-cell", - "label": "Energy Cell", - "storage": "bulk-liquid", - "summary": "Charged energy reserves that can be stored, traded, and discharged into station power grids." + "name": "Raw Ore", + "description": "Unprocessed asteroid ore used as the main industrial feedstock.", + "type": "resource", + "cargoKind": "bulk-solid", + "volume": 1.2 }, { "id": "water", - "label": "Water", - "storage": "bulk-liquid", - "summary": "Life-support and agricultural input." + "name": "Water", + "description": "Life-support and agricultural input.", + "type": "commodity", + "cargoKind": "bulk-liquid", + "volume": 1.0, + "construction": { + "recipeId": "water-reclamation", + "facilityCategory": "farm", + "requiredModules": ["liquid-tank", "solar-array"], + "requirements": [], + "cycleTime": 6, + "batchSize": 12, + "productsPerHour": 7200, + "maxEfficiency": 1, + "priority": 14 + } + }, + { + "id": "refined-metals", + "name": "Refined Metals", + "description": "Processed structural metals used by stations and shipyards.", + "type": "material", + "cargoKind": "manufactured", + "volume": 1.0, + "construction": { + "recipeId": "ore-refining", + "facilityCategory": "station", + "requiredModules": ["refinery-stack"], + "requirements": [ + { "itemId": "ore", "amount": 60 } + ], + "cycleTime": 8, + "batchSize": 60, + "productsPerHour": 27000, + "maxEfficiency": 1, + "priority": 100 + } + }, + { + "id": "hull-sections", + "name": "Hull Sections", + "description": "Prefabricated structural assemblies for ships and stations.", + "type": "component", + "cargoKind": "manufactured", + "volume": 1.5, + "construction": { + "recipeId": "hull-fabrication", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "refined-metals", "amount": 70 } + ], + "cycleTime": 10, + "batchSize": 35, + "productsPerHour": 12600, + "maxEfficiency": 1, + "priority": 40 + } + }, + { + "id": "ammo-crates", + "name": "Ammo Crates", + "description": "Containerized magazines for turrets, launchers, and point defense.", + "type": "component", + "cargoKind": "container", + "volume": 1.0, + "construction": { + "recipeId": "ammo-fabrication", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "refined-metals", "amount": 24 } + ], + "cycleTime": 6, + "batchSize": 30, + "productsPerHour": 18000, + "maxEfficiency": 1, + "priority": 34 + } + }, + { + "id": "naval-guns", + "name": "Naval Guns", + "description": "Shipboard turret and cannon assemblies.", + "type": "component", + "cargoKind": "manufactured", + "volume": 1.4, + "construction": { + "recipeId": "gun-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "refined-metals", "amount": 36 } + ], + "cycleTime": 9, + "batchSize": 12, + "productsPerHour": 4800, + "maxEfficiency": 1, + "priority": 32 + } + }, + { + "id": "ship-equipment", + "name": "Ship Equipment", + "description": "Shield emitters, avionics, cooling loops, and service kits.", + "type": "component", + "cargoKind": "container", + "volume": 1.0, + "construction": { + "recipeId": "equipment-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "refined-metals", "amount": 28 }, + { "itemId": "water", "amount": 8 } + ], + "cycleTime": 11, + "batchSize": 18, + "productsPerHour": 5890.9, + "maxEfficiency": 1, + "priority": 30 + } + }, + { + "id": "ship-parts", + "name": "Ship Parts", + "description": "High-value integration kits for hull fitting and final assembly.", + "type": "component", + "cargoKind": "manufactured", + "volume": 1.3, + "construction": { + "recipeId": "ship-parts-integration", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "hull-sections", "amount": 24 }, + { "itemId": "naval-guns", "amount": 6 }, + { "itemId": "ship-equipment", "amount": 10 } + ], + "cycleTime": 14, + "batchSize": 20, + "productsPerHour": 5142.9, + "maxEfficiency": 1, + "priority": 50 + } }, { "id": "drone-parts", - "label": "Drone Parts", - "storage": "container", - "summary": "Containerized industrial freight." + "name": "Drone Parts", + "description": "Containerized industrial freight for construction support.", + "type": "component", + "cargoKind": "container", + "volume": 1.0, + "construction": { + "recipeId": "drone-parts-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "refined-metals", "amount": 12 }, + { "itemId": "ship-equipment", "amount": 6 } + ], + "cycleTime": 7, + "batchSize": 16, + "productsPerHour": 8228.6, + "maxEfficiency": 1, + "priority": 18 + } + }, + { + "id": "command-bridge-module", + "name": "Command Bridge Module", + "description": "Packaged bridge and combat-information-center assembly for final ship integration.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.0, + "construction": { + "recipeId": "command-bridge-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 20 }, + { "itemId": "ship-equipment", "amount": 10 } + ], + "cycleTime": 9, + "batchSize": 1, + "productsPerHour": 400, + "maxEfficiency": 1, + "priority": 52 + } + }, + { + "id": "reactor-core-module", + "name": "Reactor Core Module", + "description": "Contained ship reactor package ready for installation into a hull.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.2, + "construction": { + "recipeId": "reactor-core-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 30 }, + { "itemId": "ship-equipment", "amount": 8 } + ], + "cycleTime": 10, + "batchSize": 1, + "productsPerHour": 360, + "maxEfficiency": 1, + "priority": 54 + } + }, + { + "id": "capacitor-bank-module", + "name": "Capacitor Bank Module", + "description": "Buffered capacitor section for propulsion, weapons, and industrial loads.", + "type": "ship-module", + "cargoKind": "container", + "volume": 1.8, + "construction": { + "recipeId": "capacitor-bank-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 18 }, + { "itemId": "ship-equipment", "amount": 4 } + ], + "cycleTime": 9, + "batchSize": 1, + "productsPerHour": 400, + "maxEfficiency": 1, + "priority": 52 + } + }, + { + "id": "ion-drive-module", + "name": "Ion Drive Module", + "description": "Preassembled sublight engine unit.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.0, + "construction": { + "recipeId": "ion-drive-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 22 }, + { "itemId": "ship-equipment", "amount": 8 } + ], + "cycleTime": 10, + "batchSize": 1, + "productsPerHour": 360, + "maxEfficiency": 1, + "priority": 53 + } + }, + { + "id": "ftl-core-module", + "name": "FTL Core Module", + "description": "Integrated FTL drive package for inter-system transit.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.4, + "construction": { + "recipeId": "ftl-core-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 34 }, + { "itemId": "ship-equipment", "amount": 14 } + ], + "cycleTime": 12, + "batchSize": 1, + "productsPerHour": 300, + "maxEfficiency": 1, + "priority": 56 + } + }, + { + "id": "gun-turret-module", + "name": "Gun Turret Module", + "description": "Shipboard turret mount and fire-control package.", + "type": "ship-module", + "cargoKind": "container", + "volume": 1.6, + "construction": { + "recipeId": "gun-turret-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "naval-guns", "amount": 8 }, + { "itemId": "refined-metals", "amount": 12 } + ], + "cycleTime": 8, + "batchSize": 1, + "productsPerHour": 450, + "maxEfficiency": 1, + "priority": 58 + } + }, + { + "id": "carrier-bay-module", + "name": "Carrier Bay Module", + "description": "Hangar and launch-recovery assembly for capital ship integration.", + "type": "ship-module", + "cargoKind": "container", + "volume": 3.0, + "construction": { + "recipeId": "carrier-bay-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "hull-sections", "amount": 18 }, + { "itemId": "refined-metals", "amount": 18 }, + { "itemId": "ship-equipment", "amount": 10 } + ], + "cycleTime": 14, + "batchSize": 1, + "productsPerHour": 257.1, + "maxEfficiency": 1, + "priority": 40 + } + }, + { + "id": "habitat-ring-module", + "name": "Habitat Ring Module", + "description": "Crew habitat section packaged for large ship installation.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.6, + "construction": { + "recipeId": "habitat-ring-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "hull-sections", "amount": 14 }, + { "itemId": "ship-equipment", "amount": 8 }, + { "itemId": "water", "amount": 10 } + ], + "cycleTime": 12, + "batchSize": 1, + "productsPerHour": 300, + "maxEfficiency": 1, + "priority": 22 + } + }, + { + "id": "bulk-bay-module", + "name": "Bulk Bay Module", + "description": "Industrial cargo hold segment for raw-solid hauling ships.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.0, + "construction": { + "recipeId": "bulk-bay-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 16 }, + { "itemId": "hull-sections", "amount": 10 } + ], + "cycleTime": 8, + "batchSize": 1, + "productsPerHour": 450, + "maxEfficiency": 1, + "priority": 18 + } + }, + { + "id": "container-bay-module", + "name": "Container Bay Module", + "description": "Freight rack segment for manufactured and palletized cargo.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.0, + "construction": { + "recipeId": "container-bay-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 12 }, + { "itemId": "ship-equipment", "amount": 4 } + ], + "cycleTime": 8, + "batchSize": 1, + "productsPerHour": 450, + "maxEfficiency": 1, + "priority": 18 + } + }, + { + "id": "liquid-tank-module", + "name": "Liquid Tank Module", + "description": "Pressurized liquid storage segment for water and liquid logistics.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.0, + "construction": { + "recipeId": "liquid-tank-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 14 }, + { "itemId": "ship-equipment", "amount": 4 } + ], + "cycleTime": 8, + "batchSize": 1, + "productsPerHour": 450, + "maxEfficiency": 1, + "priority": 18 + } + }, + { + "id": "mining-turret-module", + "name": "Mining Turret Module", + "description": "Ship-mounted hard-rock extraction head.", + "type": "ship-module", + "cargoKind": "container", + "volume": 1.8, + "construction": { + "recipeId": "mining-turret-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 18 }, + { "itemId": "ship-equipment", "amount": 6 } + ], + "cycleTime": 9, + "batchSize": 1, + "productsPerHour": 400, + "maxEfficiency": 1, + "priority": 24 + } + }, + { + "id": "fabricator-array-module", + "name": "Fabricator Array Module", + "description": "Mobile industrial fabrication block for constructors.", + "type": "ship-module", + "cargoKind": "container", + "volume": 2.4, + "construction": { + "recipeId": "fabricator-array-module-assembly", + "facilityCategory": "station", + "requiredModules": ["component-factory", "container-bay"], + "requirements": [ + { "itemId": "refined-metals", "amount": 24 }, + { "itemId": "ship-equipment", "amount": 10 } + ], + "cycleTime": 11, + "batchSize": 1, + "productsPerHour": 327.3, + "maxEfficiency": 1, + "priority": 20 + } }, { "id": "trade-hub-kit", - "label": "Trade Hub Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for a trade hub station." + "name": "Trade Hub Kit", + "description": "Deployable prefab package for a trade hub station.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 6.0, + "construction": { + "recipeId": "trade-hub-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 26 }, + { "itemId": "ship-equipment", "amount": 16 }, + { "itemId": "drone-parts", "amount": 10 } + ], + "cycleTime": 18, + "batchSize": 1, + "productsPerHour": 200, + "maxEfficiency": 1, + "priority": 24 + } }, { "id": "refinery-kit", - "label": "Refinery Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for a refining station." + "name": "Refinery Kit", + "description": "Deployable prefab package for a refining station.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 6.5, + "construction": { + "recipeId": "refinery-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 32 }, + { "itemId": "hull-sections", "amount": 24 }, + { "itemId": "ship-equipment", "amount": 14 } + ], + "cycleTime": 20, + "batchSize": 1, + "productsPerHour": 180, + "maxEfficiency": 1, + "priority": 26 + } }, { "id": "farm-ring-kit", - "label": "Farm Ring Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for a farm station." + "name": "Farm Ring Kit", + "description": "Deployable prefab package for a farm station.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 6.0, + "construction": { + "recipeId": "farm-ring-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 22 }, + { "itemId": "ship-equipment", "amount": 18 }, + { "itemId": "water", "amount": 22 } + ], + "cycleTime": 18, + "batchSize": 1, + "productsPerHour": 200, + "maxEfficiency": 1, + "priority": 22 + } }, { "id": "manufactory-kit", - "label": "Manufactory Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for an orbital manufactory." + "name": "Manufactory Kit", + "description": "Deployable prefab package for an orbital manufactory.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 7.0, + "construction": { + "recipeId": "manufactory-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 34 }, + { "itemId": "hull-sections", "amount": 16 }, + { "itemId": "ship-equipment", "amount": 18 } + ], + "cycleTime": 22, + "batchSize": 1, + "productsPerHour": 163.6, + "maxEfficiency": 1, + "priority": 28 + } }, { "id": "shipyard-kit", - "label": "Shipyard Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for an orbital shipyard." + "name": "Shipyard Kit", + "description": "Deployable prefab package for an orbital shipyard.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 8.0, + "construction": { + "recipeId": "shipyard-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 42 }, + { "itemId": "hull-sections", "amount": 30 }, + { "itemId": "naval-guns", "amount": 10 } + ], + "cycleTime": 26, + "batchSize": 1, + "productsPerHour": 138.5, + "maxEfficiency": 1, + "priority": 30 + } }, { "id": "defense-grid-kit", - "label": "Defense Grid Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for a defense platform." + "name": "Defense Grid Kit", + "description": "Deployable prefab package for a defense platform.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 7.0, + "construction": { + "recipeId": "defense-grid-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 18 }, + { "itemId": "naval-guns", "amount": 12 }, + { "itemId": "ammo-crates", "amount": 18 } + ], + "cycleTime": 16, + "batchSize": 1, + "productsPerHour": 225, + "maxEfficiency": 1, + "priority": 20 + } }, { "id": "stargate-kit", - "label": "Stargate Kit", - "storage": "manufactured", - "summary": "Deployable prefab package for a stargate structure." + "name": "Stargate Kit", + "description": "Deployable prefab package for a stargate structure.", + "type": "kit", + "cargoKind": "manufactured", + "volume": 10.0, + "construction": { + "recipeId": "stargate-assembly", + "facilityCategory": "station", + "requiredModules": ["fabricator-array"], + "requirements": [ + { "itemId": "ship-parts", "amount": 60 }, + { "itemId": "hull-sections", "amount": 44 }, + { "itemId": "ship-equipment", "amount": 26 }, + { "itemId": "naval-guns", "amount": 8 } + ], + "cycleTime": 34, + "batchSize": 1, + "productsPerHour": 105.9, + "maxEfficiency": 1, + "priority": 36 + } } ] diff --git a/shared/data/module-recipes.json b/shared/data/module-recipes.json deleted file mode 100644 index 196074a..0000000 --- a/shared/data/module-recipes.json +++ /dev/null @@ -1,68 +0,0 @@ -[ - { - "moduleId": "dock-bay-small", - "duration": 12, - "inputs": [ - { "itemId": "refined-metals", "amount": 34 } - ] - }, - { - "moduleId": "gas-tank", - "duration": 10, - "inputs": [ - { "itemId": "refined-metals", "amount": 30 } - ] - }, - { - "moduleId": "container-bay", - "duration": 10, - "inputs": [ - { "itemId": "refined-metals", "amount": 26 } - ] - }, - { - "moduleId": "fuel-processor", - "duration": 14, - "inputs": [ - { "itemId": "refined-metals", "amount": 42 } - ] - }, - { - "moduleId": "refinery-stack", - "duration": 14, - "inputs": [ - { "itemId": "refined-metals", "amount": 38 } - ] - }, - { - "moduleId": "fabricator-array", - "duration": 16, - "inputs": [ - { "itemId": "refined-metals", "amount": 48 } - ] - }, - { - "moduleId": "component-factory", - "duration": 18, - "inputs": [ - { "itemId": "refined-metals", "amount": 54 }, - { "itemId": "ship-equipment", "amount": 12 } - ] - }, - { - "moduleId": "ship-factory", - "duration": 22, - "inputs": [ - { "itemId": "refined-metals", "amount": 60 }, - { "itemId": "hull-sections", "amount": 24 }, - { "itemId": "ship-equipment", "amount": 14 } - ] - }, - { - "moduleId": "solar-array", - "duration": 12, - "inputs": [ - { "itemId": "refined-metals", "amount": 28 } - ] - } -] diff --git a/shared/data/modules.json b/shared/data/modules.json new file mode 100644 index 0000000..b7864a2 --- /dev/null +++ b/shared/data/modules.json @@ -0,0 +1,247 @@ +[ + { + "id": "dock-bay-small", + "name": "Small Dock Bay", + "description": "External docking pad cluster for small and medium hulls.", + "type": "Dock", + "hull": 160, + "workforceNeeded": 10, + "construction": { + "productionTime": 12, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 34 + } + ] + } + }, + { + "id": "container-bay", + "name": "Container Bay", + "description": "Manufactured cargo storage and container handling racks.", + "type": "Storage", + "hull": 140, + "workforceNeeded": 8, + "construction": { + "productionTime": 10, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 26 + } + ] + } + }, + { + "id": "bulk-bay", + "name": "Bulk Bay", + "description": "Raw solid storage and ore handling volume.", + "type": "Storage", + "hull": 140, + "workforceNeeded": 8 + }, + { + "id": "liquid-tank", + "name": "Liquid Tank", + "description": "Liquid cargo and water tankage.", + "type": "Storage", + "hull": 130, + "workforceNeeded": 6, + "construction": { + "productionTime": 10, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 20 + } + ] + } + }, + { + "id": "refinery-stack", + "name": "Refinery Stack", + "description": "Heavy refining line for ore to refined metals.", + "type": "Production", + "product": "refined-metals", + "hull": 180, + "workforceNeeded": 18, + "construction": { + "productionTime": 14, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 38 + } + ] + } + }, + { + "id": "solar-array", + "name": "Solar Array", + "description": "Orbital solar generation and utility frame.", + "type": "Production", + "hull": 110, + "workforceNeeded": 6, + "construction": { + "productionTime": 12, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 28 + } + ] + } + }, + { + "id": "fabricator-array", + "name": "Fabricator Array", + "description": "General fabrication line for industrial goods and prefab kits.", + "type": "Build Module", + "hull": 200, + "workforceNeeded": 20, + "construction": { + "productionTime": 16, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 48 + } + ] + } + }, + { + "id": "component-factory", + "name": "Component Factory", + "description": "Assembly line for ship-grade modules and integrated components.", + "type": "Build Module", + "hull": 220, + "workforceNeeded": 24, + "construction": { + "productionTime": 18, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 54 + }, + { + "itemId": "ship-equipment", + "amount": 12 + } + ] + } + }, + { + "id": "ship-factory", + "name": "Ship Factory", + "description": "Slip-line and integration yard for final ship assembly.", + "type": "Build Module", + "hull": 260, + "workforceNeeded": 28, + "construction": { + "productionTime": 22, + "requirements": [ + { + "itemId": "refined-metals", + "amount": 60 + }, + { + "itemId": "hull-sections", + "amount": 24 + }, + { + "itemId": "ship-equipment", + "amount": 14 + } + ] + } + }, + { + "id": "power-core", + "name": "Power Core", + "description": "Station backbone for power routing and core services.", + "type": "Connection", + "hull": 220, + "workforceNeeded": 10 + }, + { + "id": "habitat-ring", + "name": "Habitat Ring", + "description": "Crew habitation and life-support section.", + "type": "Habitation", + "hull": 180, + "workforceNeeded": 12 + }, + { + "id": "turret-grid", + "name": "Turret Grid", + "description": "Defensive hardpoints and fire-control grid.", + "type": "Defense", + "hull": 180, + "workforceNeeded": 10 + }, + { + "id": "command-bridge", + "name": "Command Bridge", + "description": "Command-and-control section for stations and capital structures.", + "type": "Connection", + "hull": 150, + "workforceNeeded": 8 + }, + { + "id": "reactor-core", + "name": "Reactor Core", + "description": "Primary reactor and power conversion system.", + "type": "Connection", + "hull": 150, + "workforceNeeded": 8 + }, + { + "id": "capacitor-bank", + "name": "Capacitor Bank", + "description": "Energy buffering and discharge system.", + "type": "Connection", + "hull": 120, + "workforceNeeded": 4 + }, + { + "id": "ion-drive", + "name": "Ion Drive", + "description": "Primary sublight propulsion module.", + "type": "Connection", + "hull": 120, + "workforceNeeded": 4 + }, + { + "id": "ftl-core", + "name": "FTL Core", + "description": "Inter-system transit drive core.", + "type": "Connection", + "hull": 140, + "workforceNeeded": 6 + }, + { + "id": "gun-turret", + "name": "Gun Turret", + "description": "General purpose shipboard turret.", + "type": "Defense", + "hull": 100, + "workforceNeeded": 3 + }, + { + "id": "carrier-bay", + "name": "Carrier Bay", + "description": "Launch and recovery bay for carried craft.", + "type": "Pier", + "hull": 160, + "workforceNeeded": 8 + }, + { + "id": "mining-turret", + "name": "Mining Turret", + "description": "Hard-rock extraction head for mining hulls.", + "type": "Production", + "hull": 90, + "workforceNeeded": 3 + } +] diff --git a/shared/data/recipes.json b/shared/data/recipes.json deleted file mode 100644 index bd5a2d6..0000000 --- a/shared/data/recipes.json +++ /dev/null @@ -1,1304 +0,0 @@ -[ - { - "id": "ore-refining", - "label": "Ore Refining", - "facilityCategory": "station", - "duration": 8, - "priority": 100, - "requiredModules": [ - "refinery-stack", - "power-core" - ], - "inputs": [ - { - "itemId": "ore", - "amount": 60 - } - ], - "outputs": [ - { - "itemId": "refined-metals", - "amount": 60 - } - ] - }, - { - "id": "ore-reclamation", - "label": "Ore Reclamation", - "facilityCategory": "station", - "duration": 7, - "priority": 8, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 16 - } - ], - "outputs": [ - { - "itemId": "ore", - "amount": 24 - } - ] - }, - { - "id": "fuel-processing", - "label": "Fuel Processing", - "facilityCategory": "station", - "duration": 6, - "priority": 96, - "requiredModules": [ - "fuel-processor", - "power-core" - ], - "inputs": [ - { - "itemId": "gas", - "amount": 20 - } - ], - "outputs": [ - { - "itemId": "fuel", - "amount": 20 - } - ] - }, - { - "id": "energy-cell-charging", - "label": "Energy Cell Charging", - "facilityCategory": "station", - "duration": 12, - "priority": 72, - "requiredModules": [ - "solar-array", - "liquid-tank" - ], - "outputs": [ - { - "itemId": "energy-cell", - "amount": 6 - } - ] - }, - { - "id": "water-reclamation", - "label": "Water Reclamation", - "facilityCategory": "farm", - "duration": 6, - "priority": 14, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "gas", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "water", - "amount": 18 - } - ] - }, - { - "id": "drone-parts-assembly", - "label": "Drone Parts Assembly", - "facilityCategory": "station", - "duration": 7, - "priority": 18, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 12 - }, - { - "itemId": "ship-equipment", - "amount": 6 - } - ], - "outputs": [ - { - "itemId": "drone-parts", - "amount": 16 - } - ] - }, - { - "id": "hull-fabrication", - "label": "Hull Fabrication", - "facilityCategory": "station", - "duration": 10, - "priority": 40, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 70 - } - ], - "outputs": [ - { - "itemId": "hull-sections", - "amount": 35 - } - ] - }, - { - "id": "ammo-fabrication", - "label": "Ammo Fabrication", - "facilityCategory": "station", - "duration": 6, - "priority": 34, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 24 - } - ], - "outputs": [ - { - "itemId": "ammo-crates", - "amount": 30 - } - ] - }, - { - "id": "gun-assembly", - "label": "Gun Assembly", - "facilityCategory": "station", - "duration": 9, - "priority": 32, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 36 - } - ], - "outputs": [ - { - "itemId": "naval-guns", - "amount": 12 - } - ] - }, - { - "id": "equipment-assembly", - "label": "Equipment Assembly", - "facilityCategory": "station", - "duration": 11, - "priority": 30, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 28 - }, - { - "itemId": "water", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "ship-equipment", - "amount": 18 - } - ] - }, - { - "id": "ship-parts-integration", - "label": "Ship Parts Integration", - "facilityCategory": "station", - "duration": 14, - "priority": 50, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "hull-sections", - "amount": 24 - }, - { - "itemId": "naval-guns", - "amount": 6 - }, - { - "itemId": "ship-equipment", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "ship-parts", - "amount": 20 - } - ] - }, - { - "id": "command-bridge-module-assembly", - "label": "Command Bridge Module Assembly", - "facilityCategory": "station", - "duration": 9, - "priority": 52, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 20 - }, - { - "itemId": "ship-equipment", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "command-bridge-module", - "amount": 1 - } - ] - }, - { - "id": "reactor-core-module-assembly", - "label": "Reactor Core Module Assembly", - "facilityCategory": "station", - "duration": 10, - "priority": 54, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 30 - }, - { - "itemId": "ship-equipment", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "reactor-core-module", - "amount": 1 - } - ] - }, - { - "id": "capacitor-bank-module-assembly", - "label": "Capacitor Bank Module Assembly", - "facilityCategory": "station", - "duration": 9, - "priority": 52, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 18 - }, - { - "itemId": "energy-cell", - "amount": 6 - } - ], - "outputs": [ - { - "itemId": "capacitor-bank-module", - "amount": 1 - } - ] - }, - { - "id": "ion-drive-module-assembly", - "label": "Ion Drive Module Assembly", - "facilityCategory": "station", - "duration": 10, - "priority": 53, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 22 - }, - { - "itemId": "ship-equipment", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "ion-drive-module", - "amount": 1 - } - ] - }, - { - "id": "ftl-core-module-assembly", - "label": "FTL Core Module Assembly", - "facilityCategory": "station", - "duration": 12, - "priority": 56, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 34 - }, - { - "itemId": "ship-equipment", - "amount": 14 - }, - { - "itemId": "fuel", - "amount": 12 - } - ], - "outputs": [ - { - "itemId": "ftl-core-module", - "amount": 1 - } - ] - }, - { - "id": "gun-turret-module-assembly", - "label": "Gun Turret Module Assembly", - "facilityCategory": "station", - "duration": 8, - "priority": 58, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "naval-guns", - "amount": 8 - }, - { - "itemId": "refined-metals", - "amount": 12 - } - ], - "outputs": [ - { - "itemId": "gun-turret-module", - "amount": 1 - } - ] - }, - { - "id": "carrier-bay-module-assembly", - "label": "Carrier Bay Module Assembly", - "facilityCategory": "station", - "duration": 14, - "priority": 40, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "hull-sections", - "amount": 18 - }, - { - "itemId": "refined-metals", - "amount": 18 - }, - { - "itemId": "ship-equipment", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "carrier-bay-module", - "amount": 1 - } - ] - }, - { - "id": "habitat-ring-module-assembly", - "label": "Habitat Ring Module Assembly", - "facilityCategory": "station", - "duration": 12, - "priority": 22, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "hull-sections", - "amount": 14 - }, - { - "itemId": "ship-equipment", - "amount": 8 - }, - { - "itemId": "water", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "habitat-ring-module", - "amount": 1 - } - ] - }, - { - "id": "bulk-bay-module-assembly", - "label": "Bulk Bay Module Assembly", - "facilityCategory": "station", - "duration": 8, - "priority": 18, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 16 - }, - { - "itemId": "hull-sections", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "bulk-bay-module", - "amount": 1 - } - ] - }, - { - "id": "container-bay-module-assembly", - "label": "Container Bay Module Assembly", - "facilityCategory": "station", - "duration": 8, - "priority": 18, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 12 - }, - { - "itemId": "ship-equipment", - "amount": 4 - } - ], - "outputs": [ - { - "itemId": "container-bay-module", - "amount": 1 - } - ] - }, - { - "id": "liquid-tank-module-assembly", - "label": "Liquid Tank Module Assembly", - "facilityCategory": "station", - "duration": 8, - "priority": 18, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 14 - }, - { - "itemId": "ship-equipment", - "amount": 4 - } - ], - "outputs": [ - { - "itemId": "liquid-tank-module", - "amount": 1 - } - ] - }, - { - "id": "gas-tank-module-assembly", - "label": "Gas Tank Module Assembly", - "facilityCategory": "station", - "duration": 8, - "priority": 18, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 14 - }, - { - "itemId": "ship-equipment", - "amount": 4 - } - ], - "outputs": [ - { - "itemId": "gas-tank-module", - "amount": 1 - } - ] - }, - { - "id": "mining-turret-module-assembly", - "label": "Mining Turret Module Assembly", - "facilityCategory": "station", - "duration": 9, - "priority": 24, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 18 - }, - { - "itemId": "ship-equipment", - "amount": 6 - } - ], - "outputs": [ - { - "itemId": "mining-turret-module", - "amount": 1 - } - ] - }, - { - "id": "gas-extractor-module-assembly", - "label": "Gas Extractor Module Assembly", - "facilityCategory": "station", - "duration": 9, - "priority": 24, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 18 - }, - { - "itemId": "ship-equipment", - "amount": 6 - }, - { - "itemId": "gas", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "gas-extractor-module", - "amount": 1 - } - ] - }, - { - "id": "fabricator-array-module-assembly", - "label": "Fabricator Array Module Assembly", - "facilityCategory": "station", - "duration": 11, - "priority": 20, - "requiredModules": [ - "component-factory", - "container-bay" - ], - "inputs": [ - { - "itemId": "refined-metals", - "amount": 24 - }, - { - "itemId": "ship-equipment", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "fabricator-array-module", - "amount": 1 - } - ] - }, - { - "id": "frigate-construction", - "label": "Frigate Construction", - "facilityCategory": "station", - "duration": 24, - "priority": 90, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "frigate", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 26 - }, - { - "itemId": "fuel", - "amount": 40 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "gun-turret-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "destroyer-construction", - "label": "Destroyer Construction", - "facilityCategory": "station", - "duration": 34, - "priority": 70, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "destroyer", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 44 - }, - { - "itemId": "fuel", - "amount": 60 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "gun-turret-module", - "amount": 2 - } - ], - "outputs": [] - }, - { - "id": "cruiser-construction", - "label": "Cruiser Construction", - "facilityCategory": "station", - "duration": 42, - "priority": 54, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "cruiser", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 60 - }, - { - "itemId": "fuel", - "amount": 80 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "gun-turret-module", - "amount": 2 - } - ], - "outputs": [] - }, - { - "id": "carrier-construction", - "label": "Carrier Construction", - "facilityCategory": "station", - "duration": 60, - "priority": 28, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "carrier", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 120 - }, - { - "itemId": "fuel", - "amount": 140 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "carrier-bay-module", - "amount": 2 - }, - { - "itemId": "gun-turret-module", - "amount": 1 - }, - { - "itemId": "habitat-ring-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "hauler-construction", - "label": "Hauler Construction", - "facilityCategory": "station", - "duration": 26, - "priority": 8, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "hauler", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 34 - }, - { - "itemId": "fuel", - "amount": 40 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "liquid-tank-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "constructor-construction", - "label": "Constructor Construction", - "facilityCategory": "station", - "duration": 30, - "priority": 8, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "constructor", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 42 - }, - { - "itemId": "fuel", - "amount": 44 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "fabricator-array-module", - "amount": 1 - }, - { - "itemId": "container-bay-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "miner-construction", - "label": "Miner Construction", - "facilityCategory": "station", - "duration": 28, - "priority": 8, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "miner", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 34 - }, - { - "itemId": "fuel", - "amount": 42 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "mining-turret-module", - "amount": 1 - }, - { - "itemId": "bulk-bay-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "gas-harvester-construction", - "label": "Gas Harvester Construction", - "facilityCategory": "station", - "duration": 28, - "priority": 8, - "requiredModules": [ - "ship-factory", - "dock-bay-small", - "container-bay", - "power-core" - ], - "shipOutputId": "gas-miner", - "inputs": [ - { - "itemId": "hull-sections", - "amount": 34 - }, - { - "itemId": "fuel", - "amount": 42 - }, - { - "itemId": "command-bridge-module", - "amount": 1 - }, - { - "itemId": "reactor-core-module", - "amount": 1 - }, - { - "itemId": "capacitor-bank-module", - "amount": 1 - }, - { - "itemId": "ion-drive-module", - "amount": 1 - }, - { - "itemId": "ftl-core-module", - "amount": 1 - }, - { - "itemId": "gas-extractor-module", - "amount": 1 - }, - { - "itemId": "gas-tank-module", - "amount": 1 - } - ], - "outputs": [] - }, - { - "id": "trade-hub-assembly", - "label": "Trade Hub Assembly", - "facilityCategory": "station", - "duration": 18, - "priority": 24, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 26 - }, - { - "itemId": "ship-equipment", - "amount": 16 - }, - { - "itemId": "drone-parts", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "trade-hub-kit", - "amount": 1 - } - ] - }, - { - "id": "refinery-assembly", - "label": "Refinery Assembly", - "facilityCategory": "station", - "duration": 20, - "priority": 26, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 32 - }, - { - "itemId": "hull-sections", - "amount": 24 - }, - { - "itemId": "ship-equipment", - "amount": 14 - } - ], - "outputs": [ - { - "itemId": "refinery-kit", - "amount": 1 - } - ] - }, - { - "id": "farm-ring-assembly", - "label": "Farm Ring Assembly", - "facilityCategory": "station", - "duration": 18, - "priority": 22, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 22 - }, - { - "itemId": "ship-equipment", - "amount": 18 - }, - { - "itemId": "water", - "amount": 22 - } - ], - "outputs": [ - { - "itemId": "farm-ring-kit", - "amount": 1 - } - ] - }, - { - "id": "manufactory-assembly", - "label": "Manufactory Assembly", - "facilityCategory": "station", - "duration": 22, - "priority": 28, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 34 - }, - { - "itemId": "hull-sections", - "amount": 16 - }, - { - "itemId": "ship-equipment", - "amount": 18 - } - ], - "outputs": [ - { - "itemId": "manufactory-kit", - "amount": 1 - } - ] - }, - { - "id": "shipyard-assembly", - "label": "Shipyard Assembly", - "facilityCategory": "station", - "duration": 26, - "priority": 30, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 42 - }, - { - "itemId": "hull-sections", - "amount": 30 - }, - { - "itemId": "naval-guns", - "amount": 10 - } - ], - "outputs": [ - { - "itemId": "shipyard-kit", - "amount": 1 - } - ] - }, - { - "id": "defense-grid-assembly", - "label": "Defense Grid Assembly", - "facilityCategory": "station", - "duration": 16, - "priority": 20, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 18 - }, - { - "itemId": "naval-guns", - "amount": 12 - }, - { - "itemId": "ammo-crates", - "amount": 18 - } - ], - "outputs": [ - { - "itemId": "defense-grid-kit", - "amount": 1 - } - ] - }, - { - "id": "stargate-assembly", - "label": "Stargate Assembly", - "facilityCategory": "station", - "duration": 34, - "priority": 36, - "requiredModules": [ - "fabricator-array" - ], - "inputs": [ - { - "itemId": "ship-parts", - "amount": 60 - }, - { - "itemId": "hull-sections", - "amount": 44 - }, - { - "itemId": "ship-equipment", - "amount": 26 - }, - { - "itemId": "naval-guns", - "amount": 8 - } - ], - "outputs": [ - { - "itemId": "stargate-kit", - "amount": 1 - } - ] - } -] diff --git a/shared/data/scenario.json b/shared/data/scenario.json index 9f8997f..cc08540 100644 --- a/shared/data/scenario.json +++ b/shared/data/scenario.json @@ -1,7 +1,13 @@ { "initialStations": [ { - "constructibleId": "station-core", + "label": "Orbital Station", + "startingModules": [ + "dock-bay-small", + "power-core", + "bulk-bay", + "liquid-tank" + ], "systemId": "helios", "planetIndex": 2, "lagrangeSide": -1 @@ -29,7 +35,7 @@ "systemId": "helios" }, { - "shipId": "gas-miner", + "shipId": "hauler", "count": 1, "center": [ 60, @@ -37,16 +43,6 @@ 28 ], "systemId": "helios" - }, - { - "shipId": "gas-miner", - "count": 1, - "center": [ - 60, - 0, - 32 - ], - "systemId": "helios" } ], "patrolRoutes": [], diff --git a/shared/data/ships.json b/shared/data/ships.json index 4451927..8f41233 100644 --- a/shared/data/ships.json +++ b/shared/data/ships.json @@ -13,7 +13,58 @@ "hullColor": "#1f4f78", "size": 4, "maxHealth": 100, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "gun-turret" + ], + "construction": { + "recipeId": "frigate-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 26 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 1 + } + ], + "cycleTime": 24, + "productsPerHour": 150, + "maxEfficiency": 1, + "priority": 90 + } }, { "id": "destroyer", @@ -29,7 +80,59 @@ "hullColor": "#6a2e26", "size": 7, "maxHealth": 240, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret", "gun-turret"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "gun-turret", + "gun-turret" + ], + "construction": { + "recipeId": "destroyer-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 44 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 2 + } + ], + "cycleTime": 34, + "productsPerHour": 105.9, + "maxEfficiency": 1, + "priority": 70 + } }, { "id": "cruiser", @@ -45,7 +148,59 @@ "hullColor": "#314562", "size": 10, "maxHealth": 340, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret", "gun-turret"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "gun-turret", + "gun-turret" + ], + "construction": { + "recipeId": "cruiser-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 60 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "gun-turret-module", + "amount": 2 + } + ], + "cycleTime": 42, + "productsPerHour": 85.7, + "maxEfficiency": 1, + "priority": 54 + } }, { "id": "carrier", @@ -61,9 +216,75 @@ "hullColor": "#35586d", "size": 16, "maxHealth": 900, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "carrier-bay", "carrier-bay", "gun-turret", "habitat-ring"], + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "carrier-bay", + "carrier-bay", + "gun-turret", + "habitat-ring" + ], "dockingCapacity": 6, - "dockingClasses": ["frigate", "destroyer", "cruiser"] + "dockingClasses": [ + "frigate", + "destroyer", + "cruiser" + ], + "construction": { + "recipeId": "carrier-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 120 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "carrier-bay-module", + "amount": 2 + }, + { + "itemId": "gun-turret-module", + "amount": 1 + }, + { + "itemId": "habitat-ring-module", + "amount": 1 + } + ], + "cycleTime": 60, + "productsPerHour": 60, + "maxEfficiency": 1, + "priority": 28 + } }, { "id": "hauler", @@ -75,13 +296,63 @@ "ftlSpeed": 0.55, "spoolTime": 3.3, "cargoCapacity": 180, - "cargoKind": "bulk-liquid", - "cargoItemId": "energy-cell", + "cargoKind": "container", "color": "#b0ff8d", "hullColor": "#365f2a", "size": 8, "maxHealth": 180, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "liquid-tank"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "container-bay" + ], + "construction": { + "recipeId": "hauler-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 34 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "container-bay-module", + "amount": 1 + } + ], + "cycleTime": 26, + "productsPerHour": 138.5, + "maxEfficiency": 1, + "priority": 8 + } }, { "id": "constructor", @@ -99,7 +370,63 @@ "hullColor": "#2d5d47", "size": 9, "maxHealth": 220, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "fabricator-array", "container-bay"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "fabricator-array", + "container-bay" + ], + "construction": { + "recipeId": "constructor-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 42 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "fabricator-array-module", + "amount": 1 + }, + { + "itemId": "container-bay-module", + "amount": 1 + } + ], + "cycleTime": 30, + "productsPerHour": 120, + "maxEfficiency": 1, + "priority": 8 + } }, { "id": "miner", @@ -117,24 +444,62 @@ "hullColor": "#68552b", "size": 6, "maxHealth": 150, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "mining-turret", "bulk-bay"] - }, - { - "id": "gas-miner", - "label": "Nimbus Gas Harvester", - "role": "mining", - "shipClass": "industrial", - "speed": 72000, - "warpSpeed": 0.145, - "ftlSpeed": 0.49, - "spoolTime": 3.2, - "cargoCapacity": 120, - "cargoKind": "bulk-gas", - "cargoItemId": "gas", - "color": "#8ce5ff", - "hullColor": "#2a5668", - "size": 6, - "maxHealth": 150, - "modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gas-extractor", "gas-tank"] + "modules": [ + "command-bridge", + "reactor-core", + "capacitor-bank", + "ion-drive", + "ftl-core", + "mining-turret", + "bulk-bay" + ], + "construction": { + "recipeId": "miner-construction", + "facilityCategory": "station", + "requiredModules": [ + "ship-factory", + "dock-bay-small", + "container-bay", + "power-core" + ], + "requirements": [ + { + "itemId": "hull-sections", + "amount": 34 + }, + { + "itemId": "command-bridge-module", + "amount": 1 + }, + { + "itemId": "reactor-core-module", + "amount": 1 + }, + { + "itemId": "capacitor-bank-module", + "amount": 1 + }, + { + "itemId": "ion-drive-module", + "amount": 1 + }, + { + "itemId": "ftl-core-module", + "amount": 1 + }, + { + "itemId": "mining-turret-module", + "amount": 1 + }, + { + "itemId": "bulk-bay-module", + "amount": 1 + } + ], + "cycleTime": 28, + "productsPerHour": 128.6, + "maxEfficiency": 1, + "priority": 8 + } } ]