Refactor modular startup and viewer ship debugging
This commit is contained in:
@@ -74,6 +74,10 @@ public sealed record ResourceNodeDelta(
|
||||
float MaxOre,
|
||||
string ItemId);
|
||||
|
||||
public sealed record InventoryEntry(
|
||||
string ItemId,
|
||||
float Amount);
|
||||
|
||||
public sealed record StationSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
@@ -82,8 +86,8 @@ public sealed record StationSnapshot(
|
||||
Vector3Dto LocalPosition,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
float OreStored,
|
||||
float RefinedStock,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId);
|
||||
|
||||
public sealed record StationDelta(
|
||||
@@ -94,8 +98,8 @@ public sealed record StationDelta(
|
||||
Vector3Dto LocalPosition,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
float OreStored,
|
||||
float RefinedStock,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId);
|
||||
|
||||
public sealed record ShipSnapshot(
|
||||
@@ -111,9 +115,9 @@ public sealed record ShipSnapshot(
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string ControllerTaskKind,
|
||||
float Cargo,
|
||||
float CargoCapacity,
|
||||
string? CargoItemId,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
float Health,
|
||||
IReadOnlyList<string> History);
|
||||
@@ -131,9 +135,9 @@ public sealed record ShipDelta(
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string ControllerTaskKind,
|
||||
float Cargo,
|
||||
float CargoCapacity,
|
||||
string? CargoItemId,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
float Health,
|
||||
IReadOnlyList<string> History);
|
||||
|
||||
@@ -60,6 +60,14 @@ public sealed class ResourceNodeDefinition
|
||||
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 PlanetDefinition
|
||||
{
|
||||
public required string Label { get; set; }
|
||||
@@ -95,6 +103,7 @@ public sealed class ShipDefinition
|
||||
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
|
||||
@@ -105,6 +114,8 @@ public sealed class ConstructibleDefinition
|
||||
public required string Color { get; set; }
|
||||
public float Radius { get; set; }
|
||||
public int DockingCapacity { get; set; }
|
||||
public Dictionary<string, float> Storage { get; set; } = new(StringComparer.Ordinal);
|
||||
public List<string> Modules { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ScenarioDefinition
|
||||
|
||||
@@ -13,6 +13,7 @@ public sealed class SimulationWorld
|
||||
public required List<ShipRuntime> Ships { get; init; }
|
||||
public required List<FactionRuntime> Factions { get; init; }
|
||||
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
||||
public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; }
|
||||
public int TickIntervalMs { get; init; } = 200;
|
||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||
}
|
||||
@@ -42,8 +43,8 @@ public sealed class StationRuntime
|
||||
public required ConstructibleDefinition Definition { get; init; }
|
||||
public required Vector3 Position { get; init; }
|
||||
public required string FactionId { get; init; }
|
||||
public float OreStored { get; set; }
|
||||
public float RefinedStock { get; set; }
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public float EnergyStored { get; set; }
|
||||
public float ProcessTimer { get; set; }
|
||||
public HashSet<string> DockedShipIds { get; } = [];
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
@@ -63,7 +64,8 @@ public sealed class ShipRuntime
|
||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||
public float ActionTimer { get; set; }
|
||||
public float Cargo { get; set; }
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public float EnergyStored { get; set; }
|
||||
public string? DockedStationId { get; set; }
|
||||
public float Health { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
|
||||
@@ -8,10 +8,10 @@ public sealed class ScenarioLoader
|
||||
private const string DefaultFactionId = "sol-dominion";
|
||||
private const int TargetSystemCount = 160;
|
||||
private const int WorldSeed = 1;
|
||||
private const float MinimumFactionCredits = 240f;
|
||||
private const float MinimumRefineryOre = 60f;
|
||||
private const float MinimumRefineryStock = 40f;
|
||||
private const float MinimumShipyardStock = 180f;
|
||||
private const float MinimumFactionCredits = 0f;
|
||||
private const float MinimumRefineryOre = 0f;
|
||||
private const float MinimumRefineryStock = 0f;
|
||||
private const float MinimumShipyardStock = 0f;
|
||||
private const float MinimumSystemSeparation = 3200f;
|
||||
private static readonly string[] GeneratedSystemNames =
|
||||
[
|
||||
@@ -87,10 +87,12 @@ public sealed class ScenarioLoader
|
||||
var scenario = Read<ScenarioDefinition>("scenario.json");
|
||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
||||
var items = Read<List<ItemDefinition>>("items.json");
|
||||
var balance = Read<BalanceDefinition>("balance.json");
|
||||
|
||||
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 systemRuntimes = systems
|
||||
.Select((definition) => new SystemRuntime
|
||||
{
|
||||
@@ -138,14 +140,18 @@ public sealed class ScenarioLoader
|
||||
Definition = definition,
|
||||
Position = ResolveStationPosition(system, plan, balance),
|
||||
FactionId = plan.FactionId ?? DefaultFactionId,
|
||||
OreStored = 0f,
|
||||
RefinedStock = 0f,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var station in stations)
|
||||
{
|
||||
station.Inventory["gas"] = 320f;
|
||||
}
|
||||
|
||||
var refinery = stations.FirstOrDefault((station) =>
|
||||
station.Definition.Category == "refining" && station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault((station) => station.Definition.Category == "refining");
|
||||
HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank") &&
|
||||
station.SystemId == scenario.MiningDefaults.RefinerySystemId)
|
||||
?? stations.FirstOrDefault((station) => HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank"));
|
||||
|
||||
var patrolRoutes = scenario.PatrolRoutes.ToDictionary(
|
||||
(route) => route.SystemId,
|
||||
@@ -177,6 +183,13 @@ public sealed class ScenarioLoader
|
||||
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold },
|
||||
Health = definition.MaxHealth,
|
||||
});
|
||||
|
||||
shipsRuntime[^1].Inventory["gas"] = definition.Id switch
|
||||
{
|
||||
"constructor" => 90f,
|
||||
"miner" => 90f,
|
||||
_ => 120f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +207,7 @@ public sealed class ScenarioLoader
|
||||
Ships = shipsRuntime,
|
||||
Factions = factions,
|
||||
ShipDefinitions = shipDefinitions,
|
||||
ItemDefinitions = itemDefinitions,
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
@@ -755,29 +769,32 @@ public sealed class ScenarioLoader
|
||||
.ToList();
|
||||
|
||||
var refineries = ownedStations
|
||||
.Where((station) => station.Definition.Category == "refining")
|
||||
.Where((station) => HasModules(station.Definition, "refinery-stack", "power-core", "liquid-tank", "gas-tank"))
|
||||
.ToList();
|
||||
|
||||
if (refineries.Count > 0)
|
||||
{
|
||||
foreach (var refinery in refineries)
|
||||
{
|
||||
refinery.RefinedStock = MathF.Max(refinery.RefinedStock, MinimumRefineryStock);
|
||||
refinery.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(refinery.Inventory, "refined-metals"), MinimumRefineryStock);
|
||||
}
|
||||
|
||||
if (refineries.All((station) => station.OreStored < MinimumRefineryOre))
|
||||
if (refineries.All((station) => GetInventoryAmount(station.Inventory, "ore") < MinimumRefineryOre))
|
||||
{
|
||||
refineries[0].OreStored = MinimumRefineryOre;
|
||||
refineries[0].Inventory["ore"] = MinimumRefineryOre;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var shipyard in ownedStations.Where((station) => station.Definition.Category == "shipyard"))
|
||||
{
|
||||
shipyard.RefinedStock = MathF.Max(shipyard.RefinedStock, MinimumShipyardStock);
|
||||
shipyard.Inventory["refined-metals"] = MathF.Max(GetInventoryAmount(shipyard.Inventory, "refined-metals"), MinimumShipyardStock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
||||
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
||||
|
||||
private static string ToFactionLabel(string factionId)
|
||||
{
|
||||
return string.Join(" ",
|
||||
@@ -801,7 +818,7 @@ public sealed class ScenarioLoader
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
StationRuntime? refinery)
|
||||
{
|
||||
if (definition.Role == "mining" && refinery is not null)
|
||||
if (HasModules(definition, "reactor-core", "capacitor-bank", "mining-turret") && refinery is not null)
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
@@ -812,7 +829,7 @@ public sealed class ScenarioLoader
|
||||
};
|
||||
}
|
||||
|
||||
if (definition.Role == "military" && patrolRoutes.TryGetValue(systemId, out var route))
|
||||
if (HasModules(definition, "reactor-core", "capacitor-bank", "gun-turret") && patrolRoutes.TryGetValue(systemId, out var route))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
@@ -863,5 +880,11 @@ public sealed class ScenarioLoader
|
||||
: raw;
|
||||
}
|
||||
|
||||
private static bool HasModules(ConstructibleDefinition definition, params string[] modules) =>
|
||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
private static bool HasModules(ShipDefinition definition, params string[] modules) =>
|
||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
using SpaceGame.Simulation.Api.Data;
|
||||
using SpaceGame.Simulation.Api.Contracts;
|
||||
|
||||
namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
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;
|
||||
|
||||
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
|
||||
{
|
||||
var events = new List<SimulationEventRecord>();
|
||||
|
||||
UpdateStationPower(world, deltaSeconds, events);
|
||||
UpdateStations(world, deltaSeconds, events);
|
||||
|
||||
foreach (var ship in world.Ships)
|
||||
@@ -17,6 +26,7 @@ public sealed class SimulationEngine
|
||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
||||
var previousTask = ship.ControllerTask.Kind;
|
||||
|
||||
UpdateShipPower(ship, world, deltaSeconds, events);
|
||||
RefreshControlLayers(ship);
|
||||
PlanControllerTask(ship, world);
|
||||
var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
|
||||
@@ -90,8 +100,8 @@ public sealed class SimulationEngine
|
||||
station.LocalPosition,
|
||||
station.Color,
|
||||
station.DockedShips,
|
||||
station.OreStored,
|
||||
station.RefinedStock,
|
||||
station.EnergyStored,
|
||||
station.Inventory,
|
||||
station.FactionId)).ToList(),
|
||||
world.Ships.Select(ToShipDelta).Select((ship) => new ShipSnapshot(
|
||||
ship.Id,
|
||||
@@ -106,9 +116,9 @@ public sealed class SimulationEngine
|
||||
ship.OrderKind,
|
||||
ship.DefaultBehaviorKind,
|
||||
ship.ControllerTaskKind,
|
||||
ship.Cargo,
|
||||
ship.CargoCapacity,
|
||||
ship.CargoItemId,
|
||||
ship.EnergyStored,
|
||||
ship.Inventory,
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History)).ToList(),
|
||||
@@ -222,7 +232,7 @@ public sealed class SimulationEngine
|
||||
$"{node.SystemId}|{node.OreRemaining:0.###}";
|
||||
|
||||
private static string BuildStationSignature(StationRuntime station) =>
|
||||
$"{station.SystemId}|{station.OreStored:0.###}|{station.RefinedStock:0.###}|{station.DockedShipIds.Count}";
|
||||
$"{station.SystemId}|{BuildInventorySignature(station.Inventory)}|{station.EnergyStored:0.###}|{station.DockedShipIds.Count}";
|
||||
|
||||
private static string BuildShipSignature(ShipRuntime ship) =>
|
||||
string.Join("|",
|
||||
@@ -240,9 +250,18 @@ public sealed class SimulationEngine
|
||||
ship.Order?.Kind ?? "none",
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.ControllerTask.Kind,
|
||||
ship.Cargo.ToString("0.###"),
|
||||
GetShipCargoAmount(ship).ToString("0.###"),
|
||||
GetInventoryAmount(ship.Inventory, "gas").ToString("0.###"),
|
||||
ship.EnergyStored.ToString("0.###"),
|
||||
ship.Health.ToString("0.###"));
|
||||
|
||||
private static string BuildInventorySignature(IReadOnlyDictionary<string, float> inventory) =>
|
||||
string.Join(",",
|
||||
inventory
|
||||
.Where((entry) => entry.Value > 0.001f)
|
||||
.OrderBy((entry) => entry.Key, StringComparer.Ordinal)
|
||||
.Select((entry) => $"{entry.Key}:{entry.Value:0.###}"));
|
||||
|
||||
private static string BuildFactionSignature(FactionRuntime faction) =>
|
||||
$"{faction.Credits:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}";
|
||||
|
||||
@@ -263,8 +282,8 @@ public sealed class SimulationEngine
|
||||
ToDto(station.Position),
|
||||
station.Definition.Color,
|
||||
station.DockedShipIds.Count,
|
||||
station.OreStored,
|
||||
station.RefinedStock,
|
||||
station.EnergyStored,
|
||||
ToInventoryEntries(station.Inventory),
|
||||
station.FactionId);
|
||||
|
||||
private static ShipDelta ToShipDelta(ShipRuntime ship) => new(
|
||||
@@ -280,13 +299,20 @@ public sealed class SimulationEngine
|
||||
ship.Order?.Kind,
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.ControllerTask.Kind,
|
||||
ship.Cargo,
|
||||
ship.Definition.CargoCapacity,
|
||||
ship.Definition.CargoItemId,
|
||||
ship.EnergyStored,
|
||||
ToInventoryEntries(ship.Inventory),
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History.ToList());
|
||||
|
||||
private static IReadOnlyList<InventoryEntry> ToInventoryEntries(IReadOnlyDictionary<string, float> inventory) =>
|
||||
inventory
|
||||
.Where((entry) => entry.Value > 0.001f)
|
||||
.OrderBy((entry) => entry.Key, StringComparer.Ordinal)
|
||||
.Select((entry) => new InventoryEntry(entry.Key, entry.Value))
|
||||
.ToList();
|
||||
|
||||
private static FactionDelta ToFactionDelta(FactionRuntime faction) => new(
|
||||
faction.Id,
|
||||
faction.Label,
|
||||
@@ -332,11 +358,17 @@ public sealed class SimulationEngine
|
||||
{
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
if (station.Definition.Category != "refining" || station.OreStored < 60f)
|
||||
if (!HasRefineryCapability(station.Definition) || GetInventoryAmount(station.Inventory, "ore") < 60f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryConsumeStationEnergy(station, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
station.ProcessTimer = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
station.ProcessTimer += deltaSeconds;
|
||||
if (station.ProcessTimer < 8f)
|
||||
{
|
||||
@@ -344,8 +376,8 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
station.ProcessTimer = 0f;
|
||||
station.OreStored -= 60f;
|
||||
station.RefinedStock += 60f;
|
||||
RemoveInventory(station.Inventory, "ore", 60f);
|
||||
AddInventory(station.Inventory, "refined-metals", 60f);
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "refined", $"{station.Definition.Label} refined 60 ore", DateTimeOffset.UtcNow));
|
||||
var faction = world.Factions.FirstOrDefault((candidate) => candidate.Id == station.FactionId);
|
||||
if (faction is not null)
|
||||
@@ -356,6 +388,171 @@ public sealed class SimulationEngine
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasRefineryCapability(ConstructibleDefinition definition) =>
|
||||
definition.Modules.Contains("refinery-stack", StringComparer.Ordinal)
|
||||
&& definition.Modules.Contains("power-core", StringComparer.Ordinal)
|
||||
&& definition.Modules.Contains("liquid-tank", StringComparer.Ordinal)
|
||||
&& definition.Modules.Contains("gas-tank", StringComparer.Ordinal)
|
||||
&& definition.Storage.ContainsKey("bulk-solid")
|
||||
&& definition.Storage.ContainsKey("manufactured");
|
||||
|
||||
private static bool HasShipModules(ShipDefinition definition, params string[] modules) =>
|
||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||
|
||||
private static void UpdateStationPower(
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
var previousEnergy = station.EnergyStored;
|
||||
GenerateStationEnergy(station, deltaSeconds);
|
||||
|
||||
if (previousEnergy > 0.01f && station.EnergyStored <= 0.01f && GetInventoryAmount(station.Inventory, "gas") <= 0.01f)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "power-lost", $"{station.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateShipPower(
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var previousEnergy = ship.EnergyStored;
|
||||
GenerateShipEnergy(ship, world, deltaSeconds);
|
||||
|
||||
if (previousEnergy > 0.01f && ship.EnergyStored <= 0.01f && GetInventoryAmount(ship.Inventory, "gas") <= 0.01f)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "power-lost", $"{ship.Definition.Label} ran out of fuel and power", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateStationEnergy(StationRuntime station, float deltaSeconds)
|
||||
{
|
||||
var powerCores = CountModules(station.Definition.Modules, "power-core");
|
||||
var tanks = CountModules(station.Definition.Modules, "gas-tank");
|
||||
if (powerCores <= 0 || tanks <= 0)
|
||||
{
|
||||
station.EnergyStored = 0f;
|
||||
station.Inventory.Remove("gas");
|
||||
return;
|
||||
}
|
||||
|
||||
var energyCapacity = powerCores * StationEnergyPerPowerCore;
|
||||
var fuelStored = GetInventoryAmount(station.Inventory, "gas");
|
||||
var desiredEnergy = MathF.Max(0f, energyCapacity - station.EnergyStored);
|
||||
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
||||
{
|
||||
station.EnergyStored = MathF.Min(station.EnergyStored, energyCapacity);
|
||||
station.Inventory["gas"] = 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, "gas", consumedFuel);
|
||||
station.EnergyStored = MathF.Min(energyCapacity, station.EnergyStored + actualGenerated);
|
||||
}
|
||||
|
||||
private static void GenerateShipEnergy(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
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("gas");
|
||||
return;
|
||||
}
|
||||
|
||||
var energyCapacity = capacitors * CapacitorEnergyPerModule;
|
||||
var fuelCapacity = reactors * ShipFuelPerReactor;
|
||||
var fuelStored = GetInventoryAmount(ship.Inventory, "gas");
|
||||
var desiredEnergy = MathF.Max(0f, energyCapacity - ship.EnergyStored);
|
||||
if (desiredEnergy <= 0.01f || fuelStored <= 0.01f)
|
||||
{
|
||||
ship.EnergyStored = MathF.Min(ship.EnergyStored, energyCapacity);
|
||||
ship.Inventory["gas"] = 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, "gas", consumedFuel);
|
||||
ship.EnergyStored = MathF.Min(energyCapacity, ship.EnergyStored + actualGenerated);
|
||||
}
|
||||
|
||||
private static bool TryConsumeShipEnergy(ShipRuntime ship, float amount)
|
||||
{
|
||||
if (ship.EnergyStored + 0.0001f < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ship.EnergyStored = MathF.Max(0f, ship.EnergyStored - amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryConsumeStationEnergy(StationRuntime station, float amount)
|
||||
{
|
||||
if (station.EnergyStored + 0.0001f < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
station.EnergyStored = MathF.Max(0f, station.EnergyStored - amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
inventory.Remove(itemId);
|
||||
}
|
||||
else
|
||||
{
|
||||
inventory[itemId] = remaining;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static float GetShipCargoAmount(ShipRuntime ship)
|
||||
{
|
||||
var cargoItemId = ship.Definition.CargoItemId;
|
||||
return cargoItemId is null ? 0f : GetInventoryAmount(ship.Inventory, cargoItemId);
|
||||
}
|
||||
|
||||
private void RefreshControlLayers(ShipRuntime ship)
|
||||
{
|
||||
if (ship.Order is not null && ship.Order.Status == "queued")
|
||||
@@ -495,6 +692,7 @@ public sealed class SimulationEngine
|
||||
switch (task.Kind)
|
||||
{
|
||||
case "idle":
|
||||
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
@@ -529,6 +727,7 @@ public sealed class SimulationEngine
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance <= task.Threshold)
|
||||
{
|
||||
TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds);
|
||||
ship.Position = task.TargetPosition.Value;
|
||||
ship.TargetPosition = ship.Position;
|
||||
ship.SystemId = task.TargetSystemId;
|
||||
@@ -537,15 +736,18 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
var speed = ship.Definition.Speed;
|
||||
var energyCost = world.Balance.Energy.MoveDrain * deltaSeconds;
|
||||
if (ship.SystemId != task.TargetSystemId)
|
||||
{
|
||||
ship.State = distance > 800f ? "ftl" : "spooling-ftl";
|
||||
speed = ship.Definition.FtlSpeed;
|
||||
energyCost = world.Balance.Energy.WarpDrain * deltaSeconds;
|
||||
}
|
||||
else if (distance > 200f)
|
||||
{
|
||||
ship.State = distance > 500f ? "warping" : "spooling-warp";
|
||||
speed = ship.Definition.Speed * 4.5f;
|
||||
energyCost = world.Balance.Energy.WarpDrain * deltaSeconds;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -553,12 +755,26 @@ public sealed class SimulationEngine
|
||||
speed = ship.Definition.Speed;
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, energyCost))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, speed * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
if (!HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", "mining-turret"))
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var task = ship.ControllerTask;
|
||||
var node = world.Nodes.FirstOrDefault((candidate) => candidate.Id == task.TargetEntityId);
|
||||
if (node is null || task.TargetPosition is null)
|
||||
@@ -572,11 +788,25 @@ public sealed class SimulationEngine
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance > task.Threshold)
|
||||
{
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = "mining-approach";
|
||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, ship.Definition.Speed * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = "mining";
|
||||
ship.ActionTimer += deltaSeconds;
|
||||
if (ship.ActionTimer < 1f)
|
||||
@@ -585,16 +815,20 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
ship.ActionTimer = 0f;
|
||||
var mined = MathF.Min(world.Balance.MiningRate, ship.Definition.CargoCapacity - ship.Cargo);
|
||||
var cargoAmount = GetShipCargoAmount(ship);
|
||||
var mined = MathF.Min(world.Balance.MiningRate, ship.Definition.CargoCapacity - cargoAmount);
|
||||
mined = MathF.Min(mined, node.OreRemaining);
|
||||
ship.Cargo += mined;
|
||||
if (ship.Definition.CargoItemId is not null)
|
||||
{
|
||||
AddInventory(ship.Inventory, ship.Definition.CargoItemId, mined);
|
||||
}
|
||||
node.OreRemaining -= mined;
|
||||
if (node.OreRemaining <= 0f)
|
||||
{
|
||||
node.OreRemaining = node.MaxOre;
|
||||
}
|
||||
|
||||
return ship.Cargo >= ship.Definition.CargoCapacity ? "cargo-full" : "none";
|
||||
return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none";
|
||||
}
|
||||
|
||||
private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
@@ -612,11 +846,32 @@ public sealed class SimulationEngine
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance > task.Threshold)
|
||||
{
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = "docking-approach";
|
||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, ship.Definition.Speed * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = "docking";
|
||||
ship.ActionTimer += deltaSeconds;
|
||||
if (ship.ActionTimer < world.Balance.DockingDuration)
|
||||
@@ -651,11 +906,23 @@ public sealed class SimulationEngine
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.IdleDrain * deltaSeconds)
|
||||
|| !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = station.Position;
|
||||
ship.State = "transferring";
|
||||
var moved = MathF.Min(ship.Cargo, world.Balance.TransferRate * deltaSeconds);
|
||||
ship.Cargo -= moved;
|
||||
station.OreStored += moved;
|
||||
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)
|
||||
{
|
||||
RemoveInventory(ship.Inventory, cargoItemId, moved);
|
||||
AddInventory(station.Inventory, cargoItemId, moved);
|
||||
}
|
||||
var faction = world.Factions.FirstOrDefault((candidate) => candidate.Id == ship.FactionId);
|
||||
if (faction is not null)
|
||||
{
|
||||
@@ -663,7 +930,7 @@ public sealed class SimulationEngine
|
||||
faction.Credits += moved * 0.4f;
|
||||
}
|
||||
|
||||
return ship.Cargo <= 0.01f ? "unloaded" : "none";
|
||||
return cargoItemId is null || GetInventoryAmount(ship.Inventory, cargoItemId) <= 0.01f ? "unloaded" : "none";
|
||||
}
|
||||
|
||||
private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
@@ -678,6 +945,20 @@ public sealed class SimulationEngine
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var station = world.Stations.FirstOrDefault((candidate) => candidate.Id == ship.DockedStationId);
|
||||
if (!TryConsumeShipEnergy(ship, world.Balance.Energy.MoveDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (station is not null && !TryConsumeStationEnergy(station, world.Balance.Energy.IdleDrain * deltaSeconds))
|
||||
{
|
||||
ship.State = "power-starved";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
station?.DockedShipIds.Remove(ship.Id);
|
||||
ship.DockedStationId = null;
|
||||
ship.State = "undocking";
|
||||
@@ -699,7 +980,7 @@ public sealed class SimulationEngine
|
||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
||||
{
|
||||
case ("travel-to-node", "arrived"):
|
||||
ship.DefaultBehavior.Phase = ship.Cargo >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
|
||||
ship.DefaultBehavior.Phase = GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
|
||||
break;
|
||||
case ("extract", "cargo-full"):
|
||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
||||
@@ -728,14 +1009,14 @@ public sealed class SimulationEngine
|
||||
|
||||
private static void TrackHistory(ShipRuntime ship)
|
||||
{
|
||||
var signature = $"{ship.State}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind}|{ship.Cargo:0.0}";
|
||||
var signature = $"{ship.State}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind}|{GetShipCargoAmount(ship):0.0}";
|
||||
if (signature == ship.LastSignature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.LastSignature = signature;
|
||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind} cargo={ship.Cargo:0.#}");
|
||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind} cargo={GetShipCargoAmount(ship):0.#}");
|
||||
if (ship.History.Count > 18)
|
||||
{
|
||||
ship.History.RemoveAt(0);
|
||||
|
||||
Reference in New Issue
Block a user