diff --git a/apps/backend/Definitions/WorldDefinitions.cs b/apps/backend/Definitions/WorldDefinitions.cs index 7c89f1b..fc2ccf9 100644 --- a/apps/backend/Definitions/WorldDefinitions.cs +++ b/apps/backend/Definitions/WorldDefinitions.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using SpaceGame.Api.Shared.Runtime; namespace SpaceGame.Api.Definitions; @@ -196,6 +197,8 @@ public sealed class ModuleDefinition public string Description { get; set; } = string.Empty; public required string Type { get; set; } [JsonIgnore] + public ModuleType ModuleType { get; set; } + [JsonIgnore] public string? Product { get; set; } public List Products { get; set; } = []; public string ProductionMode { get; set; } = "passive"; diff --git a/apps/backend/Shared/Runtime/SimulationKinds.cs b/apps/backend/Shared/Runtime/SimulationKinds.cs index d6d7084..7ce027b 100644 --- a/apps/backend/Shared/Runtime/SimulationKinds.cs +++ b/apps/backend/Shared/Runtime/SimulationKinds.cs @@ -94,12 +94,25 @@ public enum SpaceLayerKind LocalSpace, } -public static class MovementRegimeKinds +public enum MovementRegimeKind { - public const string LocalFlight = "local-flight"; - public const string Warp = "warp"; - public const string StargateTransit = "stargate-transit"; - public const string FtlTransit = "ftl-transit"; + LocalFlight, + Warp, + StargateTransit, + FtlTransit, +} + +public enum ModuleType +{ + BuildModule, + ConnectionModule, + DefenceModule, + DockArea, + Habitation, + Pier, + ProcessingModule, + Production, + Storage, } public static class CommanderKind @@ -182,6 +195,34 @@ public static class MarketOrderStateKinds public static class SimulationEnumMappings { + public static string ToDataValue(this ModuleType moduleType) => moduleType switch + { + ModuleType.BuildModule => "buildmodule", + ModuleType.ConnectionModule => "connectionmodule", + ModuleType.DefenceModule => "defencemodule", + ModuleType.DockArea => "dockarea", + ModuleType.Habitation => "habitation", + ModuleType.Pier => "pier", + ModuleType.ProcessingModule => "processingmodule", + ModuleType.Production => "production", + ModuleType.Storage => "storage", + _ => throw new ArgumentOutOfRangeException(nameof(moduleType), moduleType, null), + }; + + public static ModuleType ToModuleType(this string value) => value.Trim() switch + { + "buildmodule" => ModuleType.BuildModule, + "connectionmodule" => ModuleType.ConnectionModule, + "defencemodule" => ModuleType.DefenceModule, + "dockarea" => ModuleType.DockArea, + "habitation" => ModuleType.Habitation, + "pier" => ModuleType.Pier, + "processingmodule" => ModuleType.ProcessingModule, + "production" => ModuleType.Production, + "storage" => ModuleType.Storage, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "Unsupported module type."), + }; + public static string ToContractValue(this SpatialNodeKind kind) => kind switch { SpatialNodeKind.Star => "star", @@ -283,4 +324,13 @@ public static class SimulationEnumMappings SpaceLayerKind.LocalSpace => "local-space", _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), }; + + public static string ToContractValue(this MovementRegimeKind kind) => kind switch + { + MovementRegimeKind.LocalFlight => "local-flight", + MovementRegimeKind.Warp => "warp", + MovementRegimeKind.StargateTransit => "stargate-transit", + MovementRegimeKind.FtlTransit => "ftl-transit", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; } diff --git a/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs b/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs index eb3c45e..f3caf37 100644 --- a/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs +++ b/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs @@ -9,6 +9,11 @@ internal static class SimulationRuntimeSupport internal static int CountStationModules(StationRuntime station, string moduleId) => station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal)); + internal static int CountStationModules(SimulationWorld world, StationRuntime station, ModuleType moduleType) => + station.Modules.Count(module => + world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) + && definition.ModuleType == moduleType); + internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) { if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition)) @@ -61,6 +66,14 @@ internal static class SimulationRuntimeSupport internal static int CountModules(IEnumerable modules, string moduleId) => modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); + internal static int CountModules( + IEnumerable modules, + IReadOnlyDictionary moduleDefinitions, + ModuleType moduleType) => + modules.Count(moduleId => + moduleDefinitions.TryGetValue(moduleId, out var definition) + && definition.ModuleType == moduleType); + internal static float GetInventoryAmount(IReadOnlyDictionary inventory, string itemId) => inventory.TryGetValue(itemId, out var amount) ? amount : 0f; diff --git a/apps/backend/Ships/Simulation/ShipAiService.cs b/apps/backend/Ships/Simulation/ShipAiService.cs index 59348d2..7e29da7 100644 --- a/apps/backend/Ships/Simulation/ShipAiService.cs +++ b/apps/backend/Ships/Simulation/ShipAiService.cs @@ -1647,7 +1647,7 @@ internal sealed class ShipAiService { var distance = ship.Position.DistanceTo(targetPosition); ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.Transit = null; ship.SpatialState.DestinationNodeId = targetCelestial?.Id; subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); @@ -1678,11 +1678,11 @@ internal sealed class ShipAiService bool completeOnArrival) { var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id) + if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id) { transit = new ShipTransitRuntime { - Regime = MovementRegimeKinds.Warp, + Regime = MovementRegimeKind.Warp, OriginNodeId = ship.SpatialState.CurrentCelestialId, DestinationNodeId = targetCelestial.Id, StartedAtUtc = world.GeneratedAtUtc, @@ -1692,7 +1692,7 @@ internal sealed class ShipAiService } ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp; + ship.SpatialState.MovementRegime = MovementRegimeKind.Warp; ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.DestinationNodeId = targetCelestial.Id; @@ -1735,11 +1735,11 @@ internal sealed class ShipAiService { var destinationNodeId = targetCelestial?.Id; var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId) + if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId) { transit = new ShipTransitRuntime { - Regime = MovementRegimeKinds.FtlTransit, + Regime = MovementRegimeKind.FtlTransit, OriginNodeId = ship.SpatialState.CurrentCelestialId, DestinationNodeId = destinationNodeId, StartedAtUtc = world.GeneratedAtUtc, @@ -1749,7 +1749,7 @@ internal sealed class ShipAiService } ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit; + ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit; ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.DestinationNodeId = destinationNodeId; @@ -1780,7 +1780,7 @@ internal sealed class ShipAiService ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.Transit = null; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.State = ShipState.Arriving; @@ -1795,7 +1795,7 @@ internal sealed class ShipAiService ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.Transit = null; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.State = ShipState.Arriving; diff --git a/apps/backend/Simulation/Core/SimulationProjectionService.cs b/apps/backend/Simulation/Core/SimulationProjectionService.cs index 345587a..03e7648 100644 --- a/apps/backend/Simulation/Core/SimulationProjectionService.cs +++ b/apps/backend/Simulation/Core/SimulationProjectionService.cs @@ -580,9 +580,9 @@ internal sealed class SimulationProjectionService ship.PolicySetId ?? "none", ship.SpatialState.SpaceLayer.ToContractValue(), ship.SpatialState.CurrentCelestialId ?? "none", - ship.SpatialState.MovementRegime, + ship.SpatialState.MovementRegime.ToContractValue(), ship.SpatialState.DestinationNodeId ?? "none", - ship.SpatialState.Transit?.Regime ?? "none", + ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none", ship.SpatialState.Transit?.OriginNodeId ?? "none", ship.SpatialState.Transit?.DestinationNodeId ?? "none", ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", @@ -921,8 +921,8 @@ internal sealed class SimulationProjectionService { return ship.SpatialState.MovementRegime switch { - MovementRegimeKinds.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"), - MovementRegimeKinds.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"), + MovementRegimeKind.FtlTransit => (ship.State == ShipState.Ftl ? ship.Definition.FtlSpeed : 0f, "ly/s"), + MovementRegimeKind.Warp => (ship.State == ShipState.Warping ? ship.Definition.WarpSpeed : 0f, "AU/s"), _ => (MathF.Sqrt(MathF.Max(0f, ship.Velocity.LengthSquared())) * SimulationUnits.MetersPerKilometer, "m/s"), }; } @@ -1877,10 +1877,10 @@ internal sealed class SimulationProjectionService state.CurrentCelestialId, state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value), state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value), - state.MovementRegime, + state.MovementRegime.ToContractValue(), state.DestinationNodeId, state.Transit is null ? null : new ShipTransitSnapshot( - state.Transit.Regime, + state.Transit.Regime.ToContractValue(), state.Transit.OriginNodeId, state.Transit.DestinationNodeId, state.Transit.StartedAtUtc, diff --git a/apps/backend/Stations/Simulation/StationLifecycleService.cs b/apps/backend/Stations/Simulation/StationLifecycleService.cs index 4863b65..7bbb7f0 100644 --- a/apps/backend/Stations/Simulation/StationLifecycleService.cs +++ b/apps/backend/Stations/Simulation/StationLifecycleService.cs @@ -1,3 +1,4 @@ +using SpaceGame.Api.Shared.Runtime; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Stations.Simulation; @@ -19,7 +20,7 @@ internal sealed class StationLifecycleService var factionPopulation = new Dictionary(StringComparer.Ordinal); foreach (var station in world.Stations) { - UpdateStationPopulation(station, deltaSeconds, events); + UpdateStationPopulation(world, station, deltaSeconds, events); _stationSimulation.ReviewStationMarketOrders(world, station); _stationSimulation.RunStationProduction(world, station, deltaSeconds, events); factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population; @@ -31,14 +32,14 @@ internal sealed class StationLifecycleService } } - private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection events) + private void UpdateStationPopulation(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection events) { station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds; var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater); var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater; - var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01"); + var habitatModules = CountModules(station.InstalledModules, world.ModuleDefinitions, ModuleType.Habitation); station.PopulationCapacity = 40f + (habitatModules * 220f); if (waterSatisfied) @@ -101,7 +102,7 @@ internal sealed class StationLifecycleService CurrentCelestialId = station.CelestialId, LocalPosition = position, SystemPosition = position, - MovementRegime = MovementRegimeKinds.LocalFlight, + MovementRegime = MovementRegimeKind.LocalFlight, }; private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) diff --git a/apps/backend/Universe/Runtime/SpatialRuntimeModels.cs b/apps/backend/Universe/Runtime/SpatialRuntimeModels.cs index c61a870..96b4b08 100644 --- a/apps/backend/Universe/Runtime/SpatialRuntimeModels.cs +++ b/apps/backend/Universe/Runtime/SpatialRuntimeModels.cs @@ -55,14 +55,14 @@ public sealed class ShipSpatialStateRuntime public string? CurrentCelestialId { get; set; } public Vector3? LocalPosition { get; set; } public Vector3? SystemPosition { get; set; } - public string MovementRegime { get; set; } = MovementRegimeKinds.LocalFlight; + public MovementRegimeKind MovementRegime { get; set; } = MovementRegimeKind.LocalFlight; public string? DestinationNodeId { get; set; } public ShipTransitRuntime? Transit { get; set; } } public sealed class ShipTransitRuntime { - public required string Regime { get; init; } + public required MovementRegimeKind Regime { get; init; } public string? OriginNodeId { get; init; } public string? DestinationNodeId { get; init; } public DateTimeOffset? StartedAtUtc { get; set; } diff --git a/apps/backend/Universe/Scenario/DataCatalogLoader.cs b/apps/backend/Universe/Scenario/DataCatalogLoader.cs index 7d13beb..c93e67d 100644 --- a/apps/backend/Universe/Scenario/DataCatalogLoader.cs +++ b/apps/backend/Universe/Scenario/DataCatalogLoader.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using SpaceGame.Api.Shared.Runtime; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; @@ -271,6 +272,17 @@ internal sealed class DataCatalogLoader(string dataRoot) { foreach (var module in modules) { + try + { + module.ModuleType = module.Type.ToModuleType(); + } + catch (ArgumentOutOfRangeException exception) + { + throw new InvalidOperationException($"Module '{module.Id}' has unsupported type '{module.Type}'.", exception); + } + + module.Type = module.ModuleType.ToDataValue(); + if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product)) { module.Products = [module.Product]; @@ -278,7 +290,7 @@ internal sealed class DataCatalogLoader(string dataRoot) if (string.IsNullOrWhiteSpace(module.ProductionMode)) { - module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal) + module.ProductionMode = module.ModuleType == ModuleType.BuildModule ? "commanded" : "passive"; } diff --git a/apps/backend/Universe/Scenario/LoaderSupport.cs b/apps/backend/Universe/Scenario/LoaderSupport.cs index 6ed6c16..4050fac 100644 --- a/apps/backend/Universe/Scenario/LoaderSupport.cs +++ b/apps/backend/Universe/Scenario/LoaderSupport.cs @@ -1,4 +1,6 @@ +using SpaceGame.Api.Shared.Runtime; + namespace SpaceGame.Api.Universe.Scenario; internal static class LoaderSupport @@ -124,6 +126,14 @@ internal static class LoaderSupport internal static int CountModules(IEnumerable modules, string moduleId) => modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal)); + internal static int CountModules( + IEnumerable modules, + IReadOnlyDictionary moduleDefinitions, + ModuleType moduleType) => + modules.Count(moduleId => + moduleDefinitions.TryGetValue(moduleId, out var definition) + && definition.ModuleType == moduleType); + internal static float ComputeWorkforceRatio(float population, float workforceRequired) { if (workforceRequired <= 0.01f) diff --git a/apps/backend/Universe/Scenario/SpatialBuilder.cs b/apps/backend/Universe/Scenario/SpatialBuilder.cs index ab52939..47b44fd 100644 --- a/apps/backend/Universe/Scenario/SpatialBuilder.cs +++ b/apps/backend/Universe/Scenario/SpatialBuilder.cs @@ -300,7 +300,7 @@ internal sealed class SpatialBuilder CurrentCelestialId = nearestCelestial?.Id, LocalPosition = position, SystemPosition = position, - MovementRegime = MovementRegimeKinds.LocalFlight, + MovementRegime = MovementRegimeKind.LocalFlight, }; } } diff --git a/apps/backend/Universe/Scenario/WorldBuilder.cs b/apps/backend/Universe/Scenario/WorldBuilder.cs index 4091fdf..48dc6ad 100644 --- a/apps/backend/Universe/Scenario/WorldBuilder.cs +++ b/apps/backend/Universe/Scenario/WorldBuilder.cs @@ -38,7 +38,7 @@ internal sealed class WorldBuilder( catalog.ModuleDefinitions, catalog.ItemDefinitions); - seedingService.InitializeStationStockpiles(stations); + seedingService.InitializeStationStockpiles(stations, catalog.ModuleDefinitions); var refinery = seedingService.SelectRefineryStation(stations, scenario); var patrolRoutes = BuildPatrolRoutes(scenario, systemsById); var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery); diff --git a/apps/backend/Universe/Scenario/WorldSeedingService.cs b/apps/backend/Universe/Scenario/WorldSeedingService.cs index b67c66c..3df2b8d 100644 --- a/apps/backend/Universe/Scenario/WorldSeedingService.cs +++ b/apps/backend/Universe/Scenario/WorldSeedingService.cs @@ -1,3 +1,4 @@ +using SpaceGame.Api.Shared.Runtime; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; @@ -60,11 +61,13 @@ internal sealed class WorldSeedingService } } - internal void InitializeStationStockpiles(IReadOnlyCollection stations) + internal void InitializeStationStockpiles( + IReadOnlyCollection stations, + IReadOnlyDictionary moduleDefinitions) { foreach (var station in stations) { - InitializeStationPopulation(station); + InitializeStationPopulation(station, moduleDefinitions); } } @@ -551,9 +554,11 @@ internal sealed class WorldSeedingService }; } - private static void InitializeStationPopulation(StationRuntime station) + private static void InitializeStationPopulation( + StationRuntime station, + IReadOnlyDictionary moduleDefinitions) { - var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01"); + var habitatModules = CountModules(station.InstalledModules, moduleDefinitions, ModuleType.Habitation); station.PopulationCapacity = 40f + (habitatModules * 220f); station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); station.Population = habitatModules > 0 diff --git a/apps/backend/Universe/Simulation/OrbitalStateUpdater.cs b/apps/backend/Universe/Simulation/OrbitalStateUpdater.cs index 77e090e..d6d3469 100644 --- a/apps/backend/Universe/Simulation/OrbitalStateUpdater.cs +++ b/apps/backend/Universe/Simulation/OrbitalStateUpdater.cs @@ -269,7 +269,7 @@ internal sealed class OrbitalStateUpdater } ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; var nearestCelestial = world.Celestials .Where(candidate => candidate.SystemId == ship.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))