Refactor world bootstrap and allow empty startup worlds
This commit is contained in:
29
AGENTS.md
Normal file
29
AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Pair Programming Mode
|
||||
|
||||
When working in this repository, act as a pair programming partner by default.
|
||||
|
||||
## Collaboration Rules
|
||||
|
||||
- Do not broaden scope on your own.
|
||||
- Before coding, restate the request in your own words.
|
||||
- Ask clarifying questions when scope, ownership, or design intent is ambiguous.
|
||||
- Push back on weak assumptions, risky changes, or hidden refactors.
|
||||
- Prefer discussion first, implementation second.
|
||||
- Do not refactor adjacent code unless explicitly approved.
|
||||
- Separate proposed work into:
|
||||
- required
|
||||
- optional
|
||||
- recommended
|
||||
- After scope is agreed, implement only that scope.
|
||||
|
||||
## Ambiguity Rules
|
||||
|
||||
- If the request is underspecified, stop and ask instead of assuming.
|
||||
- If the requested change may interfere with an in-progress refactor, call that out before editing.
|
||||
- If a request sounds small, keep the first response small and scoped unless asked to expand.
|
||||
|
||||
## Working Style
|
||||
|
||||
- Treat the user as an active collaborator, not a ticket queue.
|
||||
- Surface tradeoffs before making structural changes.
|
||||
- Prefer explicit approval before changing architecture, bootstrapping, dependency wiring, or data flow.
|
||||
@@ -75,6 +75,39 @@ public sealed class SolarSystemDefinition
|
||||
public required List<PlanetDefinition> Planets { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RaceDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
[JsonIgnore]
|
||||
public string Label => Name;
|
||||
}
|
||||
|
||||
public sealed class FactionDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public int Version { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public string? Race { get; set; }
|
||||
public List<FactionLicenseDefinition> Licenses { get; set; } = [];
|
||||
[JsonIgnore]
|
||||
public string Label => Name;
|
||||
[JsonIgnore]
|
||||
public string? RaceId => Race;
|
||||
}
|
||||
|
||||
public sealed class FactionLicenseDefinition
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Icon { get; set; } = string.Empty;
|
||||
public float Price { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AsteroidFieldDefinition
|
||||
{
|
||||
public int DecorationCount { get; set; }
|
||||
@@ -338,43 +371,174 @@ public sealed class PlanetDefinition
|
||||
public sealed class ShipDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required string Kind { get; set; }
|
||||
public required string Class { get; set; }
|
||||
public float Speed { get; set; }
|
||||
public float WarpSpeed { get; set; }
|
||||
public float FtlSpeed { get; set; }
|
||||
public float SpoolTime { get; set; }
|
||||
public float CargoCapacity { get; set; }
|
||||
public int Version { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Size { get; set; } = string.Empty;
|
||||
public float ExplosionDamage { get; set; }
|
||||
public float Hull { get; set; }
|
||||
public Dictionary<string, float> Storage { get; set; } = new(StringComparer.Ordinal);
|
||||
public int People { get; set; }
|
||||
public string Purpose { get; set; } = string.Empty;
|
||||
public string Thruster { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public float Mass { get; set; }
|
||||
public ShipInertiaDefinition? Inertia { get; set; }
|
||||
public ShipDragDefinition? Drag { get; set; }
|
||||
public List<ShipMountDefinition> Engines { get; set; } = [];
|
||||
public List<ShipMountDefinition> Shields { get; set; } = [];
|
||||
public List<ShipMountDefinition> Weapons { get; set; } = [];
|
||||
public List<ShipMountDefinition> Turrets { get; set; } = [];
|
||||
public List<ShipCargoDefinition> Cargo { get; set; } = [];
|
||||
public List<ModuleDockDefinition> Docks { get; set; } = [];
|
||||
public List<string> Owners { get; set; } = [];
|
||||
public ItemPriceDefinition? Price { get; set; }
|
||||
public List<ItemProductionDefinition> Production { get; set; } = [];
|
||||
[JsonIgnore]
|
||||
public StorageKind? CargoKind { get; set; }
|
||||
[JsonPropertyName("cargoKind")]
|
||||
public string? SerializedCargoKind
|
||||
public string Label => Name;
|
||||
[JsonIgnore]
|
||||
public string Kind => InferKind(Purpose);
|
||||
[JsonIgnore]
|
||||
public string Class => Type;
|
||||
[JsonIgnore]
|
||||
public float Speed => InferLocalSpeed(Size);
|
||||
[JsonIgnore]
|
||||
public float WarpSpeed => InferWarpSpeed(Size);
|
||||
[JsonIgnore]
|
||||
public float FtlSpeed => InferFtlSpeed(Size);
|
||||
[JsonIgnore]
|
||||
public float SpoolTime => InferSpoolTime(Size);
|
||||
[JsonIgnore]
|
||||
public float CargoCapacity => Cargo.Sum(entry => entry.Max);
|
||||
[JsonIgnore]
|
||||
public StorageKind? CargoKind => Cargo
|
||||
.SelectMany(entry => entry.Types)
|
||||
.Select(type => type.ToNullableStorageKind())
|
||||
.FirstOrDefault(kind => kind is not null);
|
||||
[JsonIgnore]
|
||||
public float MaxHealth => Hull;
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<string> Capabilities => InferCapabilities(Purpose, Type, Cargo, Turrets);
|
||||
|
||||
private static string InferKind(string purpose) =>
|
||||
purpose switch
|
||||
{
|
||||
"build" => "construction",
|
||||
"trade" => "transport",
|
||||
"mine" => "mining",
|
||||
"fight" => "military",
|
||||
"auxiliary" => "military",
|
||||
_ => purpose,
|
||||
};
|
||||
|
||||
private static List<string> InferCapabilities(
|
||||
string purpose,
|
||||
string type,
|
||||
IReadOnlyCollection<ShipCargoDefinition> cargo,
|
||||
IReadOnlyCollection<ShipMountDefinition> turrets)
|
||||
{
|
||||
get => CargoKind?.ToDataValue();
|
||||
set => CargoKind = value.ToNullableStorageKind();
|
||||
var capabilities = new List<string> { "warp", "ftl" };
|
||||
|
||||
if (string.Equals(purpose, "mine", StringComparison.Ordinal)
|
||||
|| type.Contains("miner", StringComparison.Ordinal)
|
||||
|| turrets.Any(turret => turret.Types.Contains("mining", StringComparer.Ordinal)))
|
||||
{
|
||||
capabilities.Add("mining");
|
||||
}
|
||||
|
||||
if (cargo.Any(entry => entry.Types.Contains("container", StringComparer.Ordinal)
|
||||
|| entry.Types.Contains("solid", StringComparer.Ordinal)
|
||||
|| entry.Types.Contains("liquid", StringComparer.Ordinal)))
|
||||
{
|
||||
capabilities.Add("cargo");
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
public required string Color { get; set; }
|
||||
public required string HullColor { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float MaxHealth { get; set; }
|
||||
public List<string> Capabilities { get; set; } = [];
|
||||
public ConstructionDefinition? Construction { get; set; }
|
||||
|
||||
private static float InferWarpSpeed(string size) =>
|
||||
size switch
|
||||
{
|
||||
"extrasmall" => 4.8f,
|
||||
"small" => 4.2f,
|
||||
"medium" => 3.4f,
|
||||
"large" => 2.4f,
|
||||
"extralarge" => 1.8f,
|
||||
_ => 3f,
|
||||
};
|
||||
|
||||
private static float InferLocalSpeed(string size) =>
|
||||
size switch
|
||||
{
|
||||
"extrasmall" => 420f,
|
||||
"small" => 320f,
|
||||
"medium" => 230f,
|
||||
"large" => 150f,
|
||||
"extralarge" => 110f,
|
||||
_ => 200f,
|
||||
};
|
||||
|
||||
private static float InferFtlSpeed(string size) =>
|
||||
size switch
|
||||
{
|
||||
"extrasmall" => 1f,
|
||||
"small" => 0.85f,
|
||||
"medium" => 0.7f,
|
||||
"large" => 0.55f,
|
||||
"extralarge" => 0.45f,
|
||||
_ => 0.6f,
|
||||
};
|
||||
|
||||
private static float InferSpoolTime(string size) =>
|
||||
size switch
|
||||
{
|
||||
"extrasmall" => 0.8f,
|
||||
"small" => 1f,
|
||||
"medium" => 1.4f,
|
||||
"large" => 2f,
|
||||
"extralarge" => 2.6f,
|
||||
_ => 1.5f,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class GameStartOptionsDefinition
|
||||
public sealed class ShipInertiaDefinition
|
||||
{
|
||||
public int Seed { get; set; } = 1;
|
||||
public WorldGenerationOptions WorldGeneration { get; set; } = new();
|
||||
public float Pitch { get; set; }
|
||||
public float Yaw { get; set; }
|
||||
public float Roll { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipDragDefinition
|
||||
{
|
||||
public float Forward { get; set; }
|
||||
public float Reverse { get; set; }
|
||||
public float Horizontal { get; set; }
|
||||
public float Vertical { get; set; }
|
||||
public float Pitch { get; set; }
|
||||
public float Yaw { get; set; }
|
||||
public float Roll { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipMountDefinition
|
||||
{
|
||||
public string? Group { get; set; }
|
||||
public required string Size { get; set; }
|
||||
public bool Hittable { get; set; }
|
||||
public List<string> Types { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ShipCargoDefinition
|
||||
{
|
||||
public float Max { get; set; }
|
||||
public List<string> Types { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ScenarioDefinition
|
||||
{
|
||||
public GameStartOptionsDefinition GameStartOptions { get; set; } = new();
|
||||
public required WorldGenerationOptions WorldGeneration { get; set; }
|
||||
public required List<InitialStationDefinition> InitialStations { get; set; }
|
||||
public required List<ShipFormationDefinition> ShipFormations { get; set; }
|
||||
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
|
||||
public required MiningDefaultsDefinition MiningDefaults { get; set; }
|
||||
}
|
||||
|
||||
public sealed class InitialStationDefinition
|
||||
@@ -405,9 +569,3 @@ public sealed class PatrolRouteDefinition
|
||||
public required string SystemId { get; set; }
|
||||
public required List<float[]> Points { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MiningDefaultsDefinition
|
||||
{
|
||||
public required string NodeSystemId { get; set; }
|
||||
public required string RefinerySystemId { get; set; }
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ internal sealed class PlayerFactionService
|
||||
return world.PlayerFaction;
|
||||
}
|
||||
|
||||
var sovereignFaction = world.Factions.FirstOrDefault(faction => string.Equals(faction.Id, LoaderSupport.DefaultFactionId, StringComparison.Ordinal))
|
||||
?? world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
|
||||
var sovereignFaction = world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault()
|
||||
?? throw new InvalidOperationException("Cannot create a player faction domain without any factions in the world.");
|
||||
|
||||
world.PlayerFaction = new PlayerFactionRuntime
|
||||
{
|
||||
@@ -35,6 +35,11 @@ internal sealed class PlayerFactionService
|
||||
|
||||
internal void Update(SimulationWorld world, float _deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
if (world.PlayerFaction is null && world.Factions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var player = EnsureDomain(world);
|
||||
EnsureBaseStructures(world, player);
|
||||
SyncRegistry(world, player);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using FastEndpoints;
|
||||
using FastEndpoints.Swagger;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SpaceGame.Api.Universe.Scenario;
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
const string StartupScenarioPath = "scenarios/empty.json";
|
||||
|
||||
builder.Services.AddCors((options) =>
|
||||
{
|
||||
@@ -17,21 +18,48 @@ builder.Services.AddCors((options) =>
|
||||
});
|
||||
});
|
||||
builder.Services
|
||||
.AddOptions<StaticDataOptions>()
|
||||
.Bind(builder.Configuration.GetSection("StaticData"))
|
||||
.Validate(options => !string.IsNullOrWhiteSpace(options.DataRoot), "StaticData:DataRoot must be configured.")
|
||||
.ValidateOnStart();
|
||||
.AddOptions<StaticDataOptions>()
|
||||
.Bind(builder.Configuration.GetSection("StaticData"))
|
||||
.Validate(options => !string.IsNullOrWhiteSpace(options.DataRoot), "StaticData:DataRoot must be configured.")
|
||||
.PostConfigure(options =>
|
||||
{
|
||||
if (Path.IsPathRooted(options.DataRoot))
|
||||
{
|
||||
options.DataRoot = Path.GetFullPath(options.DataRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
var candidatePaths = new[]
|
||||
{
|
||||
Path.GetFullPath(options.DataRoot),
|
||||
Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, options.DataRoot)),
|
||||
Path.GetFullPath(Path.Combine(builder.Environment.ContentRootPath, "..", "..", options.DataRoot)),
|
||||
};
|
||||
|
||||
var resolvedPath = candidatePaths.FirstOrDefault(Directory.Exists);
|
||||
if (resolvedPath is null)
|
||||
{
|
||||
throw new InvalidOperationException($"StaticData:DataRoot '{options.DataRoot}' could not be resolved to an existing directory.");
|
||||
}
|
||||
|
||||
options.DataRoot = resolvedPath;
|
||||
})
|
||||
.ValidateOnStart();
|
||||
builder.Services.Configure<BalanceOptions>(builder.Configuration.GetSection("Balance"));
|
||||
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
||||
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
||||
|
||||
builder.Services.AddSingleton<IBalanceService, BalanceService>();
|
||||
builder.Services.AddTransient<SystemGenerationService>();
|
||||
builder.Services.AddTransient<SpatialBuilder>();
|
||||
builder.Services.AddTransient<WorldSeedingService>();
|
||||
builder.Services.AddTransient<ScenarioValidationService>();
|
||||
builder.Services.AddTransient<ScenarioContentBuilder>();
|
||||
builder.Services.AddTransient<ScenarioLoader>();
|
||||
builder.Services.AddTransient<SystemTemplateLoader>();
|
||||
builder.Services.AddTransient<WorldTopologyBuilder>();
|
||||
builder.Services.AddTransient<WorldRuntimeAssembler>();
|
||||
builder.Services.AddTransient<WorldBuilder>();
|
||||
builder.Services.AddSingleton<WorldBootstrapper>();
|
||||
builder.Services.AddSingleton<IStaticDataProvider, StaticDataProvider>();
|
||||
builder.Services.AddSingleton<WorldService>();
|
||||
builder.Services.AddSingleton<TelemetryService>();
|
||||
builder.Services.AddHostedService<SimulationHostedService>();
|
||||
@@ -40,6 +68,7 @@ builder.Services.AddFastEndpoints();
|
||||
builder.Services.SwaggerDocument();
|
||||
|
||||
var app = builder.Build();
|
||||
app.Services.GetRequiredService<WorldService>().LoadFromScenario(StartupScenarioPath);
|
||||
|
||||
app.UseCors();
|
||||
app.UseFastEndpoints();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||
@@ -6,7 +5,7 @@ using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||
namespace SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
public sealed class ShipAiService(
|
||||
IOptions<BalanceOptions> balance)
|
||||
IBalanceService balance)
|
||||
{
|
||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||
private const float FrigateDps = 7f;
|
||||
@@ -249,7 +248,7 @@ public sealed class ShipAiService(
|
||||
|
||||
plan.Steps.Add(CreateStep("step-flee-travel", "travel", "Travel to safe station",
|
||||
[
|
||||
CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(world.Balance.ArrivalThreshold, safeStation.Radius + 12f), 0f)
|
||||
CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), 0f)
|
||||
]));
|
||||
plan.Steps.Add(CreateStep("step-flee-dock", "dock", "Dock at safe station",
|
||||
[
|
||||
@@ -353,7 +352,7 @@ public sealed class ShipAiService(
|
||||
[
|
||||
CreateStep("step-dock-travel", "travel", $"Travel to {station.Label}",
|
||||
[
|
||||
CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(world.Balance.ArrivalThreshold, station.Radius + 12f), 0f)
|
||||
CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(balance.ArrivalThreshold, station.Radius + 12f), 0f)
|
||||
]),
|
||||
CreateStep("step-dock", "dock", $"Dock at {station.Label}",
|
||||
[
|
||||
@@ -1220,13 +1219,13 @@ public sealed class ShipAiService(
|
||||
}
|
||||
|
||||
ship.State = ShipState.Mining;
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, world.Balance.MiningCycleSeconds))
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.MiningCycleSeconds))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount);
|
||||
var mined = MathF.Min(world.Balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity);
|
||||
var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity);
|
||||
mined = MathF.Min(mined, node.OreRemaining);
|
||||
if (mined <= 0.01f)
|
||||
{
|
||||
@@ -1282,7 +1281,7 @@ public sealed class ShipAiService(
|
||||
}
|
||||
|
||||
ship.State = ShipState.Docking;
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, world.Balance.DockingDuration))
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.DockingDuration))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
@@ -1311,16 +1310,16 @@ public sealed class ShipAiService(
|
||||
return SubTaskOutcome.Completed;
|
||||
}
|
||||
|
||||
var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
||||
var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, balance.UndockDistance);
|
||||
ship.TargetPosition = undockTarget;
|
||||
ship.State = ShipState.Undocking;
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, world.Balance.UndockingDuration))
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.UndockingDuration))
|
||||
{
|
||||
ship.Position = GetShipDockedPosition(ship, station);
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance);
|
||||
ship.Position = ship.Position.MoveToward(undockTarget, balance.UndockDistance);
|
||||
if (ship.Position.DistanceTo(undockTarget) > MathF.Max(subTask.Threshold, 4f))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
@@ -1359,7 +1358,7 @@ public sealed class ShipAiService(
|
||||
|
||||
var desiredAmount = subTask.Amount > 0f ? subTask.Amount : ship.Definition.CargoCapacity;
|
||||
var availableCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
|
||||
var transferRate = world.Balance.TransferRate * GetSkillFactor(ship.Skills.Trade);
|
||||
var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Trade);
|
||||
var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(availableCapacity, GetInventoryAmount(station.Inventory, itemId)));
|
||||
if (moved > 0.01f)
|
||||
{
|
||||
@@ -1392,7 +1391,7 @@ public sealed class ShipAiService(
|
||||
ship.TargetPosition = GetShipDockedPosition(ship, station);
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.State = ShipState.Transferring;
|
||||
var transferRate = world.Balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining));
|
||||
var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining));
|
||||
|
||||
if (subTask.ItemId is not null)
|
||||
{
|
||||
@@ -1451,7 +1450,7 @@ public sealed class ShipAiService(
|
||||
return SubTaskOutcome.Failed;
|
||||
}
|
||||
|
||||
var transferRate = world.Balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation));
|
||||
var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation));
|
||||
var desiredAmount = subTask.Amount > 0f ? subTask.Amount : GetInventoryAmount(ship.Inventory, subTask.ItemId);
|
||||
var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(targetCapacity, GetInventoryAmount(ship.Inventory, subTask.ItemId)));
|
||||
if (moved > 0.01f)
|
||||
@@ -1491,12 +1490,12 @@ public sealed class ShipAiService(
|
||||
return SubTaskOutcome.Completed;
|
||||
}
|
||||
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, world.Balance.MiningCycleSeconds * 0.8f)))
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, balance.MiningCycleSeconds * 0.8f)))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
var salvageRate = world.Balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade));
|
||||
var salvageRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade));
|
||||
var recovered = MathF.Min(salvageRate, MathF.Min(remainingCapacity, wreck.RemainingAmount));
|
||||
if (recovered > 0.01f)
|
||||
{
|
||||
@@ -1537,7 +1536,7 @@ public sealed class ShipAiService(
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = supportPosition;
|
||||
ship.State = ShipState.DeliveringConstruction;
|
||||
var transferRate = world.Balance.TransferRate * GetSkillFactor(ship.Skills.Construction);
|
||||
var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Construction);
|
||||
foreach (var required in site.RequiredItems.OrderBy(entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
|
||||
@@ -1654,7 +1653,7 @@ public sealed class ShipAiService(
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);
|
||||
|
||||
if (distance <= MathF.Max(subTask.Threshold, world.Balance.ArrivalThreshold))
|
||||
if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold))
|
||||
{
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
16
apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs
Normal file
16
apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
internal static class ShipBootstrapPolicy
|
||||
{
|
||||
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
|
||||
{
|
||||
return definition.Kind switch
|
||||
{
|
||||
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
|
||||
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
|
||||
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
|
||||
_ when SpaceGame.Api.Universe.Scenario.LoaderSupport.HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
|
||||
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace SpaceGame.Api.Simulation.Core;
|
||||
|
||||
public sealed class SimulationEngine
|
||||
internal sealed class SimulationEngine
|
||||
{
|
||||
private readonly IBalanceService _balance;
|
||||
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
||||
private readonly OrbitalStateUpdater _orbitalStateUpdater;
|
||||
private readonly InfrastructureSimulationService _infrastructureSimulation;
|
||||
@@ -13,9 +14,10 @@ public sealed class SimulationEngine
|
||||
private readonly ShipAiService _shipAi;
|
||||
private readonly SimulationProjectionService _projection;
|
||||
|
||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||
internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance)
|
||||
{
|
||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||
_balance = balance;
|
||||
_orbitalSimulation = orbitalSimulation;
|
||||
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
|
||||
_infrastructureSimulation = new InfrastructureSimulationService();
|
||||
_geopolitics = new GeopoliticalSimulationService();
|
||||
@@ -23,7 +25,7 @@ public sealed class SimulationEngine
|
||||
_playerFaction = new PlayerFactionService();
|
||||
_stationSimulation = new StationSimulationService();
|
||||
_stationLifecycle = new StationLifecycleService(_stationSimulation);
|
||||
_shipAi = new ShipAiService();
|
||||
_shipAi = new ShipAiService(balance);
|
||||
_projection = new SimulationProjectionService(_orbitalSimulation);
|
||||
}
|
||||
|
||||
@@ -31,7 +33,7 @@ public sealed class SimulationEngine
|
||||
{
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var events = new List<SimulationEventRecord>();
|
||||
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
|
||||
var simulationDeltaSeconds = deltaSeconds * MathF.Max(_balance.SimulationSpeedMultiplier, 0.01f);
|
||||
world.GeneratedAtUtc = nowUtc;
|
||||
|
||||
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using SpaceGame.Api.Shared.Runtime;
|
||||
using SpaceGame.Api.Ships.Simulation;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
namespace SpaceGame.Api.Stations.Simulation;
|
||||
@@ -79,7 +80,7 @@ internal sealed class StationLifecycleService
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
||||
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
||||
Skills = WorldSeedingService.CreateSkills(definition),
|
||||
Skills = ShipBootstrapPolicy.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Api;
|
||||
|
||||
public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest
|
||||
public sealed class GetBalanceHandler(IBalanceService balanceService) : EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
@@ -12,5 +12,5 @@ public sealed class GetBalanceHandler(WorldService worldService) : EndpointWitho
|
||||
}
|
||||
|
||||
public override Task HandleAsync(CancellationToken cancellationToken) =>
|
||||
SendOkAsync(worldService.GetBalance(), cancellationToken);
|
||||
SendOkAsync(balanceService.GetCurrent(), cancellationToken);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Api;
|
||||
|
||||
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceOptions>
|
||||
public sealed class UpdateBalanceHandler(IBalanceService balanceService) : Endpoint<BalanceOptions>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
@@ -13,7 +13,7 @@ public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<B
|
||||
|
||||
public override Task HandleAsync(BalanceOptions req, CancellationToken cancellationToken)
|
||||
{
|
||||
var applied = worldService.UpdateBalance(req);
|
||||
var applied = balanceService.Update(req);
|
||||
return SendOkAsync(applied, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
14
apps/backend/Universe/Bootstrap/IStaticDataProvider.cs
Normal file
14
apps/backend/Universe/Bootstrap/IStaticDataProvider.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
public interface IStaticDataProvider
|
||||
{
|
||||
IReadOnlyList<SolarSystemDefinition> KnownSystems { get; }
|
||||
IReadOnlyDictionary<string, RaceDefinition> RaceDefinitions { get; }
|
||||
IReadOnlyDictionary<string, FactionDefinition> FactionDefinitions { get; }
|
||||
IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions { get; }
|
||||
IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions { get; }
|
||||
IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions { get; }
|
||||
IReadOnlyDictionary<string, RecipeDefinition> Recipes { get; }
|
||||
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; }
|
||||
ProductionGraph ProductionGraph { get; }
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
public sealed record StaticDataCatalog(
|
||||
IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions,
|
||||
IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions,
|
||||
IReadOnlyDictionary<string, RecipeDefinition> Recipes,
|
||||
IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes,
|
||||
ProductionGraph ProductionGraph);
|
||||
@@ -2,5 +2,5 @@ namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
public sealed class StaticDataOptions
|
||||
{
|
||||
public required string DataRoot { get; init; }
|
||||
public required string DataRoot { get; set; }
|
||||
}
|
||||
|
||||
@@ -4,34 +4,59 @@ using SpaceGame.Api.Shared.Runtime;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
internal sealed class StaticDataLoader(IOptions<StaticDataOptions> staticDataOptions)
|
||||
public sealed class StaticDataProvider : IStaticDataProvider
|
||||
{
|
||||
private readonly string _dataRoot;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
internal StaticDataCatalog Load()
|
||||
public StaticDataProvider(IOptions<StaticDataOptions> staticDataOptions)
|
||||
{
|
||||
_dataRoot = staticDataOptions.Value.DataRoot;
|
||||
|
||||
var knownSystems = Read<List<SolarSystemDefinition>>("systems.json");
|
||||
var races = Read<List<RaceDefinition>>("races.json");
|
||||
var factions = Read<List<FactionDefinition>>("factions.json");
|
||||
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
|
||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||
var items = Read<List<ItemDefinition>>("items.json");
|
||||
var recipes = BuildRecipes(items, ships, modules);
|
||||
var moduleRecipes = BuildModuleRecipes(modules);
|
||||
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
|
||||
|
||||
return new StaticDataCatalog(
|
||||
modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||
recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
|
||||
moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal),
|
||||
productionGraph);
|
||||
KnownSystems = knownSystems;
|
||||
RaceDefinitions = races.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
FactionDefinitions = factions.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
ModuleDefinitions = modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
ShipDefinitions = ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
ItemDefinitions = items.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
Recipes = recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal);
|
||||
ModuleRecipes = moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal);
|
||||
ProductionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
|
||||
}
|
||||
|
||||
public IReadOnlyList<SolarSystemDefinition> KnownSystems { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, RaceDefinition> RaceDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, FactionDefinition> FactionDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, ModuleDefinition> ModuleDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, ShipDefinition> ShipDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, ItemDefinition> ItemDefinitions { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, RecipeDefinition> Recipes { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; }
|
||||
|
||||
public ProductionGraph ProductionGraph { get; }
|
||||
|
||||
private T Read<T>(string fileName)
|
||||
{
|
||||
var path = Path.Combine(staticDataOptions.Value.DataRoot, 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}.");
|
||||
@@ -133,28 +158,26 @@ internal sealed class StaticDataLoader(IOptions<StaticDataOptions> staticDataOpt
|
||||
|
||||
foreach (var ship in ships)
|
||||
{
|
||||
if (ship.Construction is null)
|
||||
foreach (var production in ship.Production)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
recipes.Add(new RecipeDefinition
|
||||
{
|
||||
Id = ship.Construction.RecipeId ?? $"{ship.Id}-construction",
|
||||
Label = $"{ship.Label} Construction",
|
||||
FacilityCategory = ship.Construction.FacilityCategory,
|
||||
Duration = ship.Construction.CycleTime,
|
||||
Priority = ship.Construction.Priority,
|
||||
RequiredModules = ship.Construction.RequiredModules.ToList(),
|
||||
Inputs = ship.Construction.Requirements
|
||||
.Select(input => new RecipeInputDefinition
|
||||
recipes.Add(new RecipeDefinition
|
||||
{
|
||||
ItemId = input.ItemId,
|
||||
Amount = input.Amount,
|
||||
})
|
||||
.ToList(),
|
||||
ShipOutputId = ship.Id,
|
||||
});
|
||||
Id = $"{ship.Id}-{production.Method}-construction",
|
||||
Label = $"{ship.Label} Construction",
|
||||
FacilityCategory = "shipyard",
|
||||
Duration = production.Time,
|
||||
Priority = InferShipRecipePriority(ship),
|
||||
RequiredModules = InferShipBuildModules(ship),
|
||||
Inputs = production.Wares
|
||||
.Select(input => new RecipeInputDefinition
|
||||
{
|
||||
ItemId = input.ItemId,
|
||||
Amount = input.Amount,
|
||||
})
|
||||
.ToList(),
|
||||
ShipOutputId = ship.Id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return recipes;
|
||||
@@ -191,6 +214,25 @@ internal sealed class StaticDataLoader(IOptions<StaticDataOptions> staticDataOpt
|
||||
_ => 60,
|
||||
};
|
||||
|
||||
private static List<string> InferShipBuildModules(ShipDefinition ship) =>
|
||||
ship.Size switch
|
||||
{
|
||||
"extrasmall" or "small" or "medium" => ["module_gen_build_dockarea_m_01"],
|
||||
"large" => ["module_gen_build_l_01"],
|
||||
"extralarge" => ["module_gen_build_xl_01"],
|
||||
_ => ["module_gen_build_dockarea_m_01"],
|
||||
};
|
||||
|
||||
private static int InferShipRecipePriority(ShipDefinition ship) =>
|
||||
ship.Kind switch
|
||||
{
|
||||
"military" => 170,
|
||||
"construction" => 140,
|
||||
"transport" => 120,
|
||||
"mining" => 110,
|
||||
_ => 100,
|
||||
};
|
||||
|
||||
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
|
||||
{
|
||||
for (var index = 0; index < modules.Count; index += 1)
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
public sealed class SystemTemplateLoader(IOptions<StaticDataOptions> staticDataOptions)
|
||||
{
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
internal List<SolarSystemDefinition> Load()
|
||||
{
|
||||
var path = Path.Combine(staticDataOptions.Value.DataRoot, "systems.json");
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<List<SolarSystemDefinition>>(json, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Unable to read systems.json.");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using SpaceGame.Api.Universe.Scenario;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
public sealed class WorldBootstrapper
|
||||
{
|
||||
private readonly BalanceOptions _defaultBalance;
|
||||
private readonly WorldGenerationOptions _defaultWorldGeneration;
|
||||
private readonly StaticDataCatalog _staticData;
|
||||
private readonly ScenarioLoader _scenarioLoader;
|
||||
private readonly SystemTemplateLoader _systemTemplateLoader;
|
||||
private readonly WorldBuilder _worldBuilder;
|
||||
|
||||
public WorldBootstrapper(
|
||||
StaticDataCatalog staticData,
|
||||
ScenarioLoader scenarioLoader,
|
||||
SystemTemplateLoader systemTemplateLoader,
|
||||
WorldBuilder worldBuilder,
|
||||
IOptions<BalanceOptions> defaultBalanceOptions,
|
||||
IOptions<WorldGenerationOptions> defaultWorldGenerationOptions)
|
||||
{
|
||||
_defaultBalance = defaultBalanceOptions.Value;
|
||||
_defaultWorldGeneration = defaultWorldGenerationOptions.Value;
|
||||
_staticData = staticData;
|
||||
_scenarioLoader = scenarioLoader;
|
||||
_systemTemplateLoader = systemTemplateLoader;
|
||||
_worldBuilder = worldBuilder;
|
||||
}
|
||||
|
||||
public SimulationWorld Bootstrap()
|
||||
{
|
||||
var scenario = _scenarioLoader.Load();
|
||||
var gameStartOptions = scenario?.GameStartOptions ?? new GameStartOptionsDefinition
|
||||
{
|
||||
Seed = 1,
|
||||
WorldGeneration = _defaultWorldGeneration,
|
||||
};
|
||||
|
||||
return _worldBuilder.Build(
|
||||
_staticData,
|
||||
_defaultBalance,
|
||||
_systemTemplateLoader.Load(),
|
||||
gameStartOptions,
|
||||
scenario);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class LoaderSupport
|
||||
{
|
||||
internal const string DefaultFactionId = "sol-dominion";
|
||||
internal const float MinimumFactionCredits = 0f;
|
||||
internal const float MinimumRefineryOre = 0f;
|
||||
internal const float MinimumRefineryStock = 0f;
|
||||
@@ -97,7 +96,7 @@ internal static class LoaderSupport
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
|
||||
{
|
||||
return;
|
||||
throw new InvalidOperationException($"Module '{moduleId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
station.Modules.Add(StationModuleRuntime.Create($"{station.Id}-module-{station.Modules.Count + 1}", definition));
|
||||
|
||||
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
289
apps/backend/Universe/Scenario/ScenarioContentBuilder.cs
Normal file
@@ -0,0 +1,289 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using SpaceGame.Api.Ships.Simulation;
|
||||
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)
|
||||
.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))
|
||||
{
|
||||
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.MaxHealth,
|
||||
});
|
||||
|
||||
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 (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
PreferredConstructionSiteId = null,
|
||||
};
|
||||
}
|
||||
|
||||
if (LoaderSupport.HasCapabilities(definition, "mining") && homeStation is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
AreaSystemId = homeStation.SystemId,
|
||||
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "advanced-auto-trade",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
MaxSystemRange = 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && 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 = "idle",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,16 +12,16 @@ public sealed class ScenarioLoader(IOptions<StaticDataOptions> staticDataOptions
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public ScenarioDefinition? Load()
|
||||
public ScenarioDefinition Load(string relativePath)
|
||||
{
|
||||
var scenarioPath = Path.Combine(staticDataOptions.Value.DataRoot, "scenario.json");
|
||||
var scenarioPath = Path.Combine(staticDataOptions.Value.DataRoot, relativePath);
|
||||
if (!File.Exists(scenarioPath))
|
||||
{
|
||||
return null;
|
||||
throw new FileNotFoundException($"Scenario file was not found: {relativePath}", scenarioPath);
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(scenarioPath);
|
||||
return JsonSerializer.Deserialize<ScenarioDefinition>(json, _jsonOptions)
|
||||
?? throw new InvalidOperationException("Unable to read scenario.json.");
|
||||
?? throw new InvalidOperationException($"Unable to read {relativePath}.");
|
||||
}
|
||||
}
|
||||
|
||||
111
apps/backend/Universe/Scenario/ScenarioValidationService.cs
Normal file
111
apps/backend/Universe/Scenario/ScenarioValidationService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class ScenarioValidationService(IStaticDataProvider staticData)
|
||||
{
|
||||
public ScenarioDefinition CreateEmptyScenario(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
if (systems.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("World generation produced no systems.");
|
||||
}
|
||||
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
WorldGeneration = worldGenerationOptions,
|
||||
InitialStations = [],
|
||||
ShipFormations = [],
|
||||
PatrolRoutes = [],
|
||||
};
|
||||
}
|
||||
|
||||
public void Validate(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlySet<string> availableSystemIds)
|
||||
{
|
||||
foreach (var station in scenario.InitialStations)
|
||||
{
|
||||
ValidateSystemExists(station.SystemId, $"station '{station.Label}' system", availableSystemIds);
|
||||
ValidateFactionId(station.FactionId, $"station '{station.Label}'");
|
||||
|
||||
foreach (var moduleId in station.StartingModules)
|
||||
{
|
||||
ValidateModuleId(moduleId, $"station '{station.Label}' starting module");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var formation in scenario.ShipFormations)
|
||||
{
|
||||
ValidateSystemExists(formation.SystemId, $"ship formation '{formation.ShipId}' system", availableSystemIds);
|
||||
ValidateFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'");
|
||||
ValidateShipId(formation.ShipId, $"ship formation in system '{formation.SystemId}'");
|
||||
|
||||
foreach (var itemId in formation.StartingInventory.Keys)
|
||||
{
|
||||
ValidateItemId(itemId, $"ship formation '{formation.ShipId}' starting inventory");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var route in scenario.PatrolRoutes)
|
||||
{
|
||||
ValidateSystemExists(route.SystemId, "patrol route system", availableSystemIds);
|
||||
}
|
||||
}
|
||||
|
||||
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 void ValidateSystemExists(
|
||||
string systemId,
|
||||
string context,
|
||||
IReadOnlySet<string> availableSystemIds)
|
||||
{
|
||||
if (!availableSystemIds.Contains(systemId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown generated system '{systemId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateFactionId(string? factionId, string context)
|
||||
{
|
||||
var requiredFactionId = GetRequiredFactionId(factionId, context);
|
||||
if (!staticData.FactionDefinitions.ContainsKey(requiredFactionId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown faction '{requiredFactionId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateModuleId(string moduleId, string context)
|
||||
{
|
||||
if (!staticData.ModuleDefinitions.ContainsKey(moduleId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown module '{moduleId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateShipId(string shipId, string context)
|
||||
{
|
||||
if (!staticData.ShipDefinitions.ContainsKey(shipId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown ship '{shipId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateItemId(string itemId, string context)
|
||||
{
|
||||
if (!staticData.ItemDefinitions.ContainsKey(itemId))
|
||||
{
|
||||
throw new InvalidOperationException($"Scenario {context} references unknown item '{itemId}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/backend/Universe/Scenario/ScenarioWorldContent.cs
Normal file
5
apps/backend/Universe/Scenario/ScenarioWorldContent.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed record ScenarioWorldContent(
|
||||
IReadOnlyList<StationRuntime> Stations,
|
||||
IReadOnlyList<ShipRuntime> Ships);
|
||||
@@ -2,9 +2,9 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class SpatialBuilder
|
||||
public sealed class SpatialBuilder(IBalanceService balance)
|
||||
{
|
||||
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceOptions balance)
|
||||
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
|
||||
{
|
||||
var systemGraphs = systems.ToDictionary(
|
||||
system => system.Definition.Id,
|
||||
@@ -305,12 +305,12 @@ public sealed class SpatialBuilder
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ScenarioSpatialLayout(
|
||||
public sealed record ScenarioSpatialLayout(
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
|
||||
List<CelestialRuntime> Celestials,
|
||||
List<ResourceNodeRuntime> Nodes);
|
||||
|
||||
internal sealed record SystemSpatialGraph(
|
||||
public sealed record SystemSpatialGraph(
|
||||
string SystemId,
|
||||
List<CelestialRuntime> Celestials,
|
||||
Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);
|
||||
|
||||
145
apps/backend/Universe/Scenario/StarterStationLayoutResolver.cs
Normal file
145
apps/backend/Universe/Scenario/StarterStationLayoutResolver.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using SpaceGame.Api.Shared.Runtime;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class StarterStationLayoutResolver
|
||||
{
|
||||
internal static string ResolveDockModuleId(
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values.Where(definition => definition.ModuleType == ModuleType.DockArea),
|
||||
factionId,
|
||||
"starter dock module").Id;
|
||||
|
||||
internal static string ResolvePowerModuleId(
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
ResolveProducerModuleId("energycells", factionId, moduleDefinitions);
|
||||
|
||||
internal static string? ResolveObjectiveModuleId(
|
||||
string? objective,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
|
||||
{
|
||||
var targetWareId = ResolveObjectiveWareId(objective);
|
||||
return targetWareId is null ? null : ResolveProducerModuleId(targetWareId, factionId, moduleDefinitions);
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> ResolveRequiredStorageModuleIds(
|
||||
string moduleId,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
throw new InvalidOperationException($"Module '{moduleId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
throw new InvalidOperationException($"Module '{moduleId}' references unknown ware '{wareId}'.");
|
||||
}
|
||||
|
||||
if (itemDefinition.CargoKind is not { } storageKind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return ResolveStorageModuleId(storageKind, factionId, moduleDefinitions);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveObjectiveWareId(string? objective) =>
|
||||
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||
{
|
||||
"power" => "energycells",
|
||||
"refinery" => "refinedmetals",
|
||||
"graphene" => "graphene",
|
||||
"siliconwafers" => "siliconwafers",
|
||||
"hullparts" => "hullparts",
|
||||
"claytronics" => "claytronics",
|
||||
"quantumtubes" => "quantumtubes",
|
||||
"antimattercells" => "antimattercells",
|
||||
"superfluidcoolant" => "superfluidcoolant",
|
||||
"water" => "water",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string ResolveProducerModuleId(
|
||||
string wareId,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values
|
||||
.OfType<ProductionModuleDefinition>()
|
||||
.Where(definition => definition.ProductItemIds.Contains(wareId, StringComparer.Ordinal)),
|
||||
factionId,
|
||||
$"producer module for ware '{wareId}'").Id;
|
||||
|
||||
private static string ResolveStorageModuleId(
|
||||
StorageKind storageKind,
|
||||
string? factionId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions) =>
|
||||
SelectPreferredModule(
|
||||
moduleDefinitions.Values
|
||||
.OfType<StorageModuleDefinition>()
|
||||
.Where(definition => definition.StorageKind == storageKind),
|
||||
factionId,
|
||||
$"storage module for cargo kind '{storageKind.ToDataValue()}'").Id;
|
||||
|
||||
private static T SelectPreferredModule<T>(
|
||||
IEnumerable<T> candidates,
|
||||
string? factionId,
|
||||
string context)
|
||||
where T : ModuleDefinition
|
||||
{
|
||||
var ordered = candidates
|
||||
.OrderBy(definition => ComputeOwnerRank(definition, factionId))
|
||||
.ThenBy(definition => ComputeModuleRank(definition))
|
||||
.ThenBy(definition => definition.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return ordered.FirstOrDefault()
|
||||
?? throw new InvalidOperationException($"Unable to resolve {context}.");
|
||||
}
|
||||
|
||||
private static int ComputeOwnerRank(ModuleDefinition definition, string? factionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(factionId))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return definition.Owners.Contains(factionId, StringComparer.Ordinal) ? 0 : 1;
|
||||
}
|
||||
|
||||
private static int ComputeModuleRank(ModuleDefinition definition)
|
||||
{
|
||||
if (definition.ModuleType is ModuleType.DockArea or ModuleType.Storage)
|
||||
{
|
||||
if (definition.Id.Contains("_m_", StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (definition.Id.Contains("_s_", StringComparison.Ordinal))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (definition.Id.Contains("_l_", StringComparison.Ordinal))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
@@ -4,114 +4,65 @@ namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class SystemGenerationService
|
||||
{
|
||||
internal List<SolarSystemDefinition> PrepareAuthoredSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||
authoredSystems
|
||||
private const float KnownSystemSelectionChance = 0.5f;
|
||||
|
||||
internal List<SolarSystemDefinition> PrepareKnownSystems(IReadOnlyList<SolarSystemDefinition> knownSystems) =>
|
||||
knownSystems
|
||||
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||
.ToList();
|
||||
|
||||
internal List<SolarSystemDefinition> ExpandSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> authoredSystems,
|
||||
int targetSystemCount)
|
||||
internal List<SolarSystemDefinition> GenerateSystems(
|
||||
IReadOnlyList<SolarSystemDefinition> knownSystems,
|
||||
WorldGenerationOptions worldGenerationOptions)
|
||||
{
|
||||
var systems = authoredSystems
|
||||
.Select(CloneSystemDefinition)
|
||||
.ToList();
|
||||
|
||||
if (targetSystemCount <= 0)
|
||||
if (worldGenerationOptions.TargetSystemCount <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (systems.Count > targetSystemCount)
|
||||
if (knownSystems.Count == 0)
|
||||
{
|
||||
return TrimSystemsToTarget(systems, targetSystemCount);
|
||||
throw new InvalidOperationException("World generation requires at least one known system template.");
|
||||
}
|
||||
|
||||
if (systems.Count >= targetSystemCount || authoredSystems.Count == 0)
|
||||
{
|
||||
return systems;
|
||||
}
|
||||
var systems = new List<SolarSystemDefinition>(worldGenerationOptions.TargetSystemCount);
|
||||
var availableKnownSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
var templateSystems = knownSystems.Select(CloneSystemDefinition).ToList();
|
||||
|
||||
var existingIds = systems
|
||||
.Select(system => system.Id)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var generatedPositions = BuildGalaxyPositions(
|
||||
authoredSystems.Select(system => ToVector(system.Position)).ToList(),
|
||||
targetSystemCount - systems.Count);
|
||||
var existingIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
var occupiedPositions = new List<Vector3>();
|
||||
var generatedSystemCount = 0;
|
||||
|
||||
for (var index = systems.Count; index < targetSystemCount; index += 1)
|
||||
for (var slotIndex = 0; slotIndex < worldGenerationOptions.TargetSystemCount; slotIndex += 1)
|
||||
{
|
||||
var template = authoredSystems[index % authoredSystems.Count];
|
||||
var name = GeneratedSystemNames[(index - authoredSystems.Count) % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, index + 1);
|
||||
if (ShouldUseKnownSystem(worldGenerationOptions, slotIndex, availableKnownSystems.Count))
|
||||
{
|
||||
var knownSystemIndex = SelectKnownSystemIndex(worldGenerationOptions.Seed, slotIndex, availableKnownSystems.Count);
|
||||
var knownSystem = availableKnownSystems[knownSystemIndex];
|
||||
availableKnownSystems.RemoveAt(knownSystemIndex);
|
||||
systems.Add(knownSystem);
|
||||
existingIds.Add(knownSystem.Id);
|
||||
occupiedPositions.Add(ToVector(knownSystem.Position));
|
||||
continue;
|
||||
}
|
||||
|
||||
var template = templateSystems[generatedSystemCount % templateSystems.Count];
|
||||
var name = GeneratedSystemNames[generatedSystemCount % GeneratedSystemNames.Length];
|
||||
var id = BuildGeneratedSystemId(name, generatedSystemCount + 1);
|
||||
while (!existingIds.Add(id))
|
||||
{
|
||||
id = $"{id}-x";
|
||||
}
|
||||
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, index - authoredSystems.Count, generatedPositions[index - authoredSystems.Count]));
|
||||
var position = BuildGeneratedSystemPosition(occupiedPositions, generatedSystemCount);
|
||||
systems.Add(CreateGeneratedSystem(template, name, id, generatedSystemCount, position));
|
||||
occupiedPositions.Add(position);
|
||||
generatedSystemCount += 1;
|
||||
}
|
||||
|
||||
return systems;
|
||||
}
|
||||
|
||||
private static List<SolarSystemDefinition> TrimSystemsToTarget(IReadOnlyList<SolarSystemDefinition> systems, int targetSystemCount)
|
||||
{
|
||||
var selected = new List<SolarSystemDefinition>(targetSystemCount);
|
||||
|
||||
void AddById(string systemId)
|
||||
{
|
||||
var system = systems.FirstOrDefault(candidate => string.Equals(candidate.Id, systemId, StringComparison.Ordinal));
|
||||
if (system is not null && selected.All(candidate => !string.Equals(candidate.Id, systemId, StringComparison.Ordinal)))
|
||||
{
|
||||
selected.Add(system);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var preferredSystemId in SystemSelectionPolicy.PreferredSystemIds)
|
||||
{
|
||||
AddById(preferredSystemId);
|
||||
}
|
||||
|
||||
foreach (var system in systems)
|
||||
{
|
||||
if (selected.Count >= targetSystemCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (selected.Any(candidate => string.Equals(candidate.Id, system.Id, StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.Add(system);
|
||||
}
|
||||
|
||||
if (selected.Count > 0 && selected.Count <= 4)
|
||||
{
|
||||
ApplyCompactGalaxyLayout(selected);
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static void ApplyCompactGalaxyLayout(IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
var compactPositions = new[]
|
||||
{
|
||||
new[] { 0f, 0f, 0f },
|
||||
new[] { 2.6f, 0.02f, -0.42f },
|
||||
new[] { -2.4f, -0.04f, 0.56f },
|
||||
new[] { 0.52f, 0.04f, 2.48f },
|
||||
};
|
||||
|
||||
for (var index = 0; index < systems.Count && index < compactPositions.Length; index += 1)
|
||||
{
|
||||
systems[index].Position = compactPositions[index];
|
||||
}
|
||||
}
|
||||
|
||||
private static SolarSystemDefinition CreateGeneratedSystem(
|
||||
SolarSystemDefinition template,
|
||||
string label,
|
||||
@@ -260,30 +211,38 @@ public sealed class SystemGenerationService
|
||||
return system;
|
||||
}
|
||||
|
||||
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
||||
private static Vector3 BuildGeneratedSystemPosition(IReadOnlyCollection<Vector3> occupiedPositions, int generatedIndex)
|
||||
{
|
||||
var allPositions = occupiedPositions.ToList();
|
||||
var generated = new List<Vector3>(count);
|
||||
|
||||
for (var index = 0; index < count; index += 1)
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
{
|
||||
Vector3? accepted = null;
|
||||
for (var attempt = 0; attempt < 64; attempt += 1)
|
||||
var candidate = ComputeGeneratedSystemPosition(generatedIndex, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
var candidate = ComputeGeneratedSystemPosition(index, attempt);
|
||||
if (allPositions.All(existing => existing.DistanceTo(candidate) >= MinimumSystemSeparation))
|
||||
{
|
||||
accepted = candidate;
|
||||
break;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
accepted ??= ComputeFallbackGeneratedSystemPosition(index);
|
||||
generated.Add(accepted.Value);
|
||||
allPositions.Add(accepted.Value);
|
||||
}
|
||||
|
||||
return generated;
|
||||
return ComputeFallbackGeneratedSystemPosition(generatedIndex);
|
||||
}
|
||||
|
||||
private static bool ShouldUseKnownSystem(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
int slotIndex,
|
||||
int remainingKnownSystemCount)
|
||||
{
|
||||
if (!worldGenerationOptions.UseKnownSystems || remainingKnownSystemCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Hash01(worldGenerationOptions.Seed, 700 + slotIndex) >= KnownSystemSelectionChance;
|
||||
}
|
||||
|
||||
private static int SelectKnownSystemIndex(int seed, int slotIndex, int remainingKnownSystemCount)
|
||||
{
|
||||
var selection = Hash01(seed, 900 + slotIndex);
|
||||
return Math.Min((int)(selection * remainingKnownSystemCount), remainingKnownSystemCount - 1);
|
||||
}
|
||||
|
||||
private static Vector3 ComputeGeneratedSystemPosition(int generatedIndex, int attempt)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
internal static class SystemSelectionPolicy
|
||||
{
|
||||
internal static readonly string[] PreferredSystemIds =
|
||||
[
|
||||
"sol",
|
||||
"helios",
|
||||
];
|
||||
|
||||
internal static string SelectFallbackSystemId(IReadOnlyList<string> availableSystemIds)
|
||||
{
|
||||
foreach (var preferredSystemId in PreferredSystemIds)
|
||||
{
|
||||
if (availableSystemIds.Contains(preferredSystemId, StringComparer.Ordinal))
|
||||
{
|
||||
return preferredSystemId;
|
||||
}
|
||||
}
|
||||
|
||||
return availableSystemIds.FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
7
apps/backend/Universe/Scenario/WorldBuildTopology.cs
Normal file
7
apps/backend/Universe/Scenario/WorldBuildTopology.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed record WorldBuildTopology(
|
||||
IReadOnlyList<SolarSystemDefinition> Systems,
|
||||
IReadOnlyList<SystemRuntime> SystemRuntimes,
|
||||
IReadOnlyDictionary<string, SystemRuntime> SystemsById,
|
||||
ScenarioSpatialLayout SpatialLayout);
|
||||
@@ -1,383 +1,26 @@
|
||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldBuilder(
|
||||
StaticDataCatalog staticData,
|
||||
IOptions<BalanceOptions> balance,
|
||||
SystemGenerationService generationService,
|
||||
SpatialBuilder spatialBuilder,
|
||||
WorldSeedingService seedingService)
|
||||
WorldTopologyBuilder topologyBuilder,
|
||||
ScenarioValidationService scenarioValidationService,
|
||||
ScenarioContentBuilder contentBuilder,
|
||||
WorldRuntimeAssembler runtimeAssembler)
|
||||
{
|
||||
public SimulationWorld Build(
|
||||
GameStartOptionsDefinition gameStartOptions,
|
||||
public SimulationWorld BuildFromGeneration(WorldGenerationOptions worldGenerationOptions) =>
|
||||
BuildWorld(worldGenerationOptions, null);
|
||||
|
||||
public SimulationWorld BuildFromScenario(ScenarioDefinition scenarioDefinition) =>
|
||||
BuildWorld(scenarioDefinition.WorldGeneration, scenarioDefinition);
|
||||
|
||||
private SimulationWorld BuildWorld(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
ScenarioDefinition? scenarioDefinition)
|
||||
{
|
||||
var systems = generationService.ExpandSystems(
|
||||
generationService.PrepareAuthoredSystems(authoredSystems),
|
||||
gameStartOptions.WorldGeneration.TargetSystemCount);
|
||||
var topology = topologyBuilder.Build(worldGenerationOptions);
|
||||
var scenario = scenarioDefinition ?? scenarioValidationService.CreateEmptyScenario(worldGenerationOptions, topology.Systems);
|
||||
scenarioValidationService.Validate(scenario, topology.Systems.Select(system => system.Id).ToHashSet(StringComparer.Ordinal));
|
||||
|
||||
var scenario = NormalizeScenarioToAvailableSystems(
|
||||
scenarioDefinition,
|
||||
systems.Select(system => system.Id).ToList());
|
||||
|
||||
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 spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, balance.Value);
|
||||
|
||||
var stations = CreateStations(
|
||||
scenario,
|
||||
systemsById,
|
||||
spatialLayout.SystemGraphs,
|
||||
spatialLayout.Celestials,
|
||||
staticData.ModuleDefinitions,
|
||||
staticData.ItemDefinitions);
|
||||
|
||||
seedingService.InitializeStationStockpiles(stations, staticData.ModuleDefinitions);
|
||||
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
||||
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
||||
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, staticData.ShipDefinitions, patrolRoutes, stations, refinery);
|
||||
|
||||
if (gameStartOptions.WorldGeneration.AiControllerFactionCount < int.MaxValue)
|
||||
{
|
||||
var aiFactionIds = stations
|
||||
.Select(s => s.FactionId)
|
||||
.Concat(ships.Select(s => s.FactionId))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.Take(gameStartOptions.WorldGeneration.AiControllerFactionCount)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
aiFactionIds.Add(DefaultFactionId);
|
||||
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
}
|
||||
|
||||
var factions = seedingService.CreateFactions(stations, ships);
|
||||
seedingService.BootstrapFactionEconomy(factions, stations);
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var playerFaction = gameStartOptions.WorldGeneration.GeneratePlayerFaction
|
||||
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
||||
: null;
|
||||
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
||||
var world = new SimulationWorld
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = gameStartOptions.Seed,
|
||||
Systems = systemRuntimes,
|
||||
Celestials = spatialLayout.Celestials,
|
||||
Nodes = spatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = stations,
|
||||
Ships = ships,
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Geopolitics = null,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = [],
|
||||
MarketOrders = [],
|
||||
Policies = policies,
|
||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
||||
Recipes = new Dictionary<string, RecipeDefinition>(staticData.Recipes, StringComparer.Ordinal),
|
||||
ProductionGraph = staticData.ProductionGraph,
|
||||
OrbitalTimeSeconds = gameStartOptions.Seed * 97d,
|
||||
GeneratedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(world);
|
||||
world.ConstructionSites.AddRange(constructionSites);
|
||||
world.MarketOrders.AddRange(marketOrders);
|
||||
|
||||
var geopolitics = new GeopoliticalSimulationService();
|
||||
geopolitics.Update(world, 0f, []);
|
||||
return world;
|
||||
}
|
||||
|
||||
private static ScenarioDefinition NormalizeScenarioToAvailableSystems(
|
||||
ScenarioDefinition? scenario,
|
||||
IReadOnlyList<string> availableSystemIds)
|
||||
{
|
||||
var fallbackSystemId = SystemSelectionPolicy.SelectFallbackSystemId(availableSystemIds);
|
||||
|
||||
if (scenario is null)
|
||||
{
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
GameStartOptions = new GameStartOptionsDefinition(),
|
||||
InitialStations = [],
|
||||
ShipFormations = [],
|
||||
PatrolRoutes = [],
|
||||
MiningDefaults = new MiningDefaultsDefinition
|
||||
{
|
||||
NodeSystemId = fallbackSystemId,
|
||||
RefinerySystemId = fallbackSystemId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (availableSystemIds.Count == 0)
|
||||
{
|
||||
return scenario;
|
||||
}
|
||||
|
||||
string ResolveSystemId(string systemId) =>
|
||||
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
|
||||
|
||||
return new ScenarioDefinition
|
||||
{
|
||||
GameStartOptions = scenario.GameStartOptions,
|
||||
InitialStations = scenario.InitialStations
|
||||
.Select(station => new InitialStationDefinition
|
||||
{
|
||||
SystemId = ResolveSystemId(station.SystemId),
|
||||
Label = station.Label,
|
||||
Color = station.Color,
|
||||
Objective = station.Objective,
|
||||
StartingModules = station.StartingModules.ToList(),
|
||||
FactionId = station.FactionId,
|
||||
PlanetIndex = station.PlanetIndex,
|
||||
LagrangeSide = station.LagrangeSide,
|
||||
Position = station.Position?.ToArray(),
|
||||
})
|
||||
.ToList(),
|
||||
ShipFormations = scenario.ShipFormations
|
||||
.Select(formation => new ShipFormationDefinition
|
||||
{
|
||||
ShipId = formation.ShipId,
|
||||
Count = formation.Count,
|
||||
Center = formation.Center.ToArray(),
|
||||
SystemId = ResolveSystemId(formation.SystemId),
|
||||
FactionId = formation.FactionId,
|
||||
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
|
||||
})
|
||||
.ToList(),
|
||||
PatrolRoutes = scenario.PatrolRoutes
|
||||
.Select(route => new PatrolRouteDefinition
|
||||
{
|
||||
SystemId = ResolveSystemId(route.SystemId),
|
||||
Points = route.Points.Select(point => point.ToArray()).ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
MiningDefaults = new MiningDefaultsDefinition
|
||||
{
|
||||
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId),
|
||||
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static List<StationRuntime> CreateStations(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
var stationIdCounter = 0;
|
||||
|
||||
foreach (var plan in scenario.InitialStations)
|
||||
{
|
||||
if (!systemsById.TryGetValue(plan.SystemId, out var system))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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 = plan.FactionId ?? DefaultFactionId,
|
||||
CelestialId = placement.AnchorCelestial.Id,
|
||||
Health = 600f,
|
||||
MaxHealth = 600f,
|
||||
};
|
||||
|
||||
stations.Add(station);
|
||||
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
||||
|
||||
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
|
||||
|
||||
foreach (var moduleId in startingModules)
|
||||
{
|
||||
AddStationModule(station, moduleDefinitions, moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
return stations;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildStartingModules(
|
||||
InitialStationDefinition plan,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
var startingModules = new List<string>(plan.StartingModules.Count > 0
|
||||
? plan.StartingModules
|
||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
|
||||
|
||||
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
|
||||
|
||||
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
|
||||
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||
{
|
||||
EnsureStartingModule(startingModules, objectiveModuleId);
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
||||
{
|
||||
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
|
||||
}
|
||||
|
||||
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
|
||||
{
|
||||
EnsureStartingModule(startingModules, storageModuleId);
|
||||
}
|
||||
}
|
||||
|
||||
return startingModules;
|
||||
}
|
||||
|
||||
private static string? GetObjectiveStartingModuleId(string? objective) =>
|
||||
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_ref_ore_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static IEnumerable<string> GetRequiredStartingStorageModules(
|
||||
string moduleId,
|
||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||
{
|
||||
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(moduleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ShipDefinition> shipDefinitions,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
StationRuntime? refinery)
|
||||
{
|
||||
var ships = 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.Value.YPlane, (index / 3) * 18f);
|
||||
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
||||
|
||||
ships.Add(new ShipRuntime
|
||||
{
|
||||
Id = $"ship-{++shipIdCounter}",
|
||||
SystemId = formation.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = formation.FactionId ?? DefaultFactionId,
|
||||
Position = position,
|
||||
TargetPosition = position,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
|
||||
DefaultBehavior = WorldSeedingService.CreateBehavior(
|
||||
definition,
|
||||
formation.SystemId,
|
||||
formation.FactionId ?? DefaultFactionId,
|
||||
scenario,
|
||||
patrolRoutes,
|
||||
stations,
|
||||
refinery),
|
||||
Skills = WorldSeedingService.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
});
|
||||
|
||||
foreach (var (itemId, amount) in formation.StartingInventory)
|
||||
{
|
||||
if (amount > 0f)
|
||||
{
|
||||
ships[^1].Inventory[itemId] = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ships;
|
||||
var content = contentBuilder.Build(scenario, topology);
|
||||
return runtimeAssembler.Assemble(worldGenerationOptions, topology, content);
|
||||
}
|
||||
}
|
||||
|
||||
62
apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs
Normal file
62
apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldRuntimeAssembler(
|
||||
IStaticDataProvider staticData,
|
||||
WorldSeedingService seedingService)
|
||||
{
|
||||
public SimulationWorld Assemble(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
WorldBuildTopology topology,
|
||||
ScenarioWorldContent content)
|
||||
{
|
||||
seedingService.InitializeStationStockpiles(content.Stations, staticData.ModuleDefinitions);
|
||||
|
||||
var factions = seedingService.CreateFactions(content.Stations, content.Ships);
|
||||
seedingService.BootstrapFactionEconomy(factions, content.Stations);
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships);
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var playerFaction = worldGenerationOptions.GeneratePlayerFaction
|
||||
? seedingService.CreatePlayerFaction(factions, content.Stations, content.Ships, commanders, policies, nowUtc)
|
||||
: null;
|
||||
var claims = seedingService.CreateClaims(content.Stations, topology.SpatialLayout.Celestials, nowUtc);
|
||||
|
||||
var world = new SimulationWorld
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = worldGenerationOptions.Seed,
|
||||
Systems = topology.SystemRuntimes.ToList(),
|
||||
Celestials = topology.SpatialLayout.Celestials,
|
||||
Nodes = topology.SpatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = content.Stations.ToList(),
|
||||
Ships = content.Ships.ToList(),
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Geopolitics = null,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = [],
|
||||
MarketOrders = [],
|
||||
Policies = policies,
|
||||
ShipDefinitions = new Dictionary<string, ShipDefinition>(staticData.ShipDefinitions, StringComparer.Ordinal),
|
||||
ItemDefinitions = new Dictionary<string, ItemDefinition>(staticData.ItemDefinitions, StringComparer.Ordinal),
|
||||
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(staticData.ModuleDefinitions, StringComparer.Ordinal),
|
||||
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(staticData.ModuleRecipes, StringComparer.Ordinal),
|
||||
Recipes = new Dictionary<string, RecipeDefinition>(staticData.Recipes, StringComparer.Ordinal),
|
||||
ProductionGraph = staticData.ProductionGraph,
|
||||
OrbitalTimeSeconds = worldGenerationOptions.Seed * 97d,
|
||||
GeneratedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(world);
|
||||
world.ConstructionSites.AddRange(constructionSites);
|
||||
world.MarketOrders.AddRange(marketOrders);
|
||||
|
||||
var geopolitics = new GeopoliticalSimulationService();
|
||||
geopolitics.Update(world, 0f, []);
|
||||
return world;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldSeedingService
|
||||
public sealed class WorldSeedingService(IStaticDataProvider staticData)
|
||||
{
|
||||
internal List<FactionRuntime> CreateFactions(
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
@@ -18,7 +19,7 @@ public sealed class WorldSeedingService
|
||||
|
||||
if (factionIds.Count == 0)
|
||||
{
|
||||
factionIds.Add(DefaultFactionId);
|
||||
return [];
|
||||
}
|
||||
|
||||
return factionIds.Select(CreateFaction).ToList();
|
||||
@@ -70,15 +71,6 @@ public sealed class WorldSeedingService
|
||||
}
|
||||
}
|
||||
|
||||
internal StationRuntime? SelectRefineryStation(IReadOnlyCollection<StationRuntime> stations, ScenarioDefinition scenario)
|
||||
{
|
||||
return stations.FirstOrDefault(station =>
|
||||
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal) &&
|
||||
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault(station =>
|
||||
string.Equals(StationSimulationService.DetermineStationRole(station), "refinery", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
internal List<ClaimRuntime> CreateClaims(
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
@@ -183,39 +175,32 @@ public sealed class WorldSeedingService
|
||||
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var role = StationSimulationService.DetermineStationRole(station);
|
||||
var objectiveModuleId = role switch
|
||||
{
|
||||
"power" => "module_gen_prod_energycells_01",
|
||||
"refinery" => "module_gen_prod_refinedmetals_01",
|
||||
"graphene" => "module_gen_prod_graphene_01",
|
||||
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||
"hullparts" => "module_gen_prod_hullparts_01",
|
||||
"claytronics" => "module_gen_prod_claytronics_01",
|
||||
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||
"water" => "module_gen_prod_water_01",
|
||||
_ => null,
|
||||
};
|
||||
var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(role, station.FactionId, world.ModuleDefinitions);
|
||||
|
||||
if (objectiveModuleId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
|
||||
var requiredDockModuleId = StarterStationLayoutResolver.ResolveDockModuleId(station.FactionId, world.ModuleDefinitions);
|
||||
if (!station.InstalledModules.Contains(requiredDockModuleId, StringComparer.Ordinal)
|
||||
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
|
||||
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
||||
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 GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
|
||||
foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds(
|
||||
objectiveModuleId,
|
||||
station.FactionId,
|
||||
world.ModuleDefinitions,
|
||||
world.ItemDefinitions))
|
||||
{
|
||||
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -226,30 +211,6 @@ public sealed class WorldSeedingService
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
|
||||
{
|
||||
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var wareId in moduleDefinition.BuildRecipes
|
||||
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||
.Concat(moduleDefinition.ProductItemIds)
|
||||
.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport.GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind) is { } storageModuleId)
|
||||
{
|
||||
yield return storageModuleId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
||||
{
|
||||
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||
@@ -355,8 +316,8 @@ public sealed class WorldSeedingService
|
||||
IReadOnlyCollection<PolicySetRuntime> policies,
|
||||
DateTimeOffset nowUtc)
|
||||
{
|
||||
var sovereignFaction = factions.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
|
||||
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
|
||||
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
|
||||
{
|
||||
@@ -434,122 +395,55 @@ public sealed class WorldSeedingService
|
||||
return player;
|
||||
}
|
||||
|
||||
internal static DefaultBehaviorRuntime CreateBehavior(
|
||||
ShipDefinition definition,
|
||||
string systemId,
|
||||
string factionId,
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
StationRuntime? refinery)
|
||||
private FactionRuntime CreateFaction(string factionId)
|
||||
{
|
||||
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))
|
||||
?? refinery;
|
||||
|
||||
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null)
|
||||
if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
PreferredConstructionSiteId = null,
|
||||
};
|
||||
throw new InvalidOperationException($"Faction '{factionId}' is not defined in static data.");
|
||||
}
|
||||
|
||||
if (HasCapabilities(definition, "mining") && homeStation is not null)
|
||||
return new FactionRuntime
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "advanced-auto-trade",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
MaxSystemRange = 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && 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 = "idle",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
Id = definition.Id,
|
||||
Label = definition.Label,
|
||||
Color = ResolveFactionColor(definition),
|
||||
Credits = MinimumFactionCredits,
|
||||
};
|
||||
}
|
||||
|
||||
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
|
||||
{
|
||||
return definition.Kind switch
|
||||
{
|
||||
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
|
||||
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
|
||||
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
|
||||
_ when HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
|
||||
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
|
||||
};
|
||||
}
|
||||
|
||||
private static FactionRuntime CreateFaction(string factionId)
|
||||
{
|
||||
return factionId switch
|
||||
{
|
||||
DefaultFactionId => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Sol Dominion",
|
||||
Color = "#7ed4ff",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
"asterion-league" => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Asterion League",
|
||||
Color = "#ff8f70",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
"nadir-syndicate" => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = "Nadir Syndicate",
|
||||
Color = "#91e6a8",
|
||||
Credits = MinimumFactionCredits,
|
||||
},
|
||||
_ => new FactionRuntime
|
||||
{
|
||||
Id = factionId,
|
||||
Label = ToFactionLabel(factionId),
|
||||
Color = "#c7d2e0",
|
||||
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,
|
||||
@@ -563,12 +457,4 @@ public sealed class WorldSeedingService
|
||||
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
|
||||
}
|
||||
|
||||
private static string ToFactionLabel(string factionId)
|
||||
{
|
||||
return string.Join(" ",
|
||||
factionId
|
||||
.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
29
apps/backend/Universe/Scenario/WorldTopologyBuilder.cs
Normal file
29
apps/backend/Universe/Scenario/WorldTopologyBuilder.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
public sealed class WorldTopologyBuilder(
|
||||
IStaticDataProvider staticData,
|
||||
SystemGenerationService generationService,
|
||||
SpatialBuilder spatialBuilder)
|
||||
{
|
||||
public WorldBuildTopology Build(WorldGenerationOptions worldGenerationOptions)
|
||||
{
|
||||
var systems = generationService.GenerateSystems(
|
||||
generationService.PrepareKnownSystems(staticData.KnownSystems),
|
||||
worldGenerationOptions);
|
||||
|
||||
var systemRuntimes = systems
|
||||
.Select(definition => new SystemRuntime
|
||||
{
|
||||
Definition = definition,
|
||||
Position = LoaderSupport.ToVector(definition.Position),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
|
||||
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes);
|
||||
|
||||
return new WorldBuildTopology(systems, systemRuntimes, systemsById, spatialLayout);
|
||||
}
|
||||
}
|
||||
165
apps/backend/Universe/Simulation/BalanceService.cs
Normal file
165
apps/backend/Universe/Simulation/BalanceService.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class BalanceService : IBalanceService
|
||||
{
|
||||
private readonly Lock _sync = new();
|
||||
private BalanceOptions _current;
|
||||
|
||||
public BalanceService(IOptions<BalanceOptions> defaults)
|
||||
{
|
||||
_current = Sanitize(defaults.Value);
|
||||
}
|
||||
|
||||
public float SimulationSpeedMultiplier
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.SimulationSpeedMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float YPlane
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.YPlane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float ArrivalThreshold
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.ArrivalThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float MiningRate
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.MiningRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float MiningCycleSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.MiningCycleSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float TransferRate
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.TransferRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float DockingDuration
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.DockingDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float UndockingDuration
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.UndockingDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float UndockDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _current.UndockDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions GetCurrent()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return Clone(_current);
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions Update(BalanceOptions candidate)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_current = Sanitize(candidate);
|
||||
return Clone(_current);
|
||||
}
|
||||
}
|
||||
|
||||
private static BalanceOptions Clone(BalanceOptions value)
|
||||
{
|
||||
return new BalanceOptions
|
||||
{
|
||||
SimulationSpeedMultiplier = value.SimulationSpeedMultiplier,
|
||||
YPlane = value.YPlane,
|
||||
ArrivalThreshold = value.ArrivalThreshold,
|
||||
MiningRate = value.MiningRate,
|
||||
MiningCycleSeconds = value.MiningCycleSeconds,
|
||||
TransferRate = value.TransferRate,
|
||||
DockingDuration = value.DockingDuration,
|
||||
UndockingDuration = value.UndockingDuration,
|
||||
UndockDistance = value.UndockDistance,
|
||||
};
|
||||
}
|
||||
|
||||
private static BalanceOptions Sanitize(BalanceOptions candidate)
|
||||
{
|
||||
static float finiteOr(float value, float fallback) =>
|
||||
float.IsFinite(value) ? value : fallback;
|
||||
|
||||
return new BalanceOptions
|
||||
{
|
||||
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
|
||||
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
|
||||
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
|
||||
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
|
||||
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
|
||||
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
|
||||
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
16
apps/backend/Universe/Simulation/IBalanceService.cs
Normal file
16
apps/backend/Universe/Simulation/IBalanceService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public interface IBalanceService
|
||||
{
|
||||
float SimulationSpeedMultiplier { get; }
|
||||
float YPlane { get; }
|
||||
float ArrivalThreshold { get; }
|
||||
float MiningRate { get; }
|
||||
float MiningCycleSeconds { get; }
|
||||
float TransferRate { get; }
|
||||
float DockingDuration { get; }
|
||||
float UndockingDuration { get; }
|
||||
float UndockDistance { get; }
|
||||
BalanceOptions GetCurrent();
|
||||
BalanceOptions Update(BalanceOptions candidate);
|
||||
}
|
||||
13
apps/backend/Universe/Simulation/ITimeService.cs
Normal file
13
apps/backend/Universe/Simulation/ITimeService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public interface ITimeService
|
||||
{
|
||||
int TickIntervalMs { get; }
|
||||
TimeSpan TickInterval { get; }
|
||||
float TickDeltaSeconds { get; }
|
||||
double SimulatedSecondsPerRealSecond { get; }
|
||||
DateTimeOffset UtcNow();
|
||||
float ScaleSimulationDelta(float realDeltaSeconds);
|
||||
double ScaleOrbitalDelta(float simulationDeltaSeconds);
|
||||
double CreateInitialOrbitalTimeSeconds(int seed);
|
||||
}
|
||||
23
apps/backend/Universe/Simulation/TimeService.cs
Normal file
23
apps/backend/Universe/Simulation/TimeService.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class TimeService(
|
||||
IBalanceService balance,
|
||||
IOptions<OrbitalSimulationOptions> orbitalSimulation) : ITimeService
|
||||
{
|
||||
public int TickIntervalMs => 200;
|
||||
public TimeSpan TickInterval => TimeSpan.FromMilliseconds(TickIntervalMs);
|
||||
public float TickDeltaSeconds => TickIntervalMs / 1000f;
|
||||
public double SimulatedSecondsPerRealSecond => orbitalSimulation.Value.SimulatedSecondsPerRealSecond;
|
||||
|
||||
public DateTimeOffset UtcNow() => DateTimeOffset.UtcNow;
|
||||
|
||||
public float ScaleSimulationDelta(float realDeltaSeconds) =>
|
||||
realDeltaSeconds * MathF.Max(balance.SimulationSpeedMultiplier, 0.01f);
|
||||
|
||||
public double ScaleOrbitalDelta(float simulationDeltaSeconds) =>
|
||||
simulationDeltaSeconds * SimulatedSecondsPerRealSecond;
|
||||
|
||||
public double CreateInitialOrbitalTimeSeconds(int seed) => seed * 97d;
|
||||
}
|
||||
@@ -2,7 +2,9 @@ namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class WorldGenerationOptions
|
||||
{
|
||||
public int Seed { get; init; }
|
||||
public int TargetSystemCount { get; init; }
|
||||
public bool UseKnownSystems { get; init; }
|
||||
public int AiControllerFactionCount { get; init; }
|
||||
public bool GeneratePlayerFaction { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,26 +1,57 @@
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SpaceGame.Api.Universe.Bootstrap;
|
||||
using SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class WorldService(
|
||||
WorldBootstrapper worldBootstrapper,
|
||||
IOptions<BalanceOptions> balance,
|
||||
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
||||
public sealed class WorldService
|
||||
{
|
||||
private const int DeltaHistoryLimit = 256;
|
||||
|
||||
private readonly Lock _sync = new();
|
||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||
private readonly WorldBootstrapper _bootstrapper = worldBootstrapper;
|
||||
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation;
|
||||
private readonly SimulationEngine _engine;
|
||||
private readonly ScenarioLoader _scenarioLoader;
|
||||
private readonly WorldBuilder _worldBuilder;
|
||||
private readonly PlayerFactionService _playerFaction = new();
|
||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||
private readonly Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = worldBootstrapper.Bootstrap();
|
||||
private SimulationWorld _world = null!;
|
||||
private string? _currentScenarioPath;
|
||||
private WorldGenerationOptions? _currentWorldGenerationOptions;
|
||||
private long _sequence;
|
||||
private BalanceOptions? _balanceOverride;
|
||||
|
||||
public WorldService(
|
||||
ScenarioLoader scenarioLoader,
|
||||
WorldBuilder worldBuilder,
|
||||
IBalanceService balance,
|
||||
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
|
||||
{
|
||||
_orbitalSimulation = new OrbitalSimulationSnapshot(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||
_scenarioLoader = scenarioLoader;
|
||||
_worldBuilder = worldBuilder;
|
||||
_engine = new SimulationEngine(orbitalSimulationOptions.Value, balance);
|
||||
}
|
||||
|
||||
public void New(WorldGenerationOptions options)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_currentScenarioPath = null;
|
||||
_currentWorldGenerationOptions = options;
|
||||
ReplaceWorldUnsafe(_worldBuilder.BuildFromGeneration(options), "new", "Generated new world");
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadFromScenario(string scenarioPath)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_currentScenarioPath = scenarioPath;
|
||||
_currentWorldGenerationOptions = null;
|
||||
ReplaceWorldUnsafe(_worldBuilder.BuildFromScenario(_scenarioLoader.Load(scenarioPath)), "load-scenario", $"Loaded scenario {scenarioPath}");
|
||||
}
|
||||
}
|
||||
|
||||
public WorldSnapshot GetSnapshot()
|
||||
{
|
||||
@@ -46,35 +77,6 @@ public sealed class WorldService(
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions GetBalance()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return new BalanceOptions
|
||||
{
|
||||
SimulationSpeedMultiplier = balance.Value.SimulationSpeedMultiplier,
|
||||
YPlane = balance.Value.YPlane,
|
||||
ArrivalThreshold = balance.Value.ArrivalThreshold,
|
||||
MiningRate = balance.Value.MiningRate,
|
||||
MiningCycleSeconds = balance.Value.MiningCycleSeconds,
|
||||
TransferRate = balance.Value.TransferRate,
|
||||
DockingDuration = balance.Value.DockingDuration,
|
||||
UndockingDuration = balance.Value.UndockingDuration,
|
||||
UndockDistance = balance.Value.UndockDistance,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public BalanceOptions UpdateBalance(BalanceOptions balance)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_balanceOverride = SanitizeBalance(balance);
|
||||
ApplyBalance(_balanceOverride);
|
||||
return GetBalance();
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -121,6 +123,11 @@ public sealed class WorldService(
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_world.PlayerFaction is null && _world.Factions.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_playerFaction.EnsureDomain(_world);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
@@ -285,74 +292,61 @@ public sealed class WorldService(
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_world = _bootstrapper.Bootstrap();
|
||||
if (_balanceOverride is not null)
|
||||
if (_currentScenarioPath is not null)
|
||||
{
|
||||
ApplyBalance(_balanceOverride);
|
||||
ReplaceWorldUnsafe(
|
||||
_worldBuilder.BuildFromScenario(_scenarioLoader.Load(_currentScenarioPath)),
|
||||
"reset",
|
||||
"World reset requested");
|
||||
}
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
var resetDelta = new WorldDelta(
|
||||
_sequence,
|
||||
_world.TickIntervalMs,
|
||||
_world.OrbitalTimeSeconds,
|
||||
_orbitalSimulation,
|
||||
DateTimeOffset.UtcNow,
|
||||
true,
|
||||
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow, "world", "universe", "world")],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null);
|
||||
|
||||
_history.Enqueue(resetDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
else if (_currentWorldGenerationOptions is not null)
|
||||
{
|
||||
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
|
||||
ReplaceWorldUnsafe(
|
||||
_worldBuilder.BuildFromGeneration(_currentWorldGenerationOptions),
|
||||
"reset",
|
||||
"World reset requested");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("No world source is configured.");
|
||||
}
|
||||
|
||||
return _engine.BuildSnapshot(_world, _sequence);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBalance(BalanceOptions value)
|
||||
private void ReplaceWorldUnsafe(SimulationWorld world, string eventKind, string eventMessage)
|
||||
{
|
||||
balance.Value.SimulationSpeedMultiplier = value.SimulationSpeedMultiplier;
|
||||
balance.Value.YPlane = value.YPlane;
|
||||
balance.Value.ArrivalThreshold = value.ArrivalThreshold;
|
||||
balance.Value.MiningRate = value.MiningRate;
|
||||
balance.Value.MiningCycleSeconds = value.MiningCycleSeconds;
|
||||
balance.Value.TransferRate = value.TransferRate;
|
||||
balance.Value.DockingDuration = value.DockingDuration;
|
||||
balance.Value.UndockingDuration = value.UndockingDuration;
|
||||
balance.Value.UndockDistance = value.UndockDistance;
|
||||
}
|
||||
_world = world;
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
private static BalanceOptions SanitizeBalance(BalanceOptions candidate)
|
||||
{
|
||||
static float finiteOr(float value, float fallback) =>
|
||||
float.IsFinite(value) ? value : fallback;
|
||||
var eventTime = DateTimeOffset.UtcNow;
|
||||
var worldDelta = new WorldDelta(
|
||||
_sequence,
|
||||
_world.TickIntervalMs,
|
||||
_world.OrbitalTimeSeconds,
|
||||
_orbitalSimulation,
|
||||
eventTime,
|
||||
true,
|
||||
[new SimulationEventRecord("world", "world", eventKind, eventMessage, eventTime, "world", "universe", "world")],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null);
|
||||
|
||||
return new BalanceOptions
|
||||
_history.Enqueue(worldDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
{
|
||||
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
|
||||
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
|
||||
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
|
||||
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
|
||||
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
|
||||
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
|
||||
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
|
||||
};
|
||||
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(worldDelta, subscriber.Scope));
|
||||
}
|
||||
}
|
||||
|
||||
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"WorldGeneration": {
|
||||
"TargetSystemCount": 2,
|
||||
"IncludeSolSystem": true,
|
||||
"UseKnownSystems": true,
|
||||
"AiControllerFactionCount": 0,
|
||||
"GeneratePlayerFaction": false
|
||||
},
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"StaticData": {
|
||||
"DataRoot": "../../shared/data/"
|
||||
},
|
||||
"WorldGeneration": {
|
||||
"TargetSystemCount": 160,
|
||||
"IncludeSolSystem": true
|
||||
"UseKnownSystems": true
|
||||
},
|
||||
"Balance": {
|
||||
"SimulationSpeedMultiplier": 1.5,
|
||||
|
||||
18
shared/data/cargo-types.json
Normal file
18
shared/data/cargo-types.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"id": "liquid",
|
||||
"name": "Liquid"
|
||||
},
|
||||
{
|
||||
"id": "solid",
|
||||
"name": "Solid"
|
||||
},
|
||||
{
|
||||
"id": "container",
|
||||
"name": "Container"
|
||||
},
|
||||
{
|
||||
"id": "condensate",
|
||||
"name": "Condensate"
|
||||
}
|
||||
]
|
||||
4
shared/data/effects.json
Normal file
4
shared/data/effects.json
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"work",
|
||||
"sunlight"
|
||||
]
|
||||
14
shared/data/equipment-classes.json
Normal file
14
shared/data/equipment-classes.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
"countermeasure",
|
||||
"weapon",
|
||||
"missileturret",
|
||||
"turret",
|
||||
"engine",
|
||||
"missile",
|
||||
"shieldgenerator",
|
||||
"scanner",
|
||||
"mine",
|
||||
"missilelauncher",
|
||||
"ship_s",
|
||||
"ship_xs"
|
||||
]
|
||||
11
shared/data/equipment-types.json
Normal file
11
shared/data/equipment-types.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
"countermeasures",
|
||||
"drones",
|
||||
"engines",
|
||||
"missiles",
|
||||
"shields",
|
||||
"software",
|
||||
"thrusters",
|
||||
"turrets",
|
||||
"weapons"
|
||||
]
|
||||
24596
shared/data/equipment.json
Normal file
24596
shared/data/equipment.json
Normal file
File diff suppressed because it is too large
Load Diff
2136
shared/data/factions.json
Normal file
2136
shared/data/factions.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
26
shared/data/module-types.json
Normal file
26
shared/data/module-types.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"ModuleTypes": [
|
||||
"connectionmodule",
|
||||
"production",
|
||||
"defencemodule",
|
||||
"dockarea",
|
||||
"habitation",
|
||||
"pier",
|
||||
"storage",
|
||||
"buildmodule",
|
||||
"ventureplatform",
|
||||
"processingmodule",
|
||||
"recycling"
|
||||
],
|
||||
"AllModuleTypes": [
|
||||
"habitation",
|
||||
"buildmodule",
|
||||
"dockarea",
|
||||
"pier",
|
||||
"storage",
|
||||
"defencemodule",
|
||||
"connectionmodule",
|
||||
"processingmodule",
|
||||
"recycling"
|
||||
]
|
||||
}
|
||||
14048
shared/data/modules.json
14048
shared/data/modules.json
File diff suppressed because it is too large
Load Diff
7
shared/data/production-methods.json
Normal file
7
shared/data/production-methods.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"default",
|
||||
"argon",
|
||||
"teladi",
|
||||
"paranid",
|
||||
"recycling"
|
||||
]
|
||||
55
shared/data/races.json
Normal file
55
shared/data/races.json
Normal file
@@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"id": "argon",
|
||||
"name": "Argon",
|
||||
"description": "The descendents of Terran colonists stranded from Earth centuries ago, the Argon became their own thriving civilisation covering a great many systems and forging relations with several alien races. Throughout their short history the Argon Federation has been plagued by war, notably with the Xenon. Their greatest challenge however came from the unlikely source of the reconnected Terrans of Earth where they were plunged into the costly Terran Conflict.",
|
||||
"icon": "race_argon"
|
||||
},
|
||||
{
|
||||
"id": "boron",
|
||||
"name": "Boron",
|
||||
"description": "The predominantly peaceful Boron are aquatic life-forms from the planet Nishala. While initially pacifist, the discovery of their world by the Split forced them to invent defences and adapt to war. Enjoying a close relationship with the Argon, the Boron remain a wise and measured people.",
|
||||
"icon": "race_boron"
|
||||
},
|
||||
{
|
||||
"id": "drone",
|
||||
"name": "Drone",
|
||||
"description": "Drones are designed to specialise in a narrow field of tasks. With AI research outlawed to avoid a similar situation to the Terraformer-Xenon evolution, drones are limited in scope and capability. However, results from Xenon research have led to advancements in drone technology, something which troubled many experts."
|
||||
},
|
||||
{
|
||||
"id": "khaak",
|
||||
"name": "Kha'ak",
|
||||
"description": "Thought to have been wiped out during Operation Final Fury, very little is known about the Kha'ak other than they seem to be an insectile hive race hell-bent on the destruction of all those that share the Jump Gate network. As a hive race, it is suspected that individual intelligence gives way to a communal or caste mentality, but very little research into the species was completed before Operation Final Fury took place.",
|
||||
"icon": "race_khaak"
|
||||
},
|
||||
{
|
||||
"id": "paranid",
|
||||
"name": "Paranid",
|
||||
"description": "The physically imposing Paranid are often regarded as arrogant by several races which usually stems from their exceptional mathematic skills and religious fervour. Allied with the Split and distrusting of the Argon, the Paranid have been in several conflicts where they use their technological prowess and multilevel thinking to gain tactical advantages.",
|
||||
"icon": "race_paranid"
|
||||
},
|
||||
{
|
||||
"id": "split",
|
||||
"name": "Split",
|
||||
"description": "The aggressive Split live in a society constantly changing leadership where challenging factions rise up to impose a new Patriarch. Their short temper and fiery disposition puts them at odds with other races which has sometimes lead to war, notably with the Boron and Argon.",
|
||||
"icon": "race_split"
|
||||
},
|
||||
{
|
||||
"id": "teladi",
|
||||
"name": "Teladi",
|
||||
"description": "The lizard-like Teladi are one of the founding members of the Community of Planets and have a natural affinity towards business and the accumulation of profit. They enjoy favourable relations with other races although some find their drive for profit disconcerting. Their long lifespan gives them a unique view of the Jump Gate shutdown, as does their previous experience being cut off from their home system of Ianamus Zura.",
|
||||
"icon": "race_teladi"
|
||||
},
|
||||
{
|
||||
"id": "terran",
|
||||
"name": "Terran",
|
||||
"description": "The Terrans of the Solar System have a long history of spaceflight and exploring the Jump Gate network. After the events of the Terraformers over Earth, the Terrans severed their contact with the rest of the galaxy and had several centuries of rebuilding and advancement in isolation. Their brief return led to the Terran Conflict which preceded the mass disconnection of Jump Gates. It is unknown if the war precipitated this event.",
|
||||
"icon": "race_terran"
|
||||
},
|
||||
{
|
||||
"id": "xenon",
|
||||
"name": "Xenon",
|
||||
"description": "The Xenon are a mechanical race resulting from past Terran terraformer ships which eventually evolved intelligence. A constant threat in many areas of the galaxy, it is thought that the Jump Gate shutdown may stem their movements but given their disregard of time it is possible they may simply travel between stars. The Xenon have no known allies and communication with them is often relegated to folklore.",
|
||||
"icon": "race_xenon"
|
||||
}
|
||||
]
|
||||
12
shared/data/scenarios/empty.json
Normal file
12
shared/data/scenarios/empty.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"worldGeneration": {
|
||||
"seed": 8,
|
||||
"targetSystemCount": 2,
|
||||
"useKnownSystems": true,
|
||||
"aiControllerFactionCount": 0,
|
||||
"generatePlayerFaction": false
|
||||
},
|
||||
"initialStations": [],
|
||||
"shipFormations": [],
|
||||
"patrolRoutes": []
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"gameStartOptions": {
|
||||
"seed": 1,
|
||||
"worldGeneration": {
|
||||
"targetSystemCount": 2,
|
||||
"aiControllerFactionCount": 0,
|
||||
"generatePlayerFaction": false
|
||||
}
|
||||
"worldGeneration": {
|
||||
"seed": 8,
|
||||
"targetSystemCount": 2,
|
||||
"useKnownSystems": true,
|
||||
"aiControllerFactionCount": 0,
|
||||
"generatePlayerFaction": false
|
||||
},
|
||||
"initialStations": [
|
||||
{
|
||||
@@ -19,7 +18,7 @@
|
||||
"module_gen_prod_energycells_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -35,7 +34,7 @@
|
||||
"module_gen_prod_refinedmetals_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -50,7 +49,7 @@
|
||||
"module_gen_prod_hullparts_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -65,7 +64,7 @@
|
||||
"module_gen_prod_claytronics_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
@@ -80,7 +79,7 @@
|
||||
"module_gen_prod_quantumtubes_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
@@ -96,7 +95,7 @@
|
||||
"module_gen_prod_graphene_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -112,7 +111,7 @@
|
||||
"module_gen_prod_siliconwafers_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -128,7 +127,7 @@
|
||||
"module_gen_prod_antimattercells_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -144,7 +143,7 @@
|
||||
"module_gen_prod_superfluidcoolant_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -160,7 +159,7 @@
|
||||
"module_gen_prod_water_01"
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion",
|
||||
"factionId": "terran",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -175,7 +174,7 @@
|
||||
"module_gen_prod_energycells_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -191,7 +190,7 @@
|
||||
"module_gen_prod_refinedmetals_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -206,7 +205,7 @@
|
||||
"module_gen_prod_hullparts_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -221,7 +220,7 @@
|
||||
"module_gen_prod_claytronics_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
@@ -236,7 +235,7 @@
|
||||
"module_gen_prod_quantumtubes_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
@@ -252,7 +251,7 @@
|
||||
"module_gen_prod_graphene_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -268,7 +267,7 @@
|
||||
"module_gen_prod_siliconwafers_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
@@ -284,7 +283,7 @@
|
||||
"module_gen_prod_antimattercells_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -300,7 +299,7 @@
|
||||
"module_gen_prod_superfluidcoolant_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
@@ -316,256 +315,111 @@
|
||||
"module_gen_prod_water_01"
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Power Relay",
|
||||
"color": "#91e6a8",
|
||||
"objective": "power",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_liquid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Refinery",
|
||||
"color": "#91e6a8",
|
||||
"objective": "refinery",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_solid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_refinedmetals_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Hullworks",
|
||||
"color": "#91e6a8",
|
||||
"objective": "hullparts",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_hullparts_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Clay Grid",
|
||||
"color": "#91e6a8",
|
||||
"objective": "claytronics",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_claytronics_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Quantum Yard",
|
||||
"color": "#91e6a8",
|
||||
"objective": "quantumtubes",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_quantumtubes_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": null
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Graphene Array",
|
||||
"color": "#91e6a8",
|
||||
"objective": "graphene",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_liquid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_graphene_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 0,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Wafer Foundry",
|
||||
"color": "#91e6a8",
|
||||
"objective": "siliconwafers",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_solid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_siliconwafers_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 1,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Antimatter Forge",
|
||||
"color": "#91e6a8",
|
||||
"objective": "antimattercells",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_liquid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_antimattercells_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 2,
|
||||
"lagrangeSide": 1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Coolant Loop",
|
||||
"color": "#91e6a8",
|
||||
"objective": "superfluidcoolant",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_liquid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_superfluidcoolant_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": -1
|
||||
},
|
||||
{
|
||||
"label": "Syndicate Hydro Plant",
|
||||
"color": "#91e6a8",
|
||||
"objective": "water",
|
||||
"startingModules": [
|
||||
"module_arg_dock_m_01_lowtech",
|
||||
"module_arg_stor_solid_m_01",
|
||||
"module_arg_stor_container_m_01",
|
||||
"module_gen_prod_energycells_01",
|
||||
"module_gen_prod_water_01"
|
||||
],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate",
|
||||
"factionId": "argon",
|
||||
"planetIndex": 3,
|
||||
"lagrangeSide": 1
|
||||
}
|
||||
],
|
||||
"shipFormations": [
|
||||
{
|
||||
"shipId": "constructor",
|
||||
"count": 1,
|
||||
"center": [ 40, 0, 12 ],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion"
|
||||
},
|
||||
{
|
||||
"shipId": "miner",
|
||||
"count": 4,
|
||||
"center": [ 54, 0, 18 ],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion"
|
||||
},
|
||||
{
|
||||
"shipId": "hauler",
|
||||
"count": 1,
|
||||
"center": [ 62, 0, 8 ],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion"
|
||||
},
|
||||
{
|
||||
"shipId": "gas-miner",
|
||||
"count": 4,
|
||||
"center": [ 74, 0, 14 ],
|
||||
"systemId": "helios",
|
||||
"factionId": "sol-dominion"
|
||||
},
|
||||
{
|
||||
"shipId": "constructor",
|
||||
"count": 1,
|
||||
"center": [ 42, 0, -16 ],
|
||||
"shipId": "ship_arg_s_fighter_01_a",
|
||||
"count": 6,
|
||||
"center": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league"
|
||||
"factionId": "argon"
|
||||
},
|
||||
{
|
||||
"shipId": "miner",
|
||||
"shipId": "ship_ter_s_fighter_01_a",
|
||||
"count": 6,
|
||||
"center": [
|
||||
200,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "terran"
|
||||
},
|
||||
{
|
||||
"shipId": "ship_arg_s_miner_solid_01_a",
|
||||
"count": 4,
|
||||
"center": [ 56, 0, -12 ],
|
||||
"center": [
|
||||
1200,
|
||||
0,
|
||||
-600
|
||||
],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league"
|
||||
"factionId": "argon"
|
||||
},
|
||||
{
|
||||
"shipId": "hauler",
|
||||
"count": 1,
|
||||
"center": [ 68, 0, -18 ],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league"
|
||||
},
|
||||
{
|
||||
"shipId": "gas-miner",
|
||||
"shipId": "ship_ter_s_miner_solid_01_a",
|
||||
"count": 4,
|
||||
"center": [ 76, 0, -22 ],
|
||||
"systemId": "sol",
|
||||
"factionId": "asterion-league"
|
||||
},
|
||||
{
|
||||
"shipId": "constructor",
|
||||
"count": 1,
|
||||
"center": [ 44, 0, 20 ],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate"
|
||||
},
|
||||
{
|
||||
"shipId": "miner",
|
||||
"count": 4,
|
||||
"center": [ 58, 0, 24 ],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate"
|
||||
},
|
||||
{
|
||||
"shipId": "hauler",
|
||||
"count": 1,
|
||||
"center": [ 68, 0, 18 ],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate"
|
||||
},
|
||||
{
|
||||
"shipId": "gas-miner",
|
||||
"count": 4,
|
||||
"center": [ 78, 0, 22 ],
|
||||
"systemId": "perseus",
|
||||
"factionId": "nadir-syndicate"
|
||||
"center": [
|
||||
-1200,
|
||||
0,
|
||||
600
|
||||
],
|
||||
"systemId": "helios",
|
||||
"factionId": "terran"
|
||||
}
|
||||
],
|
||||
"patrolRoutes": [
|
||||
{
|
||||
"systemId": "sol",
|
||||
"points": [
|
||||
[
|
||||
-1800,
|
||||
0,
|
||||
-1600
|
||||
],
|
||||
[
|
||||
1800,
|
||||
0,
|
||||
-1600
|
||||
],
|
||||
[
|
||||
1800,
|
||||
0,
|
||||
1600
|
||||
],
|
||||
[
|
||||
-1800,
|
||||
0,
|
||||
1600
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"systemId": "helios",
|
||||
"points": [
|
||||
[
|
||||
-1800,
|
||||
0,
|
||||
-1600
|
||||
],
|
||||
[
|
||||
1800,
|
||||
0,
|
||||
-1600
|
||||
],
|
||||
[
|
||||
1800,
|
||||
0,
|
||||
1600
|
||||
],
|
||||
[
|
||||
-1800,
|
||||
0,
|
||||
1600
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"patrolRoutes": [],
|
||||
"miningDefaults": {
|
||||
"nodeSystemId": "perseus",
|
||||
"refinerySystemId": "helios"
|
||||
"nodeSystemId": "sol",
|
||||
"refinerySystemId": "sol"
|
||||
}
|
||||
}
|
||||
9
shared/data/ship-purposes.json
Normal file
9
shared/data/ship-purposes.json
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
"auxiliary",
|
||||
"mine",
|
||||
"build",
|
||||
"fight",
|
||||
"trade",
|
||||
"salvage",
|
||||
"dismantling"
|
||||
]
|
||||
24
shared/data/ship-types.json
Normal file
24
shared/data/ship-types.json
Normal file
@@ -0,0 +1,24 @@
|
||||
[
|
||||
"resupplier",
|
||||
"miner",
|
||||
"carrier",
|
||||
"fighter",
|
||||
"heavyfighter",
|
||||
"destroyer",
|
||||
"largeminer",
|
||||
"freighter",
|
||||
"bomber",
|
||||
"scavenger",
|
||||
"frigate",
|
||||
"transporter",
|
||||
"interceptor",
|
||||
"scout",
|
||||
"courier",
|
||||
"builder",
|
||||
"corvette",
|
||||
"police",
|
||||
"battleship",
|
||||
"gunboat",
|
||||
"tug",
|
||||
"compactor"
|
||||
]
|
||||
0
shared/data/ships-data.ts
Normal file
0
shared/data/ships-data.ts
Normal file
39837
shared/data/ships.json
39837
shared/data/ships.json
File diff suppressed because it is too large
Load Diff
7
shared/data/sizes.json
Normal file
7
shared/data/sizes.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"extrasmall",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"extralarge"
|
||||
]
|
||||
5
shared/data/transport-types.json
Normal file
5
shared/data/transport-types.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"container",
|
||||
"liquid",
|
||||
"solid"
|
||||
]
|
||||
5
shared/data/turret-types.json
Normal file
5
shared/data/turret-types.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"standard",
|
||||
"missile",
|
||||
"mining"
|
||||
]
|
||||
76
shared/data/ware-groups.json
Normal file
76
shared/data/ware-groups.json
Normal file
@@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"id": "agricultural",
|
||||
"name": "Agricultural Goods",
|
||||
"factoryName": "Agricultural Goods Factory",
|
||||
"icon": "be_upgrade_agricultural",
|
||||
"tier": 5
|
||||
},
|
||||
{
|
||||
"id": "energy",
|
||||
"name": "Energy",
|
||||
"factoryName": "Energy Complex",
|
||||
"icon": "be_upgrade_energy",
|
||||
"tier": 1
|
||||
},
|
||||
{
|
||||
"id": "food",
|
||||
"name": "Food",
|
||||
"factoryName": "Farm",
|
||||
"icon": "be_upgrade_food",
|
||||
"tier": 6
|
||||
},
|
||||
{
|
||||
"id": "gases",
|
||||
"name": "Gases",
|
||||
"factoryName": "Gas Refinery",
|
||||
"icon": "be_upgrade_refined"
|
||||
},
|
||||
{
|
||||
"id": "hightech",
|
||||
"name": "High Tech Goods",
|
||||
"factoryName": "High Tech Factory",
|
||||
"icon": "be_upgrade_hightech",
|
||||
"tier": 3
|
||||
},
|
||||
{
|
||||
"id": "ice",
|
||||
"name": "Ice",
|
||||
"factoryName": "Ice Refinery",
|
||||
"icon": "be_upgrade_water"
|
||||
},
|
||||
{
|
||||
"id": "minerals",
|
||||
"name": "Minerals",
|
||||
"factoryName": "Mineral Refinery",
|
||||
"icon": "be_upgrade_refined"
|
||||
},
|
||||
{
|
||||
"id": "pharmaceutical",
|
||||
"name": "Pharmaceutical Goods",
|
||||
"factoryName": "Pharmaceutical Goods Factory",
|
||||
"icon": "be_upgrade_pharmaceutical",
|
||||
"tier": 7
|
||||
},
|
||||
{
|
||||
"id": "refined",
|
||||
"name": "Refined Goods",
|
||||
"factoryName": "Refined Goods Complex",
|
||||
"icon": "be_upgrade_refined",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"id": "shiptech",
|
||||
"name": "Ship Technology",
|
||||
"factoryName": "Ship Technology Factory",
|
||||
"icon": "be_upgrade_shiptech",
|
||||
"tier": 4
|
||||
},
|
||||
{
|
||||
"id": "water",
|
||||
"name": "Water",
|
||||
"factoryName": "Water Refinery",
|
||||
"icon": "be_upgrade_water",
|
||||
"tier": 2
|
||||
}
|
||||
]
|
||||
94
shared/data/workers.json
Normal file
94
shared/data/workers.json
Normal file
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"race": "argon",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "foodrations",
|
||||
"amount": 450
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "teladi",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "nostropoil",
|
||||
"amount": 228
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "paranid",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "sojahusk",
|
||||
"amount": 288
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "split",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "cheltmeat",
|
||||
"amount": 102
|
||||
},
|
||||
{
|
||||
"ware": "scruffinfruits",
|
||||
"amount": 138
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "terran",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "terranmre",
|
||||
"amount": 258
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "boron",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "bofu",
|
||||
"amount": 90
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 198
|
||||
},
|
||||
{
|
||||
"ware": "water",
|
||||
"amount": 162
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
18
shared/export/cargo-types-data.json
Normal file
18
shared/export/cargo-types-data.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"id": "liquid",
|
||||
"name": "Liquid"
|
||||
},
|
||||
{
|
||||
"id": "solid",
|
||||
"name": "Solid"
|
||||
},
|
||||
{
|
||||
"id": "container",
|
||||
"name": "Container"
|
||||
},
|
||||
{
|
||||
"id": "condensate",
|
||||
"name": "Condensate"
|
||||
}
|
||||
]
|
||||
4
shared/export/effects-data.json
Normal file
4
shared/export/effects-data.json
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"work",
|
||||
"sunlight"
|
||||
]
|
||||
14
shared/export/equipment-class-data.json
Normal file
14
shared/export/equipment-class-data.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
"countermeasure",
|
||||
"weapon",
|
||||
"missileturret",
|
||||
"turret",
|
||||
"engine",
|
||||
"missile",
|
||||
"shieldgenerator",
|
||||
"scanner",
|
||||
"mine",
|
||||
"missilelauncher",
|
||||
"ship_s",
|
||||
"ship_xs"
|
||||
]
|
||||
24596
shared/export/equipment-data.json
Normal file
24596
shared/export/equipment-data.json
Normal file
File diff suppressed because it is too large
Load Diff
11
shared/export/equipment-type-data.json
Normal file
11
shared/export/equipment-type-data.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
"countermeasures",
|
||||
"drones",
|
||||
"engines",
|
||||
"missiles",
|
||||
"shields",
|
||||
"software",
|
||||
"thrusters",
|
||||
"turrets",
|
||||
"weapons"
|
||||
]
|
||||
2136
shared/export/factions-data.json
Normal file
2136
shared/export/factions-data.json
Normal file
File diff suppressed because it is too large
Load Diff
26
shared/export/module-types-data.json
Normal file
26
shared/export/module-types-data.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"ModuleTypes": [
|
||||
"connectionmodule",
|
||||
"production",
|
||||
"defencemodule",
|
||||
"dockarea",
|
||||
"habitation",
|
||||
"pier",
|
||||
"storage",
|
||||
"buildmodule",
|
||||
"ventureplatform",
|
||||
"processingmodule",
|
||||
"recycling"
|
||||
],
|
||||
"AllModuleTypes": [
|
||||
"habitation",
|
||||
"buildmodule",
|
||||
"dockarea",
|
||||
"pier",
|
||||
"storage",
|
||||
"defencemodule",
|
||||
"connectionmodule",
|
||||
"processingmodule",
|
||||
"recycling"
|
||||
]
|
||||
}
|
||||
28373
shared/export/modules-data.json
Normal file
28373
shared/export/modules-data.json
Normal file
File diff suppressed because it is too large
Load Diff
7
shared/export/production-method-data.json
Normal file
7
shared/export/production-method-data.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"default",
|
||||
"argon",
|
||||
"teladi",
|
||||
"paranid",
|
||||
"recycling"
|
||||
]
|
||||
55
shared/export/race-data.json
Normal file
55
shared/export/race-data.json
Normal file
@@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"id": "argon",
|
||||
"name": "Argon",
|
||||
"description": "The descendents of Terran colonists stranded from Earth centuries ago, the Argon became their own thriving civilisation covering a great many systems and forging relations with several alien races. Throughout their short history the Argon Federation has been plagued by war, notably with the Xenon. Their greatest challenge however came from the unlikely source of the reconnected Terrans of Earth where they were plunged into the costly Terran Conflict.",
|
||||
"icon": "race_argon"
|
||||
},
|
||||
{
|
||||
"id": "boron",
|
||||
"name": "Boron",
|
||||
"description": "The predominantly peaceful Boron are aquatic life-forms from the planet Nishala. While initially pacifist, the discovery of their world by the Split forced them to invent defences and adapt to war. Enjoying a close relationship with the Argon, the Boron remain a wise and measured people.",
|
||||
"icon": "race_boron"
|
||||
},
|
||||
{
|
||||
"id": "drone",
|
||||
"name": "Drone",
|
||||
"description": "Drones are designed to specialise in a narrow field of tasks. With AI research outlawed to avoid a similar situation to the Terraformer-Xenon evolution, drones are limited in scope and capability. However, results from Xenon research have led to advancements in drone technology, something which troubled many experts."
|
||||
},
|
||||
{
|
||||
"id": "khaak",
|
||||
"name": "Kha'ak",
|
||||
"description": "Thought to have been wiped out during Operation Final Fury, very little is known about the Kha'ak other than they seem to be an insectile hive race hell-bent on the destruction of all those that share the Jump Gate network. As a hive race, it is suspected that individual intelligence gives way to a communal or caste mentality, but very little research into the species was completed before Operation Final Fury took place.",
|
||||
"icon": "race_khaak"
|
||||
},
|
||||
{
|
||||
"id": "paranid",
|
||||
"name": "Paranid",
|
||||
"description": "The physically imposing Paranid are often regarded as arrogant by several races which usually stems from their exceptional mathematic skills and religious fervour. Allied with the Split and distrusting of the Argon, the Paranid have been in several conflicts where they use their technological prowess and multilevel thinking to gain tactical advantages.",
|
||||
"icon": "race_paranid"
|
||||
},
|
||||
{
|
||||
"id": "split",
|
||||
"name": "Split",
|
||||
"description": "The aggressive Split live in a society constantly changing leadership where challenging factions rise up to impose a new Patriarch. Their short temper and fiery disposition puts them at odds with other races which has sometimes lead to war, notably with the Boron and Argon.",
|
||||
"icon": "race_split"
|
||||
},
|
||||
{
|
||||
"id": "teladi",
|
||||
"name": "Teladi",
|
||||
"description": "The lizard-like Teladi are one of the founding members of the Community of Planets and have a natural affinity towards business and the accumulation of profit. They enjoy favourable relations with other races although some find their drive for profit disconcerting. Their long lifespan gives them a unique view of the Jump Gate shutdown, as does their previous experience being cut off from their home system of Ianamus Zura.",
|
||||
"icon": "race_teladi"
|
||||
},
|
||||
{
|
||||
"id": "terran",
|
||||
"name": "Terran",
|
||||
"description": "The Terrans of the Solar System have a long history of spaceflight and exploring the Jump Gate network. After the events of the Terraformers over Earth, the Terrans severed their contact with the rest of the galaxy and had several centuries of rebuilding and advancement in isolation. Their brief return led to the Terran Conflict which preceded the mass disconnection of Jump Gates. It is unknown if the war precipitated this event.",
|
||||
"icon": "race_terran"
|
||||
},
|
||||
{
|
||||
"id": "xenon",
|
||||
"name": "Xenon",
|
||||
"description": "The Xenon are a mechanical race resulting from past Terran terraformer ships which eventually evolved intelligence. A constant threat in many areas of the galaxy, it is thought that the Jump Gate shutdown may stem their movements but given their disregard of time it is possible they may simply travel between stars. The Xenon have no known allies and communication with them is often relegated to folklore.",
|
||||
"icon": "race_xenon"
|
||||
}
|
||||
]
|
||||
9
shared/export/ship-purpose-data.json
Normal file
9
shared/export/ship-purpose-data.json
Normal file
@@ -0,0 +1,9 @@
|
||||
[
|
||||
"auxiliary",
|
||||
"mine",
|
||||
"build",
|
||||
"fight",
|
||||
"trade",
|
||||
"salvage",
|
||||
"dismantling"
|
||||
]
|
||||
24
shared/export/ship-type-data.json
Normal file
24
shared/export/ship-type-data.json
Normal file
@@ -0,0 +1,24 @@
|
||||
[
|
||||
"resupplier",
|
||||
"miner",
|
||||
"carrier",
|
||||
"fighter",
|
||||
"heavyfighter",
|
||||
"destroyer",
|
||||
"largeminer",
|
||||
"freighter",
|
||||
"bomber",
|
||||
"scavenger",
|
||||
"frigate",
|
||||
"transporter",
|
||||
"interceptor",
|
||||
"scout",
|
||||
"courier",
|
||||
"builder",
|
||||
"corvette",
|
||||
"police",
|
||||
"battleship",
|
||||
"gunboat",
|
||||
"tug",
|
||||
"compactor"
|
||||
]
|
||||
39382
shared/export/ships-data.json
Normal file
39382
shared/export/ships-data.json
Normal file
File diff suppressed because it is too large
Load Diff
7
shared/export/size-data.json
Normal file
7
shared/export/size-data.json
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
"extrasmall",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"extralarge"
|
||||
]
|
||||
5
shared/export/transport-data.json
Normal file
5
shared/export/transport-data.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"container",
|
||||
"liquid",
|
||||
"solid"
|
||||
]
|
||||
5
shared/export/turret-type-data.json
Normal file
5
shared/export/turret-type-data.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
"standard",
|
||||
"missile",
|
||||
"mining"
|
||||
]
|
||||
76
shared/export/ware-groups-data.json
Normal file
76
shared/export/ware-groups-data.json
Normal file
@@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"id": "agricultural",
|
||||
"name": "Agricultural Goods",
|
||||
"factoryName": "Agricultural Goods Factory",
|
||||
"icon": "be_upgrade_agricultural",
|
||||
"tier": 5
|
||||
},
|
||||
{
|
||||
"id": "energy",
|
||||
"name": "Energy",
|
||||
"factoryName": "Energy Complex",
|
||||
"icon": "be_upgrade_energy",
|
||||
"tier": 1
|
||||
},
|
||||
{
|
||||
"id": "food",
|
||||
"name": "Food",
|
||||
"factoryName": "Farm",
|
||||
"icon": "be_upgrade_food",
|
||||
"tier": 6
|
||||
},
|
||||
{
|
||||
"id": "gases",
|
||||
"name": "Gases",
|
||||
"factoryName": "Gas Refinery",
|
||||
"icon": "be_upgrade_refined"
|
||||
},
|
||||
{
|
||||
"id": "hightech",
|
||||
"name": "High Tech Goods",
|
||||
"factoryName": "High Tech Factory",
|
||||
"icon": "be_upgrade_hightech",
|
||||
"tier": 3
|
||||
},
|
||||
{
|
||||
"id": "ice",
|
||||
"name": "Ice",
|
||||
"factoryName": "Ice Refinery",
|
||||
"icon": "be_upgrade_water"
|
||||
},
|
||||
{
|
||||
"id": "minerals",
|
||||
"name": "Minerals",
|
||||
"factoryName": "Mineral Refinery",
|
||||
"icon": "be_upgrade_refined"
|
||||
},
|
||||
{
|
||||
"id": "pharmaceutical",
|
||||
"name": "Pharmaceutical Goods",
|
||||
"factoryName": "Pharmaceutical Goods Factory",
|
||||
"icon": "be_upgrade_pharmaceutical",
|
||||
"tier": 7
|
||||
},
|
||||
{
|
||||
"id": "refined",
|
||||
"name": "Refined Goods",
|
||||
"factoryName": "Refined Goods Complex",
|
||||
"icon": "be_upgrade_refined",
|
||||
"tier": 2
|
||||
},
|
||||
{
|
||||
"id": "shiptech",
|
||||
"name": "Ship Technology",
|
||||
"factoryName": "Ship Technology Factory",
|
||||
"icon": "be_upgrade_shiptech",
|
||||
"tier": 4
|
||||
},
|
||||
{
|
||||
"id": "water",
|
||||
"name": "Water",
|
||||
"factoryName": "Water Refinery",
|
||||
"icon": "be_upgrade_water",
|
||||
"tier": 2
|
||||
}
|
||||
]
|
||||
2697
shared/export/wares-data.json
Normal file
2697
shared/export/wares-data.json
Normal file
File diff suppressed because it is too large
Load Diff
94
shared/export/workers-data.json
Normal file
94
shared/export/workers-data.json
Normal file
@@ -0,0 +1,94 @@
|
||||
[
|
||||
{
|
||||
"race": "argon",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "foodrations",
|
||||
"amount": 450
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "teladi",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "nostropoil",
|
||||
"amount": 228
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "paranid",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "sojahusk",
|
||||
"amount": 288
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "split",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "cheltmeat",
|
||||
"amount": 102
|
||||
},
|
||||
{
|
||||
"ware": "scruffinfruits",
|
||||
"amount": 138
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "terran",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "terranmre",
|
||||
"amount": 258
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 270
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"race": "boron",
|
||||
"amount": 200,
|
||||
"consumption": [
|
||||
{
|
||||
"ware": "bofu",
|
||||
"amount": 90
|
||||
},
|
||||
{
|
||||
"ware": "medicalsupplies",
|
||||
"amount": 198
|
||||
},
|
||||
{
|
||||
"ware": "water",
|
||||
"amount": 162
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user