feat: simplifying the simulation

This commit is contained in:
2026-03-17 16:08:02 -04:00
parent 3234b628ea
commit d5d0a39244
20 changed files with 374 additions and 496 deletions

View File

@@ -3,8 +3,8 @@ namespace SpaceGame.Simulation.Api.Contracts;
public sealed record ShipSnapshot(
string Id,
string Label,
string Role,
string ShipClass,
string Kind,
string Class,
string SystemId,
Vector3Dto LocalPosition,
Vector3Dto LocalVelocity,
@@ -19,8 +19,7 @@ public sealed record ShipSnapshot(
string? CommanderId,
string? PolicySetId,
float CargoCapacity,
string? CargoItemId,
float WorkerPopulation,
float TravelSpeed,
string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory,
@@ -33,8 +32,8 @@ public sealed record ShipSnapshot(
public sealed record ShipDelta(
string Id,
string Label,
string Role,
string ShipClass,
string Kind,
string Class,
string SystemId,
Vector3Dto LocalPosition,
Vector3Dto LocalVelocity,
@@ -49,8 +48,7 @@ public sealed record ShipDelta(
string? CommanderId,
string? PolicySetId,
float CargoCapacity,
string? CargoItemId,
float WorkerPopulation,
float TravelSpeed,
string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory,

View File

@@ -147,20 +147,19 @@ 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 required string Kind { get; set; }
public required string Class { get; set; }
public float Speed { get; set; }
public float WarpSpeed { get; set; }
public float FtlSpeed { get; set; }
public float SpoolTime { get; set; }
public float CargoCapacity { get; set; }
public 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 List<string> Capabilities { get; set; } = [];
public ConstructionDefinition? Construction { get; set; }
}

View File

@@ -18,7 +18,7 @@ internal sealed class ShipBehaviorStateMachine
{
idleState,
new PatrolShipBehaviorState(),
new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining-turret"),
new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining"),
new ConstructStationShipBehaviorState(),
};

View File

@@ -18,7 +18,7 @@ public sealed class ShipRuntime
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; }
@@ -62,4 +62,5 @@ public sealed class ControllerTaskRuntime
public string? TargetNodeId { get; set; }
public Vector3? TargetPosition { get; set; }
public float Threshold { get; set; }
public string? ItemId { get; set; }
}

View File

@@ -62,8 +62,7 @@ public enum ControllerTaskKind
Unload,
DeliverConstruction,
BuildConstructionSite,
LoadWorkers,
UnloadWorkers,
ConstructModule,
Undock,
}
@@ -105,8 +104,7 @@ public static class ShipTaskKinds
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";
@@ -229,8 +227,7 @@ public static class SimulationEnumMappings
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),

View File

@@ -379,7 +379,7 @@ public sealed partial class ScenarioLoader
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
StationRuntime? refinery)
{
if (string.Equals(definition.Role, "construction", StringComparison.Ordinal) && refinery is not null)
if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && refinery is not null)
{
return new DefaultBehaviorRuntime
{
@@ -389,12 +389,12 @@ public sealed partial class ScenarioLoader
};
}
if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null)
if (HasCapabilities(definition, "mining") && refinery is not null)
{
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, refinery.Id);
}
if (HasModules(definition, "reactor-core", "capacitor-bank", "gun-turret") && patrolRoutes.TryGetValue(systemId, out var route))
if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route))
{
return new DefaultBehaviorRuntime
{

View File

@@ -400,8 +400,8 @@ public sealed partial class ScenarioLoader
private static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
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 bool HasCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All((cap) => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
private static void AddStationModule(StationRuntime station, IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, string moduleId)
{

View File

@@ -27,8 +27,7 @@ public sealed partial class SimulationEngine
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),
@@ -58,6 +57,12 @@ public sealed partial class SimulationEngine
if (ship.SystemId != task.TargetSystemId)
{
if (!HasShipCapabilities(ship.Definition, "ftl"))
{
ship.State = ShipState.Idle;
return "none";
}
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
@@ -66,6 +71,11 @@ public sealed partial class SimulationEngine
var currentNode = ResolveCurrentNode(world, ship);
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
{
if (!HasShipCapabilities(ship.Definition, "warp"))
{
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
}
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
}

View File

@@ -5,14 +5,8 @@ namespace SpaceGame.Simulation.Api.Simulation;
public sealed partial class SimulationEngine
{
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 float GetWorkerTransportCapacity(ShipRuntime ship) =>
CountModules(ship.Definition.Modules, "habitat-ring") * 120f;
private static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
private static int CountStationModules(StationRuntime station, string moduleId) =>
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
@@ -56,8 +50,8 @@ public sealed partial class SimulationEngine
var moduleCapacity = storageClass switch
{
"bulk-solid" => bulkBays * 1000f,
"bulk-liquid" => liquidTanks * 500f,
"solid" => bulkBays * 1000f,
"liquid" => liquidTanks * 500f,
"container" => containerBays * 800f,
"manufactured" => containerBays * 200f,
_ => 0f,
@@ -102,15 +96,13 @@ public sealed partial class SimulationEngine
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 bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node) =>
node.ItemId switch
{
"ore" => HasShipModules(ship.Definition, "mining-turret"),
_ => false,
};
private static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) =>
HasShipCapabilities(ship.Definition, "mining")
&& world.ItemDefinitions.TryGetValue(node.ItemId, out var item)
&& string.Equals(item.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal);
private static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal);
string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal);
private static float ComputeWorkforceRatio(float population, float workforceRequired)
{
@@ -126,8 +118,8 @@ public sealed partial class SimulationEngine
private static string? GetStorageRequirement(string storageClass) =>
storageClass switch
{
"bulk-solid" => "bulk-bay",
"bulk-liquid" => "liquid-tank",
"solid" => "bulk-bay",
"liquid" => "liquid-tank",
_ => null,
};

View File

@@ -143,8 +143,8 @@ public sealed partial class SimulationEngine
world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot(
ship.Id,
ship.Label,
ship.Role,
ship.ShipClass,
ship.Kind,
ship.Class,
ship.SystemId,
ship.LocalPosition,
ship.LocalVelocity,
@@ -159,8 +159,6 @@ public sealed partial class SimulationEngine
ship.CommanderId,
ship.PolicySetId,
ship.CargoCapacity,
ship.CargoItemId,
ship.WorkerPopulation,
ship.TravelSpeed,
ship.TravelSpeedUnit,
ship.Inventory,
@@ -482,7 +480,6 @@ public sealed partial class SimulationEngine
ship.DockedStationId ?? "none",
ship.CommanderId ?? "none",
ship.PolicySetId ?? "none",
ship.WorkerPopulation.ToString("0.###"),
ship.SpatialState.SpaceLayer,
ship.SpatialState.CurrentNodeId ?? "none",
ship.SpatialState.CurrentBubbleId ?? "none",
@@ -586,7 +583,7 @@ public sealed partial class SimulationEngine
private static IReadOnlyList<StationStorageUsageSnapshot> ToStationStorageUsageSnapshots(SimulationWorld world, StationRuntime station)
{
string[] storageClasses = ["bulk-solid", "bulk-liquid", "container", "manufactured"];
string[] storageClasses = ["solid", "liquid", "container", "manufactured"];
return storageClasses
.Select(storageClass => new StationStorageUsageSnapshot(
storageClass,
@@ -666,8 +663,8 @@ public sealed partial class SimulationEngine
private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship) => new(
ship.Id,
ship.Definition.Label,
ship.Definition.Role,
ship.Definition.ShipClass,
ship.Definition.Kind,
ship.Definition.Class,
ship.SystemId,
ToDto(ship.Position),
ToDto(ship.Velocity),
@@ -682,8 +679,7 @@ public sealed partial class SimulationEngine
ship.CommanderId,
ship.PolicySetId,
ship.Definition.CargoCapacity,
ship.Definition.CargoItemId,
ship.WorkerPopulation,
ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit,
ToInventoryEntries(ship.Inventory),
@@ -705,14 +701,7 @@ public sealed partial class SimulationEngine
ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)),
ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)),
ShipState.Loading => CreateShipRemainingActionProgress(
"Load workers",
ship.TrackedActionTotal,
MathF.Max(0f, ship.TrackedActionTotal - ship.WorkerPopulation)),
ShipState.Unloading => CreateShipRemainingActionProgress(
"Unload workers",
ship.TrackedActionTotal,
ship.WorkerPopulation),
ShipState.Loading or ShipState.Unloading => null,
ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null
? null
: world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site

View File

@@ -106,10 +106,38 @@ public sealed partial class SimulationEngine
private static string? GetNextStationModuleToBuild(StationRuntime station, SimulationWorld world)
{
// Expand storage before it becomes a bottleneck
const float StorageExpansionThreshold = 0.85f;
var storageExpansionCandidates = new[]
{
("solid", "bulk-bay"),
("liquid", "liquid-tank"),
("container", "container-bay"),
};
foreach (var (storageClass, moduleId) in storageExpansionCandidates)
{
var capacity = GetStationStorageCapacity(station, storageClass);
if (capacity <= 0.01f)
{
continue;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var def) && def.CargoKind == storageClass)
.Sum(entry => entry.Value);
if (used / capacity >= StorageExpansionThreshold && world.ModuleRecipes.ContainsKey(moduleId))
{
return moduleId;
}
}
var priorities = GetFactionExpansionPressure(world, station.FactionId) > 0f
? new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("bulk-bay", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),
@@ -120,6 +148,7 @@ public sealed partial class SimulationEngine
: new (string ModuleId, int TargetCount)[]
{
("refinery-stack", 1),
("bulk-bay", 1),
("container-bay", 1),
("fabricator-array", 2),
("component-factory", 1),

View File

@@ -25,17 +25,14 @@ public sealed partial class SimulationEngine
ship.TrackedActionTotal = MathF.Max(total, 0.01f);
}
internal static float GetShipCargoAmount(ShipRuntime ship)
{
var cargoItemId = ship.Definition.CargoItemId;
return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId);
}
internal static float GetShipCargoAmount(ShipRuntime ship) =>
ship.Inventory.Values.Sum();
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
var task = ship.ControllerTask;
var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node))
if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node, world))
{
ship.State = ShipState.Idle;
ship.TargetPosition = ship.Position;
@@ -79,10 +76,7 @@ public sealed partial class SimulationEngine
return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full";
}
if (ship.Definition.CargoItemId is not null)
{
AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined);
}
AddInventory(ship.Inventory, node.ItemId, mined);
node.OreRemaining -= mined;
node.OreRemaining = MathF.Max(0f, node.OreRemaining);
@@ -167,23 +161,21 @@ public sealed partial class SimulationEngine
ship.ActionTimer = 0f;
ship.State = ShipState.Transferring;
BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship));
var cargoItemId = ship.Definition.CargoItemId;
var moved = cargoItemId is null ? 0f : MathF.Min(GetInventoryAmount(ship.Inventory, cargoItemId), world.Balance.TransferRate * deltaSeconds);
if (cargoItemId is not null)
{
var accepted = TryAddStationInventory(world, station, cargoItemId, moved);
RemoveInventory(ship.Inventory, cargoItemId, accepted);
moved = accepted;
}
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId);
if (faction is not null && cargoItemId == "ore")
foreach (var (itemId, amount) in ship.Inventory.ToList())
{
faction.OreMined += moved;
faction.Credits += moved * 0.4f;
var moved = MathF.Min(amount, world.Balance.TransferRate * deltaSeconds);
var accepted = TryAddStationInventory(world, station, itemId, moved);
RemoveInventory(ship.Inventory, itemId, accepted);
if (faction is not null && string.Equals(itemId, "ore", StringComparison.Ordinal))
{
faction.OreMined += accepted;
faction.Credits += accepted * 0.4f;
}
}
return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none";
return GetShipCargoAmount(ship) <= 0.01f ? "unloaded" : "none";
}
private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
@@ -209,19 +201,19 @@ public sealed partial class SimulationEngine
ship.Position = ship.TargetPosition;
ship.ActionTimer = 0f;
ship.State = ShipState.Loading;
var itemId = ship.ControllerTask.ItemId;
BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)));
var cargoItemId = ship.Definition.CargoItemId;
var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
var moved = cargoItemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, cargoItemId));
if (cargoItemId is not null && moved > 0.01f)
var moved = itemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, itemId));
if (itemId is not null && moved > 0.01f)
{
RemoveInventory(station.Inventory, cargoItemId, moved);
AddInventory(ship.Inventory, cargoItemId, moved);
RemoveInventory(station.Inventory, itemId, moved);
AddInventory(ship.Inventory, itemId, moved);
}
return cargoItemId is null
return itemId is null
|| GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|| GetInventoryAmount(station.Inventory, cargoItemId) <= 0.01f
|| GetInventoryAmount(station.Inventory, itemId) <= 0.01f
? "loaded"
: "none";
}
@@ -411,65 +403,6 @@ public sealed partial class SimulationEngine
private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) =>
ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f);
private string UpdateLoadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null || !CanTransportWorkers(ship))
{
ship.State = ShipState.Blocked;
return "failed";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null || station.Population <= 0.01f)
{
ship.State = ShipState.Idle;
return "none";
}
var transfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation);
var totalTransfer = MathF.Min(station.Population, GetWorkerTransportCapacity(ship) - ship.WorkerPopulation);
transfer = MathF.Min(transfer, 4f * deltaSeconds);
if (transfer <= 0.01f)
{
return "none";
}
station.Population = MathF.Max(0f, station.Population - transfer);
ship.WorkerPopulation += transfer;
ship.State = ShipState.Loading;
BeginTrackedAction(ship, "loading", totalTransfer);
return ship.WorkerPopulation >= GetWorkerTransportCapacity(ship) - 0.01f ? "workers-loaded" : "none";
}
private string UpdateUnloadWorkers(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{
if (ship.DockedStationId is null || !CanTransportWorkers(ship))
{
ship.State = ShipState.Blocked;
return "failed";
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station is null || ship.WorkerPopulation <= 0.01f)
{
ship.State = ShipState.Idle;
return "none";
}
var transfer = MathF.Min(ship.WorkerPopulation, MathF.Max(0f, station.PopulationCapacity - station.Population));
var totalTransfer = transfer;
transfer = MathF.Min(transfer, 4f * deltaSeconds);
if (transfer <= 0.01f)
{
return "none";
}
ship.WorkerPopulation = MathF.Max(0f, ship.WorkerPopulation - transfer);
station.Population = MathF.Min(station.PopulationCapacity, station.Population + transfer);
ship.State = ShipState.Unloading;
BeginTrackedAction(ship, "unloading", totalTransfer);
return ship.WorkerPopulation <= 0.01f ? "workers-unloaded" : "none";
}
private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
{

View File

@@ -152,7 +152,7 @@ public sealed partial class SimulationEngine
.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))
if (refinery is null || node is null || !HasShipCapabilities(ship.Definition, requiredModule))
{
behavior.Kind = "idle";
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
@@ -505,8 +505,7 @@ public sealed partial class SimulationEngine
"unload" => ControllerTaskKind.Unload,
"deliver-construction" => ControllerTaskKind.DeliverConstruction,
"build-construction-site" => ControllerTaskKind.BuildConstructionSite,
"load-workers" => ControllerTaskKind.LoadWorkers,
"unload-workers" => ControllerTaskKind.UnloadWorkers,
"construct-module" => ControllerTaskKind.ConstructModule,
"undock" => ControllerTaskKind.Undock,
_ => ControllerTaskKind.Idle,

View File

@@ -235,7 +235,7 @@ public sealed partial class SimulationEngine
return false;
}
if (!string.Equals(shipDefinition.Role, "military", StringComparison.Ordinal)
if (!string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal)
|| !FactionNeedsMoreWarships(world, station.FactionId))
{
return false;
@@ -394,7 +394,7 @@ public sealed partial class SimulationEngine
{
var militaryShipCount = world.Ships.Count(ship =>
ship.FactionId == factionId
&& string.Equals(ship.Definition.Role, "military", StringComparison.Ordinal));
&& string.Equals(ship.Definition.Kind, "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);
@@ -448,7 +448,7 @@ public sealed partial class SimulationEngine
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
{
if (!string.Equals(definition.Role, "military", StringComparison.Ordinal))
if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{

View File

@@ -3,8 +3,8 @@ import type { InventoryEntry, Vector3Dto } from "./contractsCommon";
export interface ShipSnapshot {
id: string;
label: string;
role: string;
shipClass: string;
kind: string;
class: string;
systemId: string;
localPosition: Vector3Dto;
localVelocity: Vector3Dto;
@@ -19,8 +19,7 @@ export interface ShipSnapshot {
commanderId?: string | null;
policySetId?: string | null;
cargoCapacity: number;
cargoItemId?: string | null;
workerPopulation: number;
travelSpeed: number;
travelSpeedUnit: string;
inventory: InventoryEntry[];

View File

@@ -26,9 +26,7 @@ export function renderFactionStrip(
return ships
.map((ship) => {
const cargo = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0;
const cargo = ship.inventory.reduce((sum, e) => sum + e.amount, 0);
const shipLocation = describeShipLocation(world, ship);
const shipState = describeShipState(world, ship);
const shipAction = describeShipCurrentAction(ship);
@@ -42,7 +40,7 @@ export function renderFactionStrip(
<div class="ship-card-header">
<h3>${ship.label}</h3>
<div class="ship-card-meta">
<span class="ship-card-badge">${ship.shipClass}</span>
<span class="ship-card-badge">${ship.class}</span>
<button
type="button"
class="ship-card-history-button"

View File

@@ -196,10 +196,7 @@ export function updateDetailPanel(
return;
}
const parent = describeSelectionParent(selected);
const cargoUsed = ship.cargoItemId
? inventoryAmount(ship.inventory, ship.cargoItemId)
: 0;
const cargoLabel = ship.cargoItemId ?? "none";
const cargoUsed = ship.inventory.reduce((sum, e) => sum + e.amount, 0);
const shipState = describeShipState(world, ship);
const shipAction = describeShipCurrentAction(ship);
detailTitleEl.textContent = ship.label;
@@ -217,7 +214,7 @@ export function updateDetailPanel(
</div>
</div>
` : ""}
<p>Cargo ${cargoLabel} ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
<p>Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}</p>
<p>Inventory ${formatInventory(ship.inventory)}</p>
<p>Speed ${formatShipSpeed(ship)}</p>
<p>Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}<br>Press C to toggle follow</p>

View File

@@ -2,7 +2,7 @@ import * as THREE from "three";
import type { ShipSnapshot } from "./contracts";
export function shipSize(ship: ShipSnapshot) {
switch (ship.shipClass) {
switch (ship.class) {
case "capital":
return 18;
case "cruiser":
@@ -20,11 +20,11 @@ export function shipLength(ship: ShipSnapshot) {
return shipSize(ship) * 2.6;
}
export function shipColor(role: ShipSnapshot["role"]) {
if (role === "mining") {
export function shipColor(kind: ShipSnapshot["kind"]) {
if (kind === "mining") {
return "#ffcf6e";
}
if (role === "transport") {
if (kind === "transport") {
return "#9ff0aa";
}
return "#8bc0ff";
@@ -40,7 +40,7 @@ export function shipPresentationColor(ship: ShipSnapshot) {
if (ship.spatialState.movementRegime === "ftl-transit") {
return "#ff6ad5";
}
return shipColor(ship.role);
return shipColor(ship.kind);
}
export function spatialNodeColor(kind: string) {

View File

@@ -4,7 +4,7 @@
"name": "Raw Ore",
"description": "Unprocessed asteroid ore used as the main industrial feedstock.",
"type": "resource",
"cargoKind": "bulk-solid",
"cargoKind": "solid",
"volume": 1.2
},
{
@@ -12,12 +12,15 @@
"name": "Water",
"description": "Life-support and agricultural input.",
"type": "commodity",
"cargoKind": "bulk-liquid",
"cargoKind": "liquid",
"volume": 1.0,
"construction": {
"recipeId": "water-reclamation",
"facilityCategory": "farm",
"requiredModules": ["liquid-tank", "solar-array"],
"requiredModules": [
"liquid-tank",
"solar-array"
],
"requirements": [],
"cycleTime": 6,
"batchSize": 12,
@@ -36,9 +39,14 @@
"construction": {
"recipeId": "ore-refining",
"facilityCategory": "station",
"requiredModules": ["refinery-stack"],
"requiredModules": [
"refinery-stack"
],
"requirements": [
{ "itemId": "ore", "amount": 60 }
{
"itemId": "ore",
"amount": 60
}
],
"cycleTime": 8,
"batchSize": 60,
@@ -57,9 +65,14 @@
"construction": {
"recipeId": "hull-fabrication",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requiredModules": [
"fabricator-array"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 70 }
{
"itemId": "refined-metals",
"amount": 70
}
],
"cycleTime": 10,
"batchSize": 35,
@@ -78,9 +91,14 @@
"construction": {
"recipeId": "ammo-fabrication",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requiredModules": [
"fabricator-array"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 24 }
{
"itemId": "refined-metals",
"amount": 24
}
],
"cycleTime": 6,
"batchSize": 30,
@@ -89,27 +107,6 @@
"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",
@@ -120,10 +117,18 @@
"construction": {
"recipeId": "equipment-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requiredModules": [
"fabricator-array"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 28 },
{ "itemId": "water", "amount": 8 }
{
"itemId": "refined-metals",
"amount": 28
},
{
"itemId": "water",
"amount": 8
}
],
"cycleTime": 11,
"batchSize": 18,
@@ -142,11 +147,18 @@
"construction": {
"recipeId": "ship-parts-integration",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requiredModules": [
"fabricator-array"
],
"requirements": [
{ "itemId": "hull-sections", "amount": 24 },
{ "itemId": "naval-guns", "amount": 6 },
{ "itemId": "ship-equipment", "amount": 10 }
{
"itemId": "hull-sections",
"amount": 24
},
{
"itemId": "ship-equipment",
"amount": 10
}
],
"cycleTime": 14,
"batchSize": 20,
@@ -165,10 +177,18 @@
"construction": {
"recipeId": "drone-parts-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requiredModules": [
"fabricator-array"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 12 },
{ "itemId": "ship-equipment", "amount": 6 }
{
"itemId": "refined-metals",
"amount": 12
},
{
"itemId": "ship-equipment",
"amount": 6
}
],
"cycleTime": 7,
"batchSize": 16,
@@ -187,10 +207,19 @@
"construction": {
"recipeId": "command-bridge-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 20 },
{ "itemId": "ship-equipment", "amount": 10 }
{
"itemId": "refined-metals",
"amount": 20
},
{
"itemId": "ship-equipment",
"amount": 10
}
],
"cycleTime": 9,
"batchSize": 1,
@@ -209,10 +238,19 @@
"construction": {
"recipeId": "reactor-core-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 30 },
{ "itemId": "ship-equipment", "amount": 8 }
{
"itemId": "refined-metals",
"amount": 30
},
{
"itemId": "ship-equipment",
"amount": 8
}
],
"cycleTime": 10,
"batchSize": 1,
@@ -231,10 +269,19 @@
"construction": {
"recipeId": "capacitor-bank-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 4 }
{
"itemId": "refined-metals",
"amount": 18
},
{
"itemId": "ship-equipment",
"amount": 4
}
],
"cycleTime": 9,
"batchSize": 1,
@@ -253,10 +300,19 @@
"construction": {
"recipeId": "ion-drive-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 22 },
{ "itemId": "ship-equipment", "amount": 8 }
{
"itemId": "refined-metals",
"amount": 22
},
{
"itemId": "ship-equipment",
"amount": 8
}
],
"cycleTime": 10,
"batchSize": 1,
@@ -275,10 +331,19 @@
"construction": {
"recipeId": "ftl-core-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 34 },
{ "itemId": "ship-equipment", "amount": 14 }
{
"itemId": "refined-metals",
"amount": 34
},
{
"itemId": "ship-equipment",
"amount": 14
}
],
"cycleTime": 12,
"batchSize": 1,
@@ -297,10 +362,15 @@
"construction": {
"recipeId": "gun-turret-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "naval-guns", "amount": 8 },
{ "itemId": "refined-metals", "amount": 12 }
{
"itemId": "refined-metals",
"amount": 12
}
],
"cycleTime": 8,
"batchSize": 1,
@@ -319,11 +389,23 @@
"construction": {
"recipeId": "carrier-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "hull-sections", "amount": 18 },
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 10 }
{
"itemId": "hull-sections",
"amount": 18
},
{
"itemId": "refined-metals",
"amount": 18
},
{
"itemId": "ship-equipment",
"amount": 10
}
],
"cycleTime": 14,
"batchSize": 1,
@@ -342,11 +424,23 @@
"construction": {
"recipeId": "habitat-ring-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "hull-sections", "amount": 14 },
{ "itemId": "ship-equipment", "amount": 8 },
{ "itemId": "water", "amount": 10 }
{
"itemId": "hull-sections",
"amount": 14
},
{
"itemId": "ship-equipment",
"amount": 8
},
{
"itemId": "water",
"amount": 10
}
],
"cycleTime": 12,
"batchSize": 1,
@@ -365,10 +459,19 @@
"construction": {
"recipeId": "bulk-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 16 },
{ "itemId": "hull-sections", "amount": 10 }
{
"itemId": "refined-metals",
"amount": 16
},
{
"itemId": "hull-sections",
"amount": 10
}
],
"cycleTime": 8,
"batchSize": 1,
@@ -387,10 +490,19 @@
"construction": {
"recipeId": "container-bay-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 12 },
{ "itemId": "ship-equipment", "amount": 4 }
{
"itemId": "refined-metals",
"amount": 12
},
{
"itemId": "ship-equipment",
"amount": 4
}
],
"cycleTime": 8,
"batchSize": 1,
@@ -409,10 +521,19 @@
"construction": {
"recipeId": "liquid-tank-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 14 },
{ "itemId": "ship-equipment", "amount": 4 }
{
"itemId": "refined-metals",
"amount": 14
},
{
"itemId": "ship-equipment",
"amount": 4
}
],
"cycleTime": 8,
"batchSize": 1,
@@ -431,10 +552,19 @@
"construction": {
"recipeId": "mining-turret-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 18 },
{ "itemId": "ship-equipment", "amount": 6 }
{
"itemId": "refined-metals",
"amount": 18
},
{
"itemId": "ship-equipment",
"amount": 6
}
],
"cycleTime": 9,
"batchSize": 1,
@@ -453,10 +583,19 @@
"construction": {
"recipeId": "fabricator-array-module-assembly",
"facilityCategory": "station",
"requiredModules": ["component-factory", "container-bay"],
"requiredModules": [
"component-factory",
"container-bay"
],
"requirements": [
{ "itemId": "refined-metals", "amount": 24 },
{ "itemId": "ship-equipment", "amount": 10 }
{
"itemId": "refined-metals",
"amount": 24
},
{
"itemId": "ship-equipment",
"amount": 10
}
],
"cycleTime": 11,
"batchSize": 1,
@@ -464,167 +603,5 @@
"maxEfficiency": 1,
"priority": 20
}
},
{
"id": "trade-hub-kit",
"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",
"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",
"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",
"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",
"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",
"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",
"name": "Stargate Kit",
"description": "Deployable prefab package for a stargate structure.",
"type": "kit",
"cargoKind": "manufactured",
"volume": 10.0,
"construction": {
"recipeId": "stargate-assembly",
"facilityCategory": "station",
"requiredModules": ["fabricator-array"],
"requirements": [
{ "itemId": "ship-parts", "amount": 60 },
{ "itemId": "hull-sections", "amount": 44 },
{ "itemId": "ship-equipment", "amount": 26 },
{ "itemId": "naval-guns", "amount": 8 }
],
"cycleTime": 34,
"batchSize": 1,
"productsPerHour": 105.9,
"maxEfficiency": 1,
"priority": 36
}
}
]

View File

@@ -2,8 +2,8 @@
{
"id": "frigate",
"label": "Vanguard Frigate",
"role": "military",
"shipClass": "frigate",
"kind": "military",
"class": "frigate",
"speed": 120000,
"warpSpeed": 0.22,
"ftlSpeed": 0.75,
@@ -13,13 +13,9 @@
"hullColor": "#1f4f78",
"size": 4,
"maxHealth": 100,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret"
"capabilities": [
"warp",
"ftl"
],
"construction": {
"recipeId": "frigate-construction",
@@ -69,8 +65,8 @@
{
"id": "destroyer",
"label": "Bulwark Destroyer",
"role": "military",
"shipClass": "destroyer",
"kind": "military",
"class": "destroyer",
"speed": 95000,
"warpSpeed": 0.18,
"ftlSpeed": 0.68,
@@ -80,14 +76,9 @@
"hullColor": "#6a2e26",
"size": 7,
"maxHealth": 240,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret",
"gun-turret"
"capabilities": [
"warp",
"ftl"
],
"construction": {
"recipeId": "destroyer-construction",
@@ -137,8 +128,8 @@
{
"id": "cruiser",
"label": "Aegis Cruiser",
"role": "military",
"shipClass": "cruiser",
"kind": "military",
"class": "cruiser",
"speed": 85000,
"warpSpeed": 0.16,
"ftlSpeed": 0.62,
@@ -148,14 +139,9 @@
"hullColor": "#314562",
"size": 10,
"maxHealth": 340,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"gun-turret",
"gun-turret"
"capabilities": [
"warp",
"ftl"
],
"construction": {
"recipeId": "cruiser-construction",
@@ -205,8 +191,8 @@
{
"id": "carrier",
"label": "Citadel Carrier",
"role": "military",
"shipClass": "capital",
"kind": "military",
"class": "capital",
"speed": 60000,
"warpSpeed": 0.12,
"ftlSpeed": 0.5,
@@ -216,16 +202,9 @@
"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"
"capabilities": [
"warp",
"ftl"
],
"dockingCapacity": 6,
"dockingClasses": [
@@ -274,10 +253,6 @@
{
"itemId": "gun-turret-module",
"amount": 1
},
{
"itemId": "habitat-ring-module",
"amount": 1
}
],
"cycleTime": 60,
@@ -289,8 +264,8 @@
{
"id": "hauler",
"label": "Atlas Hauler",
"role": "transport",
"shipClass": "industrial",
"kind": "transport",
"class": "industrial",
"speed": 70000,
"warpSpeed": 0.14,
"ftlSpeed": 0.55,
@@ -301,13 +276,9 @@
"hullColor": "#365f2a",
"size": 8,
"maxHealth": 180,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"container-bay"
"capabilities": [
"warp",
"ftl"
],
"construction": {
"recipeId": "hauler-construction",
@@ -357,27 +328,21 @@
{
"id": "constructor",
"label": "Pioneer Constructor",
"role": "construction",
"shipClass": "industrial",
"kind": "construction",
"class": "industrial",
"speed": 65000,
"warpSpeed": 0.13,
"ftlSpeed": 0.48,
"spoolTime": 3.5,
"cargoCapacity": 160,
"cargoKind": "manufactured",
"cargoItemId": "drone-parts",
"color": "#9af0c1",
"hullColor": "#2d5d47",
"size": 9,
"maxHealth": 220,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"fabricator-array",
"container-bay"
"capabilities": [
"warp",
"ftl"
],
"construction": {
"recipeId": "constructor-construction",
@@ -431,27 +396,22 @@
{
"id": "miner",
"label": "Prospector Miner",
"role": "mining",
"shipClass": "industrial",
"kind": "mining",
"class": "industrial",
"speed": 75000,
"warpSpeed": 0.15,
"ftlSpeed": 0.5,
"spoolTime": 3.1,
"cargoCapacity": 120,
"cargoKind": "bulk-solid",
"cargoItemId": "ore",
"cargoKind": "solid",
"color": "#ffdd75",
"hullColor": "#68552b",
"size": 6,
"maxHealth": 150,
"modules": [
"command-bridge",
"reactor-core",
"capacitor-bank",
"ion-drive",
"ftl-core",
"mining-turret",
"bulk-bay"
"capabilities": [
"warp",
"ftl",
"mining"
],
"construction": {
"recipeId": "miner-construction",