using SpaceGame.Simulation.Api.Data; using SpaceGame.Simulation.Api.Contracts; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { private const int StrategicControlTargetSystems = 5; private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection events) { 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); } } 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, "habitat-ring"); 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); } 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 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), ], }; } }