Files
space-game/apps/backend/Simulation/ScenarioLoader.Seeding.cs

426 lines
13 KiB
C#

using SpaceGame.Simulation.Api.Data;
namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class ScenarioLoader
{
private static 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)
{
factionIds.Add(DefaultFactionId);
}
return factionIds
.Select(CreateFaction)
.ToList();
}
private static FactionRuntime CreateFaction(string factionId)
{
return factionId switch
{
DefaultFactionId => new FactionRuntime
{
Id = factionId,
Label = "Sol Dominion",
Color = "#7ed4ff",
Credits = MinimumFactionCredits,
},
_ => new FactionRuntime
{
Id = factionId,
Label = ToFactionLabel(factionId),
Color = "#c7d2e0",
Credits = MinimumFactionCredits,
},
};
}
private static 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) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank"))
.ToList();
if (refineries.Count > 0)
{
foreach (var refinery in refineries)
{
refinery.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refined-metals"), MinimumRefineryStock);
}
if (refineries.All((station) => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre))
{
refineries[0].Inventory["ore"] = MinimumRefineryOre;
}
}
foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory")))
{
shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock);
}
}
}
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
private static List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<NodeRuntime> nodes,
DateTimeOffset nowUtc)
{
var stationsByAnchorNodeId = stations
.Where((station) => station.AnchorNodeId is not null)
.ToDictionary((station) => station.AnchorNodeId!, StringComparer.Ordinal);
var claims = new List<ClaimRuntime>();
foreach (var node in nodes.Where((candidate) => candidate.Kind == SpatialNodeKind.LagrangePoint))
{
if (!stationsByAnchorNodeId.TryGetValue(node.Id, out var station))
{
continue;
}
claims.Add(new ClaimRuntime
{
Id = $"claim-{node.Id}",
FactionId = station.FactionId,
SystemId = node.SystemId,
NodeId = node.Id,
BubbleId = node.BubbleId,
PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating,
Health = 100f,
});
}
return claims;
}
private static (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<ClaimRuntime> claims,
IReadOnlyCollection<NodeRuntime> nodes,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
{
var sites = new List<ConstructionSiteRuntime>();
var orders = new List<MarketOrderRuntime>();
foreach (var station in stations)
{
var moduleId = GetNextConstructionSiteModule(station, moduleRecipes);
if (moduleId is null || station.AnchorNodeId is null)
{
continue;
}
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
if (anchorNode is null)
{
continue;
}
var claim = claims.FirstOrDefault((candidate) => candidate.NodeId == anchorNode.Id);
if (claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe))
{
continue;
}
var site = new ConstructionSiteRuntime
{
Id = $"site-{station.Id}",
FactionId = station.FactionId,
SystemId = station.SystemId,
NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId,
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 string? GetNextConstructionSiteModule(
StationRuntime station,
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
{
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("solar-array", 2),
("dock-bay-small", 2),
})
{
if (CountModules(station.InstalledModules, moduleId) < targetCount
&& moduleRecipes.ContainsKey(moduleId))
{
return moduleId;
}
}
return null;
}
private static void InitializeStationPopulation(StationRuntime station)
{
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
station.Population = habitatModules > 0
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: MathF.Min(28f, station.PopulationCapacity);
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
private static 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;
}
private static 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-expansionist",
};
commander.Goals.Add("control-all-systems");
commander.Goals.Add("control-five-systems-fast");
commander.Goals.Add("expand-industrial-base");
commander.Goals.Add("grow-war-fleet");
commander.Goals.Add("deter-pirate-harassment");
commander.Goals.Add("contest-rival-expansion");
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-default",
};
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-default",
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
ActiveTask = CopyTask(ship.ControllerTask, null),
};
if (ship.Order is not null)
{
commander.ActiveOrder = CopyOrder(ship.Order);
}
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;
}
private static string ToFactionLabel(string factionId)
{
return string.Join(" ",
factionId
.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select((segment) => char.ToUpperInvariant(segment[0]) + segment[1..]));
}
private static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
ScenarioDefinition scenario,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
StationRuntime? refinery)
{
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && refinery is not null)
{
return new DefaultBehaviorRuntime
{
Kind = "construct-station",
StationId = refinery.Id,
Phase = "travel-to-station",
};
}
if (HasCapabilities(definition, "mining") && refinery is not null)
{
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id);
}
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
{
return new DefaultBehaviorRuntime
{
Kind = "patrol",
PatrolPoints = route,
PatrolIndex = 0,
};
}
return new DefaultBehaviorRuntime
{
Kind = "idle",
};
}
private static DefaultBehaviorRuntime CreateResourceHarvestBehavior(string kind, string areaSystemId, string stationId) => new()
{
Kind = kind,
AreaSystemId = areaSystemId,
StationId = stationId,
Phase = "travel-to-node",
};
private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
{
Kind = behavior.Kind,
AreaSystemId = behavior.AreaSystemId,
ModuleId = behavior.ModuleId,
NodeId = behavior.NodeId,
Phase = behavior.Phase,
PatrolIndex = behavior.PatrolIndex,
StationId = behavior.StationId,
};
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
{
Kind = order.Kind,
Status = order.Status,
DestinationSystemId = order.DestinationSystemId,
DestinationPosition = order.DestinationPosition,
};
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
{
Kind = task.Kind.ToContractValue(),
Status = task.Status,
TargetEntityId = task.TargetEntityId,
TargetNodeId = targetNodeId ?? task.TargetNodeId,
TargetPosition = task.TargetPosition,
TargetSystemId = task.TargetSystemId,
Threshold = task.Threshold,
};
}