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,5 +1,18 @@
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; }
@@ -10,16 +23,6 @@ public sealed class BalanceDefinition
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
@@ -62,9 +65,18 @@ public sealed class ResourceNodeDefinition
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 ConstructionDefinition? Construction { get; set; }
}
public sealed class RecipeOutputDefinition
{
public required string ItemId { get; set; }
public float Amount { get; set; }
} }
public sealed class RecipeInputDefinition public sealed class RecipeInputDefinition
@@ -73,6 +85,25 @@ public sealed class RecipeInputDefinition
public float Amount { 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 sealed class ModuleRecipeDefinition
{ {
public required string ModuleId { get; set; } public required string ModuleId { get; set; }
@@ -80,12 +111,6 @@ public sealed class ModuleRecipeDefinition
public required List<RecipeInputDefinition> Inputs { get; set; } public required List<RecipeInputDefinition> Inputs { get; set; }
} }
public sealed class RecipeOutputDefinition
{
public required string ItemId { get; set; }
public float Amount { get; set; }
}
public sealed class RecipeDefinition public sealed class RecipeDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
@@ -136,18 +161,7 @@ public sealed class ShipDefinition
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
@@ -160,8 +174,10 @@ public sealed class ScenarioDefinition
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 Color { get; set; } = "#8df0d2";
public List<string> StartingModules { get; set; } = [];
public string? FactionId { get; set; } public string? FactionId { get; set; }
public int? PlanetIndex { get; set; } public int? PlanetIndex { get; set; }
public int? LagrangeSide { get; set; } public int? LagrangeSide { 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

@@ -90,13 +90,7 @@ internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
ship.DefaultBehavior.Phase = "dock"; ship.DefaultBehavior.Phase = "dock";
break; break;
case ("dock", "docked"): case ("dock", "docked"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "refuel"; ship.DefaultBehavior.Phase = "unload";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = "refuel";
break;
case ("refuel", "refueled"):
ship.DefaultBehavior.Phase = "undock";
break; break;
case ("undock", "undocked"): case ("undock", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-node"; ship.DefaultBehavior.Phase = "travel-to-node";
@@ -118,9 +112,6 @@ internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
switch (ship.DefaultBehavior.Phase, controllerEvent) switch (ship.DefaultBehavior.Phase, controllerEvent)
{ {
case ("travel-to-station", "arrived"): case ("travel-to-station", "arrived"):
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "deliver-to-site";
break;
case ("refuel", "refueled"):
ship.DefaultBehavior.Phase = "deliver-to-site"; ship.DefaultBehavior.Phase = "deliver-to-site";
break; break;
case ("deliver-to-site", "construction-delivered"): case ("deliver-to-site", "construction-delivered"):
@@ -134,37 +125,3 @@ internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
} }
} }
} }
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

@@ -19,8 +19,7 @@ public sealed class ShipRuntime
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; }

View File

@@ -28,7 +28,6 @@ public enum ShipState
{ {
Idle, Idle,
Arriving, Arriving,
CapacitorStarved,
LocalFlight, LocalFlight,
SpoolingWarp, SpoolingWarp,
Warping, Warping,
@@ -45,7 +44,6 @@ public enum ShipState
Transferring, Transferring,
Loading, Loading,
Unloading, Unloading,
Refueling,
WaitingMaterials, WaitingMaterials,
ConstructionBlocked, ConstructionBlocked,
Constructing, Constructing,
@@ -62,7 +60,6 @@ public enum ControllerTaskKind
Dock, Dock,
Load, Load,
Unload, Unload,
Refuel,
DeliverConstruction, DeliverConstruction,
BuildConstructionSite, BuildConstructionSite,
LoadWorkers, LoadWorkers,
@@ -197,7 +194,6 @@ public static class SimulationEnumMappings
{ {
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",
@@ -214,7 +210,6 @@ public static class SimulationEnumMappings
ShipState.Transferring => "transferring", ShipState.Transferring => "transferring",
ShipState.Loading => "loading", ShipState.Loading => "loading",
ShipState.Unloading => "unloading", ShipState.Unloading => "unloading",
ShipState.Refueling => "refueling",
ShipState.WaitingMaterials => "waiting-materials", ShipState.WaitingMaterials => "waiting-materials",
ShipState.ConstructionBlocked => "construction-blocked", ShipState.ConstructionBlocked => "construction-blocked",
ShipState.Constructing => "constructing", ShipState.Constructing => "constructing",
@@ -232,7 +227,6 @@ public static class SimulationEnumMappings
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",

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,25 +1,26 @@
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 string Category { get; set; } = "station";
public string Color { get; set; } = "#8df0d2";
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public float Radius { get; set; } = 24f;
public required string FactionId { get; init; } public required string FactionId { get; init; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? BubbleId { get; set; } public string? BubbleId { get; set; }
public string? AnchorNodeId { get; set; } public string? AnchorNodeId { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public List<string> InstalledModules { get; } = []; public List<StationModuleRuntime> Modules { get; } = [];
public IEnumerable<string> InstalledModules => Modules.Select((module) => module.ModuleId);
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal);
public Dictionary<int, string> DockingPadAssignments { get; } = new(); public Dictionary<int, string> DockingPadAssignments { get; } = new();
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float EnergyStored { get; set; }
public float Population { get; set; } public float Population { get; set; }
public float PopulationCapacity { get; set; } public float PopulationCapacity { get; set; }
public float WorkforceRequired { get; set; } public float WorkforceRequired { get; set; }
@@ -31,6 +32,14 @@ public sealed class StationRuntime
public string LastDeltaSignature { get; set; } = string.Empty; 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; }

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

@@ -25,7 +25,6 @@ public sealed partial class SimulationEngine
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds), ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds), ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds), ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
ControllerTaskKind.Refuel => UpdateRefuel(ship, world, deltaSeconds),
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds), ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds), ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds), ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds),
@@ -38,7 +37,6 @@ public sealed partial class SimulationEngine
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds) private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{ {
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
ship.State = ShipState.Idle; ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position; ship.TargetPosition = ship.Position;
return "none"; return "none";
@@ -133,7 +131,6 @@ public sealed partial class SimulationEngine
if (distance <= threshold) if (distance <= threshold)
{ {
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
ship.Position = targetPosition; ship.Position = targetPosition;
ship.TargetPosition = ship.Position; ship.TargetPosition = ship.Position;
ship.SystemId = targetSystemId; ship.SystemId = targetSystemId;
@@ -143,13 +140,6 @@ public sealed partial class SimulationEngine
return "arrived"; return "arrived";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
ship.State = ShipState.LocalFlight; ship.State = ShipState.LocalFlight;
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
@@ -185,13 +175,6 @@ public sealed partial class SimulationEngine
ship.ActionTimer = 0f; 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; ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration)) if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
{ {
@@ -201,13 +184,6 @@ public sealed partial class SimulationEngine
ship.State = ShipState.Warping; 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 var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition) ? ship.Position.DistanceTo(targetPosition)
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); : (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
@@ -247,13 +223,6 @@ public sealed partial class SimulationEngine
ship.ActionTimer = 0f; 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; ship.State = ShipState.SpoolingFtl;
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime)) if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
{ {
@@ -263,13 +232,6 @@ public sealed partial class SimulationEngine
ship.State = ShipState.Ftl; 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 originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));

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,8 +5,6 @@ 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) => private static bool HasShipModules(ShipDefinition definition, params string[] modules) =>
modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal)); modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
@@ -16,169 +14,56 @@ public sealed partial class SimulationEngine
private static float GetWorkerTransportCapacity(ShipRuntime ship) => private static float GetWorkerTransportCapacity(ShipRuntime ship) =>
CountModules(ship.Definition.Modules, "habitat-ring") * 120f; CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
private static void UpdateStationPower(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events) private static int CountStationModules(StationRuntime station, string moduleId) =>
{ station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
foreach (var station in world.Stations)
{
var previousEnergy = station.EnergyStored;
GenerateStationEnergy(station, world, deltaSeconds);
if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "fuel") <= 0.01f) private static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{ {
events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow)); if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
}
}
}
private static void UpdateShipPower(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
var previousEnergy = ship.EnergyStored;
GenerateShipEnergy(ship, world, deltaSeconds);
if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "fuel") <= 0.01f)
{
events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
}
}
private static void GenerateStationEnergy(StationRuntime station, SimulationWorld world, float deltaSeconds)
{
var powerCores = CountModules(station.InstalledModules, "power-core");
var tanks = CountModules(station.InstalledModules, "liquid-tank");
if (powerCores <= 0 || tanks <= 0)
{
station.EnergyStored = 0f;
station.Inventory.Remove("fuel");
return; return;
} }
var energyCapacity = powerCores * StationEnergyPerPowerCore; station.Modules.Add(new StationModuleRuntime
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); Id = $"{station.Id}-module-{station.Modules.Count + 1}",
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank); ModuleId = moduleId,
return; Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(world, station);
} }
var solarGenerated = MathF.Min(desiredEnergy, GetStationSolarGeneration(station, world) * deltaSeconds); private static float GetStationRadius(SimulationWorld world, StationRuntime station)
if (solarGenerated > 0.01f)
{ {
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + solarGenerated); var totalArea = station.Modules
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored); .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)));
} }
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) =>
CountModules(station.InstalledModules, "liquid-tank") * StationFuelPerTank;
private static float GetStationEnergyCapacity(StationRuntime station) =>
CountModules(station.InstalledModules, "power-core") * StationEnergyPerPowerCore;
private static float GetStationSolarGeneration(StationRuntime station, SimulationWorld world) =>
world.Balance.Energy.StationSolarCharge * (1f + CountModules(station.InstalledModules, "solar-array"));
private static float GetStationStorageCapacity(StationRuntime station, string storageClass) private static float GetStationStorageCapacity(StationRuntime station, string storageClass)
{ {
var baseCapacity = station.Definition.Storage.TryGetValue(storageClass, out var capacity) var baseCapacity = storageClass switch
? 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, "manufactured" => 400f,
"bulk-liquid" => extraLiquidTanks * 500f,
"bulk-gas" => extraGasTanks * 500f,
"container" => extraContainerBays * 800f,
_ => 0f, _ => 0f,
}; };
return baseCapacity + moduleBonus; var bulkBays = CountStationModules(station, "bulk-bay");
} var liquidTanks = CountStationModules(station, "liquid-tank");
var containerBays = CountStationModules(station, "container-bay");
private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds) var moduleCapacity = storageClass switch
{ {
var reactors = CountModules(ship.Definition.Modules, "reactor-core"); "bulk-solid" => bulkBays * 1000f,
var capacitors = CountModules(ship.Definition.Modules, "capacitor-bank"); "bulk-liquid" => liquidTanks * 500f,
if (reactors <= 0 || capacitors <= 0) "container" => containerBays * 800f,
{ "manufactured" => containerBays * 200f,
ship.EnergyStored = 0f; _ => 0f,
ship.Inventory.Remove("fuel"); };
return;
}
var energyCapacity = capacitors * CapacitorEnergyPerModule; return baseCapacity + moduleCapacity;
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)
{
if (ship.EnergyStored + 0.0001f < amount)
{
return false;
}
ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount);
return true;
}
private static bool TryConsumeStationEnergy(StationRuntime station, float amount)
{
if (station.EnergyStored + 0.0001f < amount)
{
return false;
}
station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount);
return true;
} }
private static int CountModules(IEnumerable<string> modules, string moduleId) => private static int CountModules(IEnumerable<string> modules, string moduleId) =>
@@ -215,278 +100,18 @@ public sealed partial class SimulationEngine
} }
private static bool HasStationModules(StationRuntime station, params string[] modules) => private static bool HasStationModules(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 CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) => private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) =>
node.ItemId switch node.ItemId switch
{ {
"ore" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "mining-turret"), "ore" => HasShipModules(ship.Definition, "mining-turret"),
"gas" => HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "gas-extractor"),
_ => false, _ => false,
}; };
private static bool CanBuildClaimBeacon(ShipRuntime ship) => private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal); 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) private static float ComputeWorkforceRatio(float population, float workforceRequired)
{ {
if (workforceRequired <= 0.01f) if (workforceRequired <= 0.01f)
@@ -503,7 +128,6 @@ public sealed partial class SimulationEngine
{ {
"bulk-solid" => "bulk-bay", "bulk-solid" => "bulk-bay",
"bulk-liquid" => "liquid-tank", "bulk-liquid" => "liquid-tank",
"bulk-gas" => "gas-tank",
_ => null, _ => null,
}; };
@@ -514,7 +138,7 @@ public sealed partial class SimulationEngine
return 0f; return 0f;
} }
var storageClass = itemDefinition.Storage; var storageClass = itemDefinition.CargoKind;
var requiredModule = GetStorageRequirement(storageClass); var requiredModule = GetStorageRequirement(storageClass);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{ {
@@ -528,7 +152,7 @@ public sealed partial class SimulationEngine
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == storageClass) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
if (accepted <= 0.01f) if (accepted <= 0.01f)

View File

@@ -79,10 +79,6 @@ public sealed partial class SimulationEngine
station.DockedShips, station.DockedShips,
station.DockedShipIds, station.DockedShipIds,
station.DockingPads, station.DockingPads,
station.FuelStored,
station.FuelCapacity,
station.EnergyStored,
station.EnergyCapacity,
station.CurrentProcesses, station.CurrentProcesses,
station.Inventory, station.Inventory,
station.FactionId, station.FactionId,
@@ -92,6 +88,7 @@ public sealed partial class SimulationEngine
station.PopulationCapacity, station.PopulationCapacity,
station.WorkforceRequired, station.WorkforceRequired,
station.WorkforceEffectiveRatio, station.WorkforceEffectiveRatio,
station.StorageUsage,
station.InstalledModules, station.InstalledModules,
station.MarketOrderIds)).ToList(), station.MarketOrderIds)).ToList(),
world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot( world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot(
@@ -104,7 +101,7 @@ public sealed partial class SimulationEngine
claim.Health, claim.Health,
claim.PlacedAtUtc, claim.PlacedAtUtc,
claim.ActivatesAtUtc)).ToList(), claim.ActivatesAtUtc)).ToList(),
world.ConstructionSites.Select(ToConstructionSiteDelta).Select(site => new ConstructionSiteSnapshot( world.ConstructionSites.Select(site => ToConstructionSiteDelta(world, site)).Select(site => new ConstructionSiteSnapshot(
site.Id, site.Id,
site.FactionId, site.FactionId,
site.SystemId, site.SystemId,
@@ -164,7 +161,6 @@ public sealed partial class SimulationEngine
ship.CargoCapacity, ship.CargoCapacity,
ship.CargoItemId, ship.CargoItemId,
ship.WorkerPopulation, ship.WorkerPopulation,
ship.EnergyStored,
ship.TravelSpeed, ship.TravelSpeed,
ship.TravelSpeedUnit, ship.TravelSpeedUnit,
ship.Inventory, ship.Inventory,
@@ -341,7 +337,7 @@ public sealed partial class SimulationEngine
} }
site.LastDeltaSignature = signature; site.LastDeltaSignature = signature;
deltas.Add(ToConstructionSiteDelta(site)); deltas.Add(ToConstructionSiteDelta(world, site));
} }
return deltas; return deltas;
@@ -439,10 +435,6 @@ public sealed partial class SimulationEngine
station.CommanderId ?? "none", station.CommanderId ?? "none",
station.PolicySetId ?? "none", station.PolicySetId ?? "none",
BuildInventorySignature(station.Inventory), BuildInventorySignature(station.Inventory),
GetInventoryAmount(station.Inventory, "fuel").ToString("0.###"),
GetStationFuelCapacity(station).ToString("0.###"),
station.EnergyStored.ToString("0.###"),
GetStationEnergyCapacity(station).ToString("0.###"),
string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")), string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")),
string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)), string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)),
station.DockingPadAssignments.Count.ToString(), station.DockingPadAssignments.Count.ToString(),
@@ -501,13 +493,11 @@ public sealed partial class SimulationEngine
ship.SpatialState.Transit?.DestinationNodeId ?? "none", ship.SpatialState.Transit?.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"), GetShipCargoAmount(ship).ToString("0.###"),
GetInventoryAmount(ship.Inventory, "fuel").ToString("0.###"),
ship.TrackedActionKey ?? "none", ship.TrackedActionKey ?? "none",
ship.TrackedActionTotal.ToString("0.###"), ship.TrackedActionTotal.ToString("0.###"),
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site
? GetRemainingConstructionDelivery(world, site).ToString("0.###") ? GetRemainingConstructionDelivery(world, site).ToString("0.###")
: "0", : "0",
ship.EnergyStored.ToString("0.###"),
ship.Health.ToString("0.###"), ship.Health.ToString("0.###"),
ship.ActionTimer.ToString("0.###")); ship.ActionTimer.ToString("0.###"));
@@ -553,21 +543,17 @@ public sealed partial class SimulationEngine
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new( private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
station.Id, station.Id,
station.Definition.Label, station.Label,
station.Definition.Category, station.Category,
station.SystemId, station.SystemId,
ToDto(station.Position), ToDto(station.Position),
station.NodeId, station.NodeId,
station.BubbleId, station.BubbleId,
station.AnchorNodeId, station.AnchorNodeId,
station.Definition.Color, station.Color,
station.DockedShipIds.Count, station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
GetDockingPadCount(station), GetDockingPadCount(station),
GetInventoryAmount(station.Inventory, "fuel"),
GetStationFuelCapacity(station),
station.EnergyStored,
GetStationEnergyCapacity(station),
ToStationActionProgressSnapshots(world, station), ToStationActionProgressSnapshots(world, station),
ToInventoryEntries(station.Inventory), ToInventoryEntries(station.Inventory),
station.FactionId, station.FactionId,
@@ -577,6 +563,7 @@ public sealed partial class SimulationEngine
station.PopulationCapacity, station.PopulationCapacity,
station.WorkforceRequired, station.WorkforceRequired,
station.WorkforceEffectiveRatio, station.WorkforceEffectiveRatio,
ToStationStorageUsageSnapshots(world, station),
station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(), station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(),
station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList()); station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList());
@@ -586,7 +573,7 @@ public sealed partial class SimulationEngine
{ {
var recipe = SelectProductionRecipe(world, station, laneKey); var recipe = SelectProductionRecipe(world, station, laneKey);
var timer = GetStationProductionTimer(station, laneKey); var timer = GetStationProductionTimer(station, laneKey);
return recipe is null || station.EnergyStored <= 0.01f || timer <= 0.01f return recipe is null || timer <= 0.01f
? null ? null
: new StationActionProgressSnapshot( : new StationActionProgressSnapshot(
laneKey, laneKey,
@@ -597,6 +584,20 @@ public sealed partial class SimulationEngine
.Cast<StationActionProgressSnapshot>() .Cast<StationActionProgressSnapshot>()
.ToList(); .ToList();
private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station)
{
string[] storageClasses = ["bulk-solid", "bulk-liquid", "container", "manufactured"];
return storageClasses
.Select(storageClass => new StationStorageUsageSnapshot(
storageClass,
station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
.Sum(entry => entry.Value),
GetStationStorageCapacity(station, storageClass)))
.Where(snapshot => snapshot.Capacity > 0.01f)
.ToList();
}
private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new( private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new(
claim.Id, claim.Id,
claim.FactionId, claim.FactionId,
@@ -608,7 +609,7 @@ public sealed partial class SimulationEngine
claim.PlacedAtUtc, claim.PlacedAtUtc,
claim.ActivatesAtUtc); claim.ActivatesAtUtc);
private static ConstructionSiteDelta ToConstructionSiteDelta(ConstructionSiteRuntime site) => new( private static ConstructionSiteDelta ToConstructionSiteDelta(SimulationWorld world, ConstructionSiteRuntime site) => new(
site.Id, site.Id,
site.FactionId, site.FactionId,
site.SystemId, site.SystemId,
@@ -620,13 +621,25 @@ public sealed partial class SimulationEngine
site.ClaimId, site.ClaimId,
site.StationId, site.StationId,
site.State, site.State,
site.Progress, GetConstructionSiteProgress(world, site),
ToInventoryEntries(site.Inventory), ToInventoryEntries(site.Inventory),
ToInventoryEntries(site.RequiredItems), ToInventoryEntries(site.RequiredItems),
ToInventoryEntries(site.DeliveredItems), ToInventoryEntries(site.DeliveredItems),
site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList()); site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
private static float GetConstructionSiteProgress(SimulationWorld world, ConstructionSiteRuntime site)
{
if (site.BlueprintId is not null
&& world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)
&& recipe.Duration > 0.01f)
{
return Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
}
return Math.Clamp(site.Progress, 0f, 1f);
}
private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new( private static MarketOrderDelta ToMarketOrderDelta(MarketOrderRuntime order) => new(
order.Id, order.Id,
order.FactionId, order.FactionId,
@@ -671,7 +684,6 @@ public sealed partial class SimulationEngine
ship.Definition.CargoCapacity, ship.Definition.CargoCapacity,
ship.Definition.CargoItemId, ship.Definition.CargoItemId,
ship.WorkerPopulation, ship.WorkerPopulation,
ship.EnergyStored,
ToShipTravelSpeed(ship).Speed, ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit, ToShipTravelSpeed(ship).Unit,
ToInventoryEntries(ship.Inventory), ToInventoryEntries(ship.Inventory),
@@ -693,10 +705,6 @@ public sealed partial class SimulationEngine
ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)), ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)),
ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)), ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)), ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)),
ShipState.Refueling => CreateShipRemainingActionProgress(
"Refuel",
ship.TrackedActionTotal,
MathF.Max(0f, GetShipRefuelTarget(ship, world) - GetInventoryAmount(ship.Inventory, "fuel"))),
ShipState.Loading => CreateShipRemainingActionProgress( ShipState.Loading => CreateShipRemainingActionProgress(
"Load workers", "Load workers",
ship.TrackedActionTotal, ship.TrackedActionTotal,

View File

@@ -109,8 +109,6 @@ public sealed partial class SimulationEngine
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
? new (string ModuleId, int TargetCount)[] ? 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),
@@ -121,8 +119,6 @@ public sealed partial class SimulationEngine
} }
: new (string ModuleId, int TargetCount)[] : 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 +234,7 @@ public sealed partial class SimulationEngine
{ {
var padCount = Math.Max(1, GetDockingPadCount(station)); var padCount = Math.Max(1, GetDockingPadCount(station));
var angle = ((MathF.PI * 2f) / padCount) * padIndex; var angle = ((MathF.PI * 2f) / padCount) * padIndex;
var radius = station.Definition.Radius + 18f; var radius = station.Radius + 18f;
return new Vector3( return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius), station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y, station.Position.Y,
@@ -249,7 +245,7 @@ public sealed partial class SimulationEngine
{ {
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f); var angle = (hash % 360) * (MathF.PI / 180f);
var radius = station.Definition.Radius + 24f; var radius = station.Radius + 24f;
return new Vector3( return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius), station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y, station.Position.Y,
@@ -288,7 +284,7 @@ public sealed partial class SimulationEngine
{ {
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal)); var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
var angle = (hash % 360) * (MathF.PI / 180f); var angle = (hash % 360) * (MathF.PI / 180f);
var radius = station.Definition.Radius + 78f; var radius = station.Radius + 78f;
return new Vector3( return new Vector3(
station.Position.X + (MathF.Cos(angle) * radius), station.Position.X + (MathF.Cos(angle) * radius),
station.Position.Y, station.Position.Y,

View File

@@ -56,25 +56,12 @@ public sealed partial class SimulationEngine
if (distance > task.Threshold) if (distance > task.Threshold)
{ {
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.MiningApproach; ship.State = ShipState.MiningApproach;
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none"; return "none";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.Mining; ship.State = ShipState.Mining;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds)) if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds))
{ {
@@ -121,7 +108,7 @@ public sealed partial class SimulationEngine
ship.State = ShipState.AwaitingDock; ship.State = ShipState.AwaitingDock;
ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition); var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
if (waitDistance > 4f && TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds)) if (waitDistance > 4f)
{ {
ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
} }
@@ -136,32 +123,12 @@ public sealed partial class SimulationEngine
if (distance > 4f) if (distance > 4f)
{ {
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.DockingApproach; ship.State = ShipState.DockingApproach;
ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none"; return "none";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.Docking; ship.State = ShipState.Docking;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration)) if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration))
{ {
@@ -195,13 +162,6 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = GetShipDockedPosition(ship, station); ship.TargetPosition = GetShipDockedPosition(ship, station);
ship.Position = ship.TargetPosition; ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
@@ -245,13 +205,6 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = GetShipDockedPosition(ship, station); ship.TargetPosition = GetShipDockedPosition(ship, station);
ship.Position = ship.TargetPosition; ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
@@ -273,50 +226,6 @@ public sealed partial class SimulationEngine
: "none"; : "none";
} }
private string UpdateRefuel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var station = ResolveShipSupportStation(ship, world);
if (station is null)
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
}
var supportPosition = ResolveShipSupportPosition(ship, station);
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
{
ship.State = ShipState.LocalFlight;
ship.TargetPosition = supportPosition;
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)
|| !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Refueling;
var refuelTarget = GetShipRefuelTarget(ship, world);
BeginTrackedAction(ship, "refueling", MathF.Max(0f, refuelTarget - GetInventoryAmount(ship.Inventory, "fuel")));
var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, refuelTarget - GetInventoryAmount(ship.Inventory, "fuel"));
var moved = MathF.Min(transfer, GetInventoryAmount(station.Inventory, "fuel"));
if (moved > 0.01f)
{
RemoveInventory(station.Inventory, "fuel", moved);
AddInventory(ship.Inventory, "fuel", moved);
}
return !NeedsRefuel(ship, world) ? "refueled" : "none";
}
private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds) private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{ {
var station = ResolveShipSupportStation(ship, world); var station = ResolveShipSupportStation(ship, world);
@@ -344,14 +253,6 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)
|| !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id)) if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id))
{ {
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
@@ -377,7 +278,7 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
station.InstalledModules.Add(station.ActiveConstruction.ModuleId); AddStationModule(world, station, station.ActiveConstruction.ModuleId);
station.ActiveConstruction = null; station.ActiveConstruction = null;
return "module-constructed"; return "module-constructed";
} }
@@ -409,14 +310,6 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)
|| !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = supportPosition; ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition; ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
@@ -487,14 +380,6 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)
|| !TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.TargetPosition = supportPosition; ship.TargetPosition = supportPosition;
ship.Position = ship.TargetPosition; ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f; ship.ActionTimer = 0f;
@@ -506,7 +391,7 @@ public sealed partial class SimulationEngine
return "none"; return "none";
} }
station.InstalledModules.Add(site.BlueprintId); AddStationModule(world, station, site.BlueprintId);
PrepareNextConstructionSiteStep(world, station, site); PrepareNextConstructionSiteStep(world, station, site);
return "site-constructed"; return "site-constructed";
} }
@@ -601,19 +486,6 @@ public sealed partial class SimulationEngine
? task.TargetPosition.Value ? task.TargetPosition.Value
: GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance); : GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
ship.TargetPosition = undockTarget; ship.TargetPosition = undockTarget;
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
if (station is not null && !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.Undocking; ship.State = ShipState.Undocking;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration)) if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration))

View File

@@ -160,10 +160,6 @@ public sealed partial class SimulationEngine
} }
behavior.NodeId ??= node.Id; behavior.NodeId ??= node.Id;
if (NeedsEmergencyReturn(ship, world) && behavior.Phase is "travel-to-node" or "extract")
{
behavior.Phase = "travel-to-station";
}
if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
&& behavior.Phase is "travel-to-node" or "extract") && behavior.Phase is "travel-to-node" or "extract")
@@ -177,21 +173,12 @@ public sealed partial class SimulationEngine
{ {
behavior.Phase = "unload"; behavior.Phase = "unload";
} }
else if (behavior.Phase == "undock") else if (behavior.Phase is "dock" or "unload")
{
// Keep the post-refuel departure decision stable for the current dock cycle.
behavior.Phase = "undock";
}
else if (NeedsRefuel(ship, world))
{
behavior.Phase = "refuel";
}
else if (behavior.Phase is "dock" or "unload" or "refuel")
{ {
behavior.Phase = "undock"; behavior.Phase = "undock";
} }
} }
else if (NeedsRefuel(ship, world) && behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract") else if (behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract")
{ {
behavior.Phase = "travel-to-station"; behavior.Phase = "travel-to-station";
} }
@@ -216,7 +203,7 @@ public sealed partial class SimulationEngine
TargetEntityId = refinery.Id, TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId, TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position, TargetPosition = refinery.Position,
Threshold = refinery.Definition.Radius + 8f, Threshold = refinery.Radius + 8f,
}; };
break; break;
case "dock": case "dock":
@@ -226,7 +213,7 @@ public sealed partial class SimulationEngine
TargetEntityId = refinery.Id, TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId, TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position, TargetPosition = refinery.Position,
Threshold = refinery.Definition.Radius + 4f, Threshold = refinery.Radius + 4f,
}; };
break; break;
case "unload": case "unload":
@@ -239,16 +226,6 @@ public sealed partial class SimulationEngine
Threshold = 0f, Threshold = 0f,
}; };
break; break;
case "refuel":
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Refuel,
TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position,
Threshold = 0f,
};
break;
case "undock": case "undock":
ship.ControllerTask = new ControllerTaskRuntime ship.ControllerTask = new ControllerTaskRuntime
{ {
@@ -298,107 +275,6 @@ public sealed partial class SimulationEngine
return bestOrder.Station ?? preferred; return bestOrder.Station ?? preferred;
} }
internal static StationRuntime? SelectBestSellStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId)
{
var preferred = preferredStationId is null
? null
: world.Stations.FirstOrDefault(station => station.Id == preferredStationId);
var bestOrder = world.MarketOrders
.Where(order =>
order.Kind == MarketOrderKinds.Sell &&
order.ConstructionSiteId is null &&
order.State != MarketOrderStateKinds.Cancelled &&
order.ItemId == itemId &&
order.RemainingAmount > 0.01f)
.Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId)))
.Where(entry => entry.Station is not null && GetInventoryAmount(entry.Station!.Inventory, itemId) > 0.01f)
.OrderByDescending(entry =>
{
var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f;
return entry.Order.Valuation - distancePenalty;
})
.FirstOrDefault();
return bestOrder.Station ?? preferred;
}
internal void PlanEnergySupply(ShipRuntime ship, SimulationWorld world)
{
var behavior = ship.DefaultBehavior;
var cargoItemId = ship.Definition.CargoItemId;
if (cargoItemId is null)
{
behavior.Kind = "idle";
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
return;
}
var cargoAmount = GetShipCargoAmount(ship);
if (cargoAmount > 0.01f)
{
var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
if (destination is null)
{
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
return;
}
behavior.StationId = destination.Id;
switch (behavior.Phase)
{
case "dock":
case "unload":
case "refuel":
case "undock":
ship.ControllerTask = CreateStationSupportTask(world, ship, destination, behavior.Phase);
break;
default:
behavior.Phase = "travel-to-destination";
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Travel,
TargetEntityId = destination.Id,
TargetSystemId = destination.SystemId,
TargetPosition = destination.Position,
Threshold = 18f,
};
break;
}
return;
}
var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId);
if (source is null)
{
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
return;
}
behavior.StationId = source.Id;
switch (behavior.Phase)
{
case "dock":
case "load":
case "refuel":
case "undock":
ship.ControllerTask = CreateStationSupportTask(world, ship, source, behavior.Phase);
break;
default:
behavior.Phase = "travel-to-source";
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Travel,
TargetEntityId = source.Id,
TargetSystemId = source.SystemId,
TargetPosition = source.Position,
Threshold = 18f,
};
break;
}
}
private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) => private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) =>
phase switch phase switch
{ {
@@ -426,14 +302,6 @@ public sealed partial class SimulationEngine
TargetPosition = station.Position, TargetPosition = station.Position,
Threshold = 8f, Threshold = 8f,
}, },
"refuel" => new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Refuel,
TargetEntityId = station.Id,
TargetSystemId = station.SystemId,
TargetPosition = station.Position,
Threshold = 12f,
},
"undock" => new ControllerTaskRuntime "undock" => new ControllerTaskRuntime
{ {
Kind = ControllerTaskKind.Undock, Kind = ControllerTaskKind.Undock,
@@ -486,11 +354,7 @@ public sealed partial class SimulationEngine
if (isAtConstructionHold) if (isAtConstructionHold)
{ {
if (NeedsRefuel(ship, world)) if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
{
behavior.Phase = "refuel";
}
else if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
{ {
behavior.Phase = "deliver-to-site"; behavior.Phase = "deliver-to-site";
} }
@@ -518,16 +382,6 @@ public sealed partial class SimulationEngine
switch (behavior.Phase) switch (behavior.Phase)
{ {
case "refuel":
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Refuel,
TargetEntityId = station.Id,
TargetSystemId = station.SystemId,
TargetPosition = constructionHoldPosition,
Threshold = 10f,
};
break;
case "construct-module": case "construct-module":
ship.ControllerTask = new ControllerTaskRuntime ship.ControllerTask = new ControllerTaskRuntime
{ {
@@ -649,7 +503,6 @@ public sealed partial class SimulationEngine
"dock" => ControllerTaskKind.Dock, "dock" => ControllerTaskKind.Dock,
"load" => ControllerTaskKind.Load, "load" => ControllerTaskKind.Load,
"unload" => ControllerTaskKind.Unload, "unload" => ControllerTaskKind.Unload,
"refuel" => ControllerTaskKind.Refuel,
"deliver-construction" => ControllerTaskKind.DeliverConstruction, "deliver-construction" => ControllerTaskKind.DeliverConstruction,
"build-construction-site" => ControllerTaskKind.BuildConstructionSite, "build-construction-site" => ControllerTaskKind.BuildConstructionSite,
"load-workers" => ControllerTaskKind.LoadWorkers, "load-workers" => ControllerTaskKind.LoadWorkers,

View File

@@ -26,16 +26,15 @@ public sealed partial class SimulationEngine
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var hasPower = station.EnergyStored > 0.01f;
var habitatModules = CountModules(station.InstalledModules, "habitat-ring"); var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f); station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied && hasPower) if (waterSatisfied)
{ {
if (habitatModules > 0 && station.Population < station.PopulationCapacity) if (habitatModules > 0 && station.Population < station.PopulationCapacity)
{ {
@@ -48,7 +47,7 @@ public sealed partial class SimulationEngine
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds)); station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
if (MathF.Floor(previous) > MathF.Floor(station.Population)) 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)); events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
} }
} }
@@ -63,31 +62,22 @@ public sealed partial class SimulationEngine
} }
var desiredOrders = new List<DesiredMarketOrder>(); 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 waterReserve = MathF.Max(30f, station.Population * 3f);
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f; var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
var oreReserve = HasRefineryCapability(station) ? 180f : 0f; var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var gasReserve = CanProcessFuel(station) ? 120f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array") var shipPartsReserve = HasStationModules(station, "fabricator-array")
&& !HasStationModules(station, "component-factory", "ship-factory") && !HasStationModules(station, "component-factory", "ship-factory")
&& FactionNeedsMoreWarships(world, station.FactionId) && FactionNeedsMoreWarships(world, station.FactionId)
? 90f ? 90f
: 0f; : 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, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f); 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, "refined-metals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f); 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, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f); 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); AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
ReconcileStationMarketOrders(world, station, desiredOrders); ReconcileStationMarketOrders(world, station, desiredOrders);
@@ -99,18 +89,13 @@ public sealed partial class SimulationEngine
foreach (var laneKey in GetStationProductionLanes(station)) foreach (var laneKey in GetStationProductionLanes(station))
{ {
var recipe = SelectProductionRecipe(world, station, laneKey); var recipe = SelectProductionRecipe(world, station, laneKey);
if (recipe is null || station.EnergyStored <= 0.01f) if (recipe is null)
{ {
station.ProductionLaneTimers[laneKey] = 0f; station.ProductionLaneTimers[laneKey] = 0f;
continue; continue;
} }
var throughput = GetStationProductionThroughput(station, recipe); var throughput = GetStationProductionThroughput(station, recipe);
if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds * throughput))
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var produced = 0f; var produced = 0f;
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput); station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
@@ -139,7 +124,7 @@ public sealed partial class SimulationEngine
continue; continue;
} }
events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Definition.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow)); events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow));
if (faction is not null) if (faction is not null)
{ {
faction.GoodsProduced += produced; faction.GoodsProduced += produced;
@@ -154,11 +139,6 @@ public sealed partial class SimulationEngine
yield return "refinery"; yield return "refinery";
} }
if (CountModules(station.InstalledModules, "fuel-processor") > 0)
{
yield return "fuel";
}
if (CountModules(station.InstalledModules, "fabricator-array") > 0) if (CountModules(station.InstalledModules, "fabricator-array") > 0)
{ {
yield return "fabrication"; yield return "fabrication";
@@ -186,11 +166,6 @@ public sealed partial class SimulationEngine
private static string? GetStationProductionLaneKey(RecipeDefinition 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)) if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
{ {
return "refinery"; return "refinery";
@@ -217,13 +192,6 @@ public sealed partial class SimulationEngine
private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe) private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{ {
var priority = (float)recipe.Priority; 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 expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f; var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
@@ -251,9 +219,9 @@ public sealed partial class SimulationEngine
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe) private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
{ {
var categoryMatch = string.Equals(station.Definition.Category, recipe.FacilityCategory, StringComparison.Ordinal) var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|| (string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal) || string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal)
&& station.Definition.Category is "station" or "shipyard" or "defense" or "gate"); || string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal);
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)); return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
} }
@@ -289,20 +257,20 @@ public sealed partial class SimulationEngine
return false; return false;
} }
var requiredModule = GetStorageRequirement(itemDefinition.Storage); var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal)) if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{ {
return false; return false;
} }
var capacity = GetStationStorageCapacity(station, itemDefinition.Storage); var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind);
if (capacity <= 0.01f) if (capacity <= 0.01f)
{ {
return false; return false;
} }
var used = station.Inventory var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == itemDefinition.Storage) .Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind)
.Sum(entry => entry.Value); .Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f; return used + amount <= capacity + 0.001f;
} }
@@ -387,9 +355,6 @@ public sealed partial class SimulationEngine
private static bool HasRefineryCapability(StationRuntime station) => private static bool HasRefineryCapability(StationRuntime station) =>
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay"); 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) 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)) if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
@@ -397,7 +362,7 @@ public sealed partial class SimulationEngine
return 0f; return 0f;
} }
var spawnPosition = new Vector3(station.Position.X + station.Definition.Radius + 32f, station.Position.Y, station.Position.Z); var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z);
var ship = new ShipRuntime var ship = new ShipRuntime
{ {
Id = $"ship-{world.Ships.Count + 1}", Id = $"ship-{world.Ships.Count + 1}",
@@ -412,14 +377,13 @@ public sealed partial class SimulationEngine
Health = definition.MaxHealth, Health = definition.MaxHealth,
}; };
ship.Inventory["fuel"] = 120f;
world.Ships.Add(ship); world.Ships.Add(ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction) if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{ {
faction.ShipsBuilt += 1; faction.ShipsBuilt += 1;
} }
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Definition.Label} launched {definition.Label}.", DateTimeOffset.UtcNow)); events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
return 1f; return 1f;
} }
@@ -492,7 +456,7 @@ public sealed partial class SimulationEngine
}; };
} }
var patrolRadius = station.Definition.Radius + 90f; var patrolRadius = station.Radius + 90f;
return new DefaultBehaviorRuntime return new DefaultBehaviorRuntime
{ {
Kind = "patrol", Kind = "patrol",
@@ -513,11 +477,6 @@ public sealed partial class SimulationEngine
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)) if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{ {
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array")); return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));

View File

@@ -5,12 +5,6 @@ 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 StationFuelToEnergyRatio = 18f;
private const float CapacitorEnergyPerModule = 120f;
private const float StationEnergyPerPowerCore = 480f;
private const float ShipFuelPerReactor = 100f;
private const float StationFuelPerTank = 500f;
private const float WaterConsumptionPerWorkerPerSecond = 0.004f; private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
private const float PopulationGrowthPerSecond = 0.012f; private const float PopulationGrowthPerSecond = 0.012f;
private const float PopulationAttritionPerSecond = 0.018f; private const float PopulationAttritionPerSecond = 0.018f;
@@ -20,12 +14,10 @@ public sealed partial class SimulationEngine
new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)), 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)),
]; ];

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",
"capacitor-bank",
"ion-drive",
"ftl-core",
"mining-turret",
"bulk-bay"
],
"construction": {
"recipeId": "miner-construction",
"facilityCategory": "station",
"requiredModules": [
"ship-factory",
"dock-bay-small",
"container-bay",
"power-core"
],
"requirements": [
{
"itemId": "hull-sections",
"amount": 34
}, },
{ {
"id": "gas-miner", "itemId": "command-bridge-module",
"label": "Nimbus Gas Harvester", "amount": 1
"role": "mining", },
"shipClass": "industrial", {
"speed": 72000, "itemId": "reactor-core-module",
"warpSpeed": 0.145, "amount": 1
"ftlSpeed": 0.49, },
"spoolTime": 3.2, {
"cargoCapacity": 120, "itemId": "capacitor-bank-module",
"cargoKind": "bulk-gas", "amount": 1
"cargoItemId": "gas", },
"color": "#8ce5ff", {
"hullColor": "#2a5668", "itemId": "ion-drive-module",
"size": 6, "amount": 1
"maxHealth": 150, },
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gas-extractor", "gas-tank"] {
"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
}
} }
] ]