212 lines
9.3 KiB
C#
212 lines
9.3 KiB
C#
using SpaceGame.Api.Shared.Runtime;
|
|
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<SimulationEventRecord> events)
|
|
{
|
|
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
|
|
foreach (var station in world.Stations)
|
|
{
|
|
UpdateStationPopulation(world, 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(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
|
{
|
|
station.WorkforceRequired = GetStationRequiredWorkforce(world.ModuleDefinitions, station);
|
|
|
|
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
|
|
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
|
|
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
|
|
station.PopulationCapacity = GetStationSupportedPopulation(world.ModuleDefinitions, station);
|
|
|
|
if (waterSatisfied)
|
|
{
|
|
if (station.PopulationCapacity > 40f && 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),
|
|
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 = SpaceLayerKind.LocalSpace,
|
|
CurrentCelestialId = station.CelestialId,
|
|
LocalPosition = position,
|
|
SystemPosition = position,
|
|
MovementRegime = MovementRegimeKind.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);
|
|
}
|
|
}
|