feat: rework modules, items and fuel

This commit is contained in:
2026-03-17 03:32:37 -04:00
parent ec1116e1ce
commit 3234b628ea
45 changed files with 4882 additions and 6052 deletions

View File

@@ -17,10 +17,6 @@ public sealed record StationSnapshot(
int DockedShips, int DockedShips,
IReadOnlyList<string> DockedShipIds, IReadOnlyList<string> DockedShipIds,
int DockingPads, int DockingPads,
float FuelStored,
float FuelCapacity,
float EnergyStored,
float EnergyCapacity,
IReadOnlyList<StationActionProgressSnapshot> CurrentProcesses, IReadOnlyList<StationActionProgressSnapshot> CurrentProcesses,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,
string FactionId, string FactionId,
@@ -30,6 +26,7 @@ public sealed record StationSnapshot(
float PopulationCapacity, float PopulationCapacity,
float WorkforceRequired, float WorkforceRequired,
float WorkforceEffectiveRatio, float WorkforceEffectiveRatio,
IReadOnlyList<StationStorageUsageSnapshot> StorageUsage,
IReadOnlyList<string> InstalledModules, IReadOnlyList<string> InstalledModules,
IReadOnlyList<string> MarketOrderIds); IReadOnlyList<string> MarketOrderIds);
@@ -46,10 +43,6 @@ public sealed record StationDelta(
int DockedShips, int DockedShips,
IReadOnlyList<string> DockedShipIds, IReadOnlyList<string> DockedShipIds,
int DockingPads, int DockingPads,
float FuelStored,
float FuelCapacity,
float EnergyStored,
float EnergyCapacity,
IReadOnlyList<StationActionProgressSnapshot> CurrentProcesses, IReadOnlyList<StationActionProgressSnapshot> CurrentProcesses,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,
string FactionId, string FactionId,
@@ -59,6 +52,7 @@ public sealed record StationDelta(
float PopulationCapacity, float PopulationCapacity,
float WorkforceRequired, float WorkforceRequired,
float WorkforceEffectiveRatio, float WorkforceEffectiveRatio,
IReadOnlyList<StationStorageUsageSnapshot> StorageUsage,
IReadOnlyList<string> InstalledModules, IReadOnlyList<string> InstalledModules,
IReadOnlyList<string> MarketOrderIds); IReadOnlyList<string> MarketOrderIds);
@@ -67,6 +61,11 @@ public sealed record StationActionProgressSnapshot(
string Label, string Label,
float Progress); float Progress);
public sealed record StationStorageUsageSnapshot(
string StorageClass,
float Used,
float Capacity);
public sealed record ClaimSnapshot( public sealed record ClaimSnapshot(
string Id, string Id,
string FactionId, string FactionId,

View File

@@ -21,7 +21,6 @@ public sealed record ShipSnapshot(
float CargoCapacity, float CargoCapacity,
string? CargoItemId, string? CargoItemId,
float WorkerPopulation, float WorkerPopulation,
float EnergyStored,
float TravelSpeed, float TravelSpeed,
string TravelSpeedUnit, string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,
@@ -52,7 +51,6 @@ public sealed record ShipDelta(
float CargoCapacity, float CargoCapacity,
string? CargoItemId, string? CargoItemId,
float WorkerPopulation, float WorkerPopulation,
float EnergyStored,
float TravelSpeed, float TravelSpeed,
string TravelSpeedUnit, string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,

View File

@@ -1,190 +1,206 @@
namespace SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Data;
public sealed class ConstructionDefinition
{
public string? RecipeId { get; set; }
public string FacilityCategory { get; set; } = "station";
public List<string> RequiredModules { get; set; } = [];
public List<RecipeInputDefinition> Requirements { get; set; } = [];
public float CycleTime { get; set; }
public float BatchSize { get; set; } = 1f;
public float ProductsPerHour { get; set; }
public float MaxEfficiency { get; set; } = 1f;
public int Priority { get; set; }
}
public sealed class BalanceDefinition public sealed class BalanceDefinition
{ {
public float YPlane { get; set; } public float YPlane { get; set; }
public float ArrivalThreshold { get; set; } public float ArrivalThreshold { get; set; }
public float MiningRate { get; set; } public float MiningRate { get; set; }
public float MiningCycleSeconds { get; set; } public float MiningCycleSeconds { get; set; }
public float TransferRate { get; set; } public float TransferRate { get; set; }
public float DockingDuration { get; set; } public float DockingDuration { get; set; }
public float UndockingDuration { get; set; } public float UndockingDuration { get; set; }
public float UndockDistance { get; set; } public float UndockDistance { get; set; }
public EnergyBalanceDefinition Energy { get; set; } = new();
}
public sealed class EnergyBalanceDefinition
{
public float IdleDrain { get; set; }
public float MoveDrain { get; set; }
public float WarpDrain { get; set; }
public float ShipRechargeRate { get; set; }
public float StationSolarCharge { get; set; }
} }
public sealed class SolarSystemDefinition public sealed class SolarSystemDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required float[] Position { get; set; } public required float[] Position { get; set; }
public string StarKind { get; set; } = "main-sequence"; public string StarKind { get; set; } = "main-sequence";
public int StarCount { get; set; } = 1; public int StarCount { get; set; } = 1;
public required string StarColor { get; set; } public required string StarColor { get; set; }
public required string StarGlow { get; set; } public required string StarGlow { get; set; }
public float StarSize { get; set; } public float StarSize { get; set; }
public float GravityWellRadius { get; set; } public float GravityWellRadius { get; set; }
public required AsteroidFieldDefinition AsteroidField { get; set; } public required AsteroidFieldDefinition AsteroidField { get; set; }
public required List<ResourceNodeDefinition> ResourceNodes { get; set; } public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
public required List<PlanetDefinition> Planets { get; set; } public required List<PlanetDefinition> Planets { get; set; }
} }
public sealed class AsteroidFieldDefinition public sealed class AsteroidFieldDefinition
{ {
public int DecorationCount { get; set; } public int DecorationCount { get; set; }
public float RadiusOffset { get; set; } public float RadiusOffset { get; set; }
public float RadiusVariance { get; set; } public float RadiusVariance { get; set; }
public float HeightVariance { get; set; } public float HeightVariance { get; set; }
} }
public sealed class ResourceNodeDefinition public sealed class ResourceNodeDefinition
{ {
public string SourceKind { get; set; } = "asteroid-belt"; public string SourceKind { get; set; } = "asteroid-belt";
public float Angle { get; set; } public float Angle { get; set; }
public float RadiusOffset { get; set; } public float RadiusOffset { get; set; }
public float InclinationDegrees { get; set; } public float InclinationDegrees { get; set; }
public int? AnchorPlanetIndex { get; set; } public int? AnchorPlanetIndex { get; set; }
public int? AnchorMoonIndex { get; set; } public int? AnchorMoonIndex { get; set; }
public float OreAmount { get; set; } public float OreAmount { get; set; }
public required string ItemId { get; set; } public required string ItemId { get; set; }
public int ShardCount { get; set; } public int ShardCount { get; set; }
} }
public sealed class ItemDefinition public sealed class ItemDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Name { get; set; }
public required string Storage { get; set; } public string Description { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty; public string Type { get; set; } = "material";
} public required string CargoKind { get; set; }
public float Volume { get; set; } = 1f;
public sealed class RecipeInputDefinition public ConstructionDefinition? Construction { get; set; }
{
public required string ItemId { get; set; }
public float Amount { get; set; }
}
public sealed class ModuleRecipeDefinition
{
public required string ModuleId { get; set; }
public float Duration { get; set; }
public required List<RecipeInputDefinition> Inputs { get; set; }
} }
public sealed class RecipeOutputDefinition public sealed class RecipeOutputDefinition
{ {
public required string ItemId { get; set; } public required string ItemId { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
}
public sealed class RecipeInputDefinition
{
public required string ItemId { get; set; }
public float Amount { get; set; }
}
public sealed class ModuleConstructionDefinition
{
public required List<RecipeInputDefinition> Requirements { get; set; }
public float ProductionTime { get; set; }
}
public sealed class ModuleDefinition
{
public required string Id { get; set; }
public required string Name { get; set; }
public string Description { get; set; } = string.Empty;
public required string Type { get; set; }
public string? Product { get; set; }
public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; }
public ModuleConstructionDefinition? Construction { get; set; }
}
public sealed class ModuleRecipeDefinition
{
public required string ModuleId { get; set; }
public float Duration { get; set; }
public required List<RecipeInputDefinition> Inputs { get; set; }
} }
public sealed class RecipeDefinition public sealed class RecipeDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required string FacilityCategory { get; set; } public required string FacilityCategory { get; set; }
public float Duration { get; set; } public float Duration { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public List<string> RequiredModules { get; set; } = []; public List<string> RequiredModules { get; set; } = [];
public List<RecipeInputDefinition> Inputs { get; set; } = []; public List<RecipeInputDefinition> Inputs { get; set; } = [];
public List<RecipeOutputDefinition> Outputs { get; set; } = []; public List<RecipeOutputDefinition> Outputs { get; set; } = [];
public string? ShipOutputId { get; set; } public string? ShipOutputId { get; set; }
} }
public sealed class PlanetDefinition public sealed class PlanetDefinition
{ {
public required string Label { get; set; } public required string Label { get; set; }
public string PlanetType { get; set; } = "terrestrial"; public string PlanetType { get; set; } = "terrestrial";
public string Shape { get; set; } = "sphere"; public string Shape { get; set; } = "sphere";
public int MoonCount { get; set; } public int MoonCount { get; set; }
public float OrbitRadius { get; set; } public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; } public float OrbitSpeed { get; set; }
public float OrbitEccentricity { get; set; } public float OrbitEccentricity { get; set; }
public float OrbitInclination { get; set; } public float OrbitInclination { get; set; }
public float OrbitLongitudeOfAscendingNode { get; set; } public float OrbitLongitudeOfAscendingNode { get; set; }
public float OrbitArgumentOfPeriapsis { get; set; } public float OrbitArgumentOfPeriapsis { get; set; }
public float OrbitPhaseAtEpoch { get; set; } public float OrbitPhaseAtEpoch { get; set; }
public float Size { get; set; } public float Size { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public float Tilt { get; set; } public float Tilt { get; set; }
public bool HasRing { get; set; } public bool HasRing { get; set; }
} }
public sealed class ShipDefinition public sealed class ShipDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required string Role { get; set; } public required string Role { get; set; }
public required string ShipClass { get; set; } public required string ShipClass { get; set; }
public float Speed { get; set; } public float Speed { get; set; }
public float WarpSpeed { get; set; } public float WarpSpeed { get; set; }
public float FtlSpeed { get; set; } public float FtlSpeed { get; set; }
public float SpoolTime { get; set; } public float SpoolTime { get; set; }
public float CargoCapacity { get; set; } public float CargoCapacity { get; set; }
public string? CargoKind { get; set; } public string? CargoKind { get; set; }
public string? CargoItemId { get; set; } public string? CargoItemId { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public required string HullColor { get; set; } public required string HullColor { get; set; }
public float Size { get; set; } public float Size { get; set; }
public float MaxHealth { get; set; } public float MaxHealth { get; set; }
public List<string> Modules { get; set; } = []; public List<string> Modules { get; set; } = [];
} public ConstructionDefinition? Construction { get; set; }
public sealed class ConstructibleDefinition
{
public required string Id { get; set; }
public required string Label { get; set; }
public required string Category { get; set; }
public required string Color { get; set; }
public float Radius { get; set; }
public int DockingCapacity { get; set; }
public Dictionary<string, float> Storage { get; set; } = new(StringComparer.Ordinal);
public List<string> Modules { get; set; } = [];
} }
public sealed class ScenarioDefinition public sealed class ScenarioDefinition
{ {
public required List<InitialStationDefinition> InitialStations { get; set; } public required List<InitialStationDefinition> InitialStations { get; set; }
public required List<ShipFormationDefinition> ShipFormations { get; set; } public required List<ShipFormationDefinition> ShipFormations { get; set; }
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; } public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
public required MiningDefaultsDefinition MiningDefaults { get; set; } public required MiningDefaultsDefinition MiningDefaults { get; set; }
} }
public sealed class InitialStationDefinition public sealed class InitialStationDefinition
{ {
public required string ConstructibleId { get; set; } public required string SystemId { get; set; }
public required string SystemId { get; set; } public string Label { get; set; } = "Orbital Station";
public string? FactionId { get; set; } public string Color { get; set; } = "#8df0d2";
public int? PlanetIndex { get; set; } public List<string> StartingModules { get; set; } = [];
public int? LagrangeSide { get; set; } public string? FactionId { get; set; }
public float[]? Position { get; set; } public int? PlanetIndex { get; set; }
public int? LagrangeSide { get; set; }
public float[]? Position { get; set; }
} }
public sealed class ShipFormationDefinition public sealed class ShipFormationDefinition
{ {
public required string ShipId { get; set; } public required string ShipId { get; set; }
public int Count { get; set; } public int Count { get; set; }
public required float[] Center { get; set; } public required float[] Center { get; set; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
} }
public sealed class PatrolRouteDefinition public sealed class PatrolRouteDefinition
{ {
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required List<float[]> Points { get; set; } public required List<float[]> Points { get; set; }
} }
public sealed class MiningDefaultsDefinition public sealed class MiningDefaultsDefinition
{ {
public required string NodeSystemId { get; set; } public required string NodeSystemId { get; set; }
public required string RefinerySystemId { get; set; } public required string RefinerySystemId { get; set; }
} }

View File

@@ -19,8 +19,6 @@ internal sealed class ShipBehaviorStateMachine
idleState, idleState,
new PatrolShipBehaviorState(), new PatrolShipBehaviorState(),
new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining-turret"), new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining-turret"),
new ResourceHarvestShipBehaviorState("auto-harvest-gas", "gas", "gas-extractor"),
new EnergySupplyShipBehaviorState(),
new ConstructStationShipBehaviorState(), new ConstructStationShipBehaviorState(),
}; };

View File

@@ -2,169 +2,126 @@ namespace SpaceGame.Simulation.Api.Simulation;
internal sealed class IdleShipBehaviorState : IShipBehaviorState internal sealed class IdleShipBehaviorState : IShipBehaviorState
{ {
public string Kind => "idle"; public string Kind => "idle";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
{
ship.ControllerTask = new ControllerTaskRuntime
{ {
ship.ControllerTask = new ControllerTaskRuntime Kind = ControllerTaskKind.Idle,
{ Threshold = world.Balance.ArrivalThreshold,
Kind = ControllerTaskKind.Idle, Status = WorkStatus.Pending,
Threshold = world.Balance.ArrivalThreshold, };
Status = WorkStatus.Pending, }
};
}
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{ {
} }
} }
internal sealed class PatrolShipBehaviorState : IShipBehaviorState internal sealed class PatrolShipBehaviorState : IShipBehaviorState
{ {
public string Kind => "patrol"; public string Kind => "patrol";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
{
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
{ {
if (ship.DefaultBehavior.PatrolPoints.Count == 0) ship.DefaultBehavior.Kind = "idle";
{ ship.ControllerTask = new ControllerTaskRuntime
ship.DefaultBehavior.Kind = "idle"; {
ship.ControllerTask = new ControllerTaskRuntime Kind = ControllerTaskKind.Idle,
{ Threshold = world.Balance.ArrivalThreshold,
Kind = ControllerTaskKind.Idle, Status = WorkStatus.Pending,
Threshold = world.Balance.ArrivalThreshold, };
Status = WorkStatus.Pending, return;
};
return;
}
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Travel,
TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
TargetSystemId = ship.SystemId,
Threshold = 18f,
};
} }
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) ship.ControllerTask = new ControllerTaskRuntime
{ {
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0) Kind = ControllerTaskKind.Travel,
{ TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count; TargetSystemId = ship.SystemId,
} Threshold = 18f,
};
}
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0)
{
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count;
} }
}
} }
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
{ {
private readonly string resourceItemId; private readonly string resourceItemId;
private readonly string requiredModule; private readonly string requiredModule;
public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule) public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule)
{
Kind = kind;
this.resourceItemId = resourceItemId;
this.requiredModule = requiredModule;
}
public string Kind { get; }
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{ {
Kind = kind; case ("travel-to-node", "arrived"):
this.resourceItemId = resourceItemId; ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
this.requiredModule = requiredModule; break;
} case ("extract", "cargo-full"):
ship.DefaultBehavior.Phase = "travel-to-station";
public string Kind { get; } break;
case ("extract", "node-depleted"):
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => ship.DefaultBehavior.Phase = "travel-to-node";
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule); ship.DefaultBehavior.NodeId = null;
break;
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) case ("travel-to-station", "arrived"):
{ ship.DefaultBehavior.Phase = "dock";
switch (ship.DefaultBehavior.Phase, controllerEvent) break;
{ case ("dock", "docked"):
case ("travel-to-node", "arrived"): ship.DefaultBehavior.Phase = "unload";
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract"; break;
break; case ("undock", "undocked"):
case ("extract", "cargo-full"): ship.DefaultBehavior.Phase = "travel-to-node";
ship.DefaultBehavior.Phase = "travel-to-station"; ship.DefaultBehavior.NodeId = null;
break; break;
case ("extract", "node-depleted"):
ship.DefaultBehavior.Phase = "travel-to-node";
ship.DefaultBehavior.NodeId = null;
break;
case ("travel-to-station", "arrived"):
ship.DefaultBehavior.Phase = "dock";
break;
case ("dock", "docked"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "refuel";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = "refuel";
break;
case ("refuel", "refueled"):
ship.DefaultBehavior.Phase = "undock";
break;
case ("undock", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-node";
ship.DefaultBehavior.NodeId = null;
break;
}
} }
}
} }
internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
{ {
public string Kind => "construct-station"; public string Kind => "construct-station";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) => public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanStationConstruction(ship, world); engine.PlanStationConstruction(ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{ {
switch (ship.DefaultBehavior.Phase, controllerEvent) case ("travel-to-station", "arrived"):
{ ship.DefaultBehavior.Phase = "deliver-to-site";
case ("travel-to-station", "arrived"): break;
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "deliver-to-site"; case ("deliver-to-site", "construction-delivered"):
break; ship.DefaultBehavior.Phase = "build-site";
case ("refuel", "refueled"): break;
ship.DefaultBehavior.Phase = "deliver-to-site"; case ("construct-module", "module-constructed"):
break; case ("build-site", "site-constructed"):
case ("deliver-to-site", "construction-delivered"): ship.DefaultBehavior.Phase = "travel-to-station";
ship.DefaultBehavior.Phase = "build-site"; ship.DefaultBehavior.ModuleId = null;
break; break;
case ("construct-module", "module-constructed"):
case ("build-site", "site-constructed"):
ship.DefaultBehavior.Phase = "travel-to-station";
ship.DefaultBehavior.ModuleId = null;
break;
}
}
}
internal sealed class EnergySupplyShipBehaviorState : IShipBehaviorState
{
public string Kind => "auto-supply-energy";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanEnergySupply(ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{
case ("travel-to-source", "arrived"):
case ("travel-to-destination", "arrived"):
ship.DefaultBehavior.Phase = "dock";
break;
case ("dock", "docked"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "load";
break;
case ("load", "loaded"):
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock";
break;
case ("refuel", "refueled"):
ship.DefaultBehavior.Phase = "undock";
break;
case ("undock", "undocked"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "travel-to-destination" : "travel-to-source";
break;
}
} }
}
} }

View File

@@ -4,63 +4,62 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed class ShipRuntime public sealed class ShipRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required ShipDefinition Definition { get; init; } public required ShipDefinition Definition { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public required Vector3 TargetPosition { get; set; } public required Vector3 TargetPosition { get; set; }
public required ShipSpatialStateRuntime SpatialState { get; set; } public required ShipSpatialStateRuntime SpatialState { get; set; }
public Vector3 Velocity { get; set; } = Vector3.Zero; public Vector3 Velocity { get; set; } = Vector3.Zero;
public ShipState State { get; set; } = ShipState.Idle; public ShipState State { get; set; } = ShipState.Idle;
public ShipOrderRuntime? Order { get; set; } public ShipOrderRuntime? Order { get; set; }
public required DefaultBehaviorRuntime DefaultBehavior { get; set; } public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
public required ControllerTaskRuntime ControllerTask { get; set; } public required ControllerTaskRuntime ControllerTask { get; set; }
public float ActionTimer { get; set; } public float ActionTimer { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public float WorkerPopulation { get; set; } public float WorkerPopulation { get; set; }
public float EnergyStored { get; set; } public string DockedStationId { get; set; }
public string? DockedStationId { get; set; } public int? AssignedDockingPadIndex { get; set; }
public int? AssignedDockingPadIndex { get; set; } public string? CommanderId { get; set; }
public string? CommanderId { get; set; } public string? PolicySetId { get; set; }
public string? PolicySetId { get; set; } public float Health { get; set; }
public float Health { get; set; } public string? TrackedActionKey { get; set; }
public string? TrackedActionKey { get; set; } public float TrackedActionTotal { get; set; }
public float TrackedActionTotal { get; set; } public List<string> History { get; } = [];
public List<string> History { get; } = []; public string LastSignature { get; set; } = string.Empty;
public string LastSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class ShipOrderRuntime public sealed class ShipOrderRuntime
{ {
public required string Kind { get; init; } public required string Kind { get; init; }
public OrderStatus Status { get; set; } = OrderStatus.Accepted; public OrderStatus Status { get; set; } = OrderStatus.Accepted;
public required string DestinationSystemId { get; init; } public required string DestinationSystemId { get; init; }
public required Vector3 DestinationPosition { get; init; } public required Vector3 DestinationPosition { get; init; }
} }
public sealed class DefaultBehaviorRuntime public sealed class DefaultBehaviorRuntime
{ {
public required string Kind { get; set; } public required string Kind { get; set; }
public string? AreaSystemId { get; set; } public string? AreaSystemId { get; set; }
public string? StationId { get; set; } public string? StationId { get; set; }
public string? RefineryId { get; set; } public string? RefineryId { get; set; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public string? Phase { get; set; } public string? Phase { get; set; }
public List<Vector3> PatrolPoints { get; set; } = []; public List<Vector3> PatrolPoints { get; set; } = [];
public int PatrolIndex { get; set; } public int PatrolIndex { get; set; }
} }
public sealed class ControllerTaskRuntime public sealed class ControllerTaskRuntime
{ {
public required ControllerTaskKind Kind { get; set; } public required ControllerTaskKind Kind { get; set; }
public WorkStatus Status { get; set; } = WorkStatus.Pending; public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetNodeId { get; set; } public string? TargetNodeId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public float Threshold { get; set; } public float Threshold { get; set; }
} }

View File

@@ -2,243 +2,237 @@ namespace SpaceGame.Simulation.Api.Simulation;
public enum SpatialNodeKind public enum SpatialNodeKind
{ {
Star, Star,
Planet, Planet,
Moon, Moon,
LagrangePoint, LagrangePoint,
Station, Station,
ResourceSite, ResourceSite,
} }
public enum WorkStatus public enum WorkStatus
{ {
Pending, Pending,
Active, Active,
Completed, Completed,
} }
public enum OrderStatus public enum OrderStatus
{ {
Queued, Queued,
Accepted, Accepted,
Completed, Completed,
} }
public enum ShipState public enum ShipState
{ {
Idle, Idle,
Arriving, Arriving,
CapacitorStarved, LocalFlight,
LocalFlight, SpoolingWarp,
SpoolingWarp, Warping,
Warping, SpoolingFtl,
SpoolingFtl, Ftl,
Ftl, CargoFull,
CargoFull, MiningApproach,
MiningApproach, Mining,
Mining, NodeDepleted,
NodeDepleted, AwaitingDock,
AwaitingDock, DockingApproach,
DockingApproach, Docking,
Docking, Docked,
Docked, Transferring,
Transferring, Loading,
Loading, Unloading,
Unloading, WaitingMaterials,
Refueling, ConstructionBlocked,
WaitingMaterials, Constructing,
ConstructionBlocked, DeliveringConstruction,
Constructing, Blocked,
DeliveringConstruction, Undocking,
Blocked,
Undocking,
} }
public enum ControllerTaskKind public enum ControllerTaskKind
{ {
Idle, Idle,
Travel, Travel,
Extract, Extract,
Dock, Dock,
Load, Load,
Unload, Unload,
Refuel, DeliverConstruction,
DeliverConstruction, BuildConstructionSite,
BuildConstructionSite, LoadWorkers,
LoadWorkers, UnloadWorkers,
UnloadWorkers, ConstructModule,
ConstructModule, Undock,
Undock,
} }
public static class SpaceLayerKinds public static class SpaceLayerKinds
{ {
public const string UniverseSpace = "universe-space"; public const string UniverseSpace = "universe-space";
public const string GalaxySpace = "galaxy-space"; public const string GalaxySpace = "galaxy-space";
public const string SystemSpace = "system-space"; public const string SystemSpace = "system-space";
public const string LocalSpace = "local-space"; public const string LocalSpace = "local-space";
} }
public static class MovementRegimeKinds public static class MovementRegimeKinds
{ {
public const string LocalFlight = "local-flight"; public const string LocalFlight = "local-flight";
public const string Warp = "warp"; public const string Warp = "warp";
public const string StargateTransit = "stargate-transit"; public const string StargateTransit = "stargate-transit";
public const string FtlTransit = "ftl-transit"; public const string FtlTransit = "ftl-transit";
} }
public static class CommanderKind public static class CommanderKind
{ {
public const string Faction = "faction"; public const string Faction = "faction";
public const string Station = "station"; public const string Station = "station";
public const string Ship = "ship"; public const string Ship = "ship";
public const string Fleet = "fleet"; public const string Fleet = "fleet";
public const string Sector = "sector"; public const string Sector = "sector";
public const string TaskGroup = "task-group"; public const string TaskGroup = "task-group";
} }
public static class ShipTaskKinds public static class ShipTaskKinds
{ {
public const string Idle = "idle"; public const string Idle = "idle";
public const string LocalMove = "local-move"; public const string LocalMove = "local-move";
public const string WarpToNode = "warp-to-node"; public const string WarpToNode = "warp-to-node";
public const string UseStargate = "use-stargate"; public const string UseStargate = "use-stargate";
public const string UseFtl = "use-ftl"; public const string UseFtl = "use-ftl";
public const string Dock = "dock"; public const string Dock = "dock";
public const string Undock = "undock"; public const string Undock = "undock";
public const string LoadCargo = "load-cargo"; public const string LoadCargo = "load-cargo";
public const string UnloadCargo = "unload-cargo"; public const string UnloadCargo = "unload-cargo";
public const string LoadWorkers = "load-workers"; public const string LoadWorkers = "load-workers";
public const string UnloadWorkers = "unload-workers"; public const string UnloadWorkers = "unload-workers";
public const string MineNode = "mine-node"; public const string MineNode = "mine-node";
public const string HarvestGas = "harvest-gas"; public const string HarvestGas = "harvest-gas";
public const string DeliverToStation = "deliver-to-station"; public const string DeliverToStation = "deliver-to-station";
public const string ClaimLagrangePoint = "claim-lagrange-point"; public const string ClaimLagrangePoint = "claim-lagrange-point";
public const string BuildConstructionSite = "build-construction-site"; public const string BuildConstructionSite = "build-construction-site";
public const string EscortTarget = "escort-target"; public const string EscortTarget = "escort-target";
public const string AttackTarget = "attack-target"; public const string AttackTarget = "attack-target";
public const string DefendBubble = "defend-bubble"; public const string DefendBubble = "defend-bubble";
public const string Retreat = "retreat"; public const string Retreat = "retreat";
public const string HoldPosition = "hold-position"; public const string HoldPosition = "hold-position";
} }
public static class ShipOrderKinds public static class ShipOrderKinds
{ {
public const string DirectMove = "direct-move"; public const string DirectMove = "direct-move";
public const string TravelToNode = "travel-to-node"; public const string TravelToNode = "travel-to-node";
public const string DockAtStation = "dock-at-station"; public const string DockAtStation = "dock-at-station";
public const string DeliverCargo = "deliver-cargo"; public const string DeliverCargo = "deliver-cargo";
public const string BuildAtSite = "build-at-site"; public const string BuildAtSite = "build-at-site";
public const string AttackTarget = "attack-target"; public const string AttackTarget = "attack-target";
public const string HoldPosition = "hold-position"; public const string HoldPosition = "hold-position";
} }
public static class ClaimStateKinds public static class ClaimStateKinds
{ {
public const string Placed = "placed"; public const string Placed = "placed";
public const string Activating = "activating"; public const string Activating = "activating";
public const string Active = "active"; public const string Active = "active";
public const string Destroyed = "destroyed"; public const string Destroyed = "destroyed";
} }
public static class ConstructionSiteStateKinds public static class ConstructionSiteStateKinds
{ {
public const string Planned = "planned"; public const string Planned = "planned";
public const string Active = "active"; public const string Active = "active";
public const string Paused = "paused"; public const string Paused = "paused";
public const string Completed = "completed"; public const string Completed = "completed";
public const string Destroyed = "destroyed"; public const string Destroyed = "destroyed";
} }
public static class MarketOrderKinds public static class MarketOrderKinds
{ {
public const string Buy = "buy"; public const string Buy = "buy";
public const string Sell = "sell"; public const string Sell = "sell";
} }
public static class MarketOrderStateKinds public static class MarketOrderStateKinds
{ {
public const string Open = "open"; public const string Open = "open";
public const string PartiallyFilled = "partially-filled"; public const string PartiallyFilled = "partially-filled";
public const string Filled = "filled"; public const string Filled = "filled";
public const string Cancelled = "cancelled"; public const string Cancelled = "cancelled";
} }
public static class SimulationEnumMappings public static class SimulationEnumMappings
{ {
public static string ToContractValue(this SpatialNodeKind kind) => kind switch public static string ToContractValue(this SpatialNodeKind kind) => kind switch
{ {
SpatialNodeKind.Star => "star", SpatialNodeKind.Star => "star",
SpatialNodeKind.Planet => "planet", SpatialNodeKind.Planet => "planet",
SpatialNodeKind.Moon => "moon", SpatialNodeKind.Moon => "moon",
SpatialNodeKind.LagrangePoint => "lagrange-point", SpatialNodeKind.LagrangePoint => "lagrange-point",
SpatialNodeKind.Station => "station", SpatialNodeKind.Station => "station",
SpatialNodeKind.ResourceSite => "resource-site", SpatialNodeKind.ResourceSite => "resource-site",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
}; };
public static string ToContractValue(this WorkStatus status) => status switch public static string ToContractValue(this WorkStatus status) => status switch
{ {
WorkStatus.Pending => "pending", WorkStatus.Pending => "pending",
WorkStatus.Active => "active", WorkStatus.Active => "active",
WorkStatus.Completed => "completed", WorkStatus.Completed => "completed",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this OrderStatus status) => status switch public static string ToContractValue(this OrderStatus status) => status switch
{ {
OrderStatus.Queued => "queued", OrderStatus.Queued => "queued",
OrderStatus.Accepted => "accepted", OrderStatus.Accepted => "accepted",
OrderStatus.Completed => "completed", OrderStatus.Completed => "completed",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this ShipState state) => state switch public static string ToContractValue(this ShipState state) => state switch
{ {
ShipState.Idle => "idle", ShipState.Idle => "idle",
ShipState.Arriving => "arriving", ShipState.Arriving => "arriving",
ShipState.CapacitorStarved => "capacitor-starved", ShipState.LocalFlight => "local-flight",
ShipState.LocalFlight => "local-flight", ShipState.SpoolingWarp => "spooling-warp",
ShipState.SpoolingWarp => "spooling-warp", ShipState.Warping => "warping",
ShipState.Warping => "warping", ShipState.SpoolingFtl => "spooling-ftl",
ShipState.SpoolingFtl => "spooling-ftl", ShipState.Ftl => "ftl",
ShipState.Ftl => "ftl", ShipState.CargoFull => "cargo-full",
ShipState.CargoFull => "cargo-full", ShipState.MiningApproach => "mining-approach",
ShipState.MiningApproach => "mining-approach", ShipState.Mining => "mining",
ShipState.Mining => "mining", ShipState.NodeDepleted => "node-depleted",
ShipState.NodeDepleted => "node-depleted", ShipState.AwaitingDock => "awaiting-dock",
ShipState.AwaitingDock => "awaiting-dock", ShipState.DockingApproach => "docking-approach",
ShipState.DockingApproach => "docking-approach", ShipState.Docking => "docking",
ShipState.Docking => "docking", ShipState.Docked => "docked",
ShipState.Docked => "docked", ShipState.Transferring => "transferring",
ShipState.Transferring => "transferring", ShipState.Loading => "loading",
ShipState.Loading => "loading", ShipState.Unloading => "unloading",
ShipState.Unloading => "unloading", ShipState.WaitingMaterials => "waiting-materials",
ShipState.Refueling => "refueling", ShipState.ConstructionBlocked => "construction-blocked",
ShipState.WaitingMaterials => "waiting-materials", ShipState.Constructing => "constructing",
ShipState.ConstructionBlocked => "construction-blocked", ShipState.DeliveringConstruction => "delivering-construction",
ShipState.Constructing => "constructing", ShipState.Blocked => "blocked",
ShipState.DeliveringConstruction => "delivering-construction", ShipState.Undocking => "undocking",
ShipState.Blocked => "blocked", _ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
ShipState.Undocking => "undocking", };
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
};
public static string ToContractValue(this ControllerTaskKind kind) => kind switch public static string ToContractValue(this ControllerTaskKind kind) => kind switch
{ {
ControllerTaskKind.Idle => "idle", ControllerTaskKind.Idle => "idle",
ControllerTaskKind.Travel => "travel", ControllerTaskKind.Travel => "travel",
ControllerTaskKind.Extract => "extract", ControllerTaskKind.Extract => "extract",
ControllerTaskKind.Dock => "dock", ControllerTaskKind.Dock => "dock",
ControllerTaskKind.Load => "load", ControllerTaskKind.Load => "load",
ControllerTaskKind.Unload => "unload", ControllerTaskKind.Unload => "unload",
ControllerTaskKind.Refuel => "refuel", ControllerTaskKind.DeliverConstruction => "deliver-construction",
ControllerTaskKind.DeliverConstruction => "deliver-construction", ControllerTaskKind.BuildConstructionSite => "build-construction-site",
ControllerTaskKind.BuildConstructionSite => "build-construction-site", ControllerTaskKind.LoadWorkers => "load-workers",
ControllerTaskKind.LoadWorkers => "load-workers", ControllerTaskKind.UnloadWorkers => "unload-workers",
ControllerTaskKind.UnloadWorkers => "unload-workers", ControllerTaskKind.ConstructModule => "construct-module",
ControllerTaskKind.ConstructModule => "construct-module", ControllerTaskKind.Undock => "undock",
ControllerTaskKind.Undock => "undock", _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), };
};
} }

View File

@@ -21,6 +21,7 @@ public sealed class SimulationWorld
public required List<PolicySetRuntime> Policies { get; init; } public required List<PolicySetRuntime> Policies { get; init; }
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; } public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; } public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; }
public required Dictionary<string, ModuleDefinition> ModuleDefinitions { get; init; }
public required Dictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; init; } public required Dictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; init; }
public required Dictionary<string, RecipeDefinition> Recipes { get; init; } public required Dictionary<string, RecipeDefinition> Recipes { get; init; }
public int TickIntervalMs { get; init; } = 200; public int TickIntervalMs { get; init; } = 200;

View File

@@ -1,40 +1,49 @@
using SpaceGame.Simulation.Api.Data;
namespace SpaceGame.Simulation.Api.Simulation; namespace SpaceGame.Simulation.Api.Simulation;
public sealed class StationRuntime public sealed class StationRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required ConstructibleDefinition Definition { get; init; } public required string Label { get; set; }
public required Vector3 Position { get; set; } public string Category { get; set; } = "station";
public required string FactionId { get; init; } public string Color { get; set; } = "#8df0d2";
public string? NodeId { get; set; } public required Vector3 Position { get; set; }
public string? BubbleId { get; set; } public float Radius { get; set; } = 24f;
public string? AnchorNodeId { get; set; } public required string FactionId { get; init; }
public string? CommanderId { get; set; } public string? NodeId { get; set; }
public string? PolicySetId { get; set; } public string? BubbleId { get; set; }
public List<string> InstalledModules { get; } = []; public string? AnchorNodeId { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public string? CommanderId { get; set; }
public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal); public string? PolicySetId { get; set; }
public Dictionary<int, string> DockingPadAssignments { get; } = new(); public List<StationModuleRuntime> Modules { get; } = [];
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal); public IEnumerable<string> InstalledModules => Modules.Select((module) => module.ModuleId);
public float EnergyStored { get; set; } public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public float Population { get; set; } public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal);
public float PopulationCapacity { get; set; } public Dictionary<int, string> DockingPadAssignments { get; } = new();
public float WorkforceRequired { get; set; } public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float WorkforceEffectiveRatio { get; set; } = 0.1f; public float Population { get; set; }
public float PopulationGrowthProgress { get; set; } public float PopulationCapacity { get; set; }
public float ShipProductionProgressSeconds { get; set; } public float WorkforceRequired { get; set; }
public HashSet<string> DockedShipIds { get; } = []; public float WorkforceEffectiveRatio { get; set; } = 0.1f;
public ModuleConstructionRuntime? ActiveConstruction { get; set; } public float PopulationGrowthProgress { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty; public float ShipProductionProgressSeconds { get; set; }
public HashSet<string> DockedShipIds { get; } = [];
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class StationModuleRuntime
{
public required string Id { get; init; }
public required string ModuleId { get; init; }
public float Health { get; set; }
public float MaxHealth { get; set; }
} }
public sealed class ModuleConstructionRuntime public sealed class ModuleConstructionRuntime
{ {
public required string ModuleId { get; init; } public required string ModuleId { get; init; }
public float ProgressSeconds { get; set; } public float ProgressSeconds { get; set; }
public float RequiredSeconds { get; init; } public float RequiredSeconds { get; init; }
public string AssignedConstructorShipId { get; set; } = string.Empty; public string AssignedConstructorShipId { get; set; } = string.Empty;
} }

View File

@@ -254,7 +254,6 @@ public sealed partial class ScenarioLoader
} }
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets)); nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
nodes.AddRange(BuildGasCloudNodes(generatedIndex, planets));
return nodes; return nodes;
} }
@@ -344,46 +343,6 @@ public sealed partial class ScenarioLoader
} }
} }
private static IEnumerable<ResourceNodeDefinition> BuildGasCloudNodes(int generatedIndex, IReadOnlyList<PlanetDefinition> planets)
{
var gasAnchor = planets
.Where((planet) => planet.PlanetType is "gas-giant" or "ice-giant")
.OrderByDescending((planet) => planet.OrbitRadius)
.FirstOrDefault();
if (gasAnchor is null)
{
yield break;
}
var gasAnchorIndex = 0;
for (var index = 0; index < planets.Count; index += 1)
{
if (ReferenceEquals(planets[index], gasAnchor))
{
gasAnchorIndex = index;
break;
}
}
var nodeCount = 2 + (generatedIndex % 3);
var gasAmount = 1000f;
for (var index = 0; index < nodeCount; index += 1)
{
yield return new ResourceNodeDefinition
{
SourceKind = "gas-cloud",
Angle = gasAnchor.OrbitPhaseAtEpoch * (MathF.PI / 180f) + (((MathF.PI * 2f) / nodeCount) * index) + Jitter(generatedIndex, 240 + index, 0.18f),
RadiusOffset = 170000f + Jitter(generatedIndex, 260 + index, 44000f),
InclinationDegrees = Jitter(generatedIndex, 320 + index, 10f),
AnchorPlanetIndex = gasAnchorIndex,
OreAmount = gasAmount,
ItemId = "gas",
ShardCount = 10 + index,
};
}
}
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets) private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
{ {
if (planets.Count == 0) if (planets.Count == 0)
@@ -566,9 +525,6 @@ public sealed partial class ScenarioLoader
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 1.8f, RadiusOffset = 148000f, InclinationDegrees = -6f, AnchorPlanetIndex = 3, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 3.5f, RadiusOffset = 138000f, InclinationDegrees = 8f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 }, new ResourceNodeDefinition { SourceKind = "asteroid-belt", Angle = 5.1f, RadiusOffset = 164000f, InclinationDegrees = -5f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "ore", ShardCount = 9 },
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 0.9f, RadiusOffset = 210000f, InclinationDegrees = 3f, AnchorPlanetIndex = 4, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 2.7f, RadiusOffset = 228000f, InclinationDegrees = -4f, AnchorPlanetIndex = 5, OreAmount = 1000f, ItemId = "gas", ShardCount = 12 },
new ResourceNodeDefinition { SourceKind = "gas-cloud", Angle = 4.8f, RadiusOffset = 186000f, InclinationDegrees = 6f, AnchorPlanetIndex = 6, OreAmount = 1000f, ItemId = "gas", ShardCount = 10 },
], ],
Planets = Planets =
[ [

View File

@@ -70,7 +70,7 @@ public sealed partial class ScenarioLoader
.ToList(); .ToList();
var refineries = ownedStations var refineries = ownedStations
.Where((station) => HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank")) .Where((station) => HasInstalledModules(station, "refinery-stack", "power-core", "liquid-tank"))
.ToList(); .ToList();
if (refineries.Count > 0) if (refineries.Count > 0)
@@ -86,7 +86,7 @@ public sealed partial class ScenarioLoader
} }
} }
foreach (var shipyard in ownedStations.Where((station) => station.Definition.Category == "shipyard")) foreach (var shipyard in ownedStations.Where((station) => HasInstalledModules(station, "ship-factory")))
{ {
shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock); shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock);
} }
@@ -171,7 +171,7 @@ public sealed partial class ScenarioLoader
NodeId = anchorNode.Id, NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId, BubbleId = anchorNode.BubbleId,
TargetKind = "station-module", TargetKind = "station-module",
TargetDefinitionId = station.Definition.Id, TargetDefinitionId = "station",
BlueprintId = moduleId, BlueprintId = moduleId,
ClaimId = claim.Id, ClaimId = claim.Id,
StationId = station.Id, StationId = station.Id,
@@ -213,8 +213,6 @@ public sealed partial class ScenarioLoader
{ {
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[] foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{ {
("gas-tank", 1),
("fuel-processor", 1),
("refinery-stack", 1), ("refinery-stack", 1),
("container-bay", 1), ("container-bay", 1),
("fabricator-array", 2), ("fabricator-array", 2),
@@ -238,7 +236,7 @@ public sealed partial class ScenarioLoader
{ {
var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f); station.PopulationCapacity = 40f + (habitatModules * 220f);
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
station.Population = habitatModules > 0 station.Population = habitatModules > 0
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f) ? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: MathF.Min(28f, station.PopulationCapacity); : MathF.Min(28f, station.PopulationCapacity);
@@ -391,21 +389,6 @@ public sealed partial class ScenarioLoader
}; };
} }
if (HasModules(definition, "reactor-core", "capacitor-bank", "gas-extractor") && refinery is not null)
{
return CreateResourceHarvestBehavior("auto-harvest-gas", scenario.MiningDefaults.NodeSystemId, refinery.Id);
}
if (string.Equals(definition.Role, "transport", StringComparison.Ordinal) && refinery is not null)
{
return new DefaultBehaviorRuntime
{
Kind = "auto-supply-energy",
StationId = refinery.Id,
Phase = "travel-to-source",
};
}
if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null) if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null)
{ {
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id); return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id);

View File

@@ -97,15 +97,15 @@ public sealed partial class ScenarioLoader
var scenario = NormalizeScenarioToAvailableSystems( var scenario = NormalizeScenarioToAvailableSystems(
Read<ScenarioDefinition>("scenario.json"), Read<ScenarioDefinition>("scenario.json"),
systems.Select((system) => system.Id).ToList()); systems.Select((system) => system.Id).ToList());
var modules = Read<List<ModuleDefinition>>("modules.json");
var ships = Read<List<ShipDefinition>>("ships.json"); var ships = Read<List<ShipDefinition>>("ships.json");
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
var items = Read<List<ItemDefinition>>("items.json"); var items = Read<List<ItemDefinition>>("items.json");
var recipes = Read<List<RecipeDefinition>>("recipes.json");
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
var balance = Read<BalanceDefinition>("balance.json"); var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships);
var moduleRecipes = BuildModuleRecipes(modules);
var moduleDefinitions = modules.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal); var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
@@ -178,7 +178,7 @@ public sealed partial class ScenarioLoader
var stationIdCounter = 0; var stationIdCounter = 0;
foreach (var plan in scenario.InitialStations) foreach (var plan in scenario.InitialStations)
{ {
if (!constructibleDefinitions.TryGetValue(plan.ConstructibleId, out var definition) || !systemsById.TryGetValue(plan.SystemId, out var system)) if (!systemsById.TryGetValue(plan.SystemId, out var system))
{ {
continue; continue;
} }
@@ -188,7 +188,8 @@ public sealed partial class ScenarioLoader
{ {
Id = $"station-{++stationIdCounter}", Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
Definition = definition, Label = plan.Label,
Color = plan.Color,
Position = placement.Position, Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId, FactionId = plan.FactionId ?? DefaultFactionId,
}; };
@@ -214,21 +215,23 @@ public sealed partial class ScenarioLoader
Id = stationBubbleId, Id = stationBubbleId,
NodeId = stationNodeId, NodeId = stationNodeId,
SystemId = station.SystemId, SystemId = station.SystemId,
Radius = MathF.Max(160f, definition.Radius + 60f), Radius = MathF.Max(160f, GetStationRadius(moduleDefinitions, station) + 60f),
}); });
localBubbles[^1].OccupantStationIds.Add(station.Id); localBubbles[^1].OccupantStationIds.Add(station.Id);
placement.AnchorNode.OccupyingStructureId = station.Id; placement.AnchorNode.OccupyingStructureId = station.Id;
foreach (var moduleId in definition.Modules) var startingModules = plan.StartingModules.Count > 0
? plan.StartingModules
: ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"];
foreach (var moduleId in startingModules)
{ {
stations[^1].InstalledModules.Add(moduleId); AddStationModule(stations[^1], moduleDefinitions, moduleId);
} }
} }
foreach (var station in stations) foreach (var station in stations)
{ {
InitializeStationPopulation(station); InitializeStationPopulation(station);
station.Inventory["fuel"] = 240f;
station.Inventory["refined-metals"] = 120f; station.Inventory["refined-metals"] = 120f;
if (station.Population > 0f) if (station.Population > 0f)
{ {
@@ -277,19 +280,6 @@ public sealed partial class ScenarioLoader
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending }, ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
Health = definition.MaxHealth, Health = definition.MaxHealth,
}); });
shipsRuntime[^1].Inventory["gas"] = definition.Id switch
{
_ => 0f,
};
shipsRuntime[^1].Inventory.Remove("gas");
shipsRuntime[^1].Inventory["fuel"] = definition.Id switch
{
"constructor" => 90f,
"miner" => 90f,
"gas-miner" => 90f,
_ => 120f,
};
} }
} }
@@ -320,6 +310,7 @@ public sealed partial class ScenarioLoader
Policies = policies, Policies = policies,
ShipDefinitions = shipDefinitions, ShipDefinitions = shipDefinitions,
ItemDefinitions = itemDefinitions, ItemDefinitions = itemDefinitions,
ModuleDefinitions = moduleDefinitions,
ModuleRecipes = moduleRecipeDefinitions, ModuleRecipes = moduleRecipeDefinitions,
Recipes = recipeDefinitions, Recipes = recipeDefinitions,
OrbitalTimeSeconds = WorldSeed * 97d, OrbitalTimeSeconds = WorldSeed * 97d,
@@ -356,8 +347,10 @@ public sealed partial class ScenarioLoader
InitialStations = scenario.InitialStations InitialStations = scenario.InitialStations
.Select((station) => new InitialStationDefinition .Select((station) => new InitialStationDefinition
{ {
ConstructibleId = station.ConstructibleId,
SystemId = ResolveSystemId(station.SystemId), SystemId = ResolveSystemId(station.SystemId),
Label = station.Label,
Color = station.Color,
StartingModules = station.StartingModules.ToList(),
FactionId = station.FactionId, FactionId = station.FactionId,
PlanetIndex = station.PlanetIndex, PlanetIndex = station.PlanetIndex,
LagrangeSide = station.LagrangeSide, LagrangeSide = station.LagrangeSide,
@@ -404,15 +397,37 @@ public sealed partial class ScenarioLoader
: raw; : raw;
} }
private static bool HasModules(ConstructibleDefinition definition, params string[] modules) =>
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
private static bool HasInstalledModules(StationRuntime station, params string[] modules) => private static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
modules.All((moduleId) => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); modules.All((moduleId) => station.Modules.Any((candidate) => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
private static bool HasModules(ShipDefinition definition, params string[] modules) => private static bool HasModules(ShipDefinition definition, params string[] modules) =>
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
private static void AddStationModule(StationRuntime station, IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, string moduleId)
{
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
{
return;
}
station.Modules.Add(new StationModuleRuntime
{
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(moduleDefinitions, station);
}
private static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station)
{
var totalArea = station.Modules
.Select((module) => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
.Sum();
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
}
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z); private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
private static int CountModules(IEnumerable<string> modules, string moduleId) => private static int CountModules(IEnumerable<string> modules, string moduleId) =>
@@ -429,6 +444,89 @@ public sealed partial class ScenarioLoader
return 0.1f + (0.9f * staffedRatio); return 0.1f + (0.9f * staffedRatio);
} }
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules
.Where((module) => module.Construction is not null)
.Select((module) => new ModuleRecipeDefinition
{
ModuleId = module.Id,
Duration = module.Construction!.ProductionTime,
Inputs = module.Construction.Requirements
.Select((input) => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
})
.ToList();
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships)
{
var recipes = new List<RecipeDefinition>();
foreach (var item in items)
{
if (item.Construction is null)
{
continue;
}
recipes.Add(new RecipeDefinition
{
Id = item.Construction.RecipeId ?? $"{item.Id}-production",
Label = item.Name,
FacilityCategory = item.Construction.FacilityCategory,
Duration = item.Construction.CycleTime,
Priority = item.Construction.Priority,
RequiredModules = item.Construction.RequiredModules.ToList(),
Inputs = item.Construction.Requirements
.Select((input) => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
Outputs =
[
new RecipeOutputDefinition
{
ItemId = item.Id,
Amount = item.Construction.BatchSize,
},
],
});
}
foreach (var ship in ships)
{
if (ship.Construction is null)
{
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
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
ShipOutputId = ship.Id,
});
}
return recipes;
}
private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale); private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f); private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);

View File

@@ -2,312 +2,274 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine public sealed partial class SimulationEngine
{ {
private const float WarpEngageDistanceKilometers = 250_000f; private const float WarpEngageDistanceKilometers = 250_000f;
private static float GetLocalTravelSpeed(ShipRuntime ship) => private static float GetLocalTravelSpeed(ShipRuntime ship) =>
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed); SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
private static float GetWarpTravelSpeed(ShipRuntime ship) => private static float GetWarpTravelSpeed(ShipRuntime ship) =>
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed); SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
?? Vector3.Zero; ?? Vector3.Zero;
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds) private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
return task.Kind switch
{ {
var task = ship.ControllerTask; ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
return task.Kind switch ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
{ ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds), ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds), ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds), ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds), ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds), ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds), ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds),
ControllerTaskKind.Refuel => UpdateRefuel(ship, world, deltaSeconds), ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds),
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds), ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds), ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds), _ => UpdateIdle(ship, world, deltaSeconds),
ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds), };
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds), }
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
_ => UpdateIdle(ship, world, deltaSeconds), private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
}; {
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
if (task.TargetPosition is null || task.TargetSystemId is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
} }
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds) var targetPosition = task.TargetPosition.Value;
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
ship.TargetPosition = targetPosition;
if (ship.SystemId != task.TargetSystemId)
{ {
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds); var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
ship.State = ShipState.Idle; var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
ship.TargetPosition = ship.Position; return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
return "none";
} }
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds) var currentNode = ResolveCurrentNode(world, ship);
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
{ {
var task = ship.ControllerTask; return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
if (task.TargetPosition is null || task.TargetSystemId is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var targetPosition = task.TargetPosition.Value;
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
ship.TargetPosition = targetPosition;
if (ship.SystemId != task.TargetSystemId)
{
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
}
var currentNode = ResolveCurrentNode(world, ship);
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
{
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
}
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
} }
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition) return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
}
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
{
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
{ {
if (!string.IsNullOrWhiteSpace(task.TargetEntityId)) var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
{ if (station?.NodeId is not null)
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); {
if (station?.NodeId is not null) return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
{ }
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
}
var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId); var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (node is not null) if (node is not null)
{ {
return node; return node;
} }
}
return world.SpatialNodes
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
.FirstOrDefault();
} }
private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship) return world.SpatialNodes
{ .Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
if (ship.SpatialState.CurrentNodeId is not null) .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
{ .FirstOrDefault();
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId); }
}
return world.SpatialNodes private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship)
.Where(candidate => candidate.SystemId == ship.SystemId) {
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) if (ship.SpatialState.CurrentNodeId is not null)
.FirstOrDefault(); {
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId);
} }
private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) => return world.SpatialNodes
world.SpatialNodes.FirstOrDefault(candidate => .Where(candidate => candidate.SystemId == ship.SystemId)
candidate.SystemId == systemId && .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
candidate.Kind == SpatialNodeKind.Star); .FirstOrDefault();
}
private string UpdateLocalTravel( private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) =>
ShipRuntime ship, world.SpatialNodes.FirstOrDefault(candidate =>
SimulationWorld world, candidate.SystemId == systemId &&
float deltaSeconds, candidate.Kind == SpatialNodeKind.Star);
string targetSystemId,
Vector3 targetPosition, private string UpdateLocalTravel(
NodeRuntime? targetNode, ShipRuntime ship,
float threshold) SimulationWorld world,
float deltaSeconds,
string targetSystemId,
Vector3 targetPosition,
NodeRuntime? targetNode,
float threshold)
{
var distance = ship.Position.DistanceTo(targetPosition);
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.Transit = null;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
if (distance <= threshold)
{ {
var distance = ship.Position.DistanceTo(targetPosition); ship.ActionTimer = 0f;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace; ship.Position = targetPosition;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; ship.TargetPosition = ship.Position;
ship.SpatialState.Transit = null; ship.SystemId = targetSystemId;
ship.SpatialState.DestinationNodeId = targetNode?.Id; ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.State = ShipState.Arriving;
return "arrived";
}
if (distance <= threshold) ship.ActionTimer = 0f;
{ ship.State = ShipState.LocalFlight;
ship.ActionTimer = 0f; ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds); return "none";
ship.Position = targetPosition; }
ship.TargetPosition = ship.Position;
ship.SystemId = targetSystemId;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.State = ShipState.Arriving;
return "arrived";
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode)
{ {
ship.State = ShipState.CapacitorStarved; var transit = ship.SpatialState.Transit;
ship.TargetPosition = ship.Position; if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id)
return "none"; {
} transit = new ShipTransitRuntime
{
Regime = MovementRegimeKinds.Warp,
OriginNodeId = ship.SpatialState.CurrentNodeId,
DestinationNodeId = targetNode.Id,
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.DestinationNodeId = targetNode.Id;
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
if (ship.State != ShipState.Warping)
{
if (ship.State != ShipState.SpoolingWarp)
{
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
ship.State = ShipState.LocalFlight; }
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
{
return "none"; return "none";
}
ship.State = ShipState.Warping;
} }
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode) var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition)
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
return ship.Position.DistanceTo(targetPosition) <= 18f
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
: "none";
}
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
{
var destinationNodeId = targetNode?.Id;
var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
{ {
var transit = ship.SpatialState.Transit; transit = new ShipTransitRuntime
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id) {
{ Regime = MovementRegimeKinds.FtlTransit,
transit = new ShipTransitRuntime OriginNodeId = ship.SpatialState.CurrentNodeId,
{ DestinationNodeId = destinationNodeId,
Regime = MovementRegimeKinds.Warp, StartedAtUtc = world.GeneratedAtUtc,
OriginNodeId = ship.SpatialState.CurrentNodeId, };
DestinationNodeId = targetNode.Id, ship.SpatialState.Transit = transit;
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.DestinationNodeId = targetNode.Id;
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
if (ship.State != ShipState.Warping)
{
if (ship.State != ShipState.SpoolingWarp)
{
ship.ActionTimer = 0f;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
{
return "none";
}
ship.State = ShipState.Warping;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition)
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
return ship.Position.DistanceTo(targetPosition) <= 18f
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
: "none";
} }
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
{ ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
var destinationNodeId = targetNode?.Id; ship.SpatialState.CurrentNodeId = null;
var transit = ship.SpatialState.Transit; ship.SpatialState.CurrentBubbleId = null;
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId) ship.SpatialState.DestinationNodeId = destinationNodeId;
{
transit = new ShipTransitRuntime if (ship.State != ShipState.Ftl)
{
Regime = MovementRegimeKinds.FtlTransit,
OriginNodeId = ship.SpatialState.CurrentNodeId,
DestinationNodeId = destinationNodeId,
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.DestinationNodeId = destinationNodeId;
if (ship.State != ShipState.Ftl)
{
if (ship.State != ShipState.SpoolingFtl)
{
ship.ActionTimer = 0f;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.SpoolingFtl;
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
{
return "none";
}
ship.State = ShipState.Ftl;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
return transit.Progress >= 0.999f
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
: "none";
}
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
{ {
if (ship.State != ShipState.SpoolingFtl)
{
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
ship.Position = targetPosition; }
ship.TargetPosition = targetPosition;
ship.SystemId = targetSystemId;
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode) ship.State = ShipState.SpoolingFtl;
{ if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
ship.ActionTimer = 0f; {
ship.Position = targetPosition;
ship.TargetPosition = targetPosition;
ship.SystemId = targetSystemId;
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.State = ShipState.Arriving;
return "none"; return "none";
}
ship.State = ShipState.Ftl;
} }
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
return transit.Progress >= 0.999f
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
: "none";
}
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
{
ship.ActionTimer = 0f;
ship.Position = targetPosition;
ship.TargetPosition = targetPosition;
ship.SystemId = targetSystemId;
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
{
ship.ActionTimer = 0f;
ship.Position = targetPosition;
ship.TargetPosition = targetPosition;
ship.SystemId = targetSystemId;
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.State = ShipState.Arriving;
return "none";
}
} }

View File

@@ -57,7 +57,7 @@ public sealed partial class SimulationEngine
private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node) private static float ComputeResourceNodeOrbitSpeed(ResourceNodeRuntime node)
{ {
var baseSpeed = node.SourceKind == "gas-cloud" ? 0.16f : 0.24f; var baseSpeed = 0.24f;
return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f)); return baseSpeed / MathF.Sqrt(MathF.Max(node.OrbitRadius / 180000f, 0.45f));
} }

View File

@@ -5,560 +5,184 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine public sealed partial class SimulationEngine
{ {
private const float StationEnergyCellToEnergyRatio = 1f; private static bool HasShipModules(ShipDefinition definition, params string[] modules) =>
modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
private static bool HasShipModules(ShipDefinition definition, params string[] modules) => private static bool CanTransportWorkers(ShipRuntime ship) =>
modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); CountModules(ship.Definition.Modules, "habitat-ring") > 0;
private static bool CanTransportWorkers(ShipRuntime ship) => private static float GetWorkerTransportCapacity(ShipRuntime ship) =>
CountModules(ship.Definition.Modules, "habitat-ring") > 0; CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
private static float GetWorkerTransportCapacity(ShipRuntime ship) => private static int CountStationModules(StationRuntime station, string moduleId) =>
CountModules(ship.Definition.Modules, "habitat-ring") * 120f; station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
private static void UpdateStationPower(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events) private static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
{ {
foreach (var station in world.Stations) return;
{
var previousEnergy = station.EnergyStored;
GenerateStationEnergy(station, world, deltaSeconds);
if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "fuel") <= 0.01f)
{
events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
}
}
} }
private static void UpdateShipPower(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events) station.Modules.Add(new StationModuleRuntime
{ {
var previousEnergy = ship.EnergyStored; Id = $"{station.Id}-module-{station.Modules.Count + 1}",
GenerateShipEnergy(ship, world, deltaSeconds); ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(world, station);
}
if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "fuel") <= 0.01f) private static float GetStationRadius(SimulationWorld world, StationRuntime station)
{ {
events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow)); var totalArea = station.Modules
} .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
.Sum();
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
}
private static float GetStationStorageCapacity(StationRuntime station, string storageClass)
{
var baseCapacity = storageClass switch
{
"manufactured" => 400f,
_ => 0f,
};
var bulkBays = CountStationModules(station, "bulk-bay");
var liquidTanks = CountStationModules(station, "liquid-tank");
var containerBays = CountStationModules(station, "container-bay");
var moduleCapacity = storageClass switch
{
"bulk-solid" => bulkBays * 1000f,
"bulk-liquid" => liquidTanks * 500f,
"container" => containerBays * 800f,
"manufactured" => containerBays * 200f,
_ => 0f,
};
return baseCapacity + moduleCapacity;
}
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
{
if (amount <= 0f)
{
return;
} }
private static void GenerateStationEnergy(StationRuntime station, SimulationWorld world, float deltaSeconds) inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
}
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
{
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
var removed = MathF.Min(current, amount);
var remaining = current - removed;
if (remaining <= 0.001f)
{ {
var powerCores = CountModules(station.InstalledModules, "power-core"); inventory.Remove(itemId);
var tanks = CountModules(station.InstalledModules, "liquid-tank"); }
if (powerCores <= 0 || tanks <= 0) else
{ {
station.EnergyStored = 0f; inventory[itemId] = remaining;
station.Inventory.Remove("fuel");
return;
}
var energyCapacity = powerCores * StationEnergyPerPowerCore;
var fuelStored = GetInventoryAmount(station.Inventory, "fuel");
var desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
if (desiredEnergy <= 0.01f)
{
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
return;
}
var solarGenerated = MathF.Min(desiredEnergy, GetStationSolarGeneration(station, world) * deltaSeconds);
if (solarGenerated > 0.01f)
{
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + solarGenerated);
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
}
if (desiredEnergy > 0.01f && fuelStored <= 0.01f)
{
var energyCells = GetInventoryAmount(station.Inventory, "energy-cell");
if (energyCells > 0.01f)
{
var consumedCells = MathF.Min(energyCells, desiredEnergy / StationEnergyCellToEnergyRatio);
RemoveInventory(station.Inventory, "energy-cell", consumedCells);
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + (consumedCells * StationEnergyCellToEnergyRatio));
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
}
}
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
{
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
return;
}
var generated = MathF.Min(desiredEnergy, powerCores * 24f * deltaSeconds);
var requiredFuel = generated / StationFuelToEnergyRatio;
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
var actualGenerated = consumedFuel * StationFuelToEnergyRatio;
RemoveInventory(station.Inventory, "fuel", consumedFuel);
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + actualGenerated);
} }
private static float GetStationFuelCapacity(StationRuntime station) => return removed;
CountModules(station.InstalledModules, "liquid-tank") * StationFuelPerTank; }
private static float GetStationEnergyCapacity(StationRuntime station) => private static bool HasStationModules(StationRuntime station, params string[] modules) =>
CountModules(station.InstalledModules, "power-core") * StationEnergyPerPowerCore; modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
private static float GetStationSolarGeneration(StationRuntime station, SimulationWorld world) => private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) =>
world.Balance.Energy.StationSolarCharge * (1f + CountModules(station.InstalledModules, "solar-array")); node.ItemId switch
{
"ore" => HasShipModules(ship.Definition, "mining-turret"),
_ => false,
};
private static float GetStationStorageCapacity(StationRuntime station, string storageClass) private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
private static float ComputeWorkforceRatio(float population, float workforceRequired)
{
if (workforceRequired <= 0.01f)
{ {
var baseCapacity = station.Definition.Storage.TryGetValue(storageClass, out var capacity) return 1f;
? capacity
: 0f;
var extraBulkBays = Math.Max(0, CountModules(station.InstalledModules, "bulk-bay") - CountModules(station.Definition.Modules, "bulk-bay"));
var extraLiquidTanks = Math.Max(0, CountModules(station.InstalledModules, "liquid-tank") - CountModules(station.Definition.Modules, "liquid-tank"));
var extraGasTanks = Math.Max(0, CountModules(station.InstalledModules, "gas-tank") - CountModules(station.Definition.Modules, "gas-tank"));
var extraContainerBays = Math.Max(0, CountModules(station.InstalledModules, "container-bay") - CountModules(station.Definition.Modules, "container-bay"));
var moduleBonus = storageClass switch
{
"bulk-solid" => extraBulkBays * 1000f,
"bulk-liquid" => extraLiquidTanks * 500f,
"bulk-gas" => extraGasTanks * 500f,
"container" => extraContainerBays * 800f,
_ => 0f,
};
return baseCapacity + moduleBonus;
} }
private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds) var staffedRatio = MathF.Min(1f, population / workforceRequired);
return 0.1f + (0.9f * staffedRatio);
}
private static string? GetStorageRequirement(string storageClass) =>
storageClass switch
{
"bulk-solid" => "bulk-bay",
"bulk-liquid" => "liquid-tank",
_ => null,
};
private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{ {
var reactors = CountModules(ship.Definition.Modules, "reactor-core"); return 0f;
var capacitors = CountModules(ship.Definition.Modules, "capacitor-bank");
if (reactors <= 0 || capacitors <= 0)
{
ship.EnergyStored = 0f;
ship.Inventory.Remove("fuel");
return;
}
var energyCapacity = capacitors * CapacitorEnergyPerModule;
var fuelCapacity = reactors * ShipFuelPerReactor;
var fuelStored = GetInventoryAmount(ship.Inventory, "fuel");
var desiredEnergy = MathF.Max(0f, energyCapacity - ship.EnergyStored);
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
{
ship.EnergyStored = MathF.Min(ship.EnergyStored, energyCapacity);
ship.Inventory["fuel"] = MathF.Min(fuelStored, fuelCapacity);
return;
}
var generated = MathF.Min(desiredEnergy, world.Balance.Energy.ShipRechargeRate * reactors * deltaSeconds);
var requiredFuel = generated / ShipFuelToEnergyRatio;
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
var actualGenerated = consumedFuel * ShipFuelToEnergyRatio;
RemoveInventory(ship.Inventory, "fuel", consumedFuel);
ship.EnergyStored = MathF.Min(energyCapacity, ship.EnergyStored + actualGenerated);
} }
private static bool TryConsumeShipEnergy(ShipRuntime ship, float amount) var storageClass = itemDefinition.CargoKind;
var requiredModule = GetStorageRequirement(storageClass);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{ {
if (ship.EnergyStored + 0.0001f < amount) return 0f;
{
return false;
}
ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount);
return true;
} }
private static bool TryConsumeStationEnergy(StationRuntime station, float amount) var capacity = GetStationStorageCapacity(station, storageClass);
if (capacity <= 0.01f)
{ {
if (station.EnergyStored + 0.0001f < amount) return 0f;
{
return false;
}
station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount);
return true;
} }
private static int CountModules(IEnumerable<string> modules, string moduleId) => var used = station.Inventory
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
.Sum(entry => entry.Value);
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) => var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
inventory.TryGetValue(itemId, out var amount) ? amount : 0f; if (accepted <= 0.01f)
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
{ {
if (amount <= 0f) return 0f;
{
return;
}
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
} }
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount) AddInventory(station.Inventory, itemId, accepted);
{ return accepted;
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId); }
var removed = MathF.Min(current, amount);
var remaining = current - removed;
if (remaining <= 0.001f)
{
inventory.Remove(itemId);
}
else
{
inventory[itemId] = remaining;
}
return removed; private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
world.ConstructionSites.FirstOrDefault(site =>
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
{
if (site.StationId is not null
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
{
return GetInventoryAmount(station.Inventory, itemId);
} }
private static bool HasStationModules(StationRuntime station, params string[] modules) => return GetInventoryAmount(site.DeliveredItems, itemId);
modules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); }
private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) => private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
node.ItemId switch site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
{
"ore" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "mining-turret"),
"gas" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "gas-extractor"),
_ => false,
};
private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
private static float GetShipFuelCapacity(ShipRuntime ship) =>
CountModules(ship.Definition.Modules, "reactor-core") * ShipFuelPerReactor;
private static float GetShipAvailableEnergyBudget(ShipRuntime ship) =>
ship.EnergyStored + (GetInventoryAmount(ship.Inventory, "fuel") * ShipFuelToEnergyRatio);
private static float GetShipFuelReserve(ShipRuntime ship, float plannedFuel)
{
var capacity = GetShipFuelCapacity(ship);
var reserveRatio = ship.Definition.CargoItemId == "gas" ? 0.4f : 0.3f;
var reserve = MathF.Max(16f, MathF.Max(capacity * 0.18f, plannedFuel * reserveRatio));
return MathF.Min(capacity, reserve);
}
private static float EstimateFuelForEnergyDemand(ShipRuntime ship, float energyDemand) =>
MathF.Max(0f, energyDemand - ship.EnergyStored) / ShipFuelToEnergyRatio;
private static float EstimateTimedEnergyUse(SimulationWorld world, float durationSeconds, float drainPerSecond) =>
MathF.Max(0f, durationSeconds) * drainPerSecond;
private static float EstimateTravelEnergy(
ShipRuntime ship,
SimulationWorld world,
Vector3 fromPosition,
string fromSystemId,
Vector3 toPosition,
string toSystemId)
{
if (!string.Equals(fromSystemId, toSystemId, StringComparison.Ordinal))
{
var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId);
var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition;
var originSystemPosition = ResolveSystemGalaxyPosition(world, fromSystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, toSystemId);
var ftlDistance = originSystemPosition.DistanceTo(destinationSystemPosition);
var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f);
return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain)
+ EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain)
+ EstimateInSystemTravelEnergy(ship, world, destinationEntryPosition, toPosition);
}
return EstimateInSystemTravelEnergy(ship, world, fromPosition, toPosition);
}
private static float EstimateInSystemTravelEnergy(ShipRuntime ship, SimulationWorld world, Vector3 fromPosition, Vector3 toPosition)
{
var distance = fromPosition.DistanceTo(toPosition);
if (distance <= world.Balance.ArrivalThreshold)
{
return 0f;
}
if (distance <= WarpEngageDistanceKilometers)
{
var localDuration = distance / MathF.Max(GetLocalTravelSpeed(ship), 0.01f);
return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain);
}
var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
var warpDuration = distance / MathF.Max(GetWarpTravelSpeed(ship), 0.01f);
return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain)
+ EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain);
}
private static float EstimateDockingEnergy(SimulationWorld world) =>
EstimateTimedEnergyUse(world, world.Balance.DockingDuration, world.Balance.Energy.MoveDrain)
+ EstimateTimedEnergyUse(world, 6f, world.Balance.Energy.IdleDrain);
private static float EstimateUndockingEnergy(SimulationWorld world) =>
EstimateTimedEnergyUse(world, world.Balance.UndockingDuration, world.Balance.Energy.MoveDrain)
+ EstimateTimedEnergyUse(world, 4f, world.Balance.Energy.IdleDrain);
private static float EstimateExtractionEnergy(ShipRuntime ship, SimulationWorld world)
{
var remainingCargo = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
if (remainingCargo <= 0.01f)
{
return 0f;
}
var cycles = MathF.Ceiling(remainingCargo / MathF.Max(world.Balance.MiningRate, 0.01f));
return EstimateTimedEnergyUse(world, cycles * world.Balance.MiningCycleSeconds, world.Balance.Energy.MoveDrain)
+ EstimateTimedEnergyUse(world, cycles * 1.5f, world.Balance.Energy.IdleDrain);
}
private static float EstimateConstructionEnergy(ShipRuntime ship, SimulationWorld world, StationRuntime station)
{
var holdPosition = GetConstructionHoldPosition(station, ship.Id);
var travelEnergy = EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, holdPosition, station.SystemId);
var site = GetConstructionSiteForStation(world, station.Id);
if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site))
{
if (world.ModuleRecipes.TryGetValue(site.BlueprintId ?? string.Empty, out var siteRecipe))
{
return travelEnergy + EstimateTimedEnergyUse(world, siteRecipe.Duration, world.Balance.Energy.IdleDrain);
}
return travelEnergy;
}
var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world);
if (moduleId is not null
&& world.ModuleRecipes.TryGetValue(moduleId, out var recipe)
&& CanStartModuleConstruction(station, recipe))
{
return travelEnergy + EstimateTimedEnergyUse(world, recipe.Duration, world.Balance.Energy.IdleDrain);
}
return travelEnergy;
}
private static float EstimateResourceHarvestEnergy(ShipRuntime ship, SimulationWorld world)
{
var cargoItemId = ship.Definition.CargoItemId;
if (cargoItemId is null)
{
return 0f;
}
var requiredModule = cargoItemId == "gas" ? "gas-extractor" : "mining-turret";
var behavior = ship.DefaultBehavior;
var refinery = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
var node = behavior.NodeId is null
? world.Nodes
.Where(candidate =>
(behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) &&
candidate.ItemId == cargoItemId &&
candidate.OreRemaining > 0.01f)
.OrderByDescending(candidate => candidate.OreRemaining)
.FirstOrDefault()
: world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f);
if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule))
{
return 0f;
}
var currentPosition = ship.Position;
var currentSystemId = ship.SystemId;
var energy = 0f;
var cargoAmount = GetShipCargoAmount(ship);
if (ship.DockedStationId == refinery.Id)
{
currentPosition = GetUndockTargetPosition(refinery, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
currentSystemId = refinery.SystemId;
energy += EstimateUndockingEnergy(world);
}
if (cargoAmount > 0.01f)
{
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId);
return energy + EstimateDockingEnergy(world);
}
var holdPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f);
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, holdPosition, node.SystemId);
energy += EstimateExtractionEnergy(ship, world);
energy += EstimateTravelEnergy(ship, world, holdPosition, node.SystemId, refinery.Position, refinery.SystemId);
energy += EstimateDockingEnergy(world);
return energy;
}
private static float EstimateResourceReturnEnergy(ShipRuntime ship, SimulationWorld world)
{
var cargoItemId = ship.Definition.CargoItemId;
if (cargoItemId is null)
{
return 0f;
}
var refinery = SelectBestBuyStation(world, ship, cargoItemId, ship.DefaultBehavior.StationId);
if (refinery is null)
{
return 0f;
}
var currentPosition = ship.Position;
var currentSystemId = ship.SystemId;
return EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId)
+ EstimateDockingEnergy(world);
}
private static float EstimateTransportEnergy(ShipRuntime ship, SimulationWorld world)
{
var cargoItemId = ship.Definition.CargoItemId;
if (cargoItemId is null)
{
return 0f;
}
var behavior = ship.DefaultBehavior;
var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId);
var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
if (source is null && destination is null)
{
return 0f;
}
var cargoAmount = GetShipCargoAmount(ship);
var currentPosition = ship.Position;
var currentSystemId = ship.SystemId;
if (ship.DockedStationId is not null)
{
var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (dockedStation is not null)
{
currentPosition = GetUndockTargetPosition(dockedStation, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
currentSystemId = dockedStation.SystemId;
}
}
var targetStation = cargoAmount > 0.01f ? destination : source;
if (targetStation is null)
{
return ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
}
var energy = ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, targetStation.Position, targetStation.SystemId);
return energy + EstimateDockingEnergy(world);
}
private static float EstimateShipMissionEnergyDemand(ShipRuntime ship, SimulationWorld world) =>
ship.DefaultBehavior.Kind switch
{
"auto-mine" or "auto-harvest-gas" => EstimateResourceHarvestEnergy(ship, world),
"auto-supply-energy" => EstimateTransportEnergy(ship, world),
"construct-station" when ship.DefaultBehavior.StationId is not null
=> world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) is { } station
? EstimateConstructionEnergy(ship, world, station)
: 0f,
_ when ship.ControllerTask.TargetPosition is { } targetPosition && ship.ControllerTask.TargetSystemId is { } targetSystemId
=> EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, targetPosition, targetSystemId),
_ => 0f,
};
private static float GetShipRefuelTarget(ShipRuntime ship, SimulationWorld world)
{
var capacity = GetShipFuelCapacity(ship);
var missionFuel = EstimateFuelForEnergyDemand(ship, EstimateShipMissionEnergyDemand(ship, world));
var reserveFuel = GetShipFuelReserve(ship, missionFuel);
return MathF.Min(capacity, missionFuel + reserveFuel);
}
internal static bool NeedsRefuel(ShipRuntime ship, SimulationWorld world) =>
GetInventoryAmount(ship.Inventory, "fuel") + 0.01f < GetShipRefuelTarget(ship, world);
internal static bool NeedsEmergencyReturn(ShipRuntime ship, SimulationWorld world)
{
if (ship.DefaultBehavior.Kind is not "auto-mine" and not "auto-harvest-gas")
{
return false;
}
var returnEnergy = EstimateResourceReturnEnergy(ship, world);
var reserveFuel = GetShipFuelReserve(ship, EstimateFuelForEnergyDemand(ship, returnEnergy));
var requiredBudget = returnEnergy + (reserveFuel * ShipFuelToEnergyRatio);
return GetShipAvailableEnergyBudget(ship) + 0.01f < requiredBudget;
}
private static float ComputeWorkforceRatio(float population, float workforceRequired)
{
if (workforceRequired <= 0.01f)
{
return 1f;
}
var staffedRatio = MathF.Min(1f, population / workforceRequired);
return 0.1f + (0.9f * staffedRatio);
}
private static string? GetStorageRequirement(string storageClass) =>
storageClass switch
{
"bulk-solid" => "bulk-bay",
"bulk-liquid" => "liquid-tank",
"bulk-gas" => "gas-tank",
_ => null,
};
private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{
return 0f;
}
var storageClass = itemDefinition.Storage;
var requiredModule = GetStorageRequirement(storageClass);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return 0f;
}
var capacity = GetStationStorageCapacity(station, storageClass);
if (capacity <= 0.01f)
{
return 0f;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == storageClass)
.Sum(entry => entry.Value);
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
if (accepted <= 0.01f)
{
return 0f;
}
AddInventory(station.Inventory, itemId, accepted);
return accepted;
}
private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
world.ConstructionSites.FirstOrDefault(site =>
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
{
if (site.StationId is not null
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
{
return GetInventoryAmount(station.Inventory, itemId);
}
return GetInventoryAmount(site.DeliveredItems, itemId);
}
private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,303 +5,299 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine public sealed partial class SimulationEngine
{ {
private static void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events) private static void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
{
foreach (var claim in world.Claims)
{ {
foreach (var claim in world.Claims) if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f)
{
if (claim.State != ClaimStateKinds.Destroyed)
{ {
if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f) claim.State = ClaimStateKinds.Destroyed;
{ events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
if (claim.State != ClaimStateKinds.Destroyed)
{
claim.State = ClaimStateKinds.Destroyed;
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
}
foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
{
site.State = ConstructionSiteStateKinds.Destroyed;
}
continue;
}
if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
{
claim.State = ClaimStateKinds.Active;
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
}
} }
foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
{
site.State = ConstructionSiteStateKinds.Destroyed;
}
continue;
}
if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
{
claim.State = ClaimStateKinds.Active;
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
}
}
}
private static void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
{
foreach (var site in world.ConstructionSites)
{
if (site.State == ConstructionSiteStateKinds.Destroyed)
{
continue;
}
var claim = site.ClaimId is null
? null
: world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
if (claim?.State == ClaimStateKinds.Destroyed)
{
site.State = ConstructionSiteStateKinds.Destroyed;
continue;
}
if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
{
site.State = ConstructionSiteStateKinds.Active;
events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
}
foreach (var orderId in site.MarketOrderIds)
{
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
{
continue;
}
var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
order.RemainingAmount = remaining;
order.State = remaining <= 0.01f
? MarketOrderStateKinds.Filled
: remaining < order.Amount
? MarketOrderStateKinds.PartiallyFilled
: MarketOrderStateKinds.Open;
}
}
}
private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId)
{
if (station.ActiveConstruction is not null)
{
return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
&& string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
} }
private static void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events) if (!CanStartModuleConstruction(station, recipe))
{ {
foreach (var site in world.ConstructionSites) return false;
{
if (site.State == ConstructionSiteStateKinds.Destroyed)
{
continue;
}
var claim = site.ClaimId is null
? null
: world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
if (claim?.State == ClaimStateKinds.Destroyed)
{
site.State = ConstructionSiteStateKinds.Destroyed;
continue;
}
if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
{
site.State = ConstructionSiteStateKinds.Active;
events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
}
foreach (var orderId in site.MarketOrderIds)
{
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
{
continue;
}
var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
order.RemainingAmount = remaining;
order.State = remaining <= 0.01f
? MarketOrderStateKinds.Filled
: remaining < order.Amount
? MarketOrderStateKinds.PartiallyFilled
: MarketOrderStateKinds.Open;
}
}
} }
private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId) foreach (var input in recipe.Inputs)
{ {
if (station.ActiveConstruction is not null) RemoveInventory(station.Inventory, input.ItemId, input.Amount);
{ }
return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
&& string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
}
if (!CanStartModuleConstruction(station, recipe)) station.ActiveConstruction = new ModuleConstructionRuntime
{ {
return false; ModuleId = recipe.ModuleId,
} RequiredSeconds = recipe.Duration,
AssignedConstructorShipId = shipId,
};
foreach (var input in recipe.Inputs) return true;
{ }
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
}
station.ActiveConstruction = new ModuleConstructionRuntime private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
{
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
? new (string ModuleId, int TargetCount)[]
{ {
ModuleId = recipe.ModuleId, ("refinery-stack", 1),
RequiredSeconds = recipe.Duration, ("container-bay", 1),
AssignedConstructorShipId = shipId, ("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("dock-bay-small", 2),
("solar-array", 2),
}
: new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("solar-array", 2),
("dock-bay-small", 2),
}; };
return true; foreach (var (moduleId, targetCount) in priorities)
}
private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
{ {
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f if (CountModules(station.InstalledModules, moduleId) < targetCount
? new (string ModuleId, int TargetCount)[] && world.ModuleRecipes.ContainsKey(moduleId))
{ {
("gas-tank", 1), return moduleId;
("fuel-processor", 1), }
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("dock-bay-small", 2),
("solar-array", 2),
}
: new (string ModuleId, int TargetCount)[]
{
("gas-tank", 1),
("fuel-processor", 1),
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
("ship-factory", 1),
("solar-array", 2),
("dock-bay-small", 2),
};
foreach (var (moduleId, targetCount) in priorities)
{
if (CountModules(station.InstalledModules, moduleId) < targetCount
&& world.ModuleRecipes.ContainsKey(moduleId))
{
return moduleId;
}
}
return null;
} }
private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site) return null;
}
private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
{
var nextModuleId = GetNextStationModuleToBuild(station, world);
foreach (var orderId in site.MarketOrderIds)
{ {
var nextModuleId = GetNextStationModuleToBuild(station, world); var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
foreach (var orderId in site.MarketOrderIds) if (order is not null)
{ {
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId); order.State = MarketOrderStateKinds.Cancelled;
if (order is not null) order.RemainingAmount = 0f;
{ world.MarketOrders.Remove(order);
order.State = MarketOrderStateKinds.Cancelled; }
order.RemainingAmount = 0f;
world.MarketOrders.Remove(order);
}
station.MarketOrderIds.Remove(orderId); station.MarketOrderIds.Remove(orderId);
}
site.MarketOrderIds.Clear();
site.Inventory.Clear();
site.DeliveredItems.Clear();
site.RequiredItems.Clear();
site.AssignedConstructorShipIds.Clear();
site.Progress = 0f;
if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
{
site.State = ConstructionSiteStateKinds.Completed;
site.BlueprintId = null;
return;
}
site.BlueprintId = nextModuleId;
site.State = ConstructionSiteStateKinds.Active;
foreach (var input in recipe.Inputs)
{
site.RequiredItems[input.ItemId] = input.Amount;
site.DeliveredItems[input.ItemId] = 0f;
var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
site.MarketOrderIds.Add(orderId);
station.MarketOrderIds.Add(orderId);
world.MarketOrders.Add(new MarketOrderRuntime
{
Id = orderId,
FactionId = station.FactionId,
StationId = station.Id,
ConstructionSiteId = site.Id,
Kind = MarketOrderKinds.Buy,
ItemId = input.ItemId,
Amount = input.Amount,
RemainingAmount = input.Amount,
Valuation = 1f,
State = MarketOrderStateKinds.Open,
});
}
} }
private static int GetDockingPadCount(StationRuntime station) => site.MarketOrderIds.Clear();
CountModules(station.InstalledModules, "dock-bay-small") * 2; site.Inventory.Clear();
site.DeliveredItems.Clear();
site.RequiredItems.Clear();
site.AssignedConstructorShipIds.Clear();
site.Progress = 0f;
private static int? ReserveDockingPad(StationRuntime station, string shipId) if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
{ {
if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing site.State = ConstructionSiteStateKinds.Completed;
&& !string.IsNullOrEmpty(existing.Value)) site.BlueprintId = null;
{ return;
return existing.Key;
}
var padCount = GetDockingPadCount(station);
for (var padIndex = 0; padIndex < padCount; padIndex += 1)
{
if (station.DockingPadAssignments.ContainsKey(padIndex))
{
continue;
}
station.DockingPadAssignments[padIndex] = shipId;
return padIndex;
}
return null;
} }
private static void ReleaseDockingPad(StationRuntime station, string shipId) site.BlueprintId = nextModuleId;
site.State = ConstructionSiteStateKinds.Active;
foreach (var input in recipe.Inputs)
{ {
var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)); site.RequiredItems[input.ItemId] = input.Amount;
if (!string.IsNullOrEmpty(assignment.Value)) site.DeliveredItems[input.ItemId] = 0f;
{ var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
station.DockingPadAssignments.Remove(assignment.Key); site.MarketOrderIds.Add(orderId);
} station.MarketOrderIds.Add(orderId);
world.MarketOrders.Add(new MarketOrderRuntime
{
Id = orderId,
FactionId = station.FactionId,
StationId = station.Id,
ConstructionSiteId = site.Id,
Kind = MarketOrderKinds.Buy,
ItemId = input.ItemId,
Amount = input.Amount,
RemainingAmount = input.Amount,
Valuation = 1f,
State = MarketOrderStateKinds.Open,
});
} }
}
private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex) private static int GetDockingPadCount(StationRuntime station) =>
CountModules(station.InstalledModules, "dock-bay-small") * 2;
private static int? ReserveDockingPad(StationRuntime station, string shipId)
{
if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
&& !string.IsNullOrEmpty(existing.Value))
{ {
var padCount = Math.Max(1, GetDockingPadCount(station)); return existing.Key;
var angle = ((MathF.PI * 2f) / padCount) * padIndex;
var radius = station.Definition.Radius + 18f;
return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y,
station.Position.Z + (MathF.Sin(angle) * radius));
} }
private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId) var padCount = GetDockingPadCount(station);
for (var padIndex = 0; padIndex < padCount; padIndex += 1)
{ {
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); if (station.DockingPadAssignments.ContainsKey(padIndex))
var angle = (hash % 360) * (MathF.PI / 180f); {
var radius = station.Definition.Radius + 24f; continue;
return new Vector3( }
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y, station.DockingPadAssignments[padIndex] = shipId;
station.Position.Z + (MathF.Sin(angle) * radius)); return padIndex;
} }
private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance) return null;
}
private static void ReleaseDockingPad(StationRuntime station, string shipId)
{
var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
if (!string.IsNullOrEmpty(assignment.Value))
{ {
if (padIndex is null) station.DockingPadAssignments.Remove(assignment.Key);
{
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
}
var pad = GetDockingPadPosition(station, padIndex.Value);
var dx = pad.X - station.Position.X;
var dz = pad.Z - station.Position.Z;
var length = MathF.Sqrt((dx * dx) + (dz * dz));
if (length <= 0.001f)
{
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
}
var scale = distance / length;
return new Vector3(
pad.X + (dx * scale),
station.Position.Y,
pad.Z + (dz * scale));
} }
}
private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) => private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex)
ship.AssignedDockingPadIndex is int padIndex {
? GetDockingPadPosition(station, padIndex) var padCount = Math.Max(1, GetDockingPadCount(station));
: station.Position; var angle = ((MathF.PI * 2f) / padCount) * padIndex;
var radius = station.Radius + 18f;
return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y,
station.Position.Z + (MathF.Sin(angle) * radius));
}
private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId) private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId)
{
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f);
var radius = station.Radius + 24f;
return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y,
station.Position.Z + (MathF.Sin(angle) * radius));
}
private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance)
{
if (padIndex is null)
{ {
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
var angle = (hash % 360) * (MathF.PI / 180f);
var radius = station.Definition.Radius + 78f;
return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y,
station.Position.Z + (MathF.Sin(angle) * radius));
} }
private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius) var pad = GetDockingPadPosition(station, padIndex.Value);
var dx = pad.X - station.Position.X;
var dz = pad.Z - station.Position.Z;
var length = MathF.Sqrt((dx * dx) + (dz * dz));
if (length <= 0.001f)
{ {
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
var angle = (hash % 360) * (MathF.PI / 180f);
return new Vector3(
nodePosition.X + (MathF.Cos(angle) * radius),
nodePosition.Y,
nodePosition.Z + (MathF.Sin(angle) * radius));
} }
var scale = distance / length;
return new Vector3(
pad.X + (dx * scale),
station.Position.Y,
pad.Z + (dz * scale));
}
private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) =>
ship.AssignedDockingPadIndex is int padIndex
? GetDockingPadPosition(station, padIndex)
: station.Position;
private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId)
{
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f);
var radius = station.Radius + 78f;
return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y,
station.Position.Z + (MathF.Sin(angle) * radius));
}
private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius)
{
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f);
return new Vector3(
nodePosition.X + (MathF.Cos(angle) * radius),
nodePosition.Y,
nodePosition.Z + (MathF.Sin(angle) * radius));
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,536 +5,495 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine public sealed partial class SimulationEngine
{ {
private const int StrategicControlTargetSystems = 5; private const int StrategicControlTargetSystems = 5;
private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events) private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
foreach (var station in world.Stations)
{ {
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal); UpdateStationPopulation(station, deltaSeconds, events);
foreach (var station in world.Stations) ReviewStationMarketOrders(world, station);
{ RunStationProduction(world, station, deltaSeconds, events);
UpdateStationPopulation(station, deltaSeconds, events); factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
ReviewStationMarketOrders(world, station);
RunStationProduction(world, station, deltaSeconds, events);
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
}
foreach (var faction in world.Factions)
{
faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
}
} }
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) foreach (var faction in world.Factions)
{ {
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f); faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
}
}
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); {
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
var hasPower = station.EnergyStored > 0.01f;
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied && hasPower) var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
{ var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
if (habitatModules > 0 && station.Population < station.PopulationCapacity) var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
{ var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds)); station.PopulationCapacity = 40f + (habitatModules * 220f);
}
}
else if (station.Population > 0f)
{
var previous = station.Population;
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
if (MathF.Floor(previous) > MathF.Floor(station.Population))
{
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Definition.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
}
}
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); if (waterSatisfied)
{
if (habitatModules > 0 && station.Population < station.PopulationCapacity)
{
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
}
}
else if (station.Population > 0f)
{
var previous = station.Population;
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
if (MathF.Floor(previous) > MathF.Floor(station.Population))
{
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
}
} }
private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station)
{
if (station.CommanderId is null)
{ {
if (station.CommanderId is null) return;
{
return;
}
var desiredOrders = new List<DesiredMarketOrder>();
var fuelReserve = MathF.Max(80f, CountModules(station.InstalledModules, "power-core") * 140f);
var energyCellReserve = HasStationModules(station, "power-core", "liquid-tank") ? MathF.Max(20f, CountModules(station.InstalledModules, "power-core") * 40f) : 0f;
var waterReserve = MathF.Max(30f, station.Population * 3f);
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var gasReserve = CanProcessFuel(station) ? 120f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array")
&& !HasStationModules(station, "component-factory", "ship-factory")
&& FactionNeedsMoreWarships(world, station.FactionId)
? 90f
: 0f;
AddDemandOrder(desiredOrders, station, "fuel", fuelReserve, valuationBase: 1.2f);
AddDemandOrder(desiredOrders, station, "energy-cell", energyCellReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
AddDemandOrder(desiredOrders, station, "gas", gasReserve, valuationBase: 0.95f);
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
AddSupplyOrder(desiredOrders, station, "fuel", fuelReserve * 1.5f, reserveFloor: fuelReserve, valuationBase: 0.8f);
AddSupplyOrder(desiredOrders, station, "energy-cell", energyCellReserve * 1.8f, reserveFloor: energyCellReserve, valuationBase: 0.82f);
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
AddSupplyOrder(desiredOrders, station, "gas", gasReserve * 1.4f, reserveFloor: gasReserve, valuationBase: 0.72f);
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
ReconcileStationMarketOrders(world, station, desiredOrders);
} }
private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) var desiredOrders = new List<DesiredMarketOrder>();
var waterReserve = MathF.Max(30f, station.Population * 3f);
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array")
&& !HasStationModules(station, "component-factory", "ship-factory")
&& FactionNeedsMoreWarships(world, station.FactionId)
? 90f
: 0f;
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
ReconcileStationMarketOrders(world, station, desiredOrders);
}
private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
foreach (var laneKey in GetStationProductionLanes(station))
{ {
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId); var recipe = SelectProductionRecipe(world, station, laneKey);
foreach (var laneKey in GetStationProductionLanes(station)) if (recipe is null)
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var throughput = GetStationProductionThroughput(station, recipe);
var produced = 0f;
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe))
{
station.ProductionLaneTimers[laneKey] -= recipe.Duration;
foreach (var input in recipe.Inputs)
{ {
var recipe = SelectProductionRecipe(world, station, laneKey); RemoveInventory(station.Inventory, input.ItemId, input.Amount);
if (recipe is null || station.EnergyStored <= 0.01f)
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var throughput = GetStationProductionThroughput(station, recipe);
if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds * throughput))
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var produced = 0f;
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe))
{
station.ProductionLaneTimers[laneKey] -= recipe.Duration;
foreach (var input in recipe.Inputs)
{
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
}
if (recipe.ShipOutputId is not null)
{
produced += CompleteShipRecipe(world, station, recipe, events);
continue;
}
foreach (var output in recipe.Outputs)
{
produced += TryAddStationInventory(world, station, output.ItemId, output.Amount);
}
}
if (produced <= 0.01f)
{
continue;
}
events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Definition.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow));
if (faction is not null)
{
faction.GoodsProduced += produced;
}
}
}
private static IEnumerable<string> GetStationProductionLanes(StationRuntime station)
{
if (CountModules(station.InstalledModules, "refinery-stack") > 0)
{
yield return "refinery";
} }
if (CountModules(station.InstalledModules, "fuel-processor") > 0)
{
yield return "fuel";
}
if (CountModules(station.InstalledModules, "fabricator-array") > 0)
{
yield return "fabrication";
}
if (CountModules(station.InstalledModules, "component-factory") > 0)
{
yield return "components";
}
if (CountModules(station.InstalledModules, "ship-factory") > 0)
{
yield return "shipyard";
}
}
private static float GetStationProductionTimer(StationRuntime station, string laneKey) =>
station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f;
private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) =>
world.Recipes.Values
.Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal))
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe))
.FirstOrDefault(recipe => CanRunRecipe(world, station, recipe));
private static string? GetStationProductionLaneKey(RecipeDefinition recipe)
{
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
{
return "fuel";
}
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
{
return "refinery";
}
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{
return "fabrication";
}
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
{
return "components";
}
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
{
return "shipyard";
}
return null;
}
private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{
var priority = (float)recipe.Priority;
var producesFuel = recipe.Outputs.Any(output => string.Equals(output.ItemId, "fuel", StringComparison.Ordinal));
if (producesFuel)
{
var fuelCapacity = MathF.Max(GetStationFuelCapacity(station), 1f);
var fuelRatio = GetInventoryAmount(station.Inventory, "fuel") / fuelCapacity;
priority += (1f - Math.Clamp(fuelRatio, 0f, 1f)) * 200f;
}
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
priority += recipe.Id switch
{
"ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
? -140f * MathF.Max(expansionPressure, fleetPressure)
: 280f * MathF.Max(expansionPressure, fleetPressure),
"hull-fabrication" => 180f * expansionPressure,
"equipment-assembly" => 170f * expansionPressure,
"gun-assembly" => 160f * expansionPressure,
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
=> 220f * MathF.Max(expansionPressure, fleetPressure),
"frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
"cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
"ammo-fabrication" => -80f * expansionPressure,
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
=> -120f * expansionPressure,
_ => 0f,
};
return priority;
}
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
{
var categoryMatch = string.Equals(station.Definition.Category, recipe.FacilityCategory, StringComparison.Ordinal)
|| (string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
&& station.Definition.Category is "station" or "shipyard" or "defense" or "gate");
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
}
private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{
if (recipe.ShipOutputId is not null) if (recipe.ShipOutputId is not null)
{ {
if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition) produced += CompleteShipRecipe(world, station, recipe, events);
|| !CanLaunchShipFromStation(station)) continue;
{
return false;
}
if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal)
|| !FactionNeedsMoreWarships(world, station.FactionId))
{
return false;
}
} }
if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount)) foreach (var output in recipe.Outputs)
{ {
return false; produced += TryAddStationInventory(world, station, output.ItemId, output.Amount);
} }
}
return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount)); if (produced <= 0.01f)
{
continue;
}
events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow));
if (faction is not null)
{
faction.GoodsProduced += produced;
}
}
}
private static IEnumerable<string> GetStationProductionLanes(StationRuntime station)
{
if (CountModules(station.InstalledModules, "refinery-stack") > 0)
{
yield return "refinery";
} }
private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount) if (CountModules(station.InstalledModules, "fabricator-array") > 0)
{ {
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) yield return "fabrication";
{
return false;
}
var requiredModule = GetStorageRequirement(itemDefinition.Storage);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return false;
}
var capacity = GetStationStorageCapacity(station, itemDefinition.Storage);
if (capacity <= 0.01f)
{
return false;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == itemDefinition.Storage)
.Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f;
} }
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase) if (CountModules(station.InstalledModules, "component-factory") > 0)
{ {
var current = GetInventoryAmount(station.Inventory, itemId); yield return "components";
if (current >= targetAmount - 0.01f)
{
return;
}
var deficit = targetAmount - current;
var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount);
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null));
} }
private static void AddSupplyOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase) if (CountModules(station.InstalledModules, "ship-factory") > 0)
{ {
var current = GetInventoryAmount(station.Inventory, itemId); yield return "shipyard";
if (current <= triggerAmount + 0.01f) }
{ }
return;
}
var surplus = current - reserveFloor; private static float GetStationProductionTimer(StationRuntime station, string laneKey) =>
if (surplus <= 0.01f) station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f;
{
return;
}
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor)); private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) =>
world.Recipes.Values
.Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal))
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe))
.FirstOrDefault(recipe => CanRunRecipe(world, station, recipe));
private static string? GetStationProductionLaneKey(RecipeDefinition recipe)
{
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
{
return "refinery";
} }
private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders) if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{ {
var existingOrders = world.MarketOrders return "fabrication";
.Where(order => order.StationId == station.Id && order.ConstructionSiteId is null)
.ToList();
foreach (var desired in desiredOrders)
{
var order = existingOrders.FirstOrDefault(candidate =>
candidate.Kind == desired.Kind &&
candidate.ItemId == desired.ItemId &&
candidate.ConstructionSiteId is null);
if (order is null)
{
order = new MarketOrderRuntime
{
Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}",
FactionId = station.FactionId,
StationId = station.Id,
Kind = desired.Kind,
ItemId = desired.ItemId,
Amount = desired.Amount,
RemainingAmount = desired.Amount,
Valuation = desired.Valuation,
ReserveThreshold = desired.ReserveThreshold,
State = MarketOrderStateKinds.Open,
};
world.MarketOrders.Add(order);
station.MarketOrderIds.Add(order.Id);
existingOrders.Add(order);
continue;
}
order.RemainingAmount = desired.Amount;
order.Valuation = desired.Valuation;
order.ReserveThreshold = desired.ReserveThreshold;
order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open;
}
foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId)))
{
order.RemainingAmount = 0f;
order.State = MarketOrderStateKinds.Cancelled;
}
} }
private static bool HasRefineryCapability(StationRuntime station) => if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
private static bool CanProcessFuel(StationRuntime station) =>
HasStationModules(station, "fuel-processor", "power-core", "gas-tank", "liquid-tank");
private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{ {
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition)) return "components";
{
return 0f;
}
var spawnPosition = new Vector3(station.Position.X + station.Definition.Radius + 32f, station.Position.Y, station.Position.Z);
var ship = new ShipRuntime
{
Id = $"ship-{world.Ships.Count + 1}",
SystemId = station.SystemId,
Definition = definition,
FactionId = station.FactionId,
Position = spawnPosition,
TargetPosition = spawnPosition,
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
Health = definition.MaxHealth,
};
ship.Inventory["fuel"] = 120f;
world.Ships.Add(ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{
faction.ShipsBuilt += 1;
}
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Definition.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
return 1f;
} }
private static bool CanLaunchShipFromStation(StationRuntime station) => if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small");
private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId)
{ {
var militaryShipCount = world.Ships.Count(ship => return "shipyard";
ship.FactionId == factionId
&& string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal));
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
var expansionDeficit = Math.Max(0, targetSystems - controlledSystems);
var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3));
return militaryShipCount < targetWarships;
} }
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId) return null;
{ }
return world.Claims
.Where(claim => claim.State != ClaimStateKinds.Destroyed)
.Select(claim => claim.SystemId)
.Distinct(StringComparer.Ordinal)
.Count(systemId => FactionControlsSystem(world, factionId, systemId));
}
private static float GetFactionExpansionPressure(SimulationWorld world, string factionId) private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{ {
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)); var priority = (float)recipe.Priority;
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
var deficit = Math.Max(0, targetSystems - controlledSystems);
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
}
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
priority += recipe.Id switch
{ {
var buildableLocations = world.Claims "ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
.Where(claim => ? -140f * MathF.Max(expansionPressure, fleetPressure)
claim.SystemId == systemId && : 280f * MathF.Max(expansionPressure, fleetPressure),
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active) "hull-fabrication" => 180f * expansionPressure,
.ToList(); "equipment-assembly" => 170f * expansionPressure,
if (buildableLocations.Count == 0) "gun-assembly" => 160f * expansionPressure,
{ "command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
return false; => 220f * MathF.Max(expansionPressure, fleetPressure),
} "frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId); "cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
return ownedLocations > (buildableLocations.Count / 2f); "ammo-fabrication" => -80f * expansionPressure,
} "trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
=> -120f * expansionPressure,
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new() _ => 0f,
{
CurrentSystemId = station.SystemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentNodeId = station.NodeId,
CurrentBubbleId = station.BubbleId,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
}; };
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) return priority;
{ }
if (!string.Equals(definition.Role, "military", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{
Kind = "idle",
};
}
var patrolRadius = station.Definition.Radius + 90f; private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
return new DefaultBehaviorRuntime {
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|| string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal)
|| string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal);
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
}
private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{
if (recipe.ShipOutputId is not null)
{
if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)
|| !CanLaunchShipFromStation(station))
{
return false;
}
if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal)
|| !FactionNeedsMoreWarships(world, station.FactionId))
{
return false;
}
}
if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount))
{
return false;
}
return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount));
}
private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{
return false;
}
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return false;
}
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind);
if (capacity <= 0.01f)
{
return false;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind)
.Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f;
}
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
{
var current = GetInventoryAmount(station.Inventory, itemId);
if (current >= targetAmount - 0.01f)
{
return;
}
var deficit = targetAmount - current;
var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount);
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null));
}
private static void AddSupplyOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase)
{
var current = GetInventoryAmount(station.Inventory, itemId);
if (current <= triggerAmount + 0.01f)
{
return;
}
var surplus = current - reserveFloor;
if (surplus <= 0.01f)
{
return;
}
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor));
}
private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
{
var existingOrders = world.MarketOrders
.Where(order => order.StationId == station.Id && order.ConstructionSiteId is null)
.ToList();
foreach (var desired in desiredOrders)
{
var order = existingOrders.FirstOrDefault(candidate =>
candidate.Kind == desired.Kind &&
candidate.ItemId == desired.ItemId &&
candidate.ConstructionSiteId is null);
if (order is null)
{
order = new MarketOrderRuntime
{ {
Kind = "patrol", Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}",
PatrolPoints = FactionId = station.FactionId,
[ StationId = station.Id,
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z), Kind = desired.Kind,
ItemId = desired.ItemId,
Amount = desired.Amount,
RemainingAmount = desired.Amount,
Valuation = desired.Valuation,
ReserveThreshold = desired.ReserveThreshold,
State = MarketOrderStateKinds.Open,
};
world.MarketOrders.Add(order);
station.MarketOrderIds.Add(order.Id);
existingOrders.Add(order);
continue;
}
order.RemainingAmount = desired.Amount;
order.Valuation = desired.Valuation;
order.ReserveThreshold = desired.ReserveThreshold;
order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open;
}
foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId)))
{
order.RemainingAmount = 0f;
order.State = MarketOrderStateKinds.Cancelled;
}
}
private static bool HasRefineryCapability(StationRuntime station) =>
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
{
return 0f;
}
var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z);
var ship = new ShipRuntime
{
Id = $"ship-{world.Ships.Count + 1}",
SystemId = station.SystemId,
Definition = definition,
FactionId = station.FactionId,
Position = spawnPosition,
TargetPosition = spawnPosition,
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
Health = definition.MaxHealth,
};
world.Ships.Add(ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{
faction.ShipsBuilt += 1;
}
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
return 1f;
}
private static bool CanLaunchShipFromStation(StationRuntime station) =>
HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small");
private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId)
{
var militaryShipCount = world.Ships.Count(ship =>
ship.FactionId == factionId
&& string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal));
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
var expansionDeficit = Math.Max(0, targetSystems - controlledSystems);
var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3));
return militaryShipCount < targetWarships;
}
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
{
return world.Claims
.Where(claim => claim.State != ClaimStateKinds.Destroyed)
.Select(claim => claim.SystemId)
.Distinct(StringComparer.Ordinal)
.Count(systemId => FactionControlsSystem(world, factionId, systemId));
}
private static float GetFactionExpansionPressure(SimulationWorld world, string factionId)
{
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
var deficit = Math.Max(0, targetSystems - controlledSystems);
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
}
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
{
var buildableLocations = world.Claims
.Where(claim =>
claim.SystemId == systemId &&
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active)
.ToList();
if (buildableLocations.Count == 0)
{
return false;
}
var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId);
return ownedLocations > (buildableLocations.Count / 2f);
}
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new()
{
CurrentSystemId = station.SystemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentNodeId = station.NodeId,
CurrentBubbleId = station.BubbleId,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
};
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
{
if (!string.Equals(definition.Role, "military", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{
Kind = "idle",
};
}
var patrolRadius = station.Radius + 90f;
return new DefaultBehaviorRuntime
{
Kind = "patrol",
PatrolPoints =
[
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius), new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius),
new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z), new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius), new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius),
], ],
}; };
} }
private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe) private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe)
{
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
{ {
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal)) return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack"));
{
return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack"));
}
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "fuel-processor"));
}
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));
}
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "component-factory"));
}
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "ship-factory"));
}
return 1f;
} }
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold); if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));
}
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "component-factory"));
}
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "ship-factory"));
}
return 1f;
}
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
} }

View File

@@ -4,106 +4,98 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine public sealed partial class SimulationEngine
{ {
private readonly OrbitalSimulationOptions _orbitalSimulation; private readonly OrbitalSimulationOptions _orbitalSimulation;
private const float ShipFuelToEnergyRatio = 12f; private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
private const float StationFuelToEnergyRatio = 18f; private const float PopulationGrowthPerSecond = 0.012f;
private const float CapacitorEnergyPerModule = 120f; private const float PopulationAttritionPerSecond = 0.018f;
private const float StationEnergyPerPowerCore = 480f; private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
private const float ShipFuelPerReactor = 100f; private static readonly IReadOnlyList<WorldUpdateStep> _worldUpdatePipeline =
private const float StationFuelPerTank = 500f; [
private const float WaterConsumptionPerWorkerPerSecond = 0.004f; new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)),
private const float PopulationGrowthPerSecond = 0.012f;
private const float PopulationAttritionPerSecond = 0.018f;
private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
private static readonly IReadOnlyList<WorldUpdateStep> _worldUpdatePipeline =
[
new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)),
new((engine, world, deltaSeconds, nowUtc, events) => UpdateClaims(world, events)), new((engine, world, deltaSeconds, nowUtc, events) => UpdateClaims(world, events)),
new((engine, world, deltaSeconds, nowUtc, events) => UpdateConstructionSites(world, events)), new((engine, world, deltaSeconds, nowUtc, events) => UpdateConstructionSites(world, events)),
new((engine, world, deltaSeconds, nowUtc, events) => UpdateStationPower(world, deltaSeconds, events)),
new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateStations(world, deltaSeconds, events)), new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateStations(world, deltaSeconds, events)),
]; ];
private static readonly IReadOnlyList<ShipUpdateStep> _shipUpdatePipeline = private static readonly IReadOnlyList<ShipUpdateStep> _shipUpdatePipeline =
[ [
new((engine, ship, world, deltaSeconds, events) => UpdateShipPower(ship, world, deltaSeconds, events)),
new((engine, ship, world, deltaSeconds, events) => engine.RefreshControlLayers(ship, world)), new((engine, ship, world, deltaSeconds, events) => engine.RefreshControlLayers(ship, world)),
new((engine, ship, world, deltaSeconds, events) => engine.PlanControllerTask(ship, world)), new((engine, ship, world, deltaSeconds, events) => engine.PlanControllerTask(ship, world)),
]; ];
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
{
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
}
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
{
var events = new List<SimulationEventRecord>();
var nowUtc = DateTimeOffset.UtcNow;
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
foreach (var step in _worldUpdatePipeline)
{ {
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions(); step.Execute(this, world, deltaSeconds, nowUtc, events);
} }
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence) foreach (var ship in world.Ships)
{ {
var events = new List<SimulationEventRecord>(); var previousPosition = ship.Position;
var nowUtc = DateTimeOffset.UtcNow; var previousState = ship.State;
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond; var previousBehavior = ship.DefaultBehavior.Kind;
var previousTask = ship.ControllerTask.Kind;
foreach (var step in _worldUpdatePipeline) foreach (var step in _shipUpdatePipeline)
{ {
step.Execute(this, world, deltaSeconds, nowUtc, events); step.Execute(this, ship, world, deltaSeconds, events);
} }
foreach (var ship in world.Ships) var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
{ AdvanceControlState(ship, world, controllerEvent);
var previousPosition = ship.Position; ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
var previousState = ship.State; TrackHistory(ship, controllerEvent);
var previousBehavior = ship.DefaultBehavior.Kind;
var previousTask = ship.ControllerTask.Kind;
foreach (var step in _shipUpdatePipeline) EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
{
step.Execute(this, ship, world, deltaSeconds, events);
}
var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
AdvanceControlState(ship, world, controllerEvent);
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
TrackHistory(ship, controllerEvent);
EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
}
SyncSpatialState(world);
world.GeneratedAtUtc = nowUtc;
return new WorldDelta(
sequence,
world.TickIntervalMs,
world.OrbitalTimeSeconds,
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
world.GeneratedAtUtc,
false,
events,
BuildSpatialNodeDeltas(world),
BuildLocalBubbleDeltas(world),
BuildNodeDeltas(world),
BuildStationDeltas(world),
BuildClaimDeltas(world),
BuildConstructionSiteDeltas(world),
BuildMarketOrderDeltas(world),
BuildPolicyDeltas(world),
BuildShipDeltas(world),
BuildFactionDeltas(world));
} }
private delegate void WorldUpdateStepAction( SyncSpatialState(world);
SimulationEngine engine, world.GeneratedAtUtc = nowUtc;
SimulationWorld world,
float deltaSeconds,
DateTimeOffset nowUtc,
List<SimulationEventRecord> events);
private delegate void ShipUpdateStepAction( return new WorldDelta(
SimulationEngine engine, sequence,
ShipRuntime ship, world.TickIntervalMs,
SimulationWorld world, world.OrbitalTimeSeconds,
float deltaSeconds, new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
List<SimulationEventRecord> events); world.GeneratedAtUtc,
false,
events,
BuildSpatialNodeDeltas(world),
BuildLocalBubbleDeltas(world),
BuildNodeDeltas(world),
BuildStationDeltas(world),
BuildClaimDeltas(world),
BuildConstructionSiteDeltas(world),
BuildMarketOrderDeltas(world),
BuildPolicyDeltas(world),
BuildShipDeltas(world),
BuildFactionDeltas(world));
}
private sealed record WorldUpdateStep(WorldUpdateStepAction Execute); private delegate void WorldUpdateStepAction(
SimulationEngine engine,
SimulationWorld world,
float deltaSeconds,
DateTimeOffset nowUtc,
List<SimulationEventRecord> events);
private sealed record ShipUpdateStep(ShipUpdateStepAction Execute); private delegate void ShipUpdateStepAction(
SimulationEngine engine,
ShipRuntime ship,
SimulationWorld world,
float deltaSeconds,
List<SimulationEventRecord> events);
private sealed record WorldUpdateStep(WorldUpdateStepAction Execute);
private sealed record ShipUpdateStep(ShipUpdateStepAction Execute);
} }

View File

@@ -6,6 +6,12 @@ export interface StationActionProgressSnapshot {
progress: number; progress: number;
} }
export interface StationStorageUsageSnapshot {
storageClass: string;
used: number;
capacity: number;
}
export interface StationSnapshot { export interface StationSnapshot {
id: string; id: string;
label: string; label: string;
@@ -19,10 +25,6 @@ export interface StationSnapshot {
dockedShips: number; dockedShips: number;
dockedShipIds: string[]; dockedShipIds: string[];
dockingPads: number; dockingPads: number;
fuelStored: number;
fuelCapacity: number;
energyStored: number;
energyCapacity: number;
currentProcesses: StationActionProgressSnapshot[]; currentProcesses: StationActionProgressSnapshot[];
inventory: InventoryEntry[]; inventory: InventoryEntry[];
factionId: string; factionId: string;
@@ -32,11 +34,12 @@ export interface StationSnapshot {
populationCapacity: number; populationCapacity: number;
workforceRequired: number; workforceRequired: number;
workforceEffectiveRatio: number; workforceEffectiveRatio: number;
storageUsage: StationStorageUsageSnapshot[];
installedModules: string[]; installedModules: string[];
marketOrderIds: string[]; marketOrderIds: string[];
} }
export interface StationDelta extends StationSnapshot {} export interface StationDelta extends StationSnapshot { }
export interface ClaimSnapshot { export interface ClaimSnapshot {
id: string; id: string;
@@ -50,7 +53,7 @@ export interface ClaimSnapshot {
activatesAtUtc: string; activatesAtUtc: string;
} }
export interface ClaimDelta extends ClaimSnapshot {} export interface ClaimDelta extends ClaimSnapshot { }
export interface ConstructionSiteSnapshot { export interface ConstructionSiteSnapshot {
id: string; id: string;
@@ -72,4 +75,4 @@ export interface ConstructionSiteSnapshot {
marketOrderIds: string[]; marketOrderIds: string[];
} }
export interface ConstructionSiteDelta extends ConstructionSiteSnapshot {} export interface ConstructionSiteDelta extends ConstructionSiteSnapshot { }

View File

@@ -21,7 +21,6 @@ export interface ShipSnapshot {
cargoCapacity: number; cargoCapacity: number;
cargoItemId?: string | null; cargoItemId?: string | null;
workerPopulation: number; workerPopulation: number;
energyStored: number;
travelSpeed: number; travelSpeed: number;
travelSpeedUnit: string; travelSpeedUnit: string;
inventory: InventoryEntry[]; inventory: InventoryEntry[];
@@ -32,7 +31,7 @@ export interface ShipSnapshot {
spatialState: ShipSpatialStateSnapshot; spatialState: ShipSpatialStateSnapshot;
} }
export interface ShipDelta extends ShipSnapshot {} export interface ShipDelta extends ShipSnapshot { }
export interface ShipActionProgressSnapshot { export interface ShipActionProgressSnapshot {
label: string; label: string;

View File

@@ -26,7 +26,6 @@ export function renderFactionStrip(
return ships return ships
.map((ship) => { .map((ship) => {
const fuel = inventoryAmount(ship.inventory, "fuel");
const cargo = ship.cargoItemId const cargo = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId) ? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0; : 0;
@@ -54,7 +53,7 @@ export function renderFactionStrip(
</div> </div>
</div> </div>
<p>${shipLocation.system}${shipLocation.local ? `<br>${shipLocation.local}` : ""}</p> <p>${shipLocation.system}${shipLocation.local ? `<br>${shipLocation.local}` : ""}</p>
<p>Fuel ${fuel.toFixed(1)} · Cap ${ship.energyStored.toFixed(1)}${ship.cargoCapacity > 0 ? ` · Cargo ${cargo.toFixed(0)}` : ""}</p> <p>Cargo ${cargo.toFixed(0)}</p>
<p>State ${shipState}</p> <p>State ${shipState}</p>
${shipAction ? ` ${shipAction ? `
<div class="ship-action-progress"> <div class="ship-action-progress">

View File

@@ -37,6 +37,94 @@ interface SystemPanelParams {
cameraTargetShipId?: string; cameraTargetShipId?: string;
} }
function laneModuleId(lane: string): string | undefined {
switch (lane) {
case "refinery":
return "refinery-stack";
case "fabrication":
return "fabricator-array";
case "components":
return "component-factory";
case "shipyard":
return "ship-factory";
default:
return undefined;
}
}
function formatModuleListWithConstruction(
world: WorldState,
stationId: string,
installedModules: string[],
currentProcesses: { lane: string; label: string; progress: number }[],
): string {
const processByModule = new Map<string, { label: string; progress: number }[]>();
for (const process of currentProcesses) {
const moduleId = laneModuleId(process.lane);
if (!moduleId) {
continue;
}
const existing = processByModule.get(moduleId) ?? [];
existing.push({ label: process.label, progress: process.progress });
processByModule.set(moduleId, existing);
}
const renderedProcessCount = new Map<string, number>();
const moduleLines = installedModules.map((moduleId) => {
const processIndex = renderedProcessCount.get(moduleId) ?? 0;
const processes = processByModule.get(moduleId) ?? [];
const process = processes[processIndex];
renderedProcessCount.set(moduleId, processIndex + 1);
if (!process) {
return moduleId;
}
return `${moduleId} -> ${process.label} (${Math.round(process.progress * 100)}%)`;
});
const activeSites = [...world.constructionSites.values()]
.filter((site) => site.stationId === stationId && site.state !== "completed")
.sort((left, right) => left.targetDefinitionId.localeCompare(right.targetDefinitionId));
for (const site of activeSites) {
const moduleId = site.blueprintId ?? site.targetDefinitionId;
const progress = Math.round(site.progress * 100);
const tooltip = site.requiredItems.length > 0
? site.requiredItems
.map((entry) => `${entry.itemId}: ${entry.amount.toFixed(0)} required / ${inventoryAmount(site.stationId ? (world.stations.get(site.stationId)?.inventory ?? []) : site.deliveredItems, entry.itemId).toFixed(0)} available`)
.join("\n")
: "No material requirements";
const escapedTooltip = tooltip
.replaceAll("&", "&amp;")
.replaceAll("\"", "&quot;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
moduleLines.push(`<span title="${escapedTooltip}">${moduleId} (${progress}% constructing)</span>`);
}
return moduleLines.length > 0 ? moduleLines.join("<br>") : "none";
}
function formatStorageClassLabel(storageClass: string): string {
return storageClass
.split("-")
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
.join(" ");
}
function formatStorageUsage(storageUsage: { storageClass: string; used: number; capacity: number }[]): string {
if (storageUsage.length === 0) {
return "none";
}
return storageUsage
.map((entry) => {
const percentUsed = entry.capacity > 0 ? Math.round((entry.used / entry.capacity) * 100) : 0;
return `${formatStorageClassLabel(entry.storageClass)} ${percentUsed}% used (${entry.used.toFixed(0)} / ${entry.capacity.toFixed(0)})`;
})
.join("<br>");
}
function renderSystemOwnership(world: WorldState, systemId: string): string { function renderSystemOwnership(world: WorldState, systemId: string): string {
const claims = [...world.claims.values()].filter((claim) => const claims = [...world.claims.values()].filter((claim) =>
claim.systemId === systemId && claim.state !== "destroyed"); claim.systemId === systemId && claim.state !== "destroyed");
@@ -108,7 +196,6 @@ export function updateDetailPanel(
return; return;
} }
const parent = describeSelectionParent(selected); const parent = describeSelectionParent(selected);
const fuelStored = inventoryAmount(ship.inventory, "fuel");
const cargoUsed = ship.cargoItemId const cargoUsed = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId) ? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0; : 0;
@@ -130,7 +217,6 @@ export function updateDetailPanel(
</div> </div>
</div> </div>
` : ""} ` : ""}
<p>Fuel ${fuelStored.toFixed(1)}<br>Capacitor ${ship.energyStored.toFixed(1)}</p>
<p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p> <p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
<p>Inventory ${formatInventory(ship.inventory)}</p> <p>Inventory ${formatInventory(ship.inventory)}</p>
<p>Speed ${formatShipSpeed(ship)}</p> <p>Speed ${formatShipSpeed(ship)}</p>
@@ -145,17 +231,12 @@ export function updateDetailPanel(
return; return;
} }
const parent = describeSelectionParent(selected); const parent = describeSelectionParent(selected);
const installedModules = station.installedModules.length > 0 const moduleList = formatModuleListWithConstruction(world, station.id, station.installedModules, station.currentProcesses);
? station.installedModules.join("<br>")
: "none";
const activeConstruction = [...world.constructionSites.values()]
.filter((site) => site.stationId === station.id && site.state !== "completed")
.map((site) => `${site.blueprintId ?? site.targetDefinitionId} (${site.state})`)
.join("<br>") || "none";
const dockedShipLabels = station.dockedShipIds.length > 0 const dockedShipLabels = station.dockedShipIds.length > 0
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>") ? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
: "none"; : "none";
const stationInventory = station.inventory.filter((entry) => entry.itemId !== "fuel"); const stationInventory = station.inventory;
const stationStorageUsage = formatStorageUsage(station.storageUsage);
const stationProcesses = station.currentProcesses; const stationProcesses = station.currentProcesses;
const stationProcessingHtml = stationProcesses.length > 0 const stationProcessingHtml = stationProcesses.length > 0
? stationProcesses.map((process) => ` ? stationProcesses.map((process) => `
@@ -175,14 +256,12 @@ export function updateDetailPanel(
<p>${station.category} · ${station.systemId}</p> <p>${station.category} · ${station.systemId}</p>
<p>Parent ${parent}</p> <p>Parent ${parent}</p>
${stationProcessingHtml} ${stationProcessingHtml}
<p>Fuel ${station.fuelStored.toFixed(1)} / ${station.fuelCapacity.toFixed(1)}<br>Capacitor ${station.energyStored.toFixed(1)} / ${station.energyCapacity.toFixed(1)}</p>
<p>Docked ${station.dockedShips} / ${station.dockingPads} <p>Docked ${station.dockedShips} / ${station.dockingPads}
<br> <br>
${dockedShipLabels}</p> ${dockedShipLabels}</p>
<p>Modules ${installedModules}</p> <p>Modules ${moduleList}</p>
<p>Constructing ${activeConstruction}</p> <p>Storage ${stationStorageUsage}</p>
<p>Inventory ${formatInventory(stationInventory)}</p> <p>Inventory ${formatInventory(stationInventory)}</p>
<p>History available in the separate history window.</p>
`; `;
return; return;
} }

View File

@@ -332,8 +332,6 @@ function describeControllerTask(taskKind: string): string {
return "docking"; return "docking";
case "unload": case "unload":
return "transfer"; return "transfer";
case "refuel":
return "refuel";
case "deliver-construction": case "deliver-construction":
return "material delivery"; return "material delivery";
case "build-construction-site": case "build-construction-site":

View File

@@ -164,7 +164,7 @@ Typical outputs:
- current destination node - current destination node
- local tactical task - local tactical task
- retreat decision - retreat decision
- docking/refuel intent - docking intent
- trade or delivery acceptance - trade or delivery acceptance
## Commander Ownership ## Commander Ownership

View File

@@ -390,19 +390,6 @@ Suggested station-side workforce fields:
Commanders should not be ordinary cargo items even if they are population-derived. Commanders should not be ordinary cargo items even if they are population-derived.
## Power State
Ships and stations both need explicit operational power state.
Suggested fields:
- `fuelInventory`
- `energyStored`
- `powerOperational`
- `powerDeficitReason?`
This matters because no fuel leads to no power, and no power halts major operations.
## Inventories ## Inventories
Inventories should remain generic item maps, but hosts should also have explicit context. Inventories should remain generic item maps, but hosts should also have explicit context.

View File

@@ -31,7 +31,6 @@ For the implementation migration path from the current codebase to this design s
- item categories - item categories
- life-support goods - life-support goods
- construction goods - construction goods
- fuel-chain goods
- population-related units - population-related units
- [WORKFORCE.md](/home/jbourdon/repos/space-game/docs/WORKFORCE.md) - [WORKFORCE.md](/home/jbourdon/repos/space-game/docs/WORKFORCE.md)

View File

@@ -84,7 +84,6 @@ A buy order should include, conceptually:
Buy orders let a station express: Buy orders let a station express:
- production input demand - production input demand
- fuel shortages
- construction material shortages - construction material shortages
- military resupply needs - military resupply needs
@@ -138,8 +137,6 @@ The station commander should:
Without a station commander, a station should not act like a healthy market participant. Without a station commander, a station should not act like a healthy market participant.
If the station has no fuel and therefore no power, it should not continue normal market operation.
However, there is an important exception during founding or emergency intervention: However, there is an important exception during founding or emergency intervention:
- a higher actor may force transfers toward the station or construction site even without ordinary market behavior - a higher actor may force transfers toward the station or construction site even without ordinary market behavior
@@ -149,7 +146,7 @@ Recommended review loop:
1. inspect current inventory 1. inspect current inventory
2. inspect production queues or goals 2. inspect production queues or goals
3. inspect incoming and outgoing reservations 3. inspect incoming and outgoing reservations
4. inspect fuel, defense, and construction reserves 4. inspect defense, and construction reserves
5. update buy orders 5. update buy orders
6. update sell orders 6. update sell orders
7. request logistics or strategic help if necessary 7. request logistics or strategic help if necessary
@@ -185,7 +182,7 @@ The intended economy should eventually support flows such as:
1. extract raw resources 1. extract raw resources
2. move them to useful stations 2. move them to useful stations
3. refine or process them 3. refine or process them
4. consume them for fuel, production, or expansion 4. consume them for production or expansion
5. produce intermediate and advanced goods 5. produce intermediate and advanced goods
6. sell surpluses or acquire shortages through the market 6. sell surpluses or acquire shortages through the market
@@ -200,7 +197,6 @@ Logistics should emerge from market demand, not only from hardcoded behavior loo
Examples: Examples:
- a hauler sees a profitable sell-to-buy opportunity - a hauler sees a profitable sell-to-buy opportunity
- a station commander requests urgent fuel delivery
- a faction commander subsidizes strategic resource movement - a faction commander subsidizes strategic resource movement
This is a better long-term basis than one-off scripted “mine and deliver to this exact station” logic. This is a better long-term basis than one-off scripted “mine and deliver to this exact station” logic.
@@ -208,7 +204,6 @@ This is a better long-term basis than one-off scripted “mine and deliver to th
Traders should generally prefer the best reachable buy opportunity within their allowed operational range, subject to: Traders should generally prefer the best reachable buy opportunity within their allowed operational range, subject to:
- travel time - travel time
- fuel cost
- risk - risk
- behavioral restrictions - behavioral restrictions
- territorial or regional limits - territorial or regional limits
@@ -236,7 +231,6 @@ The economy will work better if stations can reserve expected inventory changes.
Examples: Examples:
- incoming fuel is reserved for station power
- outbound metals are reserved for a construction project - outbound metals are reserved for a construction project
- a hauler claims part of a sell order before pickup - a hauler claims part of a sell order before pickup

View File

@@ -289,7 +289,6 @@ Every event should be capable of producing a concise human-readable summary.
Example style: Example style:
- `Claim at Helios IV L4 destroyed by pirates` - `Claim at Helios IV L4 destroyed by pirates`
- `Station buy order for fuel opened`
- `Miner completed warp to refinery node` - `Miner completed warp to refinery node`
This helps reuse the same event model for: This helps reuse the same event model for:

View File

@@ -33,10 +33,9 @@ The intended categories are:
2. processed industrial goods 2. processed industrial goods
3. life-support goods 3. life-support goods
4. civilian goods 4. civilian goods
5. fuel and power-chain goods 5. construction goods
6. construction goods 6. population-related units
7. population-related units 7. special logistics goods later
8. special logistics goods later
## Raw Resources ## Raw Resources
@@ -86,20 +85,6 @@ Current important example:
These goods should matter for workforce health, quality of life, and possibly future growth modifiers. These goods should matter for workforce health, quality of life, and possibly future growth modifiers.
## Fuel And Power-Chain Goods
These are the goods that keep ships and stations running.
Examples:
- gas as an energy-chain input
- fuel as a refined operational good
The exact chain may evolve, but the important distinction is:
- some goods are energy inputs
- some goods are operational fuels
## Construction Goods ## Construction Goods
These are the goods used to build stations and possibly ships. These are the goods used to build stations and possibly ships.
@@ -116,7 +101,7 @@ Construction storage at a station site should create demand for these goods thro
## Population-Related Units ## Population-Related Units
Population itself should be treated as a tracked resource, but not as an ordinary trade good in the same sense as metal or fuel. Population itself should be treated as a tracked resource, but not as an ordinary trade good in the same sense as ore.
Important distinctions: Important distinctions:
@@ -144,12 +129,6 @@ The current design implies at least these roles:
- `ore` - `ore`
- raw industrial input - raw industrial input
- `gas`
- raw fuel-chain input
- `fuel`
- operational energy good
- `food` - `food`
- workforce life-support - workforce life-support
@@ -173,12 +152,11 @@ Not every item should necessarily fit in every hold type forever.
Useful distinctions later may include: Useful distinctions later may include:
- bulk industrial cargo - solid storage
- liquid cargo - liquid storage
- gas cargo - container storage
- containerized finished goods - passengers
- human transport capacity - livestock
- livestock capacity
For now, the important rule is simply: For now, the important rule is simply:
@@ -191,7 +169,6 @@ Items should participate in the market according to their role.
Examples: Examples:
- life-support goods generate recurring demand - life-support goods generate recurring demand
- fuel goods generate operational demand
- construction goods generate burst demand during expansion - construction goods generate burst demand during expansion
- industrial goods feed production chains - industrial goods feed production chains
- worker transport supports station staffing - worker transport supports station staffing
@@ -220,7 +197,7 @@ The following rules should remain true unless deliberately revised:
- workforce depends on real support goods - workforce depends on real support goods
- station construction depends on real construction goods - station construction depends on real construction goods
- fuel and industrial chains are item-based - industrial chains are item-based
- workers are movable population units - workers are movable population units
- commanders are not ordinary trade cargo - commanders are not ordinary trade cargo
- livestock is distinct from workers - livestock is distinct from workers

View File

@@ -48,7 +48,6 @@ Examples:
- no docking module means no docking service - no docking module means no docking service
- no habitat module means no population growth or human transport - no habitat module means no population growth or human transport
- no refinery module means no refining - no refinery module means no refining
- no fuel-processing module means no gas-to-fuel conversion
- no storage module means reduced or absent inventory capability - no storage module means reduced or absent inventory capability
- no shipyard-related module means no ship production - no shipyard-related module means no ship production
@@ -92,7 +91,6 @@ Likely station-side categories include:
- storage - storage
- habitat - habitat
- refinery - refinery
- fuel processing
- manufacturing - manufacturing
- shipyard or construction support - shipyard or construction support
- defense - defense
@@ -141,7 +139,6 @@ Examples:
- reactor - reactor
- capacitor - capacitor
- station power core - station power core
- fuel systems
### Production Modules ### Production Modules
@@ -150,7 +147,6 @@ These convert goods into other goods or into built output.
Examples: Examples:
- refinery - refinery
- fuel processor
- factory - factory
- shipyard support - shipyard support
@@ -198,7 +194,6 @@ They may require:
- build time - build time
- power - power
- workforce - workforce
- fuel or energy inputs
- docking or logistics support - docking or logistics support
This should let stations and ships fail in believable ways when underbuilt or undersupplied. This should let stations and ships fail in believable ways when underbuilt or undersupplied.
@@ -219,7 +214,6 @@ Modules should define which item flows an entity can participate in.
Examples: Examples:
- a habitat module enables population support - a habitat module enables population support
- a fuel-processing module consumes gas and produces fuel
- a refinery consumes raw resources and produces processed goods - a refinery consumes raw resources and produces processed goods
- a storage module determines what volume or class of goods can be held - a storage module determines what volume or class of goods can be held
- a livestock module participates in the food chain - a livestock module participates in the food chain

View File

@@ -49,7 +49,6 @@ A recipe should conceptually define:
- cycle time - cycle time
- valid producing module types - valid producing module types
- optional workforce requirement - optional workforce requirement
- optional power or fuel requirement
Recipes should be first-class design objects, not hidden assumptions inside modules. Recipes should be first-class design objects, not hidden assumptions inside modules.
@@ -60,7 +59,6 @@ Recipes are executed by production-capable modules.
Examples: Examples:
- refinery module - refinery module
- fuel processing module
- factory module - factory module
- food-chain module later - food-chain module later
- shipyard support module - shipyard support module
@@ -112,16 +110,6 @@ For now:
This keeps the initial system consistent and simple. This keeps the initial system consistent and simple.
## Power Interaction
Production should also respect power and fuel state.
Without power:
- production stops
This is especially important for stations because no-fuel means no-power, and no-power means no normal operation.
## Input Shortage Behavior ## Input Shortage Behavior
If inputs are missing: If inputs are missing:
@@ -153,7 +141,6 @@ The exact recipes can evolve, but the intended shape includes chains like:
2. refining or processing 2. refining or processing
- ore -> refined goods - ore -> refined goods
- gas -> fuel
- food-loop conversions later - food-loop conversions later
3. industrial use 3. industrial use

View File

@@ -151,23 +151,6 @@ Not:
This means friendly or otherwise permitted factions may build stations within the same system, so long as they use different valid locations. This means friendly or otherwise permitted factions may build stations within the same system, so long as they use different valid locations.
## Failure State
Without fuel there is no power.
Without power, station function collapses.
A powerless station should not continue normal market or industrial behavior.
At that point, recovery should require outside intervention such as emergency restoration, delivered fuel, or a dedicated support operation.
This also means:
- no loading
- no unloading
- no ordinary trade handling
- no ordinary production
## Services ## Services
Depending on modules and category, a station may provide: Depending on modules and category, a station may provide:
@@ -175,11 +158,9 @@ Depending on modules and category, a station may provide:
- docking - docking
- storage - storage
- refining - refining
- fuel processing
- manufacturing - manufacturing
- repair later - repair
- fitting later - fitting, rearm and resupply later
- rearm and resupply later
- habitats - habitats
The exact conversion and factory behavior behind these services is described in [PRODUCTION.md](/home/jbourdon/repos/space-game/docs/PRODUCTION.md). The exact conversion and factory behavior behind these services is described in [PRODUCTION.md](/home/jbourdon/repos/space-game/docs/PRODUCTION.md).

View File

@@ -41,7 +41,6 @@ Goals are high-level commander intentions.
Examples: Examples:
- expand into this system - expand into this system
- keep this station fueled
- defend this claim - defend this claim
- protect trade in this region - protect trade in this region
- supply this station with workers - supply this station with workers
@@ -60,7 +59,6 @@ Examples:
- dock at station - dock at station
- claim Lagrange point - claim Lagrange point
- build station here - build station here
- deliver fuel
- escort this ship - escort this ship
- defend this bubble - defend this bubble
@@ -223,7 +221,6 @@ Examples:
- deny dock request - deny dock request
- transfer goods - transfer goods
- request defense - request defense
- request emergency fuel support
These may be implemented as station jobs, station operations, or station-side tasks. These may be implemented as station jobs, station operations, or station-side tasks.
@@ -237,7 +234,7 @@ Examples:
- flee to nearest allowed station - flee to nearest allowed station
- hold position if no valid route exists - hold position if no valid route exists
- suspend trade when no legal destination exists - suspend trade when no legal destination exists
- wait for fuel, escort, or dock access - wait for escort, or dock access
This prevents autonomous loops from becoming self-destructive. This prevents autonomous loops from becoming self-destructive.

View File

@@ -52,7 +52,6 @@ Workers consume, per worker:
- food - food
- water - water
- energy
- consumer goods - consumer goods
These should be understood using the item roles defined in [ITEMS.md](/home/jbourdon/repos/space-game/docs/ITEMS.md). These should be understood using the item roles defined in [ITEMS.md](/home/jbourdon/repos/space-game/docs/ITEMS.md).
@@ -146,7 +145,6 @@ A newly founded station may begin with:
It can still exist and operate at baseline efficiency, but it remains weak until supplied with: It can still exist and operate at baseline efficiency, but it remains weak until supplied with:
- fuel
- workers - workers
- support goods - support goods
- eventually a station commander - eventually a station commander
@@ -159,7 +157,6 @@ Relevant shortages include:
- food shortage - food shortage
- water shortage - water shortage
- energy shortage
- consumer goods shortage - consumer goods shortage
This gives logistics failure lasting demographic consequences. This gives logistics failure lasting demographic consequences.
@@ -178,7 +175,7 @@ The following rules should remain true unless deliberately revised:
- population grows only at stations for now - population grows only at stations for now
- habitat modules are required for growth - habitat modules are required for growth
- workers consume food, water, energy, and consumer goods - workers consume food, water and consumer goods
- workforce affects station efficiency - workforce affects station efficiency
- stations retain a small baseline efficiency at zero workforce - stations retain a small baseline efficiency at zero workforce
- population can be transported between stations - population can be transported between stations

View File

@@ -6,12 +6,5 @@
"transferRate": 56, "transferRate": 56,
"dockingDuration": 1.2, "dockingDuration": 1.2,
"undockingDuration": 1.2, "undockingDuration": 1.2,
"undockDistance": 42, "undockDistance": 42
"energy": {
"idleDrain": 0.7,
"moveDrain": 1.8,
"warpDrain": 7,
"shipRechargeRate": 10,
"stationSolarCharge": 5
}
} }

View File

@@ -1,87 +0,0 @@
[
{
"id": "station-core",
"label": "Orbital Station",
"category": "station",
"color": "#8df0d2",
"radius": 24,
"dockingCapacity": 4,
"storage": {
"bulk-solid": 2000,
"manufactured": 1200,
"bulk-liquid": 600,
"bulk-gas": 600
},
"modules": ["dock-bay-small", "power-core", "bulk-bay", "liquid-tank"]
},
{
"id": "trade-hub",
"label": "Trade Hub",
"category": "station",
"color": "#8bd3ff",
"radius": 20,
"dockingCapacity": 4,
"storage": { "container": 1200, "manufactured": 800 },
"modules": ["habitat-ring", "container-bay"]
},
{
"id": "refinery",
"label": "Refining Station",
"category": "station",
"color": "#ffb86c",
"radius": 24,
"dockingCapacity": 3,
"storage": { "bulk-solid": 2000, "manufactured": 1000, "bulk-liquid": 400, "bulk-gas": 400 },
"modules": ["power-core", "bulk-bay", "liquid-tank", "gas-tank", "refinery-stack", "fuel-processor"]
},
{
"id": "farm-ring",
"label": "Farm Station",
"category": "farm",
"color": "#92ef8a",
"radius": 22,
"dockingCapacity": 2,
"storage": { "bulk-liquid": 600, "container": 400 },
"modules": ["habitat-ring", "fabricator-array", "container-bay"]
},
{
"id": "manufactory",
"label": "Orbital Manufactory",
"category": "station",
"color": "#8df0d2",
"radius": 24,
"dockingCapacity": 3,
"storage": { "manufactured": 2200, "container": 1600 },
"modules": ["fabricator-array", "fabricator-array", "container-bay"]
},
{
"id": "shipyard",
"label": "Orbital Shipyard",
"category": "shipyard",
"color": "#d0a2ff",
"radius": 28,
"dockingCapacity": 5,
"storage": { "manufactured": 1800, "container": 1200 },
"modules": ["component-factory", "ship-factory", "container-bay", "dock-bay-small", "power-core"]
},
{
"id": "defense-grid",
"label": "Defense Platform",
"category": "defense",
"color": "#ff7a95",
"radius": 18,
"dockingCapacity": 1,
"storage": { "manufactured": 300 },
"modules": ["turret-grid", "command-bridge"]
},
{
"id": "stargate",
"label": "Stargate",
"category": "gate",
"color": "#76f0ff",
"radius": 34,
"dockingCapacity": 0,
"storage": { "manufactured": 2400, "container": 800 },
"modules": ["ftl-core", "fabricator-array"]
}
]

View File

@@ -1,206 +1,630 @@
[ [
{ {
"id": "ore", "id": "ore",
"label": "Raw Ore", "name": "Raw Ore",
"storage": "bulk-solid", "description": "Unprocessed asteroid ore used as the main industrial feedstock.",
"summary": "Unprocessed asteroid ore used as the main industrial feedstock." "type": "resource",
}, "cargoKind": "bulk-solid",
{ "volume": 1.2
"id": "refined-metals",
"label": "Refined Metals",
"storage": "manufactured",
"summary": "Processed structural metals used by stations and shipyards."
},
{
"id": "hull-sections",
"label": "Hull Sections",
"storage": "manufactured",
"summary": "Prefabricated structural assemblies for ships and stations."
},
{
"id": "ammo-crates",
"label": "Ammo Crates",
"storage": "container",
"summary": "Containerized magazines for turrets, launchers, and point defense."
},
{
"id": "naval-guns",
"label": "Naval Guns",
"storage": "manufactured",
"summary": "Shipboard turret and cannon assemblies."
},
{
"id": "ship-equipment",
"label": "Ship Equipment",
"storage": "container",
"summary": "Shield emitters, avionics, cooling loops, and service kits."
},
{
"id": "ship-parts",
"label": "Ship Parts",
"storage": "manufactured",
"summary": "High-value integration kits for hull fitting and final assembly."
},
{
"id": "command-bridge-module",
"label": "Command Bridge Module",
"storage": "container",
"summary": "Packaged bridge and combat-information-center assembly for final ship integration."
},
{
"id": "reactor-core-module",
"label": "Reactor Core Module",
"storage": "container",
"summary": "Contained ship reactor package ready for installation into a hull."
},
{
"id": "capacitor-bank-module",
"label": "Capacitor Bank Module",
"storage": "container",
"summary": "Buffered capacitor section for propulsion, weapons, and industrial loads."
},
{
"id": "ion-drive-module",
"label": "Ion Drive Module",
"storage": "container",
"summary": "Preassembled sublight engine unit."
},
{
"id": "ftl-core-module",
"label": "FTL Core Module",
"storage": "container",
"summary": "Integrated FTL drive package for inter-system transit."
},
{
"id": "gun-turret-module",
"label": "Gun Turret Module",
"storage": "container",
"summary": "Shipboard turret mount and fire-control package."
},
{
"id": "carrier-bay-module",
"label": "Carrier Bay Module",
"storage": "container",
"summary": "Hangar and launch-recovery assembly for capital ship integration."
},
{
"id": "habitat-ring-module",
"label": "Habitat Ring Module",
"storage": "container",
"summary": "Crew habitat section packaged for large ship installation."
},
{
"id": "bulk-bay-module",
"label": "Bulk Bay Module",
"storage": "container",
"summary": "Industrial cargo hold segment for raw-solid hauling ships."
},
{
"id": "container-bay-module",
"label": "Container Bay Module",
"storage": "container",
"summary": "Freight rack segment for manufactured and palletized cargo."
},
{
"id": "liquid-tank-module",
"label": "Liquid Tank Module",
"storage": "container",
"summary": "Pressurized liquid storage segment for fuel and energy logistics."
},
{
"id": "gas-tank-module",
"label": "Gas Tank Module",
"storage": "container",
"summary": "Pressurized gas storage segment for volatile cargo hauling."
},
{
"id": "mining-turret-module",
"label": "Mining Turret Module",
"storage": "container",
"summary": "Ship-mounted hard-rock extraction head."
},
{
"id": "gas-extractor-module",
"label": "Gas Extractor Module",
"storage": "container",
"summary": "Cryogenic intake and compression package for gas harvesting ships."
},
{
"id": "fabricator-array-module",
"label": "Fabricator Array Module",
"storage": "container",
"summary": "Mobile industrial fabrication block for constructors."
},
{
"id": "gas",
"label": "Volatile Gas",
"storage": "bulk-gas",
"summary": "Compressed gas reserves for future chemical and fuel chains."
},
{
"id": "fuel",
"label": "Reactor Fuel",
"storage": "bulk-liquid",
"summary": "Processed liquid fuel consumed by ships and station power systems."
},
{
"id": "energy-cell",
"label": "Energy Cell",
"storage": "bulk-liquid",
"summary": "Charged energy reserves that can be stored, traded, and discharged into station power grids."
}, },
{ {
"id": "water", "id": "water",
"label": "Water", "name": "Water",
"storage": "bulk-liquid", "description": "Life-support and agricultural input.",
"summary": "Life-support and agricultural input." "type": "commodity",
"cargoKind": "bulk-liquid",
"volume": 1.0,
"construction": {
"recipeId": "water-reclamation",
"facilityCategory": "farm",
"requiredModules": ["liquid-tank", "solar-array"],
"requirements": [],
"cycleTime": 6,
"batchSize": 12,
"productsPerHour": 7200,
"maxEfficiency": 1,
"priority": 14
}
},
{
"id": "refined-metals",
"name": "Refined Metals",
"description": "Processed structural metals used by stations and shipyards.",
"type": "material",
"cargoKind": "manufactured",
"volume": 1.0,
"construction": {
"recipeId": "ore-refining",
"facilityCategory": "station",
"requiredModules": ["refinery-stack"],
"requirements": [
{ "itemId": "ore", "amount": 60 }
],
"cycleTime": 8,
"batchSize": 60,
"productsPerHour": 27000,
"maxEfficiency": 1,
"priority": 100
}
},
{
"id": "hull-sections",
"name": "Hull Sections",
"description": "Prefabricated structural assemblies for ships and stations.",
"type": "component",
"cargoKind": "manufactured",
"volume": 1.5,
"construction": {
"recipeId": "hull-fabrication",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "refined-metals", "amount": 70 }
],
"cycleTime": 10,
"batchSize": 35,
"productsPerHour": 12600,
"maxEfficiency": 1,
"priority": 40
}
},
{
"id": "ammo-crates",
"name": "Ammo Crates",
"description": "Containerized magazines for turrets, launchers, and point defense.",
"type": "component",
"cargoKind": "container",
"volume": 1.0,
"construction": {
"recipeId": "ammo-fabrication",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "refined-metals", "amount": 24 }
],
"cycleTime": 6,
"batchSize": 30,
"productsPerHour": 18000,
"maxEfficiency": 1,
"priority": 34
}
},
{
"id": "naval-guns",
"name": "Naval Guns",
"description": "Shipboard turret and cannon assemblies.",
"type": "component",
"cargoKind": "manufactured",
"volume": 1.4,
"construction": {
"recipeId": "gun-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "refined-metals", "amount": 36 }
],
"cycleTime": 9,
"batchSize": 12,
"productsPerHour": 4800,
"maxEfficiency": 1,
"priority": 32
}
},
{
"id": "ship-equipment",
"name": "Ship Equipment",
"description": "Shield emitters, avionics, cooling loops, and service kits.",
"type": "component",
"cargoKind": "container",
"volume": 1.0,
"construction": {
"recipeId": "equipment-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "refined-metals", "amount": 28 },
{ "itemId": "water", "amount": 8 }
],
"cycleTime": 11,
"batchSize": 18,
"productsPerHour": 5890.9,
"maxEfficiency": 1,
"priority": 30
}
},
{
"id": "ship-parts",
"name": "Ship Parts",
"description": "High-value integration kits for hull fitting and final assembly.",
"type": "component",
"cargoKind": "manufactured",
"volume": 1.3,
"construction": {
"recipeId": "ship-parts-integration",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "hull-sections", "amount": 24 },
{ "itemId": "naval-guns", "amount": 6 },
{ "itemId": "ship-equipment", "amount": 10 }
],
"cycleTime": 14,
"batchSize": 20,
"productsPerHour": 5142.9,
"maxEfficiency": 1,
"priority": 50
}
}, },
{ {
"id": "drone-parts", "id": "drone-parts",
"label": "Drone Parts", "name": "Drone Parts",
"storage": "container", "description": "Containerized industrial freight for construction support.",
"summary": "Containerized industrial freight." "type": "component",
"cargoKind": "container",
"volume": 1.0,
"construction": {
"recipeId": "drone-parts-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "refined-metals", "amount": 12 },
{ "itemId": "ship-equipment", "amount": 6 }
],
"cycleTime": 7,
"batchSize": 16,
"productsPerHour": 8228.6,
"maxEfficiency": 1,
"priority": 18
}
},
{
"id": "command-bridge-module",
"name": "Command Bridge Module",
"description": "Packaged bridge and combat-information-center assembly for final ship integration.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.0,
"construction": {
"recipeId": "command-bridge-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 20 },
{ "itemId": "ship-equipment", "amount": 10 }
],
"cycleTime": 9,
"batchSize": 1,
"productsPerHour": 400,
"maxEfficiency": 1,
"priority": 52
}
},
{
"id": "reactor-core-module",
"name": "Reactor Core Module",
"description": "Contained ship reactor package ready for installation into a hull.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.2,
"construction": {
"recipeId": "reactor-core-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 30 },
{ "itemId": "ship-equipment", "amount": 8 }
],
"cycleTime": 10,
"batchSize": 1,
"productsPerHour": 360,
"maxEfficiency": 1,
"priority": 54
}
},
{
"id": "capacitor-bank-module",
"name": "Capacitor Bank Module",
"description": "Buffered capacitor section for propulsion, weapons, and industrial loads.",
"type": "ship-module",
"cargoKind": "container",
"volume": 1.8,
"construction": {
"recipeId": "capacitor-bank-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 4 }
],
"cycleTime": 9,
"batchSize": 1,
"productsPerHour": 400,
"maxEfficiency": 1,
"priority": 52
}
},
{
"id": "ion-drive-module",
"name": "Ion Drive Module",
"description": "Preassembled sublight engine unit.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.0,
"construction": {
"recipeId": "ion-drive-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 22 },
{ "itemId": "ship-equipment", "amount": 8 }
],
"cycleTime": 10,
"batchSize": 1,
"productsPerHour": 360,
"maxEfficiency": 1,
"priority": 53
}
},
{
"id": "ftl-core-module",
"name": "FTL Core Module",
"description": "Integrated FTL drive package for inter-system transit.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.4,
"construction": {
"recipeId": "ftl-core-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 34 },
{ "itemId": "ship-equipment", "amount": 14 }
],
"cycleTime": 12,
"batchSize": 1,
"productsPerHour": 300,
"maxEfficiency": 1,
"priority": 56
}
},
{
"id": "gun-turret-module",
"name": "Gun Turret Module",
"description": "Shipboard turret mount and fire-control package.",
"type": "ship-module",
"cargoKind": "container",
"volume": 1.6,
"construction": {
"recipeId": "gun-turret-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "naval-guns", "amount": 8 },
{ "itemId": "refined-metals", "amount": 12 }
],
"cycleTime": 8,
"batchSize": 1,
"productsPerHour": 450,
"maxEfficiency": 1,
"priority": 58
}
},
{
"id": "carrier-bay-module",
"name": "Carrier Bay Module",
"description": "Hangar and launch-recovery assembly for capital ship integration.",
"type": "ship-module",
"cargoKind": "container",
"volume": 3.0,
"construction": {
"recipeId": "carrier-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "hull-sections", "amount": 18 },
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 10 }
],
"cycleTime": 14,
"batchSize": 1,
"productsPerHour": 257.1,
"maxEfficiency": 1,
"priority": 40
}
},
{
"id": "habitat-ring-module",
"name": "Habitat Ring Module",
"description": "Crew habitat section packaged for large ship installation.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.6,
"construction": {
"recipeId": "habitat-ring-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "hull-sections", "amount": 14 },
{ "itemId": "ship-equipment", "amount": 8 },
{ "itemId": "water", "amount": 10 }
],
"cycleTime": 12,
"batchSize": 1,
"productsPerHour": 300,
"maxEfficiency": 1,
"priority": 22
}
},
{
"id": "bulk-bay-module",
"name": "Bulk Bay Module",
"description": "Industrial cargo hold segment for raw-solid hauling ships.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.0,
"construction": {
"recipeId": "bulk-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 16 },
{ "itemId": "hull-sections", "amount": 10 }
],
"cycleTime": 8,
"batchSize": 1,
"productsPerHour": 450,
"maxEfficiency": 1,
"priority": 18
}
},
{
"id": "container-bay-module",
"name": "Container Bay Module",
"description": "Freight rack segment for manufactured and palletized cargo.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.0,
"construction": {
"recipeId": "container-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 12 },
{ "itemId": "ship-equipment", "amount": 4 }
],
"cycleTime": 8,
"batchSize": 1,
"productsPerHour": 450,
"maxEfficiency": 1,
"priority": 18
}
},
{
"id": "liquid-tank-module",
"name": "Liquid Tank Module",
"description": "Pressurized liquid storage segment for water and liquid logistics.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.0,
"construction": {
"recipeId": "liquid-tank-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 14 },
{ "itemId": "ship-equipment", "amount": 4 }
],
"cycleTime": 8,
"batchSize": 1,
"productsPerHour": 450,
"maxEfficiency": 1,
"priority": 18
}
},
{
"id": "mining-turret-module",
"name": "Mining Turret Module",
"description": "Ship-mounted hard-rock extraction head.",
"type": "ship-module",
"cargoKind": "container",
"volume": 1.8,
"construction": {
"recipeId": "mining-turret-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 6 }
],
"cycleTime": 9,
"batchSize": 1,
"productsPerHour": 400,
"maxEfficiency": 1,
"priority": 24
}
},
{
"id": "fabricator-array-module",
"name": "Fabricator Array Module",
"description": "Mobile industrial fabrication block for constructors.",
"type": "ship-module",
"cargoKind": "container",
"volume": 2.4,
"construction": {
"recipeId": "fabricator-array-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requirements": [
{ "itemId": "refined-metals", "amount": 24 },
{ "itemId": "ship-equipment", "amount": 10 }
],
"cycleTime": 11,
"batchSize": 1,
"productsPerHour": 327.3,
"maxEfficiency": 1,
"priority": 20
}
}, },
{ {
"id": "trade-hub-kit", "id": "trade-hub-kit",
"label": "Trade Hub Kit", "name": "Trade Hub Kit",
"storage": "manufactured", "description": "Deployable prefab package for a trade hub station.",
"summary": "Deployable prefab package for a trade hub station." "type": "kit",
"cargoKind": "manufactured",
"volume": 6.0,
"construction": {
"recipeId": "trade-hub-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 26 },
{ "itemId": "ship-equipment", "amount": 16 },
{ "itemId": "drone-parts", "amount": 10 }
],
"cycleTime": 18,
"batchSize": 1,
"productsPerHour": 200,
"maxEfficiency": 1,
"priority": 24
}
}, },
{ {
"id": "refinery-kit", "id": "refinery-kit",
"label": "Refinery Kit", "name": "Refinery Kit",
"storage": "manufactured", "description": "Deployable prefab package for a refining station.",
"summary": "Deployable prefab package for a refining station." "type": "kit",
"cargoKind": "manufactured",
"volume": 6.5,
"construction": {
"recipeId": "refinery-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 32 },
{ "itemId": "hull-sections", "amount": 24 },
{ "itemId": "ship-equipment", "amount": 14 }
],
"cycleTime": 20,
"batchSize": 1,
"productsPerHour": 180,
"maxEfficiency": 1,
"priority": 26
}
}, },
{ {
"id": "farm-ring-kit", "id": "farm-ring-kit",
"label": "Farm Ring Kit", "name": "Farm Ring Kit",
"storage": "manufactured", "description": "Deployable prefab package for a farm station.",
"summary": "Deployable prefab package for a farm station." "type": "kit",
"cargoKind": "manufactured",
"volume": 6.0,
"construction": {
"recipeId": "farm-ring-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 22 },
{ "itemId": "ship-equipment", "amount": 18 },
{ "itemId": "water", "amount": 22 }
],
"cycleTime": 18,
"batchSize": 1,
"productsPerHour": 200,
"maxEfficiency": 1,
"priority": 22
}
}, },
{ {
"id": "manufactory-kit", "id": "manufactory-kit",
"label": "Manufactory Kit", "name": "Manufactory Kit",
"storage": "manufactured", "description": "Deployable prefab package for an orbital manufactory.",
"summary": "Deployable prefab package for an orbital manufactory." "type": "kit",
"cargoKind": "manufactured",
"volume": 7.0,
"construction": {
"recipeId": "manufactory-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 34 },
{ "itemId": "hull-sections", "amount": 16 },
{ "itemId": "ship-equipment", "amount": 18 }
],
"cycleTime": 22,
"batchSize": 1,
"productsPerHour": 163.6,
"maxEfficiency": 1,
"priority": 28
}
}, },
{ {
"id": "shipyard-kit", "id": "shipyard-kit",
"label": "Shipyard Kit", "name": "Shipyard Kit",
"storage": "manufactured", "description": "Deployable prefab package for an orbital shipyard.",
"summary": "Deployable prefab package for an orbital shipyard." "type": "kit",
"cargoKind": "manufactured",
"volume": 8.0,
"construction": {
"recipeId": "shipyard-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 42 },
{ "itemId": "hull-sections", "amount": 30 },
{ "itemId": "naval-guns", "amount": 10 }
],
"cycleTime": 26,
"batchSize": 1,
"productsPerHour": 138.5,
"maxEfficiency": 1,
"priority": 30
}
}, },
{ {
"id": "defense-grid-kit", "id": "defense-grid-kit",
"label": "Defense Grid Kit", "name": "Defense Grid Kit",
"storage": "manufactured", "description": "Deployable prefab package for a defense platform.",
"summary": "Deployable prefab package for a defense platform." "type": "kit",
"cargoKind": "manufactured",
"volume": 7.0,
"construction": {
"recipeId": "defense-grid-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 18 },
{ "itemId": "naval-guns", "amount": 12 },
{ "itemId": "ammo-crates", "amount": 18 }
],
"cycleTime": 16,
"batchSize": 1,
"productsPerHour": 225,
"maxEfficiency": 1,
"priority": 20
}
}, },
{ {
"id": "stargate-kit", "id": "stargate-kit",
"label": "Stargate Kit", "name": "Stargate Kit",
"storage": "manufactured", "description": "Deployable prefab package for a stargate structure.",
"summary": "Deployable prefab package for a stargate structure." "type": "kit",
"cargoKind": "manufactured",
"volume": 10.0,
"construction": {
"recipeId": "stargate-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 60 },
{ "itemId": "hull-sections", "amount": 44 },
{ "itemId": "ship-equipment", "amount": 26 },
{ "itemId": "naval-guns", "amount": 8 }
],
"cycleTime": 34,
"batchSize": 1,
"productsPerHour": 105.9,
"maxEfficiency": 1,
"priority": 36
}
} }
] ]

View File

@@ -1,68 +0,0 @@
[
{
"moduleId": "dock-bay-small",
"duration": 12,
"inputs": [
{ "itemId": "refined-metals", "amount": 34 }
]
},
{
"moduleId": "gas-tank",
"duration": 10,
"inputs": [
{ "itemId": "refined-metals", "amount": 30 }
]
},
{
"moduleId": "container-bay",
"duration": 10,
"inputs": [
{ "itemId": "refined-metals", "amount": 26 }
]
},
{
"moduleId": "fuel-processor",
"duration": 14,
"inputs": [
{ "itemId": "refined-metals", "amount": 42 }
]
},
{
"moduleId": "refinery-stack",
"duration": 14,
"inputs": [
{ "itemId": "refined-metals", "amount": 38 }
]
},
{
"moduleId": "fabricator-array",
"duration": 16,
"inputs": [
{ "itemId": "refined-metals", "amount": 48 }
]
},
{
"moduleId": "component-factory",
"duration": 18,
"inputs": [
{ "itemId": "refined-metals", "amount": 54 },
{ "itemId": "ship-equipment", "amount": 12 }
]
},
{
"moduleId": "ship-factory",
"duration": 22,
"inputs": [
{ "itemId": "refined-metals", "amount": 60 },
{ "itemId": "hull-sections", "amount": 24 },
{ "itemId": "ship-equipment", "amount": 14 }
]
},
{
"moduleId": "solar-array",
"duration": 12,
"inputs": [
{ "itemId": "refined-metals", "amount": 28 }
]
}
]

247
shared/data/modules.json Normal file
View File

@@ -0,0 +1,247 @@
[
{
"id": "dock-bay-small",
"name": "Small Dock Bay",
"description": "External docking pad cluster for small and medium hulls.",
"type": "Dock",
"hull": 160,
"workforceNeeded": 10,
"construction": {
"productionTime": 12,
"requirements": [
{
"itemId": "refined-metals",
"amount": 34
}
]
}
},
{
"id": "container-bay",
"name": "Container Bay",
"description": "Manufactured cargo storage and container handling racks.",
"type": "Storage",
"hull": 140,
"workforceNeeded": 8,
"construction": {
"productionTime": 10,
"requirements": [
{
"itemId": "refined-metals",
"amount": 26
}
]
}
},
{
"id": "bulk-bay",
"name": "Bulk Bay",
"description": "Raw solid storage and ore handling volume.",
"type": "Storage",
"hull": 140,
"workforceNeeded": 8
},
{
"id": "liquid-tank",
"name": "Liquid Tank",
"description": "Liquid cargo and water tankage.",
"type": "Storage",
"hull": 130,
"workforceNeeded": 6,
"construction": {
"productionTime": 10,
"requirements": [
{
"itemId": "refined-metals",
"amount": 20
}
]
}
},
{
"id": "refinery-stack",
"name": "Refinery Stack",
"description": "Heavy refining line for ore to refined metals.",
"type": "Production",
"product": "refined-metals",
"hull": 180,
"workforceNeeded": 18,
"construction": {
"productionTime": 14,
"requirements": [
{
"itemId": "refined-metals",
"amount": 38
}
]
}
},
{
"id": "solar-array",
"name": "Solar Array",
"description": "Orbital solar generation and utility frame.",
"type": "Production",
"hull": 110,
"workforceNeeded": 6,
"construction": {
"productionTime": 12,
"requirements": [
{
"itemId": "refined-metals",
"amount": 28
}
]
}
},
{
"id": "fabricator-array",
"name": "Fabricator Array",
"description": "General fabrication line for industrial goods and prefab kits.",
"type": "Build Module",
"hull": 200,
"workforceNeeded": 20,
"construction": {
"productionTime": 16,
"requirements": [
{
"itemId": "refined-metals",
"amount": 48
}
]
}
},
{
"id": "component-factory",
"name": "Component Factory",
"description": "Assembly line for ship-grade modules and integrated components.",
"type": "Build Module",
"hull": 220,
"workforceNeeded": 24,
"construction": {
"productionTime": 18,
"requirements": [
{
"itemId": "refined-metals",
"amount": 54
},
{
"itemId": "ship-equipment",
"amount": 12
}
]
}
},
{
"id": "ship-factory",
"name": "Ship Factory",
"description": "Slip-line and integration yard for final ship assembly.",
"type": "Build Module",
"hull": 260,
"workforceNeeded": 28,
"construction": {
"productionTime": 22,
"requirements": [
{
"itemId": "refined-metals",
"amount": 60
},
{
"itemId": "hull-sections",
"amount": 24
},
{
"itemId": "ship-equipment",
"amount": 14
}
]
}
},
{
"id": "power-core",
"name": "Power Core",
"description": "Station backbone for power routing and core services.",
"type": "Connection",
"hull": 220,
"workforceNeeded": 10
},
{
"id": "habitat-ring",
"name": "Habitat Ring",
"description": "Crew habitation and life-support section.",
"type": "Habitation",
"hull": 180,
"workforceNeeded": 12
},
{
"id": "turret-grid",
"name": "Turret Grid",
"description": "Defensive hardpoints and fire-control grid.",
"type": "Defense",
"hull": 180,
"workforceNeeded": 10
},
{
"id": "command-bridge",
"name": "Command Bridge",
"description": "Command-and-control section for stations and capital structures.",
"type": "Connection",
"hull": 150,
"workforceNeeded": 8
},
{
"id": "reactor-core",
"name": "Reactor Core",
"description": "Primary reactor and power conversion system.",
"type": "Connection",
"hull": 150,
"workforceNeeded": 8
},
{
"id": "capacitor-bank",
"name": "Capacitor Bank",
"description": "Energy buffering and discharge system.",
"type": "Connection",
"hull": 120,
"workforceNeeded": 4
},
{
"id": "ion-drive",
"name": "Ion Drive",
"description": "Primary sublight propulsion module.",
"type": "Connection",
"hull": 120,
"workforceNeeded": 4
},
{
"id": "ftl-core",
"name": "FTL Core",
"description": "Inter-system transit drive core.",
"type": "Connection",
"hull": 140,
"workforceNeeded": 6
},
{
"id": "gun-turret",
"name": "Gun Turret",
"description": "General purpose shipboard turret.",
"type": "Defense",
"hull": 100,
"workforceNeeded": 3
},
{
"id": "carrier-bay",
"name": "Carrier Bay",
"description": "Launch and recovery bay for carried craft.",
"type": "Pier",
"hull": 160,
"workforceNeeded": 8
},
{
"id": "mining-turret",
"name": "Mining Turret",
"description": "Hard-rock extraction head for mining hulls.",
"type": "Production",
"hull": 90,
"workforceNeeded": 3
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,13 @@
{ {
"initialStations": [ "initialStations": [
{ {
"constructibleId": "station-core", "label": "Orbital Station",
"startingModules": [
"dock-bay-small",
"power-core",
"bulk-bay",
"liquid-tank"
],
"systemId": "helios", "systemId": "helios",
"planetIndex": 2, "planetIndex": 2,
"lagrangeSide": -1 "lagrangeSide": -1
@@ -29,7 +35,7 @@
"systemId": "helios" "systemId": "helios"
}, },
{ {
"shipId": "gas-miner", "shipId": "hauler",
"count": 1, "count": 1,
"center": [ "center": [
60, 60,
@@ -37,16 +43,6 @@
28 28
], ],
"systemId": "helios" "systemId": "helios"
},
{
"shipId": "gas-miner",
"count": 1,
"center": [
60,
0,
32
],
"systemId": "helios"
} }
], ],
"patrolRoutes": [], "patrolRoutes": [],

View File

@@ -13,7 +13,58 @@
"hullColor": "#1f4f78", "hullColor": "#1f4f78",
"size": 4, "size": 4,
"maxHealth": 100, "maxHealth": 100,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret"] "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret"
],
"construction": {
"recipeId": "frigate-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 26
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "gun-turret-module",
"amount": 1
}
],
"cycleTime": 24,
"productsPerHour": 150,
"maxEfficiency": 1,
"priority": 90
}
}, },
{ {
"id": "destroyer", "id": "destroyer",
@@ -29,7 +80,59 @@
"hullColor": "#6a2e26", "hullColor": "#6a2e26",
"size": 7, "size": 7,
"maxHealth": 240, "maxHealth": 240,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret", "gun-turret"] "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret",
"gun-turret"
],
"construction": {
"recipeId": "destroyer-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 44
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "gun-turret-module",
"amount": 2
}
],
"cycleTime": 34,
"productsPerHour": 105.9,
"maxEfficiency": 1,
"priority": 70
}
}, },
{ {
"id": "cruiser", "id": "cruiser",
@@ -45,7 +148,59 @@
"hullColor": "#314562", "hullColor": "#314562",
"size": 10, "size": 10,
"maxHealth": 340, "maxHealth": 340,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gun-turret", "gun-turret"] "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret",
"gun-turret"
],
"construction": {
"recipeId": "cruiser-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 60
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "gun-turret-module",
"amount": 2
}
],
"cycleTime": 42,
"productsPerHour": 85.7,
"maxEfficiency": 1,
"priority": 54
}
}, },
{ {
"id": "carrier", "id": "carrier",
@@ -61,9 +216,75 @@
"hullColor": "#35586d", "hullColor": "#35586d",
"size": 16, "size": 16,
"maxHealth": 900, "maxHealth": 900,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "carrier-bay", "carrier-bay", "gun-turret", "habitat-ring"], "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"carrier-bay",
"carrier-bay",
"gun-turret",
"habitat-ring"
],
"dockingCapacity": 6, "dockingCapacity": 6,
"dockingClasses": ["frigate", "destroyer", "cruiser"] "dockingClasses": [
"frigate",
"destroyer",
"cruiser"
],
"construction": {
"recipeId": "carrier-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 120
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "carrier-bay-module",
"amount": 2
},
{
"itemId": "gun-turret-module",
"amount": 1
},
{
"itemId": "habitat-ring-module",
"amount": 1
}
],
"cycleTime": 60,
"productsPerHour": 60,
"maxEfficiency": 1,
"priority": 28
}
}, },
{ {
"id": "hauler", "id": "hauler",
@@ -75,13 +296,63 @@
"ftlSpeed": 0.55, "ftlSpeed": 0.55,
"spoolTime": 3.3, "spoolTime": 3.3,
"cargoCapacity": 180, "cargoCapacity": 180,
"cargoKind": "bulk-liquid", "cargoKind": "container",
"cargoItemId": "energy-cell",
"color": "#b0ff8d", "color": "#b0ff8d",
"hullColor": "#365f2a", "hullColor": "#365f2a",
"size": 8, "size": 8,
"maxHealth": 180, "maxHealth": 180,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "liquid-tank"] "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"container-bay"
],
"construction": {
"recipeId": "hauler-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 34
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "container-bay-module",
"amount": 1
}
],
"cycleTime": 26,
"productsPerHour": 138.5,
"maxEfficiency": 1,
"priority": 8
}
}, },
{ {
"id": "constructor", "id": "constructor",
@@ -99,7 +370,63 @@
"hullColor": "#2d5d47", "hullColor": "#2d5d47",
"size": 9, "size": 9,
"maxHealth": 220, "maxHealth": 220,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "fabricator-array", "container-bay"] "modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"fabricator-array",
"container-bay"
],
"construction": {
"recipeId": "constructor-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 42
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "fabricator-array-module",
"amount": 1
},
{
"itemId": "container-bay-module",
"amount": 1
}
],
"cycleTime": 30,
"productsPerHour": 120,
"maxEfficiency": 1,
"priority": 8
}
}, },
{ {
"id": "miner", "id": "miner",
@@ -117,24 +444,62 @@
"hullColor": "#68552b", "hullColor": "#68552b",
"size": 6, "size": 6,
"maxHealth": 150, "maxHealth": 150,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "mining-turret", "bulk-bay"] "modules": [
}, "command-bridge",
{ "reactor-core",
"id": "gas-miner", "capacitor-bank",
"label": "Nimbus Gas Harvester", "ion-drive",
"role": "mining", "ftl-core",
"shipClass": "industrial", "mining-turret",
"speed": 72000, "bulk-bay"
"warpSpeed": 0.145, ],
"ftlSpeed": 0.49, "construction": {
"spoolTime": 3.2, "recipeId": "miner-construction",
"cargoCapacity": 120, "facilityCategory": "station",
"cargoKind": "bulk-gas", "requiredModules": [
"cargoItemId": "gas", "ship-factory",
"color": "#8ce5ff", "dock-bay-small",
"hullColor": "#2a5668", "container-bay",
"size": 6, "power-core"
"maxHealth": 150, ],
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gas-extractor", "gas-tank"] "requirements": [
{
"itemId": "hull-sections",
"amount": 34
},
{
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "mining-turret-module",
"amount": 1
},
{
"itemId": "bulk-bay-module",
"amount": 1
}
],
"cycleTime": 28,
"productsPerHour": 128.6,
"maxEfficiency": 1,
"priority": 8
}
} }
] ]