feat: rework modules, items and fuel
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,190 +1,206 @@
|
||||
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; }
|
||||
public float ArrivalThreshold { get; set; }
|
||||
public float MiningRate { get; set; }
|
||||
public float MiningCycleSeconds { get; set; }
|
||||
public float TransferRate { get; set; }
|
||||
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 float YPlane { get; set; }
|
||||
public float ArrivalThreshold { get; set; }
|
||||
public float MiningRate { get; set; }
|
||||
public float MiningCycleSeconds { get; set; }
|
||||
public float TransferRate { get; set; }
|
||||
public float DockingDuration { get; set; }
|
||||
public float UndockingDuration { get; set; }
|
||||
public float UndockDistance { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SolarSystemDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required float[] Position { get; set; }
|
||||
public string StarKind { get; set; } = "main-sequence";
|
||||
public int StarCount { get; set; } = 1;
|
||||
public required string StarColor { get; set; }
|
||||
public required string StarGlow { get; set; }
|
||||
public float StarSize { get; set; }
|
||||
public float GravityWellRadius { get; set; }
|
||||
public required AsteroidFieldDefinition AsteroidField { get; set; }
|
||||
public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
|
||||
public required List<PlanetDefinition> Planets { get; set; }
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required float[] Position { get; set; }
|
||||
public string StarKind { get; set; } = "main-sequence";
|
||||
public int StarCount { get; set; } = 1;
|
||||
public required string StarColor { get; set; }
|
||||
public required string StarGlow { get; set; }
|
||||
public float StarSize { get; set; }
|
||||
public float GravityWellRadius { get; set; }
|
||||
public required AsteroidFieldDefinition AsteroidField { get; set; }
|
||||
public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
|
||||
public required List<PlanetDefinition> Planets { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AsteroidFieldDefinition
|
||||
{
|
||||
public int DecorationCount { get; set; }
|
||||
public float RadiusOffset { get; set; }
|
||||
public float RadiusVariance { get; set; }
|
||||
public float HeightVariance { get; set; }
|
||||
public int DecorationCount { get; set; }
|
||||
public float RadiusOffset { get; set; }
|
||||
public float RadiusVariance { get; set; }
|
||||
public float HeightVariance { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ResourceNodeDefinition
|
||||
{
|
||||
public string SourceKind { get; set; } = "asteroid-belt";
|
||||
public float Angle { get; set; }
|
||||
public float RadiusOffset { get; set; }
|
||||
public float InclinationDegrees { get; set; }
|
||||
public int? AnchorPlanetIndex { get; set; }
|
||||
public int? AnchorMoonIndex { get; set; }
|
||||
public float OreAmount { get; set; }
|
||||
public required string ItemId { get; set; }
|
||||
public int ShardCount { get; set; }
|
||||
public string SourceKind { get; set; } = "asteroid-belt";
|
||||
public float Angle { get; set; }
|
||||
public float RadiusOffset { get; set; }
|
||||
public float InclinationDegrees { get; set; }
|
||||
public int? AnchorPlanetIndex { get; set; }
|
||||
public int? AnchorMoonIndex { get; set; }
|
||||
public float OreAmount { get; set; }
|
||||
public required string ItemId { get; set; }
|
||||
public int ShardCount { get; set; }
|
||||
}
|
||||
|
||||
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 sealed class RecipeInputDefinition
|
||||
{
|
||||
public required string ItemId { get; set; }
|
||||
public float Amount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleRecipeDefinition
|
||||
{
|
||||
public required string ModuleId { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public required List<RecipeInputDefinition> Inputs { get; set; }
|
||||
public required string Id { get; set; }
|
||||
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 required string ItemId { get; set; }
|
||||
public float Amount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RecipeInputDefinition
|
||||
{
|
||||
public required string ItemId { get; set; }
|
||||
public float Amount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleConstructionDefinition
|
||||
{
|
||||
public required List<RecipeInputDefinition> Requirements { get; set; }
|
||||
public float ProductionTime { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public required string Type { get; set; }
|
||||
public string? Product { get; set; }
|
||||
public float Radius { get; set; } = 12f;
|
||||
public float Hull { get; set; } = 100f;
|
||||
public float WorkforceNeeded { get; set; }
|
||||
public ModuleConstructionDefinition? Construction { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleRecipeDefinition
|
||||
{
|
||||
public required string ModuleId { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public required List<RecipeInputDefinition> Inputs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RecipeDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required string FacilityCategory { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public List<string> RequiredModules { get; set; } = [];
|
||||
public List<RecipeInputDefinition> Inputs { get; set; } = [];
|
||||
public List<RecipeOutputDefinition> Outputs { get; set; } = [];
|
||||
public string? ShipOutputId { get; set; }
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required string FacilityCategory { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public List<string> RequiredModules { get; set; } = [];
|
||||
public List<RecipeInputDefinition> Inputs { get; set; } = [];
|
||||
public List<RecipeOutputDefinition> Outputs { get; set; } = [];
|
||||
public string? ShipOutputId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlanetDefinition
|
||||
{
|
||||
public required string Label { get; set; }
|
||||
public string PlanetType { get; set; } = "terrestrial";
|
||||
public string Shape { get; set; } = "sphere";
|
||||
public int MoonCount { get; set; }
|
||||
public float OrbitRadius { get; set; }
|
||||
public float OrbitSpeed { get; set; }
|
||||
public float OrbitEccentricity { get; set; }
|
||||
public float OrbitInclination { get; set; }
|
||||
public float OrbitLongitudeOfAscendingNode { get; set; }
|
||||
public float OrbitArgumentOfPeriapsis { get; set; }
|
||||
public float OrbitPhaseAtEpoch { get; set; }
|
||||
public float Size { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public float Tilt { get; set; }
|
||||
public bool HasRing { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public string PlanetType { get; set; } = "terrestrial";
|
||||
public string Shape { get; set; } = "sphere";
|
||||
public int MoonCount { get; set; }
|
||||
public float OrbitRadius { get; set; }
|
||||
public float OrbitSpeed { get; set; }
|
||||
public float OrbitEccentricity { get; set; }
|
||||
public float OrbitInclination { get; set; }
|
||||
public float OrbitLongitudeOfAscendingNode { get; set; }
|
||||
public float OrbitArgumentOfPeriapsis { get; set; }
|
||||
public float OrbitPhaseAtEpoch { get; set; }
|
||||
public float Size { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public float Tilt { get; set; }
|
||||
public bool HasRing { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipDefinition
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required string Role { get; set; }
|
||||
public required string ShipClass { get; set; }
|
||||
public float Speed { get; set; }
|
||||
public float WarpSpeed { get; set; }
|
||||
public float FtlSpeed { get; set; }
|
||||
public float SpoolTime { get; set; }
|
||||
public float CargoCapacity { get; set; }
|
||||
public string? CargoKind { get; set; }
|
||||
public string? CargoItemId { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public required string HullColor { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float MaxHealth { get; set; }
|
||||
public List<string> 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 required string Id { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public required string Role { get; set; }
|
||||
public required string ShipClass { get; set; }
|
||||
public float Speed { get; set; }
|
||||
public float WarpSpeed { get; set; }
|
||||
public float FtlSpeed { get; set; }
|
||||
public float SpoolTime { get; set; }
|
||||
public float CargoCapacity { get; set; }
|
||||
public string? CargoKind { get; set; }
|
||||
public string? CargoItemId { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public required string HullColor { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float MaxHealth { get; set; }
|
||||
public List<string> Modules { get; set; } = [];
|
||||
public ConstructionDefinition? Construction { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ScenarioDefinition
|
||||
{
|
||||
public required List<InitialStationDefinition> InitialStations { get; set; }
|
||||
public required List<ShipFormationDefinition> ShipFormations { get; set; }
|
||||
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
|
||||
public required MiningDefaultsDefinition MiningDefaults { get; set; }
|
||||
public required List<InitialStationDefinition> InitialStations { get; set; }
|
||||
public required List<ShipFormationDefinition> ShipFormations { get; set; }
|
||||
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
|
||||
public required MiningDefaultsDefinition MiningDefaults { get; set; }
|
||||
}
|
||||
|
||||
public sealed class InitialStationDefinition
|
||||
{
|
||||
public required string ConstructibleId { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public string? FactionId { get; set; }
|
||||
public int? PlanetIndex { get; set; }
|
||||
public int? LagrangeSide { get; set; }
|
||||
public float[]? Position { 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; }
|
||||
public float[]? Position { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipFormationDefinition
|
||||
{
|
||||
public required string ShipId { get; set; }
|
||||
public int Count { get; set; }
|
||||
public required float[] Center { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public string? FactionId { get; set; }
|
||||
public required string ShipId { get; set; }
|
||||
public int Count { get; set; }
|
||||
public required float[] Center { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public string? FactionId { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PatrolRouteDefinition
|
||||
{
|
||||
public required string SystemId { get; set; }
|
||||
public required List<float[]> Points { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public required List<float[]> Points { get; set; }
|
||||
}
|
||||
|
||||
public sealed class MiningDefaultsDefinition
|
||||
{
|
||||
public required string NodeSystemId { get; set; }
|
||||
public required string RefinerySystemId { get; set; }
|
||||
public required string NodeSystemId { get; set; }
|
||||
public required string RefinerySystemId { get; set; }
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,169 +2,126 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
internal sealed class IdleShipBehaviorState : IShipBehaviorState
|
||||
{
|
||||
public string Kind => "idle";
|
||||
public string Kind => "idle";
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
Threshold = world.Balance.ArrivalThreshold,
|
||||
Status = WorkStatus.Pending,
|
||||
};
|
||||
}
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
Threshold = world.Balance.ArrivalThreshold,
|
||||
Status = WorkStatus.Pending,
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
}
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PatrolShipBehaviorState : IShipBehaviorState
|
||||
{
|
||||
public string Kind => "patrol";
|
||||
public string Kind => "patrol";
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
|
||||
{
|
||||
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
|
||||
{
|
||||
ship.DefaultBehavior.Kind = "idle";
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
Threshold = world.Balance.ArrivalThreshold,
|
||||
Status = WorkStatus.Pending,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
|
||||
TargetSystemId = ship.SystemId,
|
||||
Threshold = 18f,
|
||||
};
|
||||
ship.DefaultBehavior.Kind = "idle";
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
Threshold = world.Balance.ArrivalThreshold,
|
||||
Status = WorkStatus.Pending,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0)
|
||||
{
|
||||
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count;
|
||||
}
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
|
||||
TargetSystemId = ship.SystemId,
|
||||
Threshold = 18f,
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0)
|
||||
{
|
||||
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
|
||||
{
|
||||
private readonly string resourceItemId;
|
||||
private readonly string requiredModule;
|
||||
private readonly string resourceItemId;
|
||||
private readonly string requiredModule;
|
||||
|
||||
public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule)
|
||||
public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule)
|
||||
{
|
||||
Kind = kind;
|
||||
this.resourceItemId = resourceItemId;
|
||||
this.requiredModule = requiredModule;
|
||||
}
|
||||
|
||||
public string Kind { get; }
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
||||
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
Kind = kind;
|
||||
this.resourceItemId = resourceItemId;
|
||||
this.requiredModule = requiredModule;
|
||||
}
|
||||
|
||||
public string Kind { get; }
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
||||
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
case ("travel-to-node", "arrived"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
|
||||
break;
|
||||
case ("extract", "cargo-full"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
||||
break;
|
||||
case ("extract", "node-depleted"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
||||
ship.DefaultBehavior.NodeId = null;
|
||||
break;
|
||||
case ("travel-to-station", "arrived"):
|
||||
ship.DefaultBehavior.Phase = "dock";
|
||||
break;
|
||||
case ("dock", "docked"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "refuel";
|
||||
break;
|
||||
case ("unload", "unloaded"):
|
||||
ship.DefaultBehavior.Phase = "refuel";
|
||||
break;
|
||||
case ("refuel", "refueled"):
|
||||
ship.DefaultBehavior.Phase = "undock";
|
||||
break;
|
||||
case ("undock", "undocked"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
||||
ship.DefaultBehavior.NodeId = null;
|
||||
break;
|
||||
}
|
||||
case ("travel-to-node", "arrived"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
|
||||
break;
|
||||
case ("extract", "cargo-full"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
||||
break;
|
||||
case ("extract", "node-depleted"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
||||
ship.DefaultBehavior.NodeId = null;
|
||||
break;
|
||||
case ("travel-to-station", "arrived"):
|
||||
ship.DefaultBehavior.Phase = "dock";
|
||||
break;
|
||||
case ("dock", "docked"):
|
||||
ship.DefaultBehavior.Phase = "unload";
|
||||
break;
|
||||
case ("undock", "undocked"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
||||
ship.DefaultBehavior.NodeId = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
|
||||
{
|
||||
public string Kind => "construct-station";
|
||||
public string Kind => "construct-station";
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
||||
engine.PlanStationConstruction(ship, world);
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
||||
engine.PlanStationConstruction(ship, world);
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
case ("travel-to-station", "arrived"):
|
||||
ship.DefaultBehavior.Phase = 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"):
|
||||
ship.DefaultBehavior.Phase = "build-site";
|
||||
break;
|
||||
case ("construct-module", "module-constructed"):
|
||||
case ("build-site", "site-constructed"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
||||
ship.DefaultBehavior.ModuleId = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class EnergySupplyShipBehaviorState : IShipBehaviorState
|
||||
{
|
||||
public string Kind => "auto-supply-energy";
|
||||
|
||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
||||
engine.PlanEnergySupply(ship, world);
|
||||
|
||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
case ("travel-to-source", "arrived"):
|
||||
case ("travel-to-destination", "arrived"):
|
||||
ship.DefaultBehavior.Phase = "dock";
|
||||
break;
|
||||
case ("dock", "docked"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "unload" : "load";
|
||||
break;
|
||||
case ("load", "loaded"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock";
|
||||
break;
|
||||
case ("unload", "unloaded"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.NeedsRefuel(ship, world) ? "refuel" : "undock";
|
||||
break;
|
||||
case ("refuel", "refueled"):
|
||||
ship.DefaultBehavior.Phase = "undock";
|
||||
break;
|
||||
case ("undock", "undocked"):
|
||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) > 0.01f ? "travel-to-destination" : "travel-to-source";
|
||||
break;
|
||||
}
|
||||
case ("travel-to-station", "arrived"):
|
||||
ship.DefaultBehavior.Phase = "deliver-to-site";
|
||||
break;
|
||||
case ("deliver-to-site", "construction-delivered"):
|
||||
ship.DefaultBehavior.Phase = "build-site";
|
||||
break;
|
||||
case ("construct-module", "module-constructed"):
|
||||
case ("build-site", "site-constructed"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
||||
ship.DefaultBehavior.ModuleId = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,63 +4,62 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed class ShipRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public required ShipDefinition Definition { get; init; }
|
||||
public required string FactionId { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required Vector3 TargetPosition { get; set; }
|
||||
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||
public ShipState State { get; set; } = ShipState.Idle;
|
||||
public ShipOrderRuntime? Order { get; set; }
|
||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||
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 int? AssignedDockingPadIndex { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public float Health { get; set; }
|
||||
public string? TrackedActionKey { get; set; }
|
||||
public float TrackedActionTotal { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public required ShipDefinition Definition { get; init; }
|
||||
public required string FactionId { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required Vector3 TargetPosition { get; set; }
|
||||
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||
public ShipState State { get; set; } = ShipState.Idle;
|
||||
public ShipOrderRuntime? Order { get; set; }
|
||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||
public float ActionTimer { get; set; }
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public float WorkerPopulation { get; set; }
|
||||
public string DockedStationId { get; set; }
|
||||
public int? AssignedDockingPadIndex { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public float Health { get; set; }
|
||||
public string? TrackedActionKey { get; set; }
|
||||
public float TrackedActionTotal { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShipOrderRuntime
|
||||
{
|
||||
public required string Kind { get; init; }
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
||||
public required string DestinationSystemId { get; init; }
|
||||
public required Vector3 DestinationPosition { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
||||
public required string DestinationSystemId { get; init; }
|
||||
public required Vector3 DestinationPosition { get; init; }
|
||||
}
|
||||
|
||||
public sealed class DefaultBehaviorRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public string? AreaSystemId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
public string? RefineryId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? Phase { get; set; }
|
||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||
public int PatrolIndex { get; set; }
|
||||
public required string Kind { get; set; }
|
||||
public string? AreaSystemId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
public string? RefineryId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? Phase { get; set; }
|
||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||
public int PatrolIndex { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ControllerTaskRuntime
|
||||
{
|
||||
public required ControllerTaskKind Kind { get; set; }
|
||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||
public string? CommanderId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetNodeId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
public required ControllerTaskKind Kind { get; set; }
|
||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||
public string? CommanderId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetNodeId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,243 +2,237 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public enum SpatialNodeKind
|
||||
{
|
||||
Star,
|
||||
Planet,
|
||||
Moon,
|
||||
LagrangePoint,
|
||||
Station,
|
||||
ResourceSite,
|
||||
Star,
|
||||
Planet,
|
||||
Moon,
|
||||
LagrangePoint,
|
||||
Station,
|
||||
ResourceSite,
|
||||
}
|
||||
|
||||
public enum WorkStatus
|
||||
{
|
||||
Pending,
|
||||
Active,
|
||||
Completed,
|
||||
Pending,
|
||||
Active,
|
||||
Completed,
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Queued,
|
||||
Accepted,
|
||||
Completed,
|
||||
Queued,
|
||||
Accepted,
|
||||
Completed,
|
||||
}
|
||||
|
||||
public enum ShipState
|
||||
{
|
||||
Idle,
|
||||
Arriving,
|
||||
CapacitorStarved,
|
||||
LocalFlight,
|
||||
SpoolingWarp,
|
||||
Warping,
|
||||
SpoolingFtl,
|
||||
Ftl,
|
||||
CargoFull,
|
||||
MiningApproach,
|
||||
Mining,
|
||||
NodeDepleted,
|
||||
AwaitingDock,
|
||||
DockingApproach,
|
||||
Docking,
|
||||
Docked,
|
||||
Transferring,
|
||||
Loading,
|
||||
Unloading,
|
||||
Refueling,
|
||||
WaitingMaterials,
|
||||
ConstructionBlocked,
|
||||
Constructing,
|
||||
DeliveringConstruction,
|
||||
Blocked,
|
||||
Undocking,
|
||||
Idle,
|
||||
Arriving,
|
||||
LocalFlight,
|
||||
SpoolingWarp,
|
||||
Warping,
|
||||
SpoolingFtl,
|
||||
Ftl,
|
||||
CargoFull,
|
||||
MiningApproach,
|
||||
Mining,
|
||||
NodeDepleted,
|
||||
AwaitingDock,
|
||||
DockingApproach,
|
||||
Docking,
|
||||
Docked,
|
||||
Transferring,
|
||||
Loading,
|
||||
Unloading,
|
||||
WaitingMaterials,
|
||||
ConstructionBlocked,
|
||||
Constructing,
|
||||
DeliveringConstruction,
|
||||
Blocked,
|
||||
Undocking,
|
||||
}
|
||||
|
||||
public enum ControllerTaskKind
|
||||
{
|
||||
Idle,
|
||||
Travel,
|
||||
Extract,
|
||||
Dock,
|
||||
Load,
|
||||
Unload,
|
||||
Refuel,
|
||||
DeliverConstruction,
|
||||
BuildConstructionSite,
|
||||
LoadWorkers,
|
||||
UnloadWorkers,
|
||||
ConstructModule,
|
||||
Undock,
|
||||
Idle,
|
||||
Travel,
|
||||
Extract,
|
||||
Dock,
|
||||
Load,
|
||||
Unload,
|
||||
DeliverConstruction,
|
||||
BuildConstructionSite,
|
||||
LoadWorkers,
|
||||
UnloadWorkers,
|
||||
ConstructModule,
|
||||
Undock,
|
||||
}
|
||||
|
||||
public static class SpaceLayerKinds
|
||||
{
|
||||
public const string UniverseSpace = "universe-space";
|
||||
public const string GalaxySpace = "galaxy-space";
|
||||
public const string SystemSpace = "system-space";
|
||||
public const string LocalSpace = "local-space";
|
||||
public const string UniverseSpace = "universe-space";
|
||||
public const string GalaxySpace = "galaxy-space";
|
||||
public const string SystemSpace = "system-space";
|
||||
public const string LocalSpace = "local-space";
|
||||
}
|
||||
|
||||
public static class MovementRegimeKinds
|
||||
{
|
||||
public const string LocalFlight = "local-flight";
|
||||
public const string Warp = "warp";
|
||||
public const string StargateTransit = "stargate-transit";
|
||||
public const string FtlTransit = "ftl-transit";
|
||||
public const string LocalFlight = "local-flight";
|
||||
public const string Warp = "warp";
|
||||
public const string StargateTransit = "stargate-transit";
|
||||
public const string FtlTransit = "ftl-transit";
|
||||
}
|
||||
|
||||
public static class CommanderKind
|
||||
{
|
||||
public const string Faction = "faction";
|
||||
public const string Station = "station";
|
||||
public const string Ship = "ship";
|
||||
public const string Fleet = "fleet";
|
||||
public const string Sector = "sector";
|
||||
public const string TaskGroup = "task-group";
|
||||
public const string Faction = "faction";
|
||||
public const string Station = "station";
|
||||
public const string Ship = "ship";
|
||||
public const string Fleet = "fleet";
|
||||
public const string Sector = "sector";
|
||||
public const string TaskGroup = "task-group";
|
||||
}
|
||||
|
||||
public static class ShipTaskKinds
|
||||
{
|
||||
public const string Idle = "idle";
|
||||
public const string LocalMove = "local-move";
|
||||
public const string WarpToNode = "warp-to-node";
|
||||
public const string UseStargate = "use-stargate";
|
||||
public const string UseFtl = "use-ftl";
|
||||
public const string Dock = "dock";
|
||||
public const string Undock = "undock";
|
||||
public const string LoadCargo = "load-cargo";
|
||||
public const string UnloadCargo = "unload-cargo";
|
||||
public const string LoadWorkers = "load-workers";
|
||||
public const string UnloadWorkers = "unload-workers";
|
||||
public const string MineNode = "mine-node";
|
||||
public const string HarvestGas = "harvest-gas";
|
||||
public const string DeliverToStation = "deliver-to-station";
|
||||
public const string ClaimLagrangePoint = "claim-lagrange-point";
|
||||
public const string BuildConstructionSite = "build-construction-site";
|
||||
public const string EscortTarget = "escort-target";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string DefendBubble = "defend-bubble";
|
||||
public const string Retreat = "retreat";
|
||||
public const string HoldPosition = "hold-position";
|
||||
public const string Idle = "idle";
|
||||
public const string LocalMove = "local-move";
|
||||
public const string WarpToNode = "warp-to-node";
|
||||
public const string UseStargate = "use-stargate";
|
||||
public const string UseFtl = "use-ftl";
|
||||
public const string Dock = "dock";
|
||||
public const string Undock = "undock";
|
||||
public const string LoadCargo = "load-cargo";
|
||||
public const string UnloadCargo = "unload-cargo";
|
||||
public const string LoadWorkers = "load-workers";
|
||||
public const string UnloadWorkers = "unload-workers";
|
||||
public const string MineNode = "mine-node";
|
||||
public const string HarvestGas = "harvest-gas";
|
||||
public const string DeliverToStation = "deliver-to-station";
|
||||
public const string ClaimLagrangePoint = "claim-lagrange-point";
|
||||
public const string BuildConstructionSite = "build-construction-site";
|
||||
public const string EscortTarget = "escort-target";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string DefendBubble = "defend-bubble";
|
||||
public const string Retreat = "retreat";
|
||||
public const string HoldPosition = "hold-position";
|
||||
}
|
||||
|
||||
public static class ShipOrderKinds
|
||||
{
|
||||
public const string DirectMove = "direct-move";
|
||||
public const string TravelToNode = "travel-to-node";
|
||||
public const string DockAtStation = "dock-at-station";
|
||||
public const string DeliverCargo = "deliver-cargo";
|
||||
public const string BuildAtSite = "build-at-site";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string HoldPosition = "hold-position";
|
||||
public const string DirectMove = "direct-move";
|
||||
public const string TravelToNode = "travel-to-node";
|
||||
public const string DockAtStation = "dock-at-station";
|
||||
public const string DeliverCargo = "deliver-cargo";
|
||||
public const string BuildAtSite = "build-at-site";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string HoldPosition = "hold-position";
|
||||
}
|
||||
|
||||
public static class ClaimStateKinds
|
||||
{
|
||||
public const string Placed = "placed";
|
||||
public const string Activating = "activating";
|
||||
public const string Active = "active";
|
||||
public const string Destroyed = "destroyed";
|
||||
public const string Placed = "placed";
|
||||
public const string Activating = "activating";
|
||||
public const string Active = "active";
|
||||
public const string Destroyed = "destroyed";
|
||||
}
|
||||
|
||||
public static class ConstructionSiteStateKinds
|
||||
{
|
||||
public const string Planned = "planned";
|
||||
public const string Active = "active";
|
||||
public const string Paused = "paused";
|
||||
public const string Completed = "completed";
|
||||
public const string Destroyed = "destroyed";
|
||||
public const string Planned = "planned";
|
||||
public const string Active = "active";
|
||||
public const string Paused = "paused";
|
||||
public const string Completed = "completed";
|
||||
public const string Destroyed = "destroyed";
|
||||
}
|
||||
|
||||
public static class MarketOrderKinds
|
||||
{
|
||||
public const string Buy = "buy";
|
||||
public const string Sell = "sell";
|
||||
public const string Buy = "buy";
|
||||
public const string Sell = "sell";
|
||||
}
|
||||
|
||||
public static class MarketOrderStateKinds
|
||||
{
|
||||
public const string Open = "open";
|
||||
public const string PartiallyFilled = "partially-filled";
|
||||
public const string Filled = "filled";
|
||||
public const string Cancelled = "cancelled";
|
||||
public const string Open = "open";
|
||||
public const string PartiallyFilled = "partially-filled";
|
||||
public const string Filled = "filled";
|
||||
public const string Cancelled = "cancelled";
|
||||
}
|
||||
|
||||
public static class SimulationEnumMappings
|
||||
{
|
||||
public static string ToContractValue(this SpatialNodeKind kind) => kind switch
|
||||
{
|
||||
SpatialNodeKind.Star => "star",
|
||||
SpatialNodeKind.Planet => "planet",
|
||||
SpatialNodeKind.Moon => "moon",
|
||||
SpatialNodeKind.LagrangePoint => "lagrange-point",
|
||||
SpatialNodeKind.Station => "station",
|
||||
SpatialNodeKind.ResourceSite => "resource-site",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
public static string ToContractValue(this SpatialNodeKind kind) => kind switch
|
||||
{
|
||||
SpatialNodeKind.Star => "star",
|
||||
SpatialNodeKind.Planet => "planet",
|
||||
SpatialNodeKind.Moon => "moon",
|
||||
SpatialNodeKind.LagrangePoint => "lagrange-point",
|
||||
SpatialNodeKind.Station => "station",
|
||||
SpatialNodeKind.ResourceSite => "resource-site",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this WorkStatus status) => status switch
|
||||
{
|
||||
WorkStatus.Pending => "pending",
|
||||
WorkStatus.Active => "active",
|
||||
WorkStatus.Completed => "completed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
public static string ToContractValue(this WorkStatus status) => status switch
|
||||
{
|
||||
WorkStatus.Pending => "pending",
|
||||
WorkStatus.Active => "active",
|
||||
WorkStatus.Completed => "completed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this OrderStatus status) => status switch
|
||||
{
|
||||
OrderStatus.Queued => "queued",
|
||||
OrderStatus.Accepted => "accepted",
|
||||
OrderStatus.Completed => "completed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
public static string ToContractValue(this OrderStatus status) => status switch
|
||||
{
|
||||
OrderStatus.Queued => "queued",
|
||||
OrderStatus.Accepted => "accepted",
|
||||
OrderStatus.Completed => "completed",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this ShipState state) => state switch
|
||||
{
|
||||
ShipState.Idle => "idle",
|
||||
ShipState.Arriving => "arriving",
|
||||
ShipState.CapacitorStarved => "capacitor-starved",
|
||||
ShipState.LocalFlight => "local-flight",
|
||||
ShipState.SpoolingWarp => "spooling-warp",
|
||||
ShipState.Warping => "warping",
|
||||
ShipState.SpoolingFtl => "spooling-ftl",
|
||||
ShipState.Ftl => "ftl",
|
||||
ShipState.CargoFull => "cargo-full",
|
||||
ShipState.MiningApproach => "mining-approach",
|
||||
ShipState.Mining => "mining",
|
||||
ShipState.NodeDepleted => "node-depleted",
|
||||
ShipState.AwaitingDock => "awaiting-dock",
|
||||
ShipState.DockingApproach => "docking-approach",
|
||||
ShipState.Docking => "docking",
|
||||
ShipState.Docked => "docked",
|
||||
ShipState.Transferring => "transferring",
|
||||
ShipState.Loading => "loading",
|
||||
ShipState.Unloading => "unloading",
|
||||
ShipState.Refueling => "refueling",
|
||||
ShipState.WaitingMaterials => "waiting-materials",
|
||||
ShipState.ConstructionBlocked => "construction-blocked",
|
||||
ShipState.Constructing => "constructing",
|
||||
ShipState.DeliveringConstruction => "delivering-construction",
|
||||
ShipState.Blocked => "blocked",
|
||||
ShipState.Undocking => "undocking",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
|
||||
};
|
||||
public static string ToContractValue(this ShipState state) => state switch
|
||||
{
|
||||
ShipState.Idle => "idle",
|
||||
ShipState.Arriving => "arriving",
|
||||
ShipState.LocalFlight => "local-flight",
|
||||
ShipState.SpoolingWarp => "spooling-warp",
|
||||
ShipState.Warping => "warping",
|
||||
ShipState.SpoolingFtl => "spooling-ftl",
|
||||
ShipState.Ftl => "ftl",
|
||||
ShipState.CargoFull => "cargo-full",
|
||||
ShipState.MiningApproach => "mining-approach",
|
||||
ShipState.Mining => "mining",
|
||||
ShipState.NodeDepleted => "node-depleted",
|
||||
ShipState.AwaitingDock => "awaiting-dock",
|
||||
ShipState.DockingApproach => "docking-approach",
|
||||
ShipState.Docking => "docking",
|
||||
ShipState.Docked => "docked",
|
||||
ShipState.Transferring => "transferring",
|
||||
ShipState.Loading => "loading",
|
||||
ShipState.Unloading => "unloading",
|
||||
ShipState.WaitingMaterials => "waiting-materials",
|
||||
ShipState.ConstructionBlocked => "construction-blocked",
|
||||
ShipState.Constructing => "constructing",
|
||||
ShipState.DeliveringConstruction => "delivering-construction",
|
||||
ShipState.Blocked => "blocked",
|
||||
ShipState.Undocking => "undocking",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this ControllerTaskKind kind) => kind switch
|
||||
{
|
||||
ControllerTaskKind.Idle => "idle",
|
||||
ControllerTaskKind.Travel => "travel",
|
||||
ControllerTaskKind.Extract => "extract",
|
||||
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",
|
||||
ControllerTaskKind.UnloadWorkers => "unload-workers",
|
||||
ControllerTaskKind.ConstructModule => "construct-module",
|
||||
ControllerTaskKind.Undock => "undock",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
public static string ToContractValue(this ControllerTaskKind kind) => kind switch
|
||||
{
|
||||
ControllerTaskKind.Idle => "idle",
|
||||
ControllerTaskKind.Travel => "travel",
|
||||
ControllerTaskKind.Extract => "extract",
|
||||
ControllerTaskKind.Dock => "dock",
|
||||
ControllerTaskKind.Load => "load",
|
||||
ControllerTaskKind.Unload => "unload",
|
||||
ControllerTaskKind.DeliverConstruction => "deliver-construction",
|
||||
ControllerTaskKind.BuildConstructionSite => "build-construction-site",
|
||||
ControllerTaskKind.LoadWorkers => "load-workers",
|
||||
ControllerTaskKind.UnloadWorkers => "unload-workers",
|
||||
ControllerTaskKind.ConstructModule => "construct-module",
|
||||
ControllerTaskKind.Undock => "undock",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
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 Vector3 Position { get; set; }
|
||||
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 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; }
|
||||
public float WorkforceEffectiveRatio { get; set; } = 0.1f;
|
||||
public float PopulationGrowthProgress { get; set; }
|
||||
public float ShipProductionProgressSeconds { get; set; }
|
||||
public HashSet<string> DockedShipIds { get; } = [];
|
||||
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { 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<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 Population { get; set; }
|
||||
public float PopulationCapacity { get; set; }
|
||||
public float WorkforceRequired { get; set; }
|
||||
public float WorkforceEffectiveRatio { get; set; } = 0.1f;
|
||||
public float PopulationGrowthProgress { get; set; }
|
||||
public float ShipProductionProgressSeconds { get; set; }
|
||||
public HashSet<string> DockedShipIds { get; } = [];
|
||||
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class StationModuleRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ModuleId { get; init; }
|
||||
public float Health { get; set; }
|
||||
public float MaxHealth { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ModuleConstructionRuntime
|
||||
{
|
||||
public required string ModuleId { get; init; }
|
||||
public float ProgressSeconds { get; set; }
|
||||
public float RequiredSeconds { get; init; }
|
||||
public string AssignedConstructorShipId { get; set; } = string.Empty;
|
||||
public required string ModuleId { get; init; }
|
||||
public float ProgressSeconds { get; set; }
|
||||
public float RequiredSeconds { get; init; }
|
||||
public string AssignedConstructorShipId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
[
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,312 +2,274 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed partial class SimulationEngine
|
||||
{
|
||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||
|
||||
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
||||
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
||||
|
||||
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
||||
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
||||
|
||||
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
||||
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
||||
?? Vector3.Zero;
|
||||
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
||||
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
||||
?? Vector3.Zero;
|
||||
|
||||
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
private string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
return task.Kind switch
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
return task.Kind switch
|
||||
{
|
||||
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
|
||||
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),
|
||||
ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
|
||||
_ => UpdateIdle(ship, world, deltaSeconds),
|
||||
};
|
||||
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.LoadWorkers => UpdateLoadWorkers(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.UnloadWorkers => UpdateUnloadWorkers(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
|
||||
_ => UpdateIdle(ship, world, deltaSeconds),
|
||||
};
|
||||
}
|
||||
|
||||
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
if (task.TargetPosition is null || task.TargetSystemId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
var targetPosition = task.TargetPosition.Value;
|
||||
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
if (ship.SystemId != task.TargetSystemId)
|
||||
{
|
||||
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
|
||||
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
|
||||
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
|
||||
}
|
||||
|
||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
var currentNode = ResolveCurrentNode(world, ship);
|
||||
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
if (task.TargetPosition is null || task.TargetSystemId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var targetPosition = task.TargetPosition.Value;
|
||||
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
if (ship.SystemId != task.TargetSystemId)
|
||||
{
|
||||
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
|
||||
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
|
||||
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
|
||||
}
|
||||
|
||||
var currentNode = ResolveCurrentNode(world, ship);
|
||||
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
|
||||
{
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
|
||||
}
|
||||
|
||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
|
||||
}
|
||||
|
||||
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
|
||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
|
||||
}
|
||||
|
||||
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
||||
{
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (station?.NodeId is not null)
|
||||
{
|
||||
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
|
||||
}
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (station?.NodeId is not null)
|
||||
{
|
||||
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
|
||||
}
|
||||
|
||||
var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (node is not null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return world.SpatialNodes
|
||||
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
|
||||
.FirstOrDefault();
|
||||
var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (node is not null)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
if (ship.SpatialState.CurrentNodeId is not null)
|
||||
{
|
||||
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId);
|
||||
}
|
||||
return world.SpatialNodes
|
||||
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
return world.SpatialNodes
|
||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
if (ship.SpatialState.CurrentNodeId is not null)
|
||||
{
|
||||
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId);
|
||||
}
|
||||
|
||||
private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) =>
|
||||
world.SpatialNodes.FirstOrDefault(candidate =>
|
||||
candidate.SystemId == systemId &&
|
||||
candidate.Kind == SpatialNodeKind.Star);
|
||||
return world.SpatialNodes
|
||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private string UpdateLocalTravel(
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
NodeRuntime? targetNode,
|
||||
float threshold)
|
||||
private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) =>
|
||||
world.SpatialNodes.FirstOrDefault(candidate =>
|
||||
candidate.SystemId == systemId &&
|
||||
candidate.Kind == SpatialNodeKind.Star);
|
||||
|
||||
private string UpdateLocalTravel(
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
NodeRuntime? targetNode,
|
||||
float threshold)
|
||||
{
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
|
||||
if (distance <= threshold)
|
||||
{
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = ship.Position;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
|
||||
if (distance <= threshold)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = ship.Position;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = ShipState.CapacitorStarved;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode)
|
||||
{
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id)
|
||||
{
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.Warp,
|
||||
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
||||
DestinationNodeId = targetNode.Id,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
|
||||
ship.SpatialState.CurrentNodeId = null;
|
||||
ship.SpatialState.CurrentBubbleId = null;
|
||||
ship.SpatialState.DestinationNodeId = targetNode.Id;
|
||||
|
||||
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
if (ship.State != ShipState.Warping)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingWarp)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
}
|
||||
|
||||
ship.State = ShipState.SpoolingWarp;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Warping;
|
||||
}
|
||||
|
||||
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode)
|
||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
||||
? ship.Position.DistanceTo(targetPosition)
|
||||
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
||||
return ship.Position.DistanceTo(targetPosition) <= 18f
|
||||
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
|
||||
: "none";
|
||||
}
|
||||
|
||||
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
{
|
||||
var destinationNodeId = targetNode?.Id;
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
||||
{
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id)
|
||||
{
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.Warp,
|
||||
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
||||
DestinationNodeId = targetNode.Id,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
|
||||
ship.SpatialState.CurrentNodeId = null;
|
||||
ship.SpatialState.CurrentBubbleId = null;
|
||||
ship.SpatialState.DestinationNodeId = targetNode.Id;
|
||||
|
||||
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
if (ship.State != ShipState.Warping)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingWarp)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = ShipState.CapacitorStarved;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.SpoolingWarp;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Warping;
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = ShipState.CapacitorStarved;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
||||
? ship.Position.DistanceTo(targetPosition)
|
||||
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
||||
return ship.Position.DistanceTo(targetPosition) <= 18f
|
||||
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
|
||||
: "none";
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.FtlTransit,
|
||||
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
||||
DestinationNodeId = destinationNodeId,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
{
|
||||
var destinationNodeId = targetNode?.Id;
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
||||
{
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.FtlTransit,
|
||||
OriginNodeId = ship.SpatialState.CurrentNodeId,
|
||||
DestinationNodeId = destinationNodeId,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
|
||||
ship.SpatialState.CurrentNodeId = null;
|
||||
ship.SpatialState.CurrentBubbleId = null;
|
||||
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
||||
|
||||
if (ship.State != ShipState.Ftl)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingFtl)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = ShipState.CapacitorStarved;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.SpoolingFtl;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Ftl;
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.WarpDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = ShipState.CapacitorStarved;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
||||
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
||||
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
||||
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
||||
return transit.Progress >= 0.999f
|
||||
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
|
||||
: "none";
|
||||
}
|
||||
|
||||
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
|
||||
ship.SpatialState.CurrentNodeId = null;
|
||||
ship.SpatialState.CurrentBubbleId = null;
|
||||
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
||||
|
||||
if (ship.State != ShipState.Ftl)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingFtl)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
}
|
||||
|
||||
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
ship.State = ShipState.SpoolingFtl;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Ftl;
|
||||
}
|
||||
|
||||
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
||||
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
||||
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
||||
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
||||
return transit.Progress >= 0.999f
|
||||
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
|
||||
: "none";
|
||||
}
|
||||
|
||||
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
|
||||
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentNodeId = targetNode?.Id;
|
||||
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
|
||||
ship.SpatialState.DestinationNodeId = targetNode?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,560 +5,184 @@ 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));
|
||||
|
||||
private static bool HasShipModules(ShipDefinition definition, params string[] modules) =>
|
||||
modules.All(moduleId => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
private static bool CanTransportWorkers(ShipRuntime ship) =>
|
||||
CountModules(ship.Definition.Modules, "habitat-ring") > 0;
|
||||
|
||||
private static bool CanTransportWorkers(ShipRuntime ship) =>
|
||||
CountModules(ship.Definition.Modules, "habitat-ring") > 0;
|
||||
private static float GetWorkerTransportCapacity(ShipRuntime ship) =>
|
||||
CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
|
||||
|
||||
private static float GetWorkerTransportCapacity(ShipRuntime ship) =>
|
||||
CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
|
||||
private static int CountStationModules(StationRuntime station, string moduleId) =>
|
||||
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
|
||||
|
||||
private static void UpdateStationPower(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
private static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
|
||||
{
|
||||
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
|
||||
{
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
var previousEnergy = station.EnergyStored;
|
||||
GenerateStationEnergy(station, world, deltaSeconds);
|
||||
|
||||
if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "fuel") <= 0.01f)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private static void UpdateShipPower(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
station.Modules.Add(new StationModuleRuntime
|
||||
{
|
||||
var previousEnergy = ship.EnergyStored;
|
||||
GenerateShipEnergy(ship, world, deltaSeconds);
|
||||
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
|
||||
ModuleId = moduleId,
|
||||
Health = definition.Hull,
|
||||
MaxHealth = definition.Hull,
|
||||
});
|
||||
station.Radius = GetStationRadius(world, station);
|
||||
}
|
||||
|
||||
if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "fuel") <= 0.01f)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
||||
}
|
||||
private static float GetStationRadius(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var totalArea = station.Modules
|
||||
.Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
|
||||
.Sum();
|
||||
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
|
||||
}
|
||||
|
||||
private static float GetStationStorageCapacity(StationRuntime station, string storageClass)
|
||||
{
|
||||
var baseCapacity = storageClass switch
|
||||
{
|
||||
"manufactured" => 400f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
var bulkBays = CountStationModules(station, "bulk-bay");
|
||||
var liquidTanks = CountStationModules(station, "liquid-tank");
|
||||
var containerBays = CountStationModules(station, "container-bay");
|
||||
|
||||
var moduleCapacity = storageClass switch
|
||||
{
|
||||
"bulk-solid" => bulkBays * 1000f,
|
||||
"bulk-liquid" => liquidTanks * 500f,
|
||||
"container" => containerBays * 800f,
|
||||
"manufactured" => containerBays * 200f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
return baseCapacity + moduleCapacity;
|
||||
}
|
||||
|
||||
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
||||
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
|
||||
|
||||
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
||||
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
||||
|
||||
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
||||
{
|
||||
if (amount <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private static void GenerateStationEnergy(StationRuntime station, SimulationWorld world, float deltaSeconds)
|
||||
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
|
||||
}
|
||||
|
||||
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
||||
{
|
||||
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
|
||||
var removed = MathF.Min(current, amount);
|
||||
var remaining = current - removed;
|
||||
if (remaining <= 0.001f)
|
||||
{
|
||||
var powerCores = CountModules(station.InstalledModules, "power-core");
|
||||
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.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
|
||||
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
|
||||
return;
|
||||
}
|
||||
|
||||
var solarGenerated = MathF.Min(desiredEnergy, GetStationSolarGeneration(station, world) * deltaSeconds);
|
||||
if (solarGenerated > 0.01f)
|
||||
{
|
||||
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + solarGenerated);
|
||||
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
||||
}
|
||||
|
||||
if (desiredEnergy > 0.01f && fuelStored <= 0.01f)
|
||||
{
|
||||
var energyCells = GetInventoryAmount(station.Inventory, "energy-cell");
|
||||
if (energyCells > 0.01f)
|
||||
{
|
||||
var consumedCells = MathF.Min(energyCells, desiredEnergy / StationEnergyCellToEnergyRatio);
|
||||
RemoveInventory(station.Inventory, "energy-cell", consumedCells);
|
||||
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + (consumedCells * StationEnergyCellToEnergyRatio));
|
||||
desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
||||
{
|
||||
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
|
||||
station.Inventory["fuel"] = MathF.Min(fuelStored, tanks * StationFuelPerTank);
|
||||
return;
|
||||
}
|
||||
|
||||
var generated = MathF.Min(desiredEnergy, powerCores * 24f * deltaSeconds);
|
||||
var requiredFuel = generated / StationFuelToEnergyRatio;
|
||||
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
|
||||
var actualGenerated = consumedFuel * StationFuelToEnergyRatio;
|
||||
|
||||
RemoveInventory(station.Inventory, "fuel", consumedFuel);
|
||||
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + actualGenerated);
|
||||
inventory.Remove(itemId);
|
||||
}
|
||||
else
|
||||
{
|
||||
inventory[itemId] = remaining;
|
||||
}
|
||||
|
||||
private static float GetStationFuelCapacity(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "liquid-tank") * StationFuelPerTank;
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static float GetStationEnergyCapacity(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "power-core") * StationEnergyPerPowerCore;
|
||||
private static bool HasStationModules(StationRuntime station, params string[] modules) =>
|
||||
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
|
||||
|
||||
private static float GetStationSolarGeneration(StationRuntime station, SimulationWorld world) =>
|
||||
world.Balance.Energy.StationSolarCharge * (1f + CountModules(station.InstalledModules, "solar-array"));
|
||||
private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) =>
|
||||
node.ItemId switch
|
||||
{
|
||||
"ore" => HasShipModules(ship.Definition, "mining-turret"),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static float GetStationStorageCapacity(StationRuntime station, string storageClass)
|
||||
private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
|
||||
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
|
||||
|
||||
private static float ComputeWorkforceRatio(float population, float workforceRequired)
|
||||
{
|
||||
if (workforceRequired <= 0.01f)
|
||||
{
|
||||
var baseCapacity = station.Definition.Storage.TryGetValue(storageClass, out var capacity)
|
||||
? capacity
|
||||
: 0f;
|
||||
|
||||
var extraBulkBays = Math.Max(0, CountModules(station.InstalledModules, "bulk-bay") - CountModules(station.Definition.Modules, "bulk-bay"));
|
||||
var extraLiquidTanks = Math.Max(0, CountModules(station.InstalledModules, "liquid-tank") - CountModules(station.Definition.Modules, "liquid-tank"));
|
||||
var extraGasTanks = Math.Max(0, CountModules(station.InstalledModules, "gas-tank") - CountModules(station.Definition.Modules, "gas-tank"));
|
||||
var extraContainerBays = Math.Max(0, CountModules(station.InstalledModules, "container-bay") - CountModules(station.Definition.Modules, "container-bay"));
|
||||
var moduleBonus = storageClass switch
|
||||
{
|
||||
"bulk-solid" => extraBulkBays * 1000f,
|
||||
"bulk-liquid" => extraLiquidTanks * 500f,
|
||||
"bulk-gas" => extraGasTanks * 500f,
|
||||
"container" => extraContainerBays * 800f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
return baseCapacity + moduleBonus;
|
||||
return 1f;
|
||||
}
|
||||
|
||||
private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
var staffedRatio = MathF.Min(1f, population / workforceRequired);
|
||||
return 0.1f + (0.9f * staffedRatio);
|
||||
}
|
||||
|
||||
private static string? GetStorageRequirement(string storageClass) =>
|
||||
storageClass switch
|
||||
{
|
||||
"bulk-solid" => "bulk-bay",
|
||||
"bulk-liquid" => "liquid-tank",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
||||
{
|
||||
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
var reactors = CountModules(ship.Definition.Modules, "reactor-core");
|
||||
var capacitors = CountModules(ship.Definition.Modules, "capacitor-bank");
|
||||
if (reactors <= 0 || capacitors <= 0)
|
||||
{
|
||||
ship.EnergyStored = 0f;
|
||||
ship.Inventory.Remove("fuel");
|
||||
return;
|
||||
}
|
||||
|
||||
var energyCapacity = capacitors * CapacitorEnergyPerModule;
|
||||
var fuelCapacity = reactors * ShipFuelPerReactor;
|
||||
var fuelStored = GetInventoryAmount(ship.Inventory, "fuel");
|
||||
var desiredEnergy = MathF.Max(0f, energyCapacity - ship.EnergyStored);
|
||||
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
||||
{
|
||||
ship.EnergyStored = MathF.Min(ship.EnergyStored, energyCapacity);
|
||||
ship.Inventory["fuel"] = MathF.Min(fuelStored, fuelCapacity);
|
||||
return;
|
||||
}
|
||||
|
||||
var generated = MathF.Min(desiredEnergy, world.Balance.Energy.ShipRechargeRate * reactors * deltaSeconds);
|
||||
var requiredFuel = generated / ShipFuelToEnergyRatio;
|
||||
var consumedFuel = MathF.Min(requiredFuel, fuelStored);
|
||||
var actualGenerated = consumedFuel * ShipFuelToEnergyRatio;
|
||||
|
||||
RemoveInventory(ship.Inventory, "fuel", consumedFuel);
|
||||
ship.EnergyStored = MathF.Min(energyCapacity, ship.EnergyStored + actualGenerated);
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static bool TryConsumeShipEnergy(ShipRuntime ship, float amount)
|
||||
var storageClass = itemDefinition.CargoKind;
|
||||
var requiredModule = GetStorageRequirement(storageClass);
|
||||
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
|
||||
{
|
||||
if (ship.EnergyStored + 0.0001f < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount);
|
||||
return true;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static bool TryConsumeStationEnergy(StationRuntime station, float amount)
|
||||
var capacity = GetStationStorageCapacity(station, storageClass);
|
||||
if (capacity <= 0.01f)
|
||||
{
|
||||
if (station.EnergyStored + 0.0001f < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount);
|
||||
return true;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
||||
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
|
||||
|
||||
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
||||
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
||||
|
||||
private static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
||||
var used = station.Inventory
|
||||
.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)
|
||||
{
|
||||
if (amount <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
|
||||
{
|
||||
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
|
||||
var removed = MathF.Min(current, amount);
|
||||
var remaining = current - removed;
|
||||
if (remaining <= 0.001f)
|
||||
{
|
||||
inventory.Remove(itemId);
|
||||
}
|
||||
else
|
||||
{
|
||||
inventory[itemId] = remaining;
|
||||
}
|
||||
AddInventory(station.Inventory, itemId, accepted);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
return removed;
|
||||
private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
|
||||
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
|
||||
|
||||
private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
|
||||
world.ConstructionSites.FirstOrDefault(site =>
|
||||
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
|
||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
||||
|
||||
private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
|
||||
{
|
||||
if (site.StationId is not null
|
||||
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
|
||||
{
|
||||
return GetInventoryAmount(station.Inventory, itemId);
|
||||
}
|
||||
|
||||
private static bool HasStationModules(StationRuntime station, params string[] modules) =>
|
||||
modules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
|
||||
return GetInventoryAmount(site.DeliveredItems, itemId);
|
||||
}
|
||||
|
||||
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"),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
|
||||
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
|
||||
|
||||
private static float GetShipFuelCapacity(ShipRuntime ship) =>
|
||||
CountModules(ship.Definition.Modules, "reactor-core") * ShipFuelPerReactor;
|
||||
|
||||
private static float GetShipAvailableEnergyBudget(ShipRuntime ship) =>
|
||||
ship.EnergyStored + (GetInventoryAmount(ship.Inventory, "fuel") * ShipFuelToEnergyRatio);
|
||||
|
||||
private static float GetShipFuelReserve(ShipRuntime ship, float plannedFuel)
|
||||
{
|
||||
var capacity = GetShipFuelCapacity(ship);
|
||||
var reserveRatio = ship.Definition.CargoItemId == "gas" ? 0.4f : 0.3f;
|
||||
var reserve = MathF.Max(16f, MathF.Max(capacity * 0.18f, plannedFuel * reserveRatio));
|
||||
return MathF.Min(capacity, reserve);
|
||||
}
|
||||
|
||||
private static float EstimateFuelForEnergyDemand(ShipRuntime ship, float energyDemand) =>
|
||||
MathF.Max(0f, energyDemand - ship.EnergyStored) / ShipFuelToEnergyRatio;
|
||||
|
||||
private static float EstimateTimedEnergyUse(SimulationWorld world, float durationSeconds, float drainPerSecond) =>
|
||||
MathF.Max(0f, durationSeconds) * drainPerSecond;
|
||||
|
||||
private static float EstimateTravelEnergy(
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
Vector3 fromPosition,
|
||||
string fromSystemId,
|
||||
Vector3 toPosition,
|
||||
string toSystemId)
|
||||
{
|
||||
if (!string.Equals(fromSystemId, toSystemId, StringComparison.Ordinal))
|
||||
{
|
||||
var destinationEntryNode = ResolveSystemEntryNode(world, toSystemId);
|
||||
var destinationEntryPosition = destinationEntryNode?.Position ?? toPosition;
|
||||
var originSystemPosition = ResolveSystemGalaxyPosition(world, fromSystemId);
|
||||
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, toSystemId);
|
||||
var ftlDistance = originSystemPosition.DistanceTo(destinationSystemPosition);
|
||||
var ftlDuration = ftlDistance / MathF.Max(ship.Definition.FtlSpeed, 0.01f);
|
||||
return EstimateTimedEnergyUse(world, ship.Definition.SpoolTime, world.Balance.Energy.IdleDrain)
|
||||
+ EstimateTimedEnergyUse(world, ftlDuration, world.Balance.Energy.WarpDrain)
|
||||
+ EstimateInSystemTravelEnergy(ship, world, destinationEntryPosition, toPosition);
|
||||
}
|
||||
|
||||
return EstimateInSystemTravelEnergy(ship, world, fromPosition, toPosition);
|
||||
}
|
||||
|
||||
private static float EstimateInSystemTravelEnergy(ShipRuntime ship, SimulationWorld world, Vector3 fromPosition, Vector3 toPosition)
|
||||
{
|
||||
var distance = fromPosition.DistanceTo(toPosition);
|
||||
if (distance <= world.Balance.ArrivalThreshold)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
if (distance <= WarpEngageDistanceKilometers)
|
||||
{
|
||||
var localDuration = distance / MathF.Max(GetLocalTravelSpeed(ship), 0.01f);
|
||||
return EstimateTimedEnergyUse(world, localDuration, world.Balance.Energy.MoveDrain);
|
||||
}
|
||||
|
||||
var warpSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
var warpDuration = distance / MathF.Max(GetWarpTravelSpeed(ship), 0.01f);
|
||||
return EstimateTimedEnergyUse(world, warpSpoolDuration, world.Balance.Energy.IdleDrain)
|
||||
+ EstimateTimedEnergyUse(world, warpDuration, world.Balance.Energy.WarpDrain);
|
||||
}
|
||||
|
||||
private static float EstimateDockingEnergy(SimulationWorld world) =>
|
||||
EstimateTimedEnergyUse(world, world.Balance.DockingDuration, world.Balance.Energy.MoveDrain)
|
||||
+ EstimateTimedEnergyUse(world, 6f, world.Balance.Energy.IdleDrain);
|
||||
|
||||
private static float EstimateUndockingEnergy(SimulationWorld world) =>
|
||||
EstimateTimedEnergyUse(world, world.Balance.UndockingDuration, world.Balance.Energy.MoveDrain)
|
||||
+ EstimateTimedEnergyUse(world, 4f, world.Balance.Energy.IdleDrain);
|
||||
|
||||
private static float EstimateExtractionEnergy(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var remainingCargo = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
|
||||
if (remainingCargo <= 0.01f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var cycles = MathF.Ceiling(remainingCargo / MathF.Max(world.Balance.MiningRate, 0.01f));
|
||||
return EstimateTimedEnergyUse(world, cycles * world.Balance.MiningCycleSeconds, world.Balance.Energy.MoveDrain)
|
||||
+ EstimateTimedEnergyUse(world, cycles * 1.5f, world.Balance.Energy.IdleDrain);
|
||||
}
|
||||
|
||||
private static float EstimateConstructionEnergy(ShipRuntime ship, SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
var holdPosition = GetConstructionHoldPosition(station, ship.Id);
|
||||
var travelEnergy = EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, holdPosition, station.SystemId);
|
||||
var site = GetConstructionSiteForStation(world, station.Id);
|
||||
if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site))
|
||||
{
|
||||
if (world.ModuleRecipes.TryGetValue(site.BlueprintId ?? string.Empty, out var siteRecipe))
|
||||
{
|
||||
return travelEnergy + EstimateTimedEnergyUse(world, siteRecipe.Duration, world.Balance.Energy.IdleDrain);
|
||||
}
|
||||
|
||||
return travelEnergy;
|
||||
}
|
||||
|
||||
var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world);
|
||||
if (moduleId is not null
|
||||
&& world.ModuleRecipes.TryGetValue(moduleId, out var recipe)
|
||||
&& CanStartModuleConstruction(station, recipe))
|
||||
{
|
||||
return travelEnergy + EstimateTimedEnergyUse(world, recipe.Duration, world.Balance.Energy.IdleDrain);
|
||||
}
|
||||
|
||||
return travelEnergy;
|
||||
}
|
||||
|
||||
private static float EstimateResourceHarvestEnergy(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var cargoItemId = ship.Definition.CargoItemId;
|
||||
if (cargoItemId is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var requiredModule = cargoItemId == "gas" ? "gas-extractor" : "mining-turret";
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var refinery = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
|
||||
var node = behavior.NodeId is null
|
||||
? world.Nodes
|
||||
.Where(candidate =>
|
||||
(behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) &&
|
||||
candidate.ItemId == cargoItemId &&
|
||||
candidate.OreRemaining > 0.01f)
|
||||
.OrderByDescending(candidate => candidate.OreRemaining)
|
||||
.FirstOrDefault()
|
||||
: world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f);
|
||||
if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var currentPosition = ship.Position;
|
||||
var currentSystemId = ship.SystemId;
|
||||
var energy = 0f;
|
||||
var cargoAmount = GetShipCargoAmount(ship);
|
||||
if (ship.DockedStationId == refinery.Id)
|
||||
{
|
||||
currentPosition = GetUndockTargetPosition(refinery, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
||||
currentSystemId = refinery.SystemId;
|
||||
energy += EstimateUndockingEnergy(world);
|
||||
}
|
||||
|
||||
if (cargoAmount > 0.01f)
|
||||
{
|
||||
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId);
|
||||
return energy + EstimateDockingEnergy(world);
|
||||
}
|
||||
|
||||
var holdPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f);
|
||||
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, holdPosition, node.SystemId);
|
||||
energy += EstimateExtractionEnergy(ship, world);
|
||||
energy += EstimateTravelEnergy(ship, world, holdPosition, node.SystemId, refinery.Position, refinery.SystemId);
|
||||
energy += EstimateDockingEnergy(world);
|
||||
return energy;
|
||||
}
|
||||
|
||||
private static float EstimateResourceReturnEnergy(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var cargoItemId = ship.Definition.CargoItemId;
|
||||
if (cargoItemId is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var refinery = SelectBestBuyStation(world, ship, cargoItemId, ship.DefaultBehavior.StationId);
|
||||
if (refinery is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var currentPosition = ship.Position;
|
||||
var currentSystemId = ship.SystemId;
|
||||
return EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, refinery.Position, refinery.SystemId)
|
||||
+ EstimateDockingEnergy(world);
|
||||
}
|
||||
|
||||
private static float EstimateTransportEnergy(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var cargoItemId = ship.Definition.CargoItemId;
|
||||
if (cargoItemId is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var source = SelectBestSellStation(world, ship, cargoItemId, behavior.StationId);
|
||||
var destination = SelectBestBuyStation(world, ship, cargoItemId, behavior.StationId);
|
||||
if (source is null && destination is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var cargoAmount = GetShipCargoAmount(ship);
|
||||
var currentPosition = ship.Position;
|
||||
var currentSystemId = ship.SystemId;
|
||||
if (ship.DockedStationId is not null)
|
||||
{
|
||||
var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
||||
if (dockedStation is not null)
|
||||
{
|
||||
currentPosition = GetUndockTargetPosition(dockedStation, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
||||
currentSystemId = dockedStation.SystemId;
|
||||
}
|
||||
}
|
||||
|
||||
var targetStation = cargoAmount > 0.01f ? destination : source;
|
||||
if (targetStation is null)
|
||||
{
|
||||
return ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
|
||||
}
|
||||
|
||||
var energy = ship.DockedStationId is not null ? EstimateUndockingEnergy(world) : 0f;
|
||||
energy += EstimateTravelEnergy(ship, world, currentPosition, currentSystemId, targetStation.Position, targetStation.SystemId);
|
||||
return energy + EstimateDockingEnergy(world);
|
||||
}
|
||||
|
||||
private static float EstimateShipMissionEnergyDemand(ShipRuntime ship, SimulationWorld world) =>
|
||||
ship.DefaultBehavior.Kind switch
|
||||
{
|
||||
"auto-mine" or "auto-harvest-gas" => EstimateResourceHarvestEnergy(ship, world),
|
||||
"auto-supply-energy" => EstimateTransportEnergy(ship, world),
|
||||
"construct-station" when ship.DefaultBehavior.StationId is not null
|
||||
=> world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId) is { } station
|
||||
? EstimateConstructionEnergy(ship, world, station)
|
||||
: 0f,
|
||||
_ when ship.ControllerTask.TargetPosition is { } targetPosition && ship.ControllerTask.TargetSystemId is { } targetSystemId
|
||||
=> EstimateTravelEnergy(ship, world, ship.Position, ship.SystemId, targetPosition, targetSystemId),
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
private static float GetShipRefuelTarget(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var capacity = GetShipFuelCapacity(ship);
|
||||
var missionFuel = EstimateFuelForEnergyDemand(ship, EstimateShipMissionEnergyDemand(ship, world));
|
||||
var reserveFuel = GetShipFuelReserve(ship, missionFuel);
|
||||
return MathF.Min(capacity, missionFuel + reserveFuel);
|
||||
}
|
||||
|
||||
internal static bool NeedsRefuel(ShipRuntime ship, SimulationWorld world) =>
|
||||
GetInventoryAmount(ship.Inventory, "fuel") + 0.01f < GetShipRefuelTarget(ship, world);
|
||||
|
||||
internal static bool NeedsEmergencyReturn(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
if (ship.DefaultBehavior.Kind is not "auto-mine" and not "auto-harvest-gas")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var returnEnergy = EstimateResourceReturnEnergy(ship, world);
|
||||
var reserveFuel = GetShipFuelReserve(ship, EstimateFuelForEnergyDemand(ship, returnEnergy));
|
||||
var requiredBudget = returnEnergy + (reserveFuel * ShipFuelToEnergyRatio);
|
||||
return GetShipAvailableEnergyBudget(ship) + 0.01f < requiredBudget;
|
||||
}
|
||||
|
||||
private static float ComputeWorkforceRatio(float population, float workforceRequired)
|
||||
{
|
||||
if (workforceRequired <= 0.01f)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
var staffedRatio = MathF.Min(1f, population / workforceRequired);
|
||||
return 0.1f + (0.9f * staffedRatio);
|
||||
}
|
||||
|
||||
private static string? GetStorageRequirement(string storageClass) =>
|
||||
storageClass switch
|
||||
{
|
||||
"bulk-solid" => "bulk-bay",
|
||||
"bulk-liquid" => "liquid-tank",
|
||||
"bulk-gas" => "gas-tank",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
||||
{
|
||||
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var storageClass = itemDefinition.Storage;
|
||||
var requiredModule = GetStorageRequirement(storageClass);
|
||||
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var capacity = GetStationStorageCapacity(station, storageClass);
|
||||
if (capacity <= 0.01f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == storageClass)
|
||||
.Sum(entry => entry.Value);
|
||||
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
|
||||
if (accepted <= 0.01f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
AddInventory(station.Inventory, itemId, accepted);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
|
||||
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
|
||||
|
||||
private static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
|
||||
world.ConstructionSites.FirstOrDefault(site =>
|
||||
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
|
||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
||||
|
||||
private static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
|
||||
{
|
||||
if (site.StationId is not null
|
||||
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
|
||||
{
|
||||
return GetInventoryAmount(station.Inventory, itemId);
|
||||
}
|
||||
|
||||
return GetInventoryAmount(site.DeliveredItems, itemId);
|
||||
}
|
||||
|
||||
private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
|
||||
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
|
||||
private static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
|
||||
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,303 +5,299 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed partial class SimulationEngine
|
||||
{
|
||||
private static void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
||||
private static void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
foreach (var claim in world.Claims)
|
||||
{
|
||||
foreach (var claim in world.Claims)
|
||||
if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f)
|
||||
{
|
||||
if (claim.State != ClaimStateKinds.Destroyed)
|
||||
{
|
||||
if (claim.State == ClaimStateKinds.Destroyed || claim.Health <= 0f)
|
||||
{
|
||||
if (claim.State != ClaimStateKinds.Destroyed)
|
||||
{
|
||||
claim.State = ClaimStateKinds.Destroyed;
|
||||
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
|
||||
}
|
||||
|
||||
foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
|
||||
{
|
||||
claim.State = ClaimStateKinds.Active;
|
||||
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
|
||||
}
|
||||
claim.State = ClaimStateKinds.Destroyed;
|
||||
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-destroyed", $"Claim {claim.Id} was destroyed.", world.GeneratedAtUtc));
|
||||
}
|
||||
|
||||
foreach (var site in world.ConstructionSites.Where(candidate => candidate.ClaimId == claim.Id))
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claim.State == ClaimStateKinds.Activating && world.GeneratedAtUtc >= claim.ActivatesAtUtc)
|
||||
{
|
||||
claim.State = ClaimStateKinds.Active;
|
||||
events.Add(new SimulationEventRecord("claim", claim.Id, "claim-activated", $"Claim {claim.Id} is now active.", world.GeneratedAtUtc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
foreach (var site in world.ConstructionSites)
|
||||
{
|
||||
if (site.State == ConstructionSiteStateKinds.Destroyed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var claim = site.ClaimId is null
|
||||
? null
|
||||
: world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
|
||||
if (claim?.State == ClaimStateKinds.Destroyed)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Active;
|
||||
events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
|
||||
}
|
||||
|
||||
foreach (var orderId in site.MarketOrderIds)
|
||||
{
|
||||
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
||||
if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
|
||||
order.RemainingAmount = remaining;
|
||||
order.State = remaining <= 0.01f
|
||||
? MarketOrderStateKinds.Filled
|
||||
: remaining < order.Amount
|
||||
? MarketOrderStateKinds.PartiallyFilled
|
||||
: MarketOrderStateKinds.Open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId)
|
||||
{
|
||||
if (station.ActiveConstruction is not null)
|
||||
{
|
||||
return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void UpdateConstructionSites(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
||||
if (!CanStartModuleConstruction(station, recipe))
|
||||
{
|
||||
foreach (var site in world.ConstructionSites)
|
||||
{
|
||||
if (site.State == ConstructionSiteStateKinds.Destroyed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var claim = site.ClaimId is null
|
||||
? null
|
||||
: world.Claims.FirstOrDefault(candidate => candidate.Id == site.ClaimId);
|
||||
if (claim?.State == ClaimStateKinds.Destroyed)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claim?.State == ClaimStateKinds.Active && site.State == ConstructionSiteStateKinds.Planned)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Active;
|
||||
events.Add(new SimulationEventRecord("construction-site", site.Id, "site-active", $"Construction site {site.Id} is active.", world.GeneratedAtUtc));
|
||||
}
|
||||
|
||||
foreach (var orderId in site.MarketOrderIds)
|
||||
{
|
||||
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
||||
if (order is null || !site.RequiredItems.TryGetValue(order.ItemId, out var required))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var remaining = MathF.Max(0f, required - GetConstructionDeliveredAmount(world, site, order.ItemId));
|
||||
order.RemainingAmount = remaining;
|
||||
order.State = remaining <= 0.01f
|
||||
? MarketOrderStateKinds.Filled
|
||||
: remaining < order.Amount
|
||||
? MarketOrderStateKinds.PartiallyFilled
|
||||
: MarketOrderStateKinds.Open;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryEnsureModuleConstructionStarted(StationRuntime station, ModuleRecipeDefinition recipe, string shipId)
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
if (station.ActiveConstruction is not null)
|
||||
{
|
||||
return string.Equals(station.ActiveConstruction.ModuleId, recipe.ModuleId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.ActiveConstruction.AssignedConstructorShipId, shipId, StringComparison.Ordinal);
|
||||
}
|
||||
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
|
||||
}
|
||||
|
||||
if (!CanStartModuleConstruction(station, recipe))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
station.ActiveConstruction = new ModuleConstructionRuntime
|
||||
{
|
||||
ModuleId = recipe.ModuleId,
|
||||
RequiredSeconds = recipe.Duration,
|
||||
AssignedConstructorShipId = shipId,
|
||||
};
|
||||
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
station.ActiveConstruction = new ModuleConstructionRuntime
|
||||
private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
|
||||
{
|
||||
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
|
||||
? new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
ModuleId = recipe.ModuleId,
|
||||
RequiredSeconds = recipe.Duration,
|
||||
AssignedConstructorShipId = shipId,
|
||||
("refinery-stack", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("dock-bay-small", 2),
|
||||
("solar-array", 2),
|
||||
}
|
||||
: new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("refinery-stack", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("solar-array", 2),
|
||||
("dock-bay-small", 2),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
|
||||
foreach (var (moduleId, targetCount) in priorities)
|
||||
{
|
||||
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),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("dock-bay-small", 2),
|
||||
("solar-array", 2),
|
||||
}
|
||||
: new (string ModuleId, int TargetCount)[]
|
||||
{
|
||||
("gas-tank", 1),
|
||||
("fuel-processor", 1),
|
||||
("refinery-stack", 1),
|
||||
("container-bay", 1),
|
||||
("fabricator-array", 2),
|
||||
("component-factory", 1),
|
||||
("ship-factory", 1),
|
||||
("solar-array", 2),
|
||||
("dock-bay-small", 2),
|
||||
};
|
||||
|
||||
foreach (var (moduleId, targetCount) in priorities)
|
||||
{
|
||||
if (CountModules(station.InstalledModules, moduleId) < targetCount
|
||||
&& world.ModuleRecipes.ContainsKey(moduleId))
|
||||
{
|
||||
return moduleId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
if (CountModules(station.InstalledModules, moduleId) < targetCount
|
||||
&& world.ModuleRecipes.ContainsKey(moduleId))
|
||||
{
|
||||
return moduleId;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
||||
{
|
||||
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
||||
foreach (var orderId in site.MarketOrderIds)
|
||||
{
|
||||
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
||||
foreach (var orderId in site.MarketOrderIds)
|
||||
{
|
||||
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
||||
if (order is not null)
|
||||
{
|
||||
order.State = MarketOrderStateKinds.Cancelled;
|
||||
order.RemainingAmount = 0f;
|
||||
world.MarketOrders.Remove(order);
|
||||
}
|
||||
var order = world.MarketOrders.FirstOrDefault(candidate => candidate.Id == orderId);
|
||||
if (order is not null)
|
||||
{
|
||||
order.State = MarketOrderStateKinds.Cancelled;
|
||||
order.RemainingAmount = 0f;
|
||||
world.MarketOrders.Remove(order);
|
||||
}
|
||||
|
||||
station.MarketOrderIds.Remove(orderId);
|
||||
}
|
||||
|
||||
site.MarketOrderIds.Clear();
|
||||
site.Inventory.Clear();
|
||||
site.DeliveredItems.Clear();
|
||||
site.RequiredItems.Clear();
|
||||
site.AssignedConstructorShipIds.Clear();
|
||||
site.Progress = 0f;
|
||||
|
||||
if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Completed;
|
||||
site.BlueprintId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
site.BlueprintId = nextModuleId;
|
||||
site.State = ConstructionSiteStateKinds.Active;
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
site.RequiredItems[input.ItemId] = input.Amount;
|
||||
site.DeliveredItems[input.ItemId] = 0f;
|
||||
var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
|
||||
site.MarketOrderIds.Add(orderId);
|
||||
station.MarketOrderIds.Add(orderId);
|
||||
world.MarketOrders.Add(new MarketOrderRuntime
|
||||
{
|
||||
Id = orderId,
|
||||
FactionId = station.FactionId,
|
||||
StationId = station.Id,
|
||||
ConstructionSiteId = site.Id,
|
||||
Kind = MarketOrderKinds.Buy,
|
||||
ItemId = input.ItemId,
|
||||
Amount = input.Amount,
|
||||
RemainingAmount = input.Amount,
|
||||
Valuation = 1f,
|
||||
State = MarketOrderStateKinds.Open,
|
||||
});
|
||||
}
|
||||
station.MarketOrderIds.Remove(orderId);
|
||||
}
|
||||
|
||||
private static int GetDockingPadCount(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "dock-bay-small") * 2;
|
||||
site.MarketOrderIds.Clear();
|
||||
site.Inventory.Clear();
|
||||
site.DeliveredItems.Clear();
|
||||
site.RequiredItems.Clear();
|
||||
site.AssignedConstructorShipIds.Clear();
|
||||
site.Progress = 0f;
|
||||
|
||||
private static int? ReserveDockingPad(StationRuntime station, string shipId)
|
||||
if (nextModuleId is null || !world.ModuleRecipes.TryGetValue(nextModuleId, out var recipe))
|
||||
{
|
||||
if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
|
||||
&& !string.IsNullOrEmpty(existing.Value))
|
||||
{
|
||||
return existing.Key;
|
||||
}
|
||||
|
||||
var padCount = GetDockingPadCount(station);
|
||||
for (var padIndex = 0; padIndex < padCount; padIndex += 1)
|
||||
{
|
||||
if (station.DockingPadAssignments.ContainsKey(padIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
station.DockingPadAssignments[padIndex] = shipId;
|
||||
return padIndex;
|
||||
}
|
||||
|
||||
return null;
|
||||
site.State = ConstructionSiteStateKinds.Completed;
|
||||
site.BlueprintId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
private static void ReleaseDockingPad(StationRuntime station, string shipId)
|
||||
site.BlueprintId = nextModuleId;
|
||||
site.State = ConstructionSiteStateKinds.Active;
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
|
||||
if (!string.IsNullOrEmpty(assignment.Value))
|
||||
{
|
||||
station.DockingPadAssignments.Remove(assignment.Key);
|
||||
}
|
||||
site.RequiredItems[input.ItemId] = input.Amount;
|
||||
site.DeliveredItems[input.ItemId] = 0f;
|
||||
var orderId = $"market-order-{station.Id}-{nextModuleId}-{input.ItemId}";
|
||||
site.MarketOrderIds.Add(orderId);
|
||||
station.MarketOrderIds.Add(orderId);
|
||||
world.MarketOrders.Add(new MarketOrderRuntime
|
||||
{
|
||||
Id = orderId,
|
||||
FactionId = station.FactionId,
|
||||
StationId = station.Id,
|
||||
ConstructionSiteId = site.Id,
|
||||
Kind = MarketOrderKinds.Buy,
|
||||
ItemId = input.ItemId,
|
||||
Amount = input.Amount,
|
||||
RemainingAmount = input.Amount,
|
||||
Valuation = 1f,
|
||||
State = MarketOrderStateKinds.Open,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex)
|
||||
private static int GetDockingPadCount(StationRuntime station) =>
|
||||
CountModules(station.InstalledModules, "dock-bay-small") * 2;
|
||||
|
||||
private static int? ReserveDockingPad(StationRuntime station, string shipId)
|
||||
{
|
||||
if (station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal)) is var existing
|
||||
&& !string.IsNullOrEmpty(existing.Value))
|
||||
{
|
||||
var padCount = Math.Max(1, GetDockingPadCount(station));
|
||||
var angle = ((MathF.PI * 2f) / padCount) * padIndex;
|
||||
var radius = station.Definition.Radius + 18f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
return existing.Key;
|
||||
}
|
||||
|
||||
private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId)
|
||||
var padCount = GetDockingPadCount(station);
|
||||
for (var padIndex = 0; padIndex < padCount; padIndex += 1)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
var radius = station.Definition.Radius + 24f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
if (station.DockingPadAssignments.ContainsKey(padIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
station.DockingPadAssignments[padIndex] = shipId;
|
||||
return padIndex;
|
||||
}
|
||||
|
||||
private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance)
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ReleaseDockingPad(StationRuntime station, string shipId)
|
||||
{
|
||||
var assignment = station.DockingPadAssignments.FirstOrDefault(entry => string.Equals(entry.Value, shipId, StringComparison.Ordinal));
|
||||
if (!string.IsNullOrEmpty(assignment.Value))
|
||||
{
|
||||
if (padIndex is null)
|
||||
{
|
||||
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
||||
}
|
||||
|
||||
var pad = GetDockingPadPosition(station, padIndex.Value);
|
||||
var dx = pad.X - station.Position.X;
|
||||
var dz = pad.Z - station.Position.Z;
|
||||
var length = MathF.Sqrt((dx * dx) + (dz * dz));
|
||||
if (length <= 0.001f)
|
||||
{
|
||||
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
||||
}
|
||||
|
||||
var scale = distance / length;
|
||||
return new Vector3(
|
||||
pad.X + (dx * scale),
|
||||
station.Position.Y,
|
||||
pad.Z + (dz * scale));
|
||||
station.DockingPadAssignments.Remove(assignment.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) =>
|
||||
ship.AssignedDockingPadIndex is int padIndex
|
||||
? GetDockingPadPosition(station, padIndex)
|
||||
: station.Position;
|
||||
private static Vector3 GetDockingPadPosition(StationRuntime station, int padIndex)
|
||||
{
|
||||
var padCount = Math.Max(1, GetDockingPadCount(station));
|
||||
var angle = ((MathF.PI * 2f) / padCount) * padIndex;
|
||||
var radius = station.Radius + 18f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
}
|
||||
|
||||
private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId)
|
||||
private static Vector3 GetDockingHoldPosition(StationRuntime station, string shipId)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
var radius = station.Radius + 24f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
}
|
||||
|
||||
private static Vector3 GetUndockTargetPosition(StationRuntime station, int? padIndex, float distance)
|
||||
{
|
||||
if (padIndex is null)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
var radius = station.Definition.Radius + 78f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
||||
}
|
||||
|
||||
private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius)
|
||||
var pad = GetDockingPadPosition(station, padIndex.Value);
|
||||
var dx = pad.X - station.Position.X;
|
||||
var dz = pad.Z - station.Position.Z;
|
||||
var length = MathF.Sqrt((dx * dx) + (dz * dz));
|
||||
if (length <= 0.001f)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
return new Vector3(
|
||||
nodePosition.X + (MathF.Cos(angle) * radius),
|
||||
nodePosition.Y,
|
||||
nodePosition.Z + (MathF.Sin(angle) * radius));
|
||||
return new Vector3(station.Position.X + distance, station.Position.Y, station.Position.Z);
|
||||
}
|
||||
|
||||
var scale = distance / length;
|
||||
return new Vector3(
|
||||
pad.X + (dx * scale),
|
||||
station.Position.Y,
|
||||
pad.Z + (dz * scale));
|
||||
}
|
||||
|
||||
private static Vector3 GetShipDockedPosition(ShipRuntime ship, StationRuntime station) =>
|
||||
ship.AssignedDockingPadIndex is int padIndex
|
||||
? GetDockingPadPosition(station, padIndex)
|
||||
: station.Position;
|
||||
|
||||
private static Vector3 GetConstructionHoldPosition(StationRuntime station, string shipId)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
var radius = station.Radius + 78f;
|
||||
return new Vector3(
|
||||
station.Position.X + (MathF.Cos(angle) * radius),
|
||||
station.Position.Y,
|
||||
station.Position.Z + (MathF.Sin(angle) * radius));
|
||||
}
|
||||
|
||||
private static Vector3 GetResourceHoldPosition(Vector3 nodePosition, string shipId, float radius)
|
||||
{
|
||||
var hash = Math.Abs(shipId.GetHashCode(StringComparison.Ordinal));
|
||||
var angle = (hash % 360) * (MathF.PI / 180f);
|
||||
return new Vector3(
|
||||
nodePosition.X + (MathF.Cos(angle) * radius),
|
||||
nodePosition.Y,
|
||||
nodePosition.Z + (MathF.Sin(angle) * radius));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,536 +5,495 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed partial class SimulationEngine
|
||||
{
|
||||
private const int StrategicControlTargetSystems = 5;
|
||||
private const int StrategicControlTargetSystems = 5;
|
||||
|
||||
private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
UpdateStationPopulation(station, deltaSeconds, events);
|
||||
ReviewStationMarketOrders(world, station);
|
||||
RunStationProduction(world, station, deltaSeconds, events);
|
||||
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
|
||||
}
|
||||
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
|
||||
}
|
||||
UpdateStationPopulation(station, deltaSeconds, events);
|
||||
ReviewStationMarketOrders(world, station);
|
||||
RunStationProduction(world, station, deltaSeconds, events);
|
||||
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
|
||||
}
|
||||
|
||||
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f);
|
||||
faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
|
||||
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);
|
||||
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
|
||||
|
||||
if (waterSatisfied && hasPower)
|
||||
{
|
||||
if (habitatModules > 0 && station.Population < station.PopulationCapacity)
|
||||
{
|
||||
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
|
||||
}
|
||||
}
|
||||
else if (station.Population > 0f)
|
||||
{
|
||||
var previous = station.Population;
|
||||
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
|
||||
if (MathF.Floor(previous) > MathF.Floor(station.Population))
|
||||
{
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Definition.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
|
||||
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
|
||||
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
|
||||
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
|
||||
station.PopulationCapacity = 40f + (habitatModules * 220f);
|
||||
|
||||
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
|
||||
if (waterSatisfied)
|
||||
{
|
||||
if (habitatModules > 0 && station.Population < station.PopulationCapacity)
|
||||
{
|
||||
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
|
||||
}
|
||||
}
|
||||
else if (station.Population > 0f)
|
||||
{
|
||||
var previous = station.Population;
|
||||
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
|
||||
if (MathF.Floor(previous) > MathF.Floor(station.Population))
|
||||
{
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station)
|
||||
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
|
||||
}
|
||||
|
||||
private void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station)
|
||||
{
|
||||
if (station.CommanderId is null)
|
||||
{
|
||||
if (station.CommanderId is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
var desiredOrders = new List<DesiredMarketOrder>();
|
||||
var waterReserve = MathF.Max(30f, station.Population * 3f);
|
||||
var refinedReserve = HasStationModules(station, "fabricator-array") ? 140f : 40f;
|
||||
var oreReserve = HasRefineryCapability(station) ? 180f : 0f;
|
||||
var shipPartsReserve = HasStationModules(station, "fabricator-array")
|
||||
&& !HasStationModules(station, "component-factory", "ship-factory")
|
||||
&& FactionNeedsMoreWarships(world, station.FactionId)
|
||||
? 90f
|
||||
: 0f;
|
||||
|
||||
AddDemandOrder(desiredOrders, station, "water", waterReserve, valuationBase: 1.1f);
|
||||
AddDemandOrder(desiredOrders, station, "ore", oreReserve, valuationBase: 1.0f);
|
||||
AddDemandOrder(desiredOrders, station, "refined-metals", refinedReserve, valuationBase: 1.15f);
|
||||
AddDemandOrder(desiredOrders, station, "ship-parts", shipPartsReserve, valuationBase: 1.3f);
|
||||
|
||||
AddSupplyOrder(desiredOrders, station, "water", waterReserve * 1.5f, reserveFloor: waterReserve, valuationBase: 0.65f);
|
||||
AddSupplyOrder(desiredOrders, station, "ore", oreReserve * 1.4f, reserveFloor: oreReserve, valuationBase: 0.7f);
|
||||
AddSupplyOrder(desiredOrders, station, "refined-metals", refinedReserve * 1.4f, reserveFloor: refinedReserve, valuationBase: 0.95f);
|
||||
|
||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||
}
|
||||
|
||||
private void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
|
||||
foreach (var laneKey in GetStationProductionLanes(station))
|
||||
{
|
||||
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
|
||||
foreach (var laneKey in GetStationProductionLanes(station))
|
||||
var recipe = SelectProductionRecipe(world, station, laneKey);
|
||||
if (recipe is null)
|
||||
{
|
||||
station.ProductionLaneTimers[laneKey] = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
var throughput = GetStationProductionThroughput(station, recipe);
|
||||
|
||||
var produced = 0f;
|
||||
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
|
||||
while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe))
|
||||
{
|
||||
station.ProductionLaneTimers[laneKey] -= recipe.Duration;
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
var recipe = SelectProductionRecipe(world, station, laneKey);
|
||||
if (recipe is null || station.EnergyStored <= 0.01f)
|
||||
{
|
||||
station.ProductionLaneTimers[laneKey] = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
var throughput = GetStationProductionThroughput(station, recipe);
|
||||
if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds * throughput))
|
||||
{
|
||||
station.ProductionLaneTimers[laneKey] = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
var produced = 0f;
|
||||
station.ProductionLaneTimers[laneKey] = GetStationProductionTimer(station, laneKey) + (deltaSeconds * station.WorkforceEffectiveRatio * throughput);
|
||||
while (station.ProductionLaneTimers[laneKey] >= recipe.Duration && CanRunRecipe(world, station, recipe))
|
||||
{
|
||||
station.ProductionLaneTimers[laneKey] -= recipe.Duration;
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
|
||||
}
|
||||
|
||||
if (recipe.ShipOutputId is not null)
|
||||
{
|
||||
produced += CompleteShipRecipe(world, station, recipe, events);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
produced += TryAddStationInventory(world, station, output.ItemId, output.Amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (produced <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Definition.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow));
|
||||
if (faction is not null)
|
||||
{
|
||||
faction.GoodsProduced += produced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetStationProductionLanes(StationRuntime station)
|
||||
{
|
||||
if (CountModules(station.InstalledModules, "refinery-stack") > 0)
|
||||
{
|
||||
yield return "refinery";
|
||||
RemoveInventory(station.Inventory, input.ItemId, input.Amount);
|
||||
}
|
||||
|
||||
if (CountModules(station.InstalledModules, "fuel-processor") > 0)
|
||||
{
|
||||
yield return "fuel";
|
||||
}
|
||||
|
||||
if (CountModules(station.InstalledModules, "fabricator-array") > 0)
|
||||
{
|
||||
yield return "fabrication";
|
||||
}
|
||||
|
||||
if (CountModules(station.InstalledModules, "component-factory") > 0)
|
||||
{
|
||||
yield return "components";
|
||||
}
|
||||
|
||||
if (CountModules(station.InstalledModules, "ship-factory") > 0)
|
||||
{
|
||||
yield return "shipyard";
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetStationProductionTimer(StationRuntime station, string laneKey) =>
|
||||
station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f;
|
||||
|
||||
private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) =>
|
||||
world.Recipes.Values
|
||||
.Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal))
|
||||
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe))
|
||||
.FirstOrDefault(recipe => CanRunRecipe(world, station, recipe));
|
||||
|
||||
private static string? GetStationProductionLaneKey(RecipeDefinition recipe)
|
||||
{
|
||||
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
|
||||
{
|
||||
return "fuel";
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
|
||||
{
|
||||
return "refinery";
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
|
||||
{
|
||||
return "fabrication";
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return "components";
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return "shipyard";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var priority = (float)recipe.Priority;
|
||||
var producesFuel = recipe.Outputs.Any(output => string.Equals(output.ItemId, "fuel", StringComparison.Ordinal));
|
||||
if (producesFuel)
|
||||
{
|
||||
var fuelCapacity = MathF.Max(GetStationFuelCapacity(station), 1f);
|
||||
var fuelRatio = GetInventoryAmount(station.Inventory, "fuel") / fuelCapacity;
|
||||
priority += (1f - Math.Clamp(fuelRatio, 0f, 1f)) * 200f;
|
||||
}
|
||||
|
||||
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
|
||||
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
|
||||
priority += recipe.Id switch
|
||||
{
|
||||
"ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
|
||||
? -140f * MathF.Max(expansionPressure, fleetPressure)
|
||||
: 280f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"hull-fabrication" => 180f * expansionPressure,
|
||||
"equipment-assembly" => 170f * expansionPressure,
|
||||
"gun-assembly" => 160f * expansionPressure,
|
||||
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
|
||||
=> 220f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"ammo-fabrication" => -80f * expansionPressure,
|
||||
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
|
||||
=> -120f * expansionPressure,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var categoryMatch = string.Equals(station.Definition.Category, recipe.FacilityCategory, StringComparison.Ordinal)
|
||||
|| (string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
||||
&& station.Definition.Category is "station" or "shipyard" or "defense" or "gate");
|
||||
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
if (recipe.ShipOutputId is not null)
|
||||
{
|
||||
if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)
|
||||
|| !CanLaunchShipFromStation(station))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal)
|
||||
|| !FactionNeedsMoreWarships(world, station.FactionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
produced += CompleteShipRecipe(world, station, recipe, events);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount))
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
return false;
|
||||
produced += TryAddStationInventory(world, station, output.ItemId, output.Amount);
|
||||
}
|
||||
}
|
||||
|
||||
return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount));
|
||||
if (produced <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "production-complete", $"{station.Label} completed {recipe.Label}.", DateTimeOffset.UtcNow));
|
||||
if (faction is not null)
|
||||
{
|
||||
faction.GoodsProduced += produced;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetStationProductionLanes(StationRuntime station)
|
||||
{
|
||||
if (CountModules(station.InstalledModules, "refinery-stack") > 0)
|
||||
{
|
||||
yield return "refinery";
|
||||
}
|
||||
|
||||
private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
||||
if (CountModules(station.InstalledModules, "fabricator-array") > 0)
|
||||
{
|
||||
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredModule = GetStorageRequirement(itemDefinition.Storage);
|
||||
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var capacity = GetStationStorageCapacity(station, itemDefinition.Storage);
|
||||
if (capacity <= 0.01f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.Storage == itemDefinition.Storage)
|
||||
.Sum(entry => entry.Value);
|
||||
return used + amount <= capacity + 0.001f;
|
||||
yield return "fabrication";
|
||||
}
|
||||
|
||||
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
|
||||
if (CountModules(station.InstalledModules, "component-factory") > 0)
|
||||
{
|
||||
var current = GetInventoryAmount(station.Inventory, itemId);
|
||||
if (current >= targetAmount - 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deficit = targetAmount - current;
|
||||
var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount);
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null));
|
||||
yield return "components";
|
||||
}
|
||||
|
||||
private static void AddSupplyOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase)
|
||||
if (CountModules(station.InstalledModules, "ship-factory") > 0)
|
||||
{
|
||||
var current = GetInventoryAmount(station.Inventory, itemId);
|
||||
if (current <= triggerAmount + 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
yield return "shipyard";
|
||||
}
|
||||
}
|
||||
|
||||
var surplus = current - reserveFloor;
|
||||
if (surplus <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
private static float GetStationProductionTimer(StationRuntime station, string laneKey) =>
|
||||
station.ProductionLaneTimers.TryGetValue(laneKey, out var timer) ? timer : 0f;
|
||||
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor));
|
||||
private static RecipeDefinition? SelectProductionRecipe(SimulationWorld world, StationRuntime station, string laneKey) =>
|
||||
world.Recipes.Values
|
||||
.Where(recipe => RecipeAppliesToStation(station, recipe) && string.Equals(GetStationProductionLaneKey(recipe), laneKey, StringComparison.Ordinal))
|
||||
.OrderByDescending(recipe => GetStationRecipePriority(world, station, recipe))
|
||||
.FirstOrDefault(recipe => CanRunRecipe(world, station, recipe));
|
||||
|
||||
private static string? GetStationProductionLaneKey(RecipeDefinition recipe)
|
||||
{
|
||||
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
|
||||
{
|
||||
return "refinery";
|
||||
}
|
||||
|
||||
private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
|
||||
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
|
||||
{
|
||||
var existingOrders = world.MarketOrders
|
||||
.Where(order => order.StationId == station.Id && order.ConstructionSiteId is null)
|
||||
.ToList();
|
||||
|
||||
foreach (var desired in desiredOrders)
|
||||
{
|
||||
var order = existingOrders.FirstOrDefault(candidate =>
|
||||
candidate.Kind == desired.Kind &&
|
||||
candidate.ItemId == desired.ItemId &&
|
||||
candidate.ConstructionSiteId is null);
|
||||
|
||||
if (order is null)
|
||||
{
|
||||
order = new MarketOrderRuntime
|
||||
{
|
||||
Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}",
|
||||
FactionId = station.FactionId,
|
||||
StationId = station.Id,
|
||||
Kind = desired.Kind,
|
||||
ItemId = desired.ItemId,
|
||||
Amount = desired.Amount,
|
||||
RemainingAmount = desired.Amount,
|
||||
Valuation = desired.Valuation,
|
||||
ReserveThreshold = desired.ReserveThreshold,
|
||||
State = MarketOrderStateKinds.Open,
|
||||
};
|
||||
world.MarketOrders.Add(order);
|
||||
station.MarketOrderIds.Add(order.Id);
|
||||
existingOrders.Add(order);
|
||||
continue;
|
||||
}
|
||||
|
||||
order.RemainingAmount = desired.Amount;
|
||||
order.Valuation = desired.Valuation;
|
||||
order.ReserveThreshold = desired.ReserveThreshold;
|
||||
order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open;
|
||||
}
|
||||
|
||||
foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId)))
|
||||
{
|
||||
order.RemainingAmount = 0f;
|
||||
order.State = MarketOrderStateKinds.Cancelled;
|
||||
}
|
||||
return "fabrication";
|
||||
}
|
||||
|
||||
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.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
|
||||
{
|
||||
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var spawnPosition = new Vector3(station.Position.X + station.Definition.Radius + 32f, station.Position.Y, station.Position.Z);
|
||||
var ship = new ShipRuntime
|
||||
{
|
||||
Id = $"ship-{world.Ships.Count + 1}",
|
||||
SystemId = station.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = station.FactionId,
|
||||
Position = spawnPosition,
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
||||
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
||||
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
|
||||
Health = definition.MaxHealth,
|
||||
};
|
||||
|
||||
ship.Inventory["fuel"] = 120f;
|
||||
world.Ships.Add(ship);
|
||||
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
|
||||
{
|
||||
faction.ShipsBuilt += 1;
|
||||
}
|
||||
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Definition.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
|
||||
return 1f;
|
||||
return "components";
|
||||
}
|
||||
|
||||
private static bool CanLaunchShipFromStation(StationRuntime station) =>
|
||||
HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small");
|
||||
|
||||
private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId)
|
||||
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
|
||||
{
|
||||
var militaryShipCount = world.Ships.Count(ship =>
|
||||
ship.FactionId == factionId
|
||||
&& string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal));
|
||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||
var expansionDeficit = Math.Max(0, targetSystems - controlledSystems);
|
||||
var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3));
|
||||
return militaryShipCount < targetWarships;
|
||||
return "shipyard";
|
||||
}
|
||||
|
||||
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
||||
{
|
||||
return world.Claims
|
||||
.Where(claim => claim.State != ClaimStateKinds.Destroyed)
|
||||
.Select(claim => claim.SystemId)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count(systemId => FactionControlsSystem(world, factionId, systemId));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static float GetFactionExpansionPressure(SimulationWorld world, string factionId)
|
||||
{
|
||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||
var deficit = Math.Max(0, targetSystems - controlledSystems);
|
||||
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
|
||||
}
|
||||
private static float GetStationRecipePriority(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var priority = (float)recipe.Priority;
|
||||
|
||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
|
||||
var fleetPressure = FactionNeedsMoreWarships(world, station.FactionId) ? 1f : 0f;
|
||||
priority += recipe.Id switch
|
||||
{
|
||||
var buildableLocations = world.Claims
|
||||
.Where(claim =>
|
||||
claim.SystemId == systemId &&
|
||||
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active)
|
||||
.ToList();
|
||||
if (buildableLocations.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId);
|
||||
return ownedLocations > (buildableLocations.Count / 2f);
|
||||
}
|
||||
|
||||
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new()
|
||||
{
|
||||
CurrentSystemId = station.SystemId,
|
||||
SpaceLayer = SpaceLayerKinds.LocalSpace,
|
||||
CurrentNodeId = station.NodeId,
|
||||
CurrentBubbleId = station.BubbleId,
|
||||
LocalPosition = position,
|
||||
SystemPosition = position,
|
||||
MovementRegime = MovementRegimeKinds.LocalFlight,
|
||||
"ship-parts-integration" => HasStationModules(station, "component-factory", "ship-factory")
|
||||
? -140f * MathF.Max(expansionPressure, fleetPressure)
|
||||
: 280f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"hull-fabrication" => 180f * expansionPressure,
|
||||
"equipment-assembly" => 170f * expansionPressure,
|
||||
"gun-assembly" => 160f * expansionPressure,
|
||||
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
|
||||
=> 220f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
|
||||
"ammo-fabrication" => -80f * expansionPressure,
|
||||
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
|
||||
=> -120f * expansionPressure,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
|
||||
{
|
||||
if (!string.Equals(definition.Role, "military", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "idle",
|
||||
};
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
|
||||
var patrolRadius = station.Definition.Radius + 90f;
|
||||
return new DefaultBehaviorRuntime
|
||||
private static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
||||
|| string.Equals(recipe.FacilityCategory, "farm", StringComparison.Ordinal)
|
||||
|| string.Equals(recipe.FacilityCategory, station.Category, StringComparison.Ordinal);
|
||||
return categoryMatch && recipe.RequiredModules.All(moduleId => station.InstalledModules.Contains(moduleId, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
private static bool CanRunRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
if (recipe.ShipOutputId is not null)
|
||||
{
|
||||
if (!world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)
|
||||
|| !CanLaunchShipFromStation(station))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal)
|
||||
|| !FactionNeedsMoreWarships(world, station.FactionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recipe.Inputs.Any(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f < input.Amount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return recipe.Outputs.All(output => CanAcceptStationInventory(world, station, output.ItemId, output.Amount));
|
||||
}
|
||||
|
||||
private static bool CanAcceptStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
|
||||
{
|
||||
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
|
||||
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var capacity = GetStationStorageCapacity(station, itemDefinition.CargoKind);
|
||||
if (capacity <= 0.01f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var used = station.Inventory
|
||||
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == itemDefinition.CargoKind)
|
||||
.Sum(entry => entry.Value);
|
||||
return used + amount <= capacity + 0.001f;
|
||||
}
|
||||
|
||||
private static void AddDemandOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float targetAmount, float valuationBase)
|
||||
{
|
||||
var current = GetInventoryAmount(station.Inventory, itemId);
|
||||
if (current >= targetAmount - 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var deficit = targetAmount - current;
|
||||
var scarcity = targetAmount <= 0.01f ? 1f : MathF.Min(1f, deficit / targetAmount);
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Buy, itemId, deficit, valuationBase + scarcity, null));
|
||||
}
|
||||
|
||||
private static void AddSupplyOrder(ICollection<DesiredMarketOrder> desiredOrders, StationRuntime station, string itemId, float triggerAmount, float reserveFloor, float valuationBase)
|
||||
{
|
||||
var current = GetInventoryAmount(station.Inventory, itemId);
|
||||
if (current <= triggerAmount + 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var surplus = current - reserveFloor;
|
||||
if (surplus <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
desiredOrders.Add(new DesiredMarketOrder(MarketOrderKinds.Sell, itemId, surplus, valuationBase, reserveFloor));
|
||||
}
|
||||
|
||||
private static void ReconcileStationMarketOrders(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
|
||||
{
|
||||
var existingOrders = world.MarketOrders
|
||||
.Where(order => order.StationId == station.Id && order.ConstructionSiteId is null)
|
||||
.ToList();
|
||||
|
||||
foreach (var desired in desiredOrders)
|
||||
{
|
||||
var order = existingOrders.FirstOrDefault(candidate =>
|
||||
candidate.Kind == desired.Kind &&
|
||||
candidate.ItemId == desired.ItemId &&
|
||||
candidate.ConstructionSiteId is null);
|
||||
|
||||
if (order is null)
|
||||
{
|
||||
order = new MarketOrderRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
PatrolPoints =
|
||||
[
|
||||
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
|
||||
Id = $"market-order-{station.Id}-{desired.Kind}-{desired.ItemId}",
|
||||
FactionId = station.FactionId,
|
||||
StationId = station.Id,
|
||||
Kind = desired.Kind,
|
||||
ItemId = desired.ItemId,
|
||||
Amount = desired.Amount,
|
||||
RemainingAmount = desired.Amount,
|
||||
Valuation = desired.Valuation,
|
||||
ReserveThreshold = desired.ReserveThreshold,
|
||||
State = MarketOrderStateKinds.Open,
|
||||
};
|
||||
world.MarketOrders.Add(order);
|
||||
station.MarketOrderIds.Add(order.Id);
|
||||
existingOrders.Add(order);
|
||||
continue;
|
||||
}
|
||||
|
||||
order.RemainingAmount = desired.Amount;
|
||||
order.Valuation = desired.Valuation;
|
||||
order.ReserveThreshold = desired.ReserveThreshold;
|
||||
order.State = desired.Amount <= 0.01f ? MarketOrderStateKinds.Cancelled : MarketOrderStateKinds.Open;
|
||||
}
|
||||
|
||||
foreach (var order in existingOrders.Where(order => desiredOrders.All(desired => desired.Kind != order.Kind || desired.ItemId != order.ItemId)))
|
||||
{
|
||||
order.RemainingAmount = 0f;
|
||||
order.State = MarketOrderStateKinds.Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasRefineryCapability(StationRuntime station) =>
|
||||
HasStationModules(station, "refinery-stack", "power-core", "bulk-bay");
|
||||
|
||||
private float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z);
|
||||
var ship = new ShipRuntime
|
||||
{
|
||||
Id = $"ship-{world.Ships.Count + 1}",
|
||||
SystemId = station.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = station.FactionId,
|
||||
Position = spawnPosition,
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
||||
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
||||
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
|
||||
Health = definition.MaxHealth,
|
||||
};
|
||||
|
||||
world.Ships.Add(ship);
|
||||
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
|
||||
{
|
||||
faction.ShipsBuilt += 1;
|
||||
}
|
||||
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
|
||||
return 1f;
|
||||
}
|
||||
|
||||
private static bool CanLaunchShipFromStation(StationRuntime station) =>
|
||||
HasStationModules(station, "power-core", "ship-factory", "container-bay", "dock-bay-small");
|
||||
|
||||
private static bool FactionNeedsMoreWarships(SimulationWorld world, string factionId)
|
||||
{
|
||||
var militaryShipCount = world.Ships.Count(ship =>
|
||||
ship.FactionId == factionId
|
||||
&& string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal));
|
||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||
var expansionDeficit = Math.Max(0, targetSystems - controlledSystems);
|
||||
var targetWarships = Math.Max(2, (controlledSystems * 2) + (expansionDeficit * 3));
|
||||
return militaryShipCount < targetWarships;
|
||||
}
|
||||
|
||||
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
||||
{
|
||||
return world.Claims
|
||||
.Where(claim => claim.State != ClaimStateKinds.Destroyed)
|
||||
.Select(claim => claim.SystemId)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count(systemId => FactionControlsSystem(world, factionId, systemId));
|
||||
}
|
||||
|
||||
private static float GetFactionExpansionPressure(SimulationWorld world, string factionId)
|
||||
{
|
||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||
var deficit = Math.Max(0, targetSystems - controlledSystems);
|
||||
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
|
||||
}
|
||||
|
||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||
{
|
||||
var buildableLocations = world.Claims
|
||||
.Where(claim =>
|
||||
claim.SystemId == systemId &&
|
||||
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active)
|
||||
.ToList();
|
||||
if (buildableLocations.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId);
|
||||
return ownedLocations > (buildableLocations.Count / 2f);
|
||||
}
|
||||
|
||||
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new()
|
||||
{
|
||||
CurrentSystemId = station.SystemId,
|
||||
SpaceLayer = SpaceLayerKinds.LocalSpace,
|
||||
CurrentNodeId = station.NodeId,
|
||||
CurrentBubbleId = station.BubbleId,
|
||||
LocalPosition = position,
|
||||
SystemPosition = position,
|
||||
MovementRegime = MovementRegimeKinds.LocalFlight,
|
||||
};
|
||||
|
||||
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
|
||||
{
|
||||
if (!string.Equals(definition.Role, "military", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "idle",
|
||||
};
|
||||
}
|
||||
|
||||
var patrolRadius = station.Radius + 90f;
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
PatrolPoints =
|
||||
[
|
||||
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
|
||||
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius),
|
||||
new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z),
|
||||
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius),
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe)
|
||||
private static float GetStationProductionThroughput(StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
|
||||
{
|
||||
if (recipe.RequiredModules.Contains("refinery-stack", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("fuel-processor", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "fuel-processor"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "component-factory"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "ship-factory"));
|
||||
}
|
||||
|
||||
return 1f;
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "refinery-stack"));
|
||||
}
|
||||
|
||||
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
|
||||
if (recipe.RequiredModules.Contains("fabricator-array", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "fabricator-array"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("component-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "component-factory"));
|
||||
}
|
||||
|
||||
if (recipe.RequiredModules.Contains("ship-factory", StringComparer.Ordinal))
|
||||
{
|
||||
return Math.Max(1, CountModules(station.InstalledModules, "ship-factory"));
|
||||
}
|
||||
|
||||
return 1f;
|
||||
}
|
||||
|
||||
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
|
||||
}
|
||||
|
||||
@@ -4,106 +4,98 @@ 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;
|
||||
private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
|
||||
private static readonly IReadOnlyList<WorldUpdateStep> _worldUpdatePipeline =
|
||||
[
|
||||
new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)),
|
||||
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
||||
private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
|
||||
private const float PopulationGrowthPerSecond = 0.012f;
|
||||
private const float PopulationAttritionPerSecond = 0.018f;
|
||||
private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
|
||||
private static readonly IReadOnlyList<WorldUpdateStep> _worldUpdatePipeline =
|
||||
[
|
||||
new((engine, world, deltaSeconds, nowUtc, events) => engine.UpdateOrbitalState(world)),
|
||||
new((engine, world, deltaSeconds, nowUtc, events) => UpdateClaims(world, events)),
|
||||
new((engine, world, deltaSeconds, nowUtc, events) => 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)),
|
||||
private static readonly IReadOnlyList<ShipUpdateStep> _shipUpdatePipeline =
|
||||
[
|
||||
new((engine, ship, world, deltaSeconds, events) => engine.RefreshControlLayers(ship, world)),
|
||||
new((engine, ship, world, deltaSeconds, events) => engine.PlanControllerTask(ship, world)),
|
||||
];
|
||||
|
||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||
{
|
||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||
}
|
||||
|
||||
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
|
||||
{
|
||||
var events = new List<SimulationEventRecord>();
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||
|
||||
foreach (var step in _worldUpdatePipeline)
|
||||
{
|
||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||
step.Execute(this, world, deltaSeconds, nowUtc, events);
|
||||
}
|
||||
|
||||
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
var events = new List<SimulationEventRecord>();
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||
var previousPosition = ship.Position;
|
||||
var previousState = ship.State;
|
||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
||||
var previousTask = ship.ControllerTask.Kind;
|
||||
|
||||
foreach (var step in _worldUpdatePipeline)
|
||||
{
|
||||
step.Execute(this, world, deltaSeconds, nowUtc, events);
|
||||
}
|
||||
foreach (var step in _shipUpdatePipeline)
|
||||
{
|
||||
step.Execute(this, ship, world, deltaSeconds, events);
|
||||
}
|
||||
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
var previousPosition = ship.Position;
|
||||
var previousState = ship.State;
|
||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
||||
var previousTask = ship.ControllerTask.Kind;
|
||||
var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
|
||||
AdvanceControlState(ship, world, controllerEvent);
|
||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
|
||||
TrackHistory(ship, controllerEvent);
|
||||
|
||||
foreach (var step in _shipUpdatePipeline)
|
||||
{
|
||||
step.Execute(this, ship, world, deltaSeconds, events);
|
||||
}
|
||||
|
||||
var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
|
||||
AdvanceControlState(ship, world, controllerEvent);
|
||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
|
||||
TrackHistory(ship, controllerEvent);
|
||||
|
||||
EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
||||
}
|
||||
|
||||
SyncSpatialState(world);
|
||||
world.GeneratedAtUtc = nowUtc;
|
||||
|
||||
return new WorldDelta(
|
||||
sequence,
|
||||
world.TickIntervalMs,
|
||||
world.OrbitalTimeSeconds,
|
||||
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
|
||||
world.GeneratedAtUtc,
|
||||
false,
|
||||
events,
|
||||
BuildSpatialNodeDeltas(world),
|
||||
BuildLocalBubbleDeltas(world),
|
||||
BuildNodeDeltas(world),
|
||||
BuildStationDeltas(world),
|
||||
BuildClaimDeltas(world),
|
||||
BuildConstructionSiteDeltas(world),
|
||||
BuildMarketOrderDeltas(world),
|
||||
BuildPolicyDeltas(world),
|
||||
BuildShipDeltas(world),
|
||||
BuildFactionDeltas(world));
|
||||
EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
||||
}
|
||||
|
||||
private delegate void WorldUpdateStepAction(
|
||||
SimulationEngine engine,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
DateTimeOffset nowUtc,
|
||||
List<SimulationEventRecord> events);
|
||||
SyncSpatialState(world);
|
||||
world.GeneratedAtUtc = nowUtc;
|
||||
|
||||
private delegate void ShipUpdateStepAction(
|
||||
SimulationEngine engine,
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
List<SimulationEventRecord> events);
|
||||
return new WorldDelta(
|
||||
sequence,
|
||||
world.TickIntervalMs,
|
||||
world.OrbitalTimeSeconds,
|
||||
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
|
||||
world.GeneratedAtUtc,
|
||||
false,
|
||||
events,
|
||||
BuildSpatialNodeDeltas(world),
|
||||
BuildLocalBubbleDeltas(world),
|
||||
BuildNodeDeltas(world),
|
||||
BuildStationDeltas(world),
|
||||
BuildClaimDeltas(world),
|
||||
BuildConstructionSiteDeltas(world),
|
||||
BuildMarketOrderDeltas(world),
|
||||
BuildPolicyDeltas(world),
|
||||
BuildShipDeltas(world),
|
||||
BuildFactionDeltas(world));
|
||||
}
|
||||
|
||||
private sealed record WorldUpdateStep(WorldUpdateStepAction Execute);
|
||||
private delegate void WorldUpdateStepAction(
|
||||
SimulationEngine engine,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
DateTimeOffset nowUtc,
|
||||
List<SimulationEventRecord> events);
|
||||
|
||||
private sealed record ShipUpdateStep(ShipUpdateStepAction Execute);
|
||||
private delegate void ShipUpdateStepAction(
|
||||
SimulationEngine engine,
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
List<SimulationEventRecord> events);
|
||||
|
||||
private sealed record WorldUpdateStep(WorldUpdateStepAction Execute);
|
||||
|
||||
private sealed record ShipUpdateStep(ShipUpdateStepAction Execute);
|
||||
}
|
||||
|
||||
@@ -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,11 +34,12 @@ export interface StationSnapshot {
|
||||
populationCapacity: number;
|
||||
workforceRequired: number;
|
||||
workforceEffectiveRatio: number;
|
||||
storageUsage: StationStorageUsageSnapshot[];
|
||||
installedModules: string[];
|
||||
marketOrderIds: string[];
|
||||
}
|
||||
|
||||
export interface StationDelta extends StationSnapshot {}
|
||||
export interface StationDelta extends StationSnapshot { }
|
||||
|
||||
export interface ClaimSnapshot {
|
||||
id: string;
|
||||
@@ -50,7 +53,7 @@ export interface ClaimSnapshot {
|
||||
activatesAtUtc: string;
|
||||
}
|
||||
|
||||
export interface ClaimDelta extends ClaimSnapshot {}
|
||||
export interface ClaimDelta extends ClaimSnapshot { }
|
||||
|
||||
export interface ConstructionSiteSnapshot {
|
||||
id: string;
|
||||
@@ -72,4 +75,4 @@ export interface ConstructionSiteSnapshot {
|
||||
marketOrderIds: string[];
|
||||
}
|
||||
|
||||
export interface ConstructionSiteDelta extends ConstructionSiteSnapshot {}
|
||||
export interface ConstructionSiteDelta extends ConstructionSiteSnapshot { }
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface ShipSnapshot {
|
||||
cargoCapacity: number;
|
||||
cargoItemId?: string | null;
|
||||
workerPopulation: number;
|
||||
energyStored: number;
|
||||
travelSpeed: number;
|
||||
travelSpeedUnit: string;
|
||||
inventory: InventoryEntry[];
|
||||
@@ -32,7 +31,7 @@ export interface ShipSnapshot {
|
||||
spatialState: ShipSpatialStateSnapshot;
|
||||
}
|
||||
|
||||
export interface ShipDelta extends ShipSnapshot {}
|
||||
export interface ShipDelta extends ShipSnapshot { }
|
||||
|
||||
export interface ShipActionProgressSnapshot {
|
||||
label: string;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("\"", """)
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
247
shared/data/modules.json
Normal 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
@@ -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": [],
|
||||
|
||||
@@ -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"]
|
||||
},
|
||||
{
|
||||
"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"]
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user