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

295 lines
11 KiB
C#

using SpaceGame.Api.Universe.Bootstrap;
using SpaceGame.Api.Ships.AI;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario;
public sealed class ScenarioContentBuilder(
IStaticDataProvider staticData,
IBalanceService balance)
{
public ScenarioWorldContent Build(
ScenarioDefinition scenario,
WorldBuildTopology topology)
{
var stations = CreateStations(
scenario,
topology.SystemsById,
topology.SpatialLayout.SystemGraphs,
topology.SpatialLayout.Celestials);
var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById);
var ships = CreateShips(
scenario,
topology.SystemsById,
topology.SpatialLayout.Celestials,
patrolRoutes,
stations);
return new ScenarioWorldContent(stations, ships);
}
private List<StationRuntime> CreateStations(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
IReadOnlyCollection<CelestialRuntime> celestials)
{
var stations = new List<StationRuntime>();
var stationIdCounter = 0;
foreach (var plan in scenario.InitialStations)
{
if (!systemsById.TryGetValue(plan.SystemId, out var system))
{
throw new InvalidOperationException($"Scenario station '{plan.Label}' references unknown system '{plan.SystemId}'.");
}
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
var station = new StationRuntime
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Label = plan.Label,
Color = plan.Color,
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
Position = placement.Position,
FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"),
CelestialId = placement.AnchorCelestial.Id,
Health = 600f,
MaxHealth = 600f,
};
stations.Add(station);
placement.AnchorCelestial.OccupyingStructureId = station.Id;
var startingModules = BuildStartingModules(plan);
foreach (var moduleId in startingModules)
{
AddStationModule(station, staticData.ModuleDefinitions, moduleId);
}
}
return stations;
}
private IReadOnlyList<string> BuildStartingModules(InitialStationDefinition plan)
{
var startingModules = new List<string>(plan.StartingModules.Count > 0
? plan.StartingModules
: []);
EnsureStartingModule(startingModules, StarterStationLayoutResolver.ResolveDockModuleId(plan.FactionId, staticData.ModuleDefinitions));
var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(plan.FactionId, staticData.ModuleDefinitions);
EnsureStartingModule(startingModules, powerModuleId);
var defaultContainerStorageModuleId = StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
powerModuleId,
plan.FactionId,
staticData.ModuleDefinitions,
staticData.ItemDefinitions,
staticData.Recipes)
.FirstOrDefault(moduleId =>
{
return staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition)
&& definition is StorageModuleDefinition storageDefinition
&& storageDefinition.StorageKind == StorageKind.Container;
});
if (defaultContainerStorageModuleId is not null)
{
EnsureStartingModule(startingModules, defaultContainerStorageModuleId);
}
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(plan.Objective, plan.FactionId, staticData.ModuleDefinitions);
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
{
EnsureStartingModule(startingModules, objectiveModuleId);
if (!string.Equals(objectiveModuleId, powerModuleId, StringComparison.Ordinal))
{
EnsureStartingModule(startingModules, powerModuleId);
}
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
objectiveModuleId,
plan.FactionId,
staticData.ModuleDefinitions,
staticData.ItemDefinitions,
staticData.Recipes))
{
EnsureStartingModule(startingModules, storageModuleId);
}
}
foreach (var moduleId in startingModules)
{
if (!staticData.ModuleDefinitions.ContainsKey(moduleId))
{
throw new InvalidOperationException($"Station '{plan.Label}' requires module '{moduleId}', but it is not defined in static data.");
}
}
return startingModules;
}
private static void EnsureStartingModule(List<string> modules, string moduleId)
{
if (!modules.Contains(moduleId, StringComparer.Ordinal))
{
modules.Add(moduleId);
}
}
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById)
{
return scenario.PatrolRoutes
.GroupBy(route => route.SystemId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group
.SelectMany(route => route.Points)
.Select(point => NormalizeScenarioPoint(systemsById[group.Key], point))
.ToList(),
StringComparer.Ordinal);
}
private List<ShipRuntime> CreateShips(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyCollection<CelestialRuntime> celestials,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations)
{
var ships = new List<ShipRuntime>();
var shipIdCounter = 0;
foreach (var formation in scenario.ShipFormations)
{
if (!staticData.ShipDefinitions.TryGetValue(formation.ShipId, out var definition))
{
throw new InvalidOperationException($"Scenario ship formation references unknown ship '{formation.ShipId}'.");
}
for (var index = 0; index < formation.Count; index += 1)
{
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
var factionId = GetRequiredFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'");
ships.Add(new ShipRuntime
{
Id = $"ship-{++shipIdCounter}",
SystemId = formation.SystemId,
Definition = definition,
FactionId = factionId,
Position = position,
TargetPosition = position,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
DefaultBehavior = CreateBehavior(
definition,
formation.SystemId,
factionId,
patrolRoutes,
stations),
Skills = ShipBootstrapPolicy.CreateSkills(definition),
Health = definition.Hull,
});
foreach (var (itemId, amount) in formation.StartingInventory)
{
if (amount > 0f)
{
ships[^1].Inventory[itemId] = amount;
}
}
}
}
return ships;
}
private static string GetRequiredFactionId(string? factionId, string context)
{
if (!string.IsNullOrWhiteSpace(factionId))
{
return factionId;
}
throw new InvalidOperationException($"Scenario {context} is missing a factionId.");
}
private static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
string factionId,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations)
{
var homeStation = stations.FirstOrDefault(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, systemId, StringComparison.Ordinal))
?? stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal));
if (IsConstructionShip(definition) && homeStation is not null)
{
return new DefaultBehaviorRuntime
{
Kind = ConstructStation,
HomeSystemId = homeStation.SystemId,
HomeStationId = homeStation.Id,
PreferredConstructionSiteId = null,
};
}
if (IsMiningShip(definition) && homeStation is not null)
{
return new DefaultBehaviorRuntime
{
Kind = definition.GetTotalCargoCapacity() >= 120f ? ExpertAutoMine : AdvancedAutoMine,
HomeSystemId = homeStation.SystemId,
HomeStationId = homeStation.Id,
AreaSystemId = homeStation.SystemId,
MaxSystemRange = definition.GetTotalCargoCapacity() >= 120f ? 3 : 1,
};
}
if (IsTransportShip(definition))
{
return new DefaultBehaviorRuntime
{
Kind = AdvancedAutoTrade,
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
MaxSystemRange = 2,
};
}
if (IsMilitaryShip(definition) && patrolRoutes.TryGetValue(systemId, out var route))
{
return new DefaultBehaviorRuntime
{
Kind = Patrol,
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
AreaSystemId = systemId,
PatrolPoints = route,
PatrolIndex = 0,
};
}
return new DefaultBehaviorRuntime
{
Kind = HoldPosition,
HomeSystemId = homeStation?.SystemId ?? systemId,
HomeStationId = homeStation?.Id,
AreaSystemId = homeStation?.SystemId ?? systemId,
};
}
}