Files
space-game/apps/backend/Simulation/Systems/StationLifecycleService.cs

131 lines
5.3 KiB
C#

using SpaceGame.Api.Data;
using SpaceGame.Api.Contracts;
using SpaceGame.Api.Simulation.Model;
using static SpaceGame.Api.Simulation.Systems.ShipControlService;
using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport;
namespace SpaceGame.Api.Simulation.Systems;
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<SimulationEventRecord> events)
{
var factionPopulation = new Dictionary<string, float>(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<SimulationEventRecord> 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<SimulationEventRecord> 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),
],
};
}
}