Files
space-game/apps/backend/Universe/Scenario/WorldSeedingService.cs

461 lines
18 KiB
C#

using SpaceGame.Api.Universe.Bootstrap;
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario;
public sealed class WorldSeedingService(IStaticDataProvider staticData)
{
internal List<FactionRuntime> CreateFactions(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ShipRuntime> ships)
{
var factionIds = stations
.Select(station => station.FactionId)
.Concat(ships.Select(ship => ship.FactionId))
.Where(factionId => !string.IsNullOrWhiteSpace(factionId))
.Distinct(StringComparer.Ordinal)
.OrderBy(factionId => factionId, StringComparer.Ordinal)
.ToList();
if (factionIds.Count == 0)
{
return [];
}
return factionIds.Select(CreateFaction).ToList();
}
internal void BootstrapFactionEconomy(
IReadOnlyCollection<FactionRuntime> factions,
IReadOnlyCollection<StationRuntime> stations)
{
foreach (var faction in factions)
{
faction.Credits = MathF.Max(faction.Credits, MinimumFactionCredits);
var ownedStations = stations
.Where(station => station.FactionId == faction.Id)
.ToList();
var refineries = ownedStations
.Where(station => string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal))
.ToList();
if (refineries.Count > 0)
{
foreach (var refinery in refineries)
{
refinery.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refinedmetals"), MinimumRefineryStock);
}
if (refineries.All(station => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre))
{
refineries[0].Inventory["ore"] = MinimumRefineryOre;
}
}
foreach (var shipyard in ownedStations.Where(station => HasInstalledModules(station, "module_gen_build_l_01")))
{
shipyard.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refinedmetals"), MinimumShipyardStock);
}
}
}
internal void InitializeStationStockpiles(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{
foreach (var station in stations)
{
InitializeStationPopulation(station, moduleDefinitions);
}
}
internal List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<CelestialRuntime> celestials,
DateTimeOffset nowUtc)
{
var stationsByCelestialId = stations
.Where(station => station.CelestialId is not null)
.ToDictionary(station => station.CelestialId!, StringComparer.Ordinal);
var claims = new List<ClaimRuntime>();
foreach (var celestial in celestials.Where(c => c.Kind == SpatialNodeKind.LagrangePoint))
{
if (!stationsByCelestialId.TryGetValue(celestial.Id, out var station))
{
continue;
}
claims.Add(new ClaimRuntime
{
Id = $"claim-{celestial.Id}",
FactionId = station.FactionId,
SystemId = celestial.SystemId,
CelestialId = celestial.Id,
PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating,
Health = 100f,
});
}
return claims;
}
internal (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
SimulationWorld world)
{
var sites = new List<ConstructionSiteRuntime>();
var orders = new List<MarketOrderRuntime>();
foreach (var station in world.Stations)
{
if (HasSatisfiedStarterObjectiveLayout(world, station))
{
continue;
}
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
if (moduleId is null || station.CelestialId is null)
{
continue;
}
var claim = world.Claims.FirstOrDefault(candidate => candidate.CelestialId == station.CelestialId);
if (claim is null || !world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
{
continue;
}
var site = new ConstructionSiteRuntime
{
Id = $"site-{station.Id}",
FactionId = station.FactionId,
SystemId = station.SystemId,
CelestialId = station.CelestialId,
TargetKind = "station-module",
TargetDefinitionId = "station",
BlueprintId = moduleId,
ClaimId = claim.Id,
StationId = station.Id,
State = claim.State == ClaimStateKinds.Active ? ConstructionSiteStateKinds.Active : ConstructionSiteStateKinds.Planned,
};
foreach (var input in recipe.Inputs)
{
site.RequiredItems[input.ItemId] = input.Amount;
site.DeliveredItems[input.ItemId] = 0f;
var orderId = $"market-order-{station.Id}-{moduleId}-{input.ItemId}";
site.MarketOrderIds.Add(orderId);
station.MarketOrderIds.Add(orderId);
orders.Add(new MarketOrderRuntime
{
Id = orderId,
FactionId = station.FactionId,
StationId = station.Id,
ConstructionSiteId = site.Id,
Kind = MarketOrderKinds.Buy,
ItemId = input.ItemId,
Amount = input.Amount,
RemainingAmount = input.Amount,
Valuation = 1f,
State = MarketOrderStateKinds.Open,
});
}
sites.Add(site);
}
return (sites, orders);
}
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
{
var role = StationSimulationService.DetermineStationRole(station);
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(role, station.FactionId, world.ModuleDefinitions);
if (objectiveModuleId is null)
{
return false;
}
var requiredDockModuleId = StarterStationLayoutResolver.ResolveDockModuleId(station.FactionId, world.ModuleDefinitions);
if (!station.InstalledModules.Contains(requiredDockModuleId, StringComparer.Ordinal)
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
{
return false;
}
var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(station.FactionId, world.ModuleDefinitions);
if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal)
&& !station.InstalledModules.Contains(powerModuleId, StringComparer.Ordinal))
{
return false;
}
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
objectiveModuleId,
station.FactionId,
world.ModuleDefinitions,
world.ItemDefinitions))
{
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
{
return false;
}
}
return true;
}
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
{
var policies = new List<PolicySetRuntime>(factions.Count);
foreach (var faction in factions)
{
var policyId = $"policy-{faction.Id}";
faction.DefaultPolicySetId = policyId;
policies.Add(new PolicySetRuntime
{
Id = policyId,
OwnerKind = "faction",
OwnerId = faction.Id,
});
}
return policies;
}
internal List<CommanderRuntime> CreateCommanders(
IReadOnlyCollection<FactionRuntime> factions,
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ShipRuntime> ships)
{
var commanders = new List<CommanderRuntime>();
var factionCommanders = new Dictionary<string, CommanderRuntime>(StringComparer.Ordinal);
var factionsById = factions.ToDictionary(faction => faction.Id, StringComparer.Ordinal);
foreach (var faction in factions)
{
var commander = new CommanderRuntime
{
Id = $"commander-faction-{faction.Id}",
Kind = CommanderKind.Faction,
FactionId = faction.Id,
ControlledEntityId = faction.Id,
PolicySetId = faction.DefaultPolicySetId,
Doctrine = "strategic-control",
};
commanders.Add(commander);
factionCommanders[faction.Id] = commander;
faction.CommanderIds.Add(commander.Id);
}
foreach (var station in stations)
{
if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander))
{
continue;
}
var commander = new CommanderRuntime
{
Id = $"commander-station-{station.Id}",
Kind = CommanderKind.Station,
FactionId = station.FactionId,
ParentCommanderId = parentCommander.Id,
ControlledEntityId = station.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "station-control",
};
station.CommanderId = commander.Id;
station.PolicySetId = parentCommander.PolicySetId;
parentCommander.SubordinateCommanderIds.Add(commander.Id);
factionsById[station.FactionId].CommanderIds.Add(commander.Id);
commanders.Add(commander);
}
foreach (var ship in ships)
{
if (!factionCommanders.TryGetValue(ship.FactionId, out var parentCommander))
{
continue;
}
var commander = new CommanderRuntime
{
Id = $"commander-ship-{ship.Id}",
Kind = CommanderKind.Ship,
FactionId = ship.FactionId,
ParentCommanderId = parentCommander.Id,
ControlledEntityId = ship.Id,
PolicySetId = parentCommander.PolicySetId,
Doctrine = "ship-control",
};
ship.CommanderId = commander.Id;
ship.PolicySetId = parentCommander.PolicySetId;
parentCommander.SubordinateCommanderIds.Add(commander.Id);
factionsById[ship.FactionId].CommanderIds.Add(commander.Id);
commanders.Add(commander);
}
return commanders;
}
internal PlayerFactionRuntime CreatePlayerFaction(
IReadOnlyCollection<FactionRuntime> factions,
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ShipRuntime> ships,
IReadOnlyCollection<CommanderRuntime> commanders,
IReadOnlyCollection<PolicySetRuntime> policies,
DateTimeOffset nowUtc)
{
var sovereignFaction = factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault()
?? throw new InvalidOperationException("Cannot create a player faction without at least one faction in the world.");
var player = new PlayerFactionRuntime
{
Id = "player-faction",
Label = $"{sovereignFaction.Label} Command",
SovereignFactionId = sovereignFaction.Id,
CreatedAtUtc = nowUtc,
UpdatedAtUtc = nowUtc,
};
foreach (var shipId in ships.Where(ship => ship.FactionId == sovereignFaction.Id).Select(ship => ship.Id))
{
player.AssetRegistry.ShipIds.Add(shipId);
}
foreach (var stationId in stations.Where(station => station.FactionId == sovereignFaction.Id).Select(station => station.Id))
{
player.AssetRegistry.StationIds.Add(stationId);
}
foreach (var commanderId in commanders.Where(commander => commander.FactionId == sovereignFaction.Id).Select(commander => commander.Id))
{
player.AssetRegistry.CommanderIds.Add(commanderId);
}
foreach (var policy in policies.Where(policy => string.Equals(policy.OwnerId, sovereignFaction.Id, StringComparison.Ordinal)))
{
player.AssetRegistry.PolicySetIds.Add(policy.Id);
}
player.Policies.Add(new PlayerFactionPolicyRuntime
{
Id = "player-core-policy",
Label = "Core Empire Policy",
ScopeKind = "player-faction",
ScopeId = player.Id,
PolicySetId = sovereignFaction.DefaultPolicySetId,
TradeAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.TradeAccessPolicy ?? "owner-and-allies",
DockingAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.DockingAccessPolicy ?? "owner-and-allies",
ConstructionAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.ConstructionAccessPolicy ?? "owner-only",
OperationalRangePolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.OperationalRangePolicy ?? "unrestricted",
CombatEngagementPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.CombatEngagementPolicy ?? "defensive",
AvoidHostileSystems = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.AvoidHostileSystems ?? true,
FleeHullRatio = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.FleeHullRatio ?? 0.35f,
UpdatedAtUtc = nowUtc,
});
if (policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId) is { } defaultPolicy)
{
foreach (var systemId in defaultPolicy.BlacklistedSystemIds)
{
player.Policies[0].BlacklistedSystemIds.Add(systemId);
}
}
player.AutomationPolicies.Add(new PlayerAutomationPolicyRuntime
{
Id = "player-core-automation",
Label = "Core Automation",
ScopeKind = "player-faction",
ScopeId = player.Id,
BehaviorKind = "idle",
UpdatedAtUtc = nowUtc,
});
player.Reserves.Add(new PlayerReserveGroupRuntime
{
Id = "player-core-reserve",
Label = "Strategic Reserve",
ReserveKind = "military",
UpdatedAtUtc = nowUtc,
});
player.AssetRegistry.ReserveIds.Add("player-core-reserve");
return player;
}
private FactionRuntime CreateFaction(string factionId)
{
if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition))
{
throw new InvalidOperationException($"Faction '{factionId}' is not defined in static data.");
}
return new FactionRuntime
{
Id = definition.Id,
Label = definition.Label,
Color = ResolveFactionColor(definition),
Credits = MinimumFactionCredits,
};
}
private static string ResolveFactionColor(FactionDefinition definition) =>
definition.Id switch
{
"alliance" => "#c084fc",
"antigone" => "#f97316",
"argon" => "#3b82f6",
"boron" => "#14b8a6",
"freesplit" => "#ef4444",
"hatikvah" => "#84cc16",
"holyorder" => "#d97706",
"loanshark" => "#f59e0b",
"ministry" => "#a3e635",
"paranid" => "#eab308",
"pioneers" => "#60a5fa",
"scaleplate" => "#94a3b8",
"scavenger" => "#64748b",
"split" => "#b91c1c",
"teladi" => "#22c55e",
"terran" => "#38bdf8",
"trinity" => "#2dd4bf",
"xenon" => "#9ca3af",
_ => definition.RaceId switch
{
"argon" => "#3b82f6",
"boron" => "#14b8a6",
"paranid" => "#eab308",
"split" => "#b91c1c",
"teladi" => "#22c55e",
"terran" => "#38bdf8",
"xenon" => "#9ca3af",
_ => "#94a3b8",
},
};
private static void InitializeStationPopulation(
StationRuntime station,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
{
station.PopulationCapacity = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationSupportedPopulation(moduleDefinitions, station);
station.WorkforceRequired = SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStationRequiredWorkforce(moduleDefinitions, station);
station.Population = station.PopulationCapacity > 40f
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: MathF.Min(28f, station.PopulationCapacity);
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
}