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), Skills = WorldSeedingService.CreateSkills(definition), 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 = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle", HomeSystemId = station.SystemId, HomeStationId = station.Id, MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0, }; } var patrolRadius = station.Radius + 90f; return new DefaultBehaviorRuntime { Kind = "patrol", HomeSystemId = station.SystemId, HomeStationId = station.Id, AreaSystemId = station.SystemId, 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-control", Skills = new CommanderSkillProfileRuntime { Leadership = 3, Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5), Strategy = 3, }, }; 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-control", Skills = new CommanderSkillProfileRuntime { Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5), Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5), Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5), }, }; ship.CommanderId = commander.Id; ship.PolicySetId = factionCommander.PolicySetId; factionCommander.SubordinateCommanderIds.Add(commander.Id); faction.CommanderIds.Add(commander.Id); world.Commanders.Add(commander); } }