451 lines
14 KiB
C#
451 lines
14 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);
|
|
}
|
|
|
|
factionIds.Add(UnclaimedFactionId);
|
|
|
|
return factionIds
|
|
.Distinct(StringComparer.Ordinal)
|
|
.Select(CreateFaction)
|
|
.ToList();
|
|
}
|
|
|
|
private static FactionRuntime CreateFaction(string factionId)
|
|
{
|
|
return factionId switch
|
|
{
|
|
DefaultFactionId => new FactionRuntime
|
|
{
|
|
Id = factionId,
|
|
Label = "Sol Dominion",
|
|
Color = "#7ed4ff",
|
|
Credits = MinimumFactionCredits,
|
|
},
|
|
UnclaimedFactionId => new FactionRuntime
|
|
{
|
|
Id = factionId,
|
|
Label = "Unclaimed",
|
|
Color = "#7f8794",
|
|
Credits = 0f,
|
|
},
|
|
_ => 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))
|
|
{
|
|
var owningFactionId = stationsByAnchorNodeId.TryGetValue(node.Id, out var station)
|
|
? station.FactionId
|
|
: UnclaimedFactionId;
|
|
var activatesAtUtc = owningFactionId == UnclaimedFactionId
|
|
? nowUtc
|
|
: nowUtc.AddSeconds(8);
|
|
var state = owningFactionId == UnclaimedFactionId
|
|
? ClaimStateKinds.Active
|
|
: ClaimStateKinds.Activating;
|
|
|
|
claims.Add(new ClaimRuntime
|
|
{
|
|
Id = $"claim-{node.Id}",
|
|
FactionId = owningFactionId,
|
|
SystemId = node.SystemId,
|
|
NodeId = node.Id,
|
|
BubbleId = node.BubbleId,
|
|
PlacedAtUtc = nowUtc,
|
|
ActivatesAtUtc = activatesAtUtc,
|
|
State = state,
|
|
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)
|
|
{
|
|
if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|