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

View File

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

View File

@@ -1,5 +1,18 @@
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 float YPlane { get; set; }
@@ -10,16 +23,6 @@ public sealed class BalanceDefinition
public float DockingDuration { get; set; }
public float UndockingDuration { 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
@@ -62,9 +65,18 @@ public sealed class ResourceNodeDefinition
public sealed class ItemDefinition
{
public required string Id { get; set; }
public required string Label { get; set; }
public required string Storage { get; set; }
public string Summary { get; set; } = string.Empty;
public required string Name { get; set; }
public string Description { 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
@@ -73,6 +85,25 @@ public sealed class RecipeInputDefinition
public float Amount { get; set; }
}
public sealed class ModuleConstructionDefinition
{
public required List<RecipeInputDefinition> Requirements { get; set; }
public float ProductionTime { get; set; }
}
public sealed class ModuleDefinition
{
public required string Id { get; set; }
public required string Name { get; set; }
public string Description { get; set; } = string.Empty;
public required string Type { get; set; }
public string? Product { get; set; }
public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; }
public ModuleConstructionDefinition? Construction { get; set; }
}
public sealed class ModuleRecipeDefinition
{
public required string ModuleId { get; set; }
@@ -80,12 +111,6 @@ public sealed class ModuleRecipeDefinition
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 required string Id { get; set; }
@@ -136,18 +161,7 @@ public sealed class ShipDefinition
public float Size { get; set; }
public float MaxHealth { get; set; }
public List<string> Modules { 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 ConstructionDefinition? Construction { get; set; }
}
public sealed class ScenarioDefinition
@@ -160,8 +174,10 @@ public sealed class ScenarioDefinition
public sealed class InitialStationDefinition
{
public required string ConstructibleId { 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 int? PlanetIndex { get; set; }
public int? LagrangeSide { get; set; }

View File

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

View File

@@ -90,13 +90,7 @@ internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
ship.DefaultBehavior.Phase = "dock";
break;
case ("dock", "docked"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "refuel";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = "refuel";
break;
case ("refuel", "refueled"):
ship.DefaultBehavior.Phase = "undock";
ship.DefaultBehavior.Phase = "unload";
break;
case ("undock", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-node";
@@ -118,9 +112,6 @@ internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
switch (ship.DefaultBehavior.Phase, controllerEvent)
{
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";
break;
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 Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
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 string? CommanderId { get; set; }
public string? PolicySetId { get; set; }

View File

@@ -28,7 +28,6 @@ public enum ShipState
{
Idle,
Arriving,
CapacitorStarved,
LocalFlight,
SpoolingWarp,
Warping,
@@ -45,7 +44,6 @@ public enum ShipState
Transferring,
Loading,
Unloading,
Refueling,
WaitingMaterials,
ConstructionBlocked,
Constructing,
@@ -62,7 +60,6 @@ public enum ControllerTaskKind
Dock,
Load,
Unload,
Refuel,
DeliverConstruction,
BuildConstructionSite,
LoadWorkers,
@@ -197,7 +194,6 @@ public static class SimulationEnumMappings
{
ShipState.Idle => "idle",
ShipState.Arriving => "arriving",
ShipState.CapacitorStarved => "capacitor-starved",
ShipState.LocalFlight => "local-flight",
ShipState.SpoolingWarp => "spooling-warp",
ShipState.Warping => "warping",
@@ -214,7 +210,6 @@ public static class SimulationEnumMappings
ShipState.Transferring => "transferring",
ShipState.Loading => "loading",
ShipState.Unloading => "unloading",
ShipState.Refueling => "refueling",
ShipState.WaitingMaterials => "waiting-materials",
ShipState.ConstructionBlocked => "construction-blocked",
ShipState.Constructing => "constructing",
@@ -232,7 +227,6 @@ public static class SimulationEnumMappings
ControllerTaskKind.Dock => "dock",
ControllerTaskKind.Load => "load",
ControllerTaskKind.Unload => "unload",
ControllerTaskKind.Refuel => "refuel",
ControllerTaskKind.DeliverConstruction => "deliver-construction",
ControllerTaskKind.BuildConstructionSite => "build-construction-site",
ControllerTaskKind.LoadWorkers => "load-workers",

View File

@@ -21,6 +21,7 @@ public sealed class SimulationWorld
public required List<PolicySetRuntime> Policies { get; init; }
public required Dictionary<string, ShipDefinition> ShipDefinitions { 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, RecipeDefinition> Recipes { get; init; }
public int TickIntervalMs { get; init; } = 200;

View File

@@ -1,25 +1,26 @@
using SpaceGame.Simulation.Api.Data;
namespace SpaceGame.Simulation.Api.Simulation;
public sealed class StationRuntime
{
public required string Id { 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 float Radius { get; set; } = 24f;
public required string FactionId { get; init; }
public string? NodeId { get; set; }
public string? BubbleId { get; set; }
public string? AnchorNodeId { get; set; }
public string? CommanderId { 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> ProductionLaneTimers { get; } = new(StringComparer.Ordinal);
public Dictionary<int, string> DockingPadAssignments { get; } = new();
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float EnergyStored { get; set; }
public float Population { get; set; }
public float PopulationCapacity { get; set; }
public float WorkforceRequired { get; set; }
@@ -31,6 +32,14 @@ public sealed class StationRuntime
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 required string ModuleId { get; init; }

View File

@@ -254,7 +254,6 @@ public sealed partial class ScenarioLoader
}
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
nodes.AddRange(BuildGasCloudNodes(generatedIndex, planets));
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)
{
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 = 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 = "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 =
[

View File

@@ -70,7 +70,7 @@ public sealed partial class ScenarioLoader
.ToList();
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();
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);
}
@@ -171,7 +171,7 @@ public sealed partial class ScenarioLoader
NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId,
TargetKind = "station-module",
TargetDefinitionId = station.Definition.Id,
TargetDefinitionId = "station",
BlueprintId = moduleId,
ClaimId = claim.Id,
StationId = station.Id,
@@ -213,8 +213,6 @@ public sealed partial class ScenarioLoader
{
foreach (var (moduleId, targetCount) in new (string ModuleId, int TargetCount)[]
{
("gas-tank", 1),
("fuel-processor", 1),
("refinery-stack", 1),
("container-bay", 1),
("fabricator-array", 2),
@@ -238,7 +236,7 @@ public sealed partial class ScenarioLoader
{
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
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
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
: 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)
{
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id);

View File

@@ -97,15 +97,15 @@ public sealed partial class ScenarioLoader
var scenario = NormalizeScenarioToAvailableSystems(
Read<ScenarioDefinition>("scenario.json"),
systems.Select((system) => system.Id).ToList());
var modules = Read<List<ModuleDefinition>>("modules.json");
var ships = Read<List<ShipDefinition>>("ships.json");
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.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 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 constructibleDefinitions = constructibles.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 moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
@@ -178,7 +178,7 @@ public sealed partial class ScenarioLoader
var stationIdCounter = 0;
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;
}
@@ -188,7 +188,8 @@ public sealed partial class ScenarioLoader
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Definition = definition,
Label = plan.Label,
Color = plan.Color,
Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId,
};
@@ -214,21 +215,23 @@ public sealed partial class ScenarioLoader
Id = stationBubbleId,
NodeId = stationNodeId,
SystemId = station.SystemId,
Radius = MathF.Max(160f, definition.Radius + 60f),
Radius = MathF.Max(160f, GetStationRadius(moduleDefinitions, station) + 60f),
});
localBubbles[^1].OccupantStationIds.Add(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)
{
InitializeStationPopulation(station);
station.Inventory["fuel"] = 240f;
station.Inventory["refined-metals"] = 120f;
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 },
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,
ShipDefinitions = shipDefinitions,
ItemDefinitions = itemDefinitions,
ModuleDefinitions = moduleDefinitions,
ModuleRecipes = moduleRecipeDefinitions,
Recipes = recipeDefinitions,
OrbitalTimeSeconds = WorldSeed * 97d,
@@ -356,8 +347,10 @@ public sealed partial class ScenarioLoader
InitialStations = scenario.InitialStations
.Select((station) => new InitialStationDefinition
{
ConstructibleId = station.ConstructibleId,
SystemId = ResolveSystemId(station.SystemId),
Label = station.Label,
Color = station.Color,
StartingModules = station.StartingModules.ToList(),
FactionId = station.FactionId,
PlanetIndex = station.PlanetIndex,
LagrangeSide = station.LagrangeSide,
@@ -404,15 +397,37 @@ public sealed partial class ScenarioLoader
: 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) =>
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) =>
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 int CountModules(IEnumerable<string> modules, string moduleId) =>
@@ -429,6 +444,89 @@ public sealed partial class ScenarioLoader
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 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.Load => UpdateLoadCargo(ship, world, deltaSeconds),
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
ControllerTaskKind.Refuel => UpdateRefuel(ship, world, deltaSeconds),
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(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)
{
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
return "none";
@@ -133,7 +131,6 @@ public sealed partial class SimulationEngine
if (distance <= threshold)
{
ship.ActionTimer = 0f;
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
ship.Position = targetPosition;
ship.TargetPosition = ship.Position;
ship.SystemId = targetSystemId;
@@ -143,13 +140,6 @@ public sealed partial class SimulationEngine
return "arrived";
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.ActionTimer = 0f;
ship.State = ShipState.LocalFlight;
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
@@ -185,13 +175,6 @@ public sealed partial class SimulationEngine
ship.ActionTimer = 0f;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
{
@@ -201,13 +184,6 @@ public sealed partial class SimulationEngine
ship.State = ShipState.Warping;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition)
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
@@ -247,13 +223,6 @@ public sealed partial class SimulationEngine
ship.ActionTimer = 0f;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.SpoolingFtl;
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
{
@@ -263,13 +232,6 @@ public sealed partial class SimulationEngine
ship.State = ShipState.Ftl;
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));

View File

@@ -57,7 +57,7 @@ public sealed partial class SimulationEngine
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));
}

View File

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

View File

@@ -79,10 +79,6 @@ public sealed partial class SimulationEngine
station.DockedShips,
station.DockedShipIds,
station.DockingPads,
station.FuelStored,
station.FuelCapacity,
station.EnergyStored,
station.EnergyCapacity,
station.CurrentProcesses,
station.Inventory,
station.FactionId,
@@ -92,6 +88,7 @@ public sealed partial class SimulationEngine
station.PopulationCapacity,
station.WorkforceRequired,
station.WorkforceEffectiveRatio,
station.StorageUsage,
station.InstalledModules,
station.MarketOrderIds)).ToList(),
world.Claims.Select(ToClaimDelta).Select(claim => new ClaimSnapshot(
@@ -104,7 +101,7 @@ public sealed partial class SimulationEngine
claim.Health,
claim.PlacedAtUtc,
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.FactionId,
site.SystemId,
@@ -164,7 +161,6 @@ public sealed partial class SimulationEngine
ship.CargoCapacity,
ship.CargoItemId,
ship.WorkerPopulation,
ship.EnergyStored,
ship.TravelSpeed,
ship.TravelSpeedUnit,
ship.Inventory,
@@ -341,7 +337,7 @@ public sealed partial class SimulationEngine
}
site.LastDeltaSignature = signature;
deltas.Add(ToConstructionSiteDelta(site));
deltas.Add(ToConstructionSiteDelta(world, site));
}
return deltas;
@@ -439,10 +435,6 @@ public sealed partial class SimulationEngine
station.CommanderId ?? "none",
station.PolicySetId ?? "none",
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(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)),
station.DockingPadAssignments.Count.ToString(),
@@ -501,13 +493,11 @@ public sealed partial class SimulationEngine
ship.SpatialState.Transit?.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"),
GetInventoryAmount(ship.Inventory, "fuel").ToString("0.###"),
ship.TrackedActionKey ?? "none",
ship.TrackedActionTotal.ToString("0.###"),
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site
? GetRemainingConstructionDelivery(world, site).ToString("0.###")
: "0",
ship.EnergyStored.ToString("0.###"),
ship.Health.ToString("0.###"),
ship.ActionTimer.ToString("0.###"));
@@ -553,21 +543,17 @@ public sealed partial class SimulationEngine
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
station.Id,
station.Definition.Label,
station.Definition.Category,
station.Label,
station.Category,
station.SystemId,
ToDto(station.Position),
station.NodeId,
station.BubbleId,
station.AnchorNodeId,
station.Definition.Color,
station.Color,
station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
GetDockingPadCount(station),
GetInventoryAmount(station.Inventory, "fuel"),
GetStationFuelCapacity(station),
station.EnergyStored,
GetStationEnergyCapacity(station),
ToStationActionProgressSnapshots(world, station),
ToInventoryEntries(station.Inventory),
station.FactionId,
@@ -577,6 +563,7 @@ public sealed partial class SimulationEngine
station.PopulationCapacity,
station.WorkforceRequired,
station.WorkforceEffectiveRatio,
ToStationStorageUsageSnapshots(world, station),
station.InstalledModules.OrderBy(moduleId => moduleId, 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 timer = GetStationProductionTimer(station, laneKey);
return recipe is null || station.EnergyStored <= 0.01f || timer <= 0.01f
return recipe is null || timer <= 0.01f
? null
: new StationActionProgressSnapshot(
laneKey,
@@ -597,6 +584,20 @@ public sealed partial class SimulationEngine
.Cast<StationActionProgressSnapshot>()
.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(
claim.Id,
claim.FactionId,
@@ -608,7 +609,7 @@ public sealed partial class SimulationEngine
claim.PlacedAtUtc,
claim.ActivatesAtUtc);
private static ConstructionSiteDelta ToConstructionSiteDelta(ConstructionSiteRuntime site) => new(
private static ConstructionSiteDelta ToConstructionSiteDelta(SimulationWorld world, ConstructionSiteRuntime site) => new(
site.Id,
site.FactionId,
site.SystemId,
@@ -620,13 +621,25 @@ public sealed partial class SimulationEngine
site.ClaimId,
site.StationId,
site.State,
site.Progress,
GetConstructionSiteProgress(world, site),
ToInventoryEntries(site.Inventory),
ToInventoryEntries(site.RequiredItems),
ToInventoryEntries(site.DeliveredItems),
site.AssignedConstructorShipIds.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(
order.Id,
order.FactionId,
@@ -671,7 +684,6 @@ public sealed partial class SimulationEngine
ship.Definition.CargoCapacity,
ship.Definition.CargoItemId,
ship.WorkerPopulation,
ship.EnergyStored,
ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit,
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.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
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(
"Load workers",
ship.TrackedActionTotal,

View File

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

View File

@@ -56,25 +56,12 @@ public sealed partial class SimulationEngine
if (distance > task.Threshold)
{
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.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
return "none";
}
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
{
ship.State = ShipState.CapacitorStarved;
ship.TargetPosition = ship.Position;
return "none";
}
ship.State = ShipState.Mining;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds))
{
@@ -121,7 +108,7 @@ public sealed partial class SimulationEngine
ship.State = ShipState.AwaitingDock;
ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
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);
}
@@ -136,32 +123,12 @@ public sealed partial class SimulationEngine
if (distance > 4f)
{
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.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
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;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration))
{
@@ -195,13 +162,6 @@ public sealed partial class SimulationEngine
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.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
@@ -245,13 +205,6 @@ public sealed partial class SimulationEngine
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.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
@@ -273,50 +226,6 @@ public sealed partial class SimulationEngine
: "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)
{
var station = ResolveShipSupportStation(ship, world);
@@ -344,14 +253,6 @@ public sealed partial class SimulationEngine
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))
{
ship.ActionTimer = 0f;
@@ -377,7 +278,7 @@ public sealed partial class SimulationEngine
return "none";
}
station.InstalledModules.Add(station.ActiveConstruction.ModuleId);
AddStationModule(world, station, station.ActiveConstruction.ModuleId);
station.ActiveConstruction = null;
return "module-constructed";
}
@@ -409,14 +310,6 @@ public sealed partial class SimulationEngine
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;
@@ -487,14 +380,6 @@ public sealed partial class SimulationEngine
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.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
@@ -506,7 +391,7 @@ public sealed partial class SimulationEngine
return "none";
}
station.InstalledModules.Add(site.BlueprintId);
AddStationModule(world, station, site.BlueprintId);
PrepareNextConstructionSiteStep(world, station, site);
return "site-constructed";
}
@@ -601,19 +486,6 @@ public sealed partial class SimulationEngine
? task.TargetPosition.Value
: GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
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;
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration))

View File

@@ -160,10 +160,6 @@ public sealed partial class SimulationEngine
}
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
&& behavior.Phase is "travel-to-node" or "extract")
@@ -177,21 +173,12 @@ public sealed partial class SimulationEngine
{
behavior.Phase = "unload";
}
else if (behavior.Phase == "undock")
{
// 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")
else if (behavior.Phase is "dock" or "unload")
{
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";
}
@@ -216,7 +203,7 @@ public sealed partial class SimulationEngine
TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position,
Threshold = refinery.Definition.Radius + 8f,
Threshold = refinery.Radius + 8f,
};
break;
case "dock":
@@ -226,7 +213,7 @@ public sealed partial class SimulationEngine
TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position,
Threshold = refinery.Definition.Radius + 4f,
Threshold = refinery.Radius + 4f,
};
break;
case "unload":
@@ -239,16 +226,6 @@ public sealed partial class SimulationEngine
Threshold = 0f,
};
break;
case "refuel":
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Refuel,
TargetEntityId = refinery.Id,
TargetSystemId = refinery.SystemId,
TargetPosition = refinery.Position,
Threshold = 0f,
};
break;
case "undock":
ship.ControllerTask = new ControllerTaskRuntime
{
@@ -298,107 +275,6 @@ public sealed partial class SimulationEngine
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) =>
phase switch
{
@@ -426,14 +302,6 @@ public sealed partial class SimulationEngine
TargetPosition = station.Position,
Threshold = 8f,
},
"refuel" => new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Refuel,
TargetEntityId = station.Id,
TargetSystemId = station.SystemId,
TargetPosition = station.Position,
Threshold = 12f,
},
"undock" => new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Undock,
@@ -486,11 +354,7 @@ public sealed partial class SimulationEngine
if (isAtConstructionHold)
{
if (NeedsRefuel(ship, world))
{
behavior.Phase = "refuel";
}
else if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
{
behavior.Phase = "deliver-to-site";
}
@@ -518,16 +382,6 @@ public sealed partial class SimulationEngine
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":
ship.ControllerTask = new ControllerTaskRuntime
{
@@ -649,7 +503,6 @@ public sealed partial class SimulationEngine
"dock" => ControllerTaskKind.Dock,
"load" => ControllerTaskKind.Load,
"unload" => ControllerTaskKind.Unload,
"refuel" => ControllerTaskKind.Refuel,
"deliver-construction" => ControllerTaskKind.DeliverConstruction,
"build-construction-site" => ControllerTaskKind.BuildConstructionSite,
"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)
{
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 consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var hasPower = station.EnergyStored > 0.01f;
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied && hasPower)
if (waterSatisfied)
{
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));
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 fuelReserve = MathF.Max(80f, CountModules(station.InstalledModules, "power-core") * 140f);
var energyCellReserve = HasStationModules(station, "power-core", "liquid-tank") ? MathF.Max(20f, CountModules(station.InstalledModules, "power-core") * 40f) : 0f;
var waterReserve = MathF.Max(30f, station.Population * 3f);
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
var gasReserve = CanProcessFuel(station) ? 120f : 0f;
var shipPartsReserve = HasStationModules(station, "fabricator-array")
&& !HasStationModules(station, "component-factory", "ship-factory")
&& FactionNeedsMoreWarships(world, station.FactionId)
? 90f
: 0f;
AddDemandOrder(desiredOrders, station, "fuel", fuelReserve, valuationBase: 1.2f);
AddDemandOrder(desiredOrders, station, "energy-cell", energyCellReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
AddDemandOrder(desiredOrders, station, "gas", gasReserve, valuationBase: 0.95f);
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
AddSupplyOrder(desiredOrders, station, "fuel", fuelReserve * 1.5f, reserveFloor: fuelReserve, valuationBase: 0.8f);
AddSupplyOrder(desiredOrders, station, "energy-cell", energyCellReserve * 1.8f, reserveFloor: energyCellReserve, valuationBase: 0.82f);
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
AddSupplyOrder(desiredOrders, station, "gas", gasReserve * 1.4f, reserveFloor: gasReserve, valuationBase: 0.72f);
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
ReconcileStationMarketOrders(world, station, desiredOrders);
@@ -99,18 +89,13 @@ public sealed partial class SimulationEngine
foreach (var laneKey in GetStationProductionLanes(station))
{
var recipe = SelectProductionRecipe(world, station, laneKey);
if (recipe is null || station.EnergyStored <= 0.01f)
if (recipe is null)
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var throughput = GetStationProductionThroughput(station, recipe);
if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds * throughput))
{
station.ProductionLaneTimers[laneKey] = 0f;
continue;
}
var produced = 0f;
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
@@ -139,7 +124,7 @@ public sealed partial class SimulationEngine
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)
{
faction.GoodsProduced += produced;
@@ -154,11 +139,6 @@ public sealed partial class SimulationEngine
yield return "refinery";
}
if (CountModules(station.InstalledModules, "fuel-processor") > 0)
{
yield return "fuel";
}
if (CountModules(station.InstalledModules, "fabricator-array") > 0)
{
yield return "fabrication";
@@ -186,11 +166,6 @@ public sealed partial class SimulationEngine
private static string? GetStationProductionLaneKey(RecipeDefinition recipe)
{
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
{
return "fuel";
}
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
{
return "refinery";
@@ -217,13 +192,6 @@ public sealed partial class SimulationEngine
private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
{
var priority = (float)recipe.Priority;
var producesFuel = recipe.Outputs.Any(output => string.Equals(output.ItemId, "fuel", StringComparison.Ordinal));
if (producesFuel)
{
var fuelCapacity = MathF.Max(GetStationFuelCapacity(station), 1f);
var fuelRatio = GetInventoryAmount(station.Inventory, "fuel") / fuelCapacity;
priority += (1f - Math.Clamp(fuelRatio, 0f, 1f)) * 200f;
}
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
@@ -251,9 +219,9 @@ public sealed partial class SimulationEngine
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
{
var categoryMatch = string.Equals(station.Definition.Category, recipe.FacilityCategory, StringComparison.Ordinal)
|| (string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
&& station.Definition.Category is "station" or "shipyard" or "defense" or "gate");
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|| string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal)
|| string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal);
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
}
@@ -289,20 +257,20 @@ public sealed partial class SimulationEngine
return false;
}
var requiredModule = GetStorageRequirement(itemDefinition.Storage);
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return false;
}
var capacity = GetStationStorageCapacity(station, itemDefinition.Storage);
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind);
if (capacity <= 0.01f)
{
return false;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == itemDefinition.Storage)
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind)
.Sum(entry => entry.Value);
return used + amount <= capacity + 0.001f;
}
@@ -387,9 +355,6 @@ public sealed partial class SimulationEngine
private static bool HasRefineryCapability(StationRuntime station) =>
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
private static bool CanProcessFuel(StationRuntime station) =>
HasStationModules(station, "fuel-processor", "power-core", "gas-tank", "liquid-tank");
private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
@@ -397,7 +362,7 @@ public sealed partial class SimulationEngine
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
{
Id = $"ship-{world.Ships.Count + 1}",
@@ -412,14 +377,13 @@ public sealed partial class SimulationEngine
Health = definition.MaxHealth,
};
ship.Inventory["fuel"] = 120f;
world.Ships.Add(ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{
faction.ShipsBuilt += 1;
}
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Definition.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
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
{
Kind = "patrol",
@@ -513,11 +477,6 @@ public sealed partial class SimulationEngine
return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack"));
}
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "fuel-processor"));
}
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
{
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));

View File

@@ -5,12 +5,6 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine
{
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 PopulationGrowthPerSecond = 0.012f;
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) => UpdateClaims(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)),
];
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.PlanControllerTask(ship, world)),
];

View File

@@ -6,6 +6,12 @@ export interface StationActionProgressSnapshot {
progress: number;
}
export interface StationStorageUsageSnapshot {
storageClass: string;
used: number;
capacity: number;
}
export interface StationSnapshot {
id: string;
label: string;
@@ -19,10 +25,6 @@ export interface StationSnapshot {
dockedShips: number;
dockedShipIds: string[];
dockingPads: number;
fuelStored: number;
fuelCapacity: number;
energyStored: number;
energyCapacity: number;
currentProcesses: StationActionProgressSnapshot[];
inventory: InventoryEntry[];
factionId: string;
@@ -32,6 +34,7 @@ export interface StationSnapshot {
populationCapacity: number;
workforceRequired: number;
workforceEffectiveRatio: number;
storageUsage: StationStorageUsageSnapshot[];
installedModules: string[];
marketOrderIds: string[];
}

View File

@@ -21,7 +21,6 @@ export interface ShipSnapshot {
cargoCapacity: number;
cargoItemId?: string | null;
workerPopulation: number;
energyStored: number;
travelSpeed: number;
travelSpeedUnit: string;
inventory: InventoryEntry[];

View File

@@ -26,7 +26,6 @@ export function renderFactionStrip(
return ships
.map((ship) => {
const fuel = inventoryAmount(ship.inventory, "fuel");
const cargo = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0;
@@ -54,7 +53,7 @@ export function renderFactionStrip(
</div>
</div>
<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>
${shipAction ? `
<div class="ship-action-progress">

View File

@@ -37,6 +37,94 @@ interface SystemPanelParams {
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 {
const claims = [...world.claims.values()].filter((claim) =>
claim.systemId === systemId && claim.state !== "destroyed");
@@ -108,7 +196,6 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const fuelStored = inventoryAmount(ship.inventory, "fuel");
const cargoUsed = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0;
@@ -130,7 +217,6 @@ export function updateDetailPanel(
</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>Inventory ${formatInventory(ship.inventory)}</p>
<p>Speed ${formatShipSpeed(ship)}</p>
@@ -145,17 +231,12 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const installedModules = station.installedModules.length > 0
? 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 moduleList = formatModuleListWithConstruction(world, station.id, station.installedModules, station.currentProcesses);
const dockedShipLabels = station.dockedShipIds.length > 0
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
: "none";
const stationInventory = station.inventory.filter((entry) => entry.itemId !== "fuel");
const stationInventory = station.inventory;
const stationStorageUsage = formatStorageUsage(station.storageUsage);
const stationProcesses = station.currentProcesses;
const stationProcessingHtml = stationProcesses.length > 0
? stationProcesses.map((process) => `
@@ -175,14 +256,12 @@ export function updateDetailPanel(
<p>${station.category} · ${station.systemId}</p>
<p>Parent ${parent}</p>
${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}
<br>
${dockedShipLabels}</p>
<p>Modules ${installedModules}</p>
<p>Constructing ${activeConstruction}</p>
<p>Modules ${moduleList}</p>
<p>Storage ${stationStorageUsage}</p>
<p>Inventory ${formatInventory(stationInventory)}</p>
<p>History available in the separate history window.</p>
`;
return;
}

View File

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

View File

@@ -164,7 +164,7 @@ Typical outputs:
- current destination node
- local tactical task
- retreat decision
- docking/refuel intent
- docking intent
- trade or delivery acceptance
## 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.
## 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 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
- life-support goods
- construction goods
- fuel-chain goods
- population-related units
- [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:
- production input demand
- fuel shortages
- construction material shortages
- 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.
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:
- 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
2. inspect production queues or goals
3. inspect incoming and outgoing reservations
4. inspect fuel, defense, and construction reserves
4. inspect defense, and construction reserves
5. update buy orders
6. update sell orders
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
2. move them to useful stations
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
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:
- a hauler sees a profitable sell-to-buy opportunity
- a station commander requests urgent fuel delivery
- 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.
@@ -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:
- travel time
- fuel cost
- risk
- behavioral restrictions
- territorial or regional limits
@@ -236,7 +231,6 @@ The economy will work better if stations can reserve expected inventory changes.
Examples:
- incoming fuel is reserved for station power
- outbound metals are reserved for a construction project
- 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:
- `Claim at Helios IV L4 destroyed by pirates`
- `Station buy order for fuel opened`
- `Miner completed warp to refinery node`
This helps reuse the same event model for:

View File

@@ -33,10 +33,9 @@ The intended categories are:
2. processed industrial goods
3. life-support goods
4. civilian goods
5. fuel and power-chain goods
6. construction goods
7. population-related units
8. special logistics goods later
5. construction goods
6. population-related units
7. special logistics goods later
## Raw Resources
@@ -86,20 +85,6 @@ Current important example:
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
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 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:
@@ -144,12 +129,6 @@ The current design implies at least these roles:
- `ore`
- raw industrial input
- `gas`
- raw fuel-chain input
- `fuel`
- operational energy good
- `food`
- workforce life-support
@@ -173,12 +152,11 @@ Not every item should necessarily fit in every hold type forever.
Useful distinctions later may include:
- bulk industrial cargo
- liquid cargo
- gas cargo
- containerized finished goods
- human transport capacity
- livestock capacity
- solid storage
- liquid storage
- container storage
- passengers
- livestock
For now, the important rule is simply:
@@ -191,7 +169,6 @@ Items should participate in the market according to their role.
Examples:
- life-support goods generate recurring demand
- fuel goods generate operational demand
- construction goods generate burst demand during expansion
- industrial goods feed production chains
- worker transport supports station staffing
@@ -220,7 +197,7 @@ The following rules should remain true unless deliberately revised:
- workforce depends on real support 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
- commanders are not ordinary trade cargo
- livestock is distinct from workers

View File

@@ -48,7 +48,6 @@ Examples:
- no docking module means no docking service
- no habitat module means no population growth or human transport
- 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 shipyard-related module means no ship production
@@ -92,7 +91,6 @@ Likely station-side categories include:
- storage
- habitat
- refinery
- fuel processing
- manufacturing
- shipyard or construction support
- defense
@@ -141,7 +139,6 @@ Examples:
- reactor
- capacitor
- station power core
- fuel systems
### Production Modules
@@ -150,7 +147,6 @@ These convert goods into other goods or into built output.
Examples:
- refinery
- fuel processor
- factory
- shipyard support
@@ -198,7 +194,6 @@ They may require:
- build time
- power
- workforce
- fuel or energy inputs
- docking or logistics support
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:
- 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 storage module determines what volume or class of goods can be held
- a livestock module participates in the food chain

View File

@@ -49,7 +49,6 @@ A recipe should conceptually define:
- cycle time
- valid producing module types
- optional workforce requirement
- optional power or fuel requirement
Recipes should be first-class design objects, not hidden assumptions inside modules.
@@ -60,7 +59,6 @@ Recipes are executed by production-capable modules.
Examples:
- refinery module
- fuel processing module
- factory module
- food-chain module later
- shipyard support module
@@ -112,16 +110,6 @@ For now:
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
If inputs are missing:
@@ -153,7 +141,6 @@ The exact recipes can evolve, but the intended shape includes chains like:
2. refining or processing
- ore -> refined goods
- gas -> fuel
- food-loop conversions later
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.
## 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
Depending on modules and category, a station may provide:
@@ -175,11 +158,9 @@ Depending on modules and category, a station may provide:
- docking
- storage
- refining
- fuel processing
- manufacturing
- repair later
- fitting later
- rearm and resupply later
- repair
- fitting, rearm and resupply later
- habitats
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:
- expand into this system
- keep this station fueled
- defend this claim
- protect trade in this region
- supply this station with workers
@@ -60,7 +59,6 @@ Examples:
- dock at station
- claim Lagrange point
- build station here
- deliver fuel
- escort this ship
- defend this bubble
@@ -223,7 +221,6 @@ Examples:
- deny dock request
- transfer goods
- request defense
- request emergency fuel support
These may be implemented as station jobs, station operations, or station-side tasks.
@@ -237,7 +234,7 @@ Examples:
- flee to nearest allowed station
- hold position if no valid route 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.

View File

@@ -52,7 +52,6 @@ Workers consume, per worker:
- food
- water
- energy
- consumer goods
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:
- fuel
- workers
- support goods
- eventually a station commander
@@ -159,7 +157,6 @@ Relevant shortages include:
- food shortage
- water shortage
- energy shortage
- consumer goods shortage
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
- 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
- stations retain a small baseline efficiency at zero workforce
- population can be transported between stations

View File

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

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",
"label": "Raw Ore",
"storage": "bulk-solid",
"summary": "Unprocessed asteroid ore used as the main industrial feedstock."
},
{
"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."
"name": "Raw Ore",
"description": "Unprocessed asteroid ore used as the main industrial feedstock.",
"type": "resource",
"cargoKind": "bulk-solid",
"volume": 1.2
},
{
"id": "water",
"label": "Water",
"storage": "bulk-liquid",
"summary": "Life-support and agricultural input."
"name": "Water",
"description": "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",
"label": "Drone Parts",
"storage": "container",
"summary": "Containerized industrial freight."
"name": "Drone Parts",
"description": "Containerized industrial freight for construction support.",
"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",
"label": "Trade Hub Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for a trade hub station."
"name": "Trade Hub Kit",
"description": "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",
"label": "Refinery Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for a refining station."
"name": "Refinery Kit",
"description": "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",
"label": "Farm Ring Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for a farm station."
"name": "Farm Ring Kit",
"description": "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",
"label": "Manufactory Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for an orbital manufactory."
"name": "Manufactory Kit",
"description": "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",
"label": "Shipyard Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for an orbital shipyard."
"name": "Shipyard Kit",
"description": "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",
"label": "Defense Grid Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for a defense platform."
"name": "Defense Grid Kit",
"description": "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",
"label": "Stargate Kit",
"storage": "manufactured",
"summary": "Deployable prefab package for a stargate structure."
"name": "Stargate Kit",
"description": "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": [
{
"constructibleId": "station-core",
"label": "Orbital Station",
"startingModules": [
"dock-bay-small",
"power-core",
"bulk-bay",
"liquid-tank"
],
"systemId": "helios",
"planetIndex": 2,
"lagrangeSide": -1
@@ -29,7 +35,7 @@
"systemId": "helios"
},
{
"shipId": "gas-miner",
"shipId": "hauler",
"count": 1,
"center": [
60,
@@ -37,16 +43,6 @@
28
],
"systemId": "helios"
},
{
"shipId": "gas-miner",
"count": 1,
"center": [
60,
0,
32
],
"systemId": "helios"
}
],
"patrolRoutes": [],

View File

@@ -13,7 +13,58 @@
"hullColor": "#1f4f78",
"size": 4,
"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",
@@ -29,7 +80,59 @@
"hullColor": "#6a2e26",
"size": 7,
"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",
@@ -45,7 +148,59 @@
"hullColor": "#314562",
"size": 10,
"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",
@@ -61,9 +216,75 @@
"hullColor": "#35586d",
"size": 16,
"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,
"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",
@@ -75,13 +296,63 @@
"ftlSpeed": 0.55,
"spoolTime": 3.3,
"cargoCapacity": 180,
"cargoKind": "bulk-liquid",
"cargoItemId": "energy-cell",
"cargoKind": "container",
"color": "#b0ff8d",
"hullColor": "#365f2a",
"size": 8,
"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",
@@ -99,7 +370,63 @@
"hullColor": "#2d5d47",
"size": 9,
"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",
@@ -117,24 +444,62 @@
"hullColor": "#68552b",
"size": 6,
"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",
"label": "Nimbus Gas Harvester",
"role": "mining",
"shipClass": "industrial",
"speed": 72000,
"warpSpeed": 0.145,
"ftlSpeed": 0.49,
"spoolTime": 3.2,
"cargoCapacity": 120,
"cargoKind": "bulk-gas",
"cargoItemId": "gas",
"color": "#8ce5ff",
"hullColor": "#2a5668",
"size": 6,
"maxHealth": 150,
"modules": ["command-bridge", "reactor-core", "capacitor-bank", "ion-drive", "ftl-core", "gas-extractor", "gas-tank"]
"itemId": "command-bridge-module",
"amount": 1
},
{
"itemId": "reactor-core-module",
"amount": 1
},
{
"itemId": "capacitor-bank-module",
"amount": 1
},
{
"itemId": "ion-drive-module",
"amount": 1
},
{
"itemId": "ftl-core-module",
"amount": 1
},
{
"itemId": "mining-turret-module",
"amount": 1
},
{
"itemId": "bulk-bay-module",
"amount": 1
}
],
"cycleTime": 28,
"productsPerHour": 128.6,
"maxEfficiency": 1,
"priority": 8
}
}
]