using static SpaceGame.Api.Ships.Simulation.ShipControlService; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Stations.Simulation; internal sealed class StationLifecycleService { private const float WaterConsumptionPerWorkerPerSecond = 0.004f; private const float PopulationGrowthPerSecond = 0.012f; private const float PopulationAttritionPerSecond = 0.018f; private readonly StationSimulationService _stationSimulation; internal StationLifecycleService(StationSimulationService stationSimulation) { _stationSimulation = stationSimulation; } internal void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) { var factionPopulation = new Dictionary(StringComparer.Ordinal); foreach (var station in world.Stations) { UpdateStationPopulation(station, deltaSeconds, events); _stationSimulation.ReviewStationMarketOrders(world, station); _stationSimulation.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); } } private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection events) { station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); 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, "module_arg_hab_m_01"); station.PopulationCapacity = 40f + (habitatModules * 220f); 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)); } } station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); } internal static 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); EnsureSpawnedShipCommander(world, station, 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 ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new() { CurrentSystemId = station.SystemId, SpaceLayer = SpaceLayerKinds.LocalSpace, CurrentCelestialId = station.CelestialId, LocalPosition = position, SystemPosition = position, MovementRegime = MovementRegimeKinds.LocalFlight, }; private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) { if (!string.Equals(definition.Kind, "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), ], }; } internal static void EnsureStationCommander(SimulationWorld world, StationRuntime station) { if (!string.IsNullOrWhiteSpace(station.CommanderId)) { return; } var factionCommander = world.Commanders.FirstOrDefault(candidate => string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal) && string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal)); var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal)); if (factionCommander is null || faction is null) { return; } var commander = new CommanderRuntime { Id = $"commander-station-{station.Id}", Kind = CommanderKind.Station, FactionId = station.FactionId, ParentCommanderId = factionCommander.Id, ControlledEntityId = station.Id, PolicySetId = factionCommander.PolicySetId, Doctrine = "station-default", }; station.CommanderId = commander.Id; station.PolicySetId = factionCommander.PolicySetId; factionCommander.SubordinateCommanderIds.Add(commander.Id); faction.CommanderIds.Add(commander.Id); world.Commanders.Add(commander); } private static void EnsureSpawnedShipCommander(SimulationWorld world, StationRuntime station, ShipRuntime ship) { var factionCommander = world.Commanders.FirstOrDefault(candidate => string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal) && string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal)); var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal)); if (factionCommander is null || faction is null) { return; } var commander = new CommanderRuntime { Id = $"commander-ship-{ship.Id}", Kind = CommanderKind.Ship, FactionId = ship.FactionId, ParentCommanderId = factionCommander.Id, ControlledEntityId = ship.Id, PolicySetId = factionCommander.PolicySetId, Doctrine = "ship-default", ActiveBehavior = new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind, AreaSystemId = ship.DefaultBehavior.AreaSystemId, TargetEntityId = ship.DefaultBehavior.TargetEntityId, ItemId = ship.DefaultBehavior.ItemId, StationId = ship.DefaultBehavior.StationId, ModuleId = ship.DefaultBehavior.ModuleId, NodeId = ship.DefaultBehavior.NodeId, Phase = ship.DefaultBehavior.Phase, PatrolIndex = ship.DefaultBehavior.PatrolIndex, }, ActiveTask = new CommanderTaskRuntime { Kind = ShipTaskKinds.Idle, Status = WorkStatus.Pending, TargetSystemId = ship.SystemId, Threshold = 0f, }, }; ship.CommanderId = commander.Id; ship.PolicySetId = factionCommander.PolicySetId; factionCommander.SubordinateCommanderIds.Add(commander.Id); faction.CommanderIds.Add(commander.Id); world.Commanders.Add(commander); } }