Split viewer and simulation into separate apps

This commit is contained in:
2026-03-12 17:18:29 -04:00
parent 0a76c60ab1
commit 2fb90162ef
45 changed files with 1982 additions and 6600 deletions

View File

@@ -0,0 +1,208 @@
using System.Text.Json;
using SpaceGame.Simulation.Api.Data;
namespace SpaceGame.Simulation.Api.Simulation;
public sealed class ScenarioLoader
{
private readonly string _dataRoot;
private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
public ScenarioLoader(string contentRootPath)
{
_dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
}
public SimulationWorld Load()
{
var systems = Read<List<SolarSystemDefinition>>("systems.json");
var scenario = Read<ScenarioDefinition>("scenario.json");
var ships = Read<List<ShipDefinition>>("ships.json");
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
var balance = Read<BalanceDefinition>("balance.json");
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var systemRuntimes = systems
.Select((definition) => new SystemRuntime
{
Definition = definition,
Position = ToVector(definition.Position),
})
.ToList();
var systemsById = systemRuntimes.ToDictionary((system) => system.Definition.Id, StringComparer.Ordinal);
var nodes = new List<ResourceNodeRuntime>();
var nodeIdCounter = 0;
foreach (var system in systemRuntimes)
{
foreach (var node in system.Definition.ResourceNodes)
{
nodes.Add(new ResourceNodeRuntime
{
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
Position = new Vector3(
system.Position.X + (MathF.Cos(node.Angle) * node.RadiusOffset),
balance.YPlane,
system.Position.Z + (MathF.Sin(node.Angle) * node.RadiusOffset)),
ItemId = node.ItemId,
OreRemaining = node.OreAmount,
MaxOre = node.OreAmount,
});
}
}
var stations = new List<StationRuntime>();
var stationIdCounter = 0;
foreach (var plan in scenario.InitialStations)
{
if (!constructibleDefinitions.TryGetValue(plan.ConstructibleId, out var definition) || !systemsById.TryGetValue(plan.SystemId, out var system))
{
continue;
}
stations.Add(new StationRuntime
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Definition = definition,
Position = ResolveStationPosition(system, plan, balance),
FactionId = plan.FactionId ?? "sol-dominion",
OreStored = definition.Category == "refining" ? 120f : 0f,
RefinedStock = definition.Category == "shipyard" ? 180f : 40f,
});
}
var refinery = stations.FirstOrDefault((station) =>
station.Definition.Category == "refining" && station.SystemId == scenario.MiningDefaults.RefinerySystemId)
?? stations.FirstOrDefault((station) => station.Definition.Category == "refining");
var patrolRoutes = scenario.PatrolRoutes.ToDictionary(
(route) => route.SystemId,
(route) => route.Points.Select(ToVector).ToList(),
StringComparer.Ordinal);
var shipsRuntime = new List<ShipRuntime>();
var shipIdCounter = 0;
foreach (var formation in scenario.ShipFormations)
{
if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition))
{
continue;
}
for (var index = 0; index < formation.Count; index += 1)
{
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(ToVector(formation.Center), offset);
shipsRuntime.Add(new ShipRuntime
{
Id = $"ship-{++shipIdCounter}",
SystemId = formation.SystemId,
Definition = definition,
FactionId = formation.FactionId ?? "sol-dominion",
Position = position,
TargetPosition = position,
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold },
Health = definition.MaxHealth,
});
}
}
var factions = new List<FactionRuntime>
{
new()
{
Id = "sol-dominion",
Label = "Sol Dominion",
Color = "#7ed4ff",
Credits = 240f,
},
};
return new SimulationWorld
{
Label = "Split Viewer / Simulation World",
Seed = 1,
Balance = balance,
Systems = systemRuntimes,
Nodes = nodes,
Stations = stations,
Ships = shipsRuntime,
Factions = factions,
ShipDefinitions = shipDefinitions,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
}
private T Read<T>(string fileName)
{
var path = Path.Combine(_dataRoot, fileName);
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
?? throw new InvalidOperationException($"Unable to read {fileName}.");
}
private static DefaultBehaviorRuntime CreateBehavior(
ShipDefinition definition,
string systemId,
ScenarioDefinition scenario,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
StationRuntime? refinery)
{
if (definition.Role == "mining" && refinery is not null)
{
return new DefaultBehaviorRuntime
{
Kind = "auto-mine",
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
RefineryId = refinery.Id,
Phase = "travel-to-node",
};
}
if (definition.Role == "military" && patrolRoutes.TryGetValue(systemId, out var route))
{
return new DefaultBehaviorRuntime
{
Kind = "patrol",
PatrolPoints = route,
PatrolIndex = 0,
};
}
return new DefaultBehaviorRuntime
{
Kind = "idle",
};
}
private static Vector3 ResolveStationPosition(SystemRuntime system, InitialStationDefinition plan, BalanceDefinition balance)
{
if (plan.Position is { Length: 3 })
{
return ToVector(plan.Position);
}
if (plan.PlanetIndex is int planetIndex && planetIndex >= 0 && planetIndex < system.Definition.Planets.Count)
{
var planet = system.Definition.Planets[planetIndex];
var side = plan.LagrangeSide ?? 1;
return new Vector3(
system.Position.X + planet.OrbitRadius + (side * 72f),
balance.YPlane,
system.Position.Z + ((planetIndex + 1) * 42f * side));
}
return new Vector3(system.Position.X + 180f, balance.YPlane, system.Position.Z);
}
private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
}