Compare commits
3 Commits
cd1fe776a5
...
a2f66b0dca
| Author | SHA1 | Date | |
|---|---|---|---|
| a2f66b0dca | |||
| f5bf7d8e3f | |||
| 892d069b92 |
@@ -6,8 +6,9 @@ internal sealed class CommanderPlanningService
|
|||||||
{
|
{
|
||||||
private const float FactionCommanderReplanInterval = 10f;
|
private const float FactionCommanderReplanInterval = 10f;
|
||||||
private const float ShipCommanderReplanInterval = 5f;
|
private const float ShipCommanderReplanInterval = 5f;
|
||||||
|
private readonly FactionObjectivePlanner _objectivePlanner = new();
|
||||||
|
private readonly FactionObjectiveExecutor _objectiveExecutor = new();
|
||||||
|
|
||||||
private static readonly GoapPlanner<FactionPlanningState> _factionPlanner = new(s => s.Clone());
|
|
||||||
private static readonly GoapPlanner<ShipPlanningState> _shipPlanner = new(s => s.Clone());
|
private static readonly GoapPlanner<ShipPlanningState> _shipPlanner = new(s => s.Clone());
|
||||||
|
|
||||||
private static readonly IReadOnlyList<GoapGoal<FactionPlanningState>> _factionGoals =
|
private static readonly IReadOnlyList<GoapGoal<FactionPlanningState>> _factionGoals =
|
||||||
@@ -76,12 +77,9 @@ internal sealed class CommanderPlanningService
|
|||||||
|
|
||||||
commander.ReplanTimer = FactionCommanderReplanInterval;
|
commander.ReplanTimer = FactionCommanderReplanInterval;
|
||||||
commander.NeedsReplan = false;
|
commander.NeedsReplan = false;
|
||||||
|
commander.PlanningCycle += 1;
|
||||||
|
|
||||||
var state = BuildFactionPlanningState(world, commander.FactionId);
|
var state = BuildFactionPlanningState(world, commander.FactionId);
|
||||||
var actions = BuildFactionActions(world);
|
|
||||||
|
|
||||||
// Clear stale directives — actions will re-assert what is still needed.
|
|
||||||
commander.ActiveDirectives.Clear();
|
|
||||||
|
|
||||||
var rankedGoals = _factionGoals
|
var rankedGoals = _factionGoals
|
||||||
.Select(g => (goal: g, priority: g.ComputePriority(state, world, commander)))
|
.Select(g => (goal: g, priority: g.ComputePriority(state, world, commander)))
|
||||||
@@ -89,28 +87,15 @@ internal sealed class CommanderPlanningService
|
|||||||
.OrderByDescending(x => x.priority)
|
.OrderByDescending(x => x.priority)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
commander.LastPlanningState = state;
|
commander.LastStrategicAssessment = state;
|
||||||
commander.LastGoalPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList();
|
commander.LastStrategicPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList();
|
||||||
|
_objectivePlanner.UpdateBlackboard(world, commander, state);
|
||||||
// Execute the first action of each active goal's plan (top 3 to avoid conflicts).
|
_objectivePlanner.RefreshObjectives(
|
||||||
foreach (var (goal, _) in rankedGoals.Take(3))
|
world,
|
||||||
{
|
commander,
|
||||||
var plan = _factionPlanner.Plan(state, goal, actions);
|
state,
|
||||||
plan?.CurrentAction?.Execute(engine, world, commander);
|
rankedGoals.Select(entry => (entry.goal.Name, entry.priority)).ToList());
|
||||||
}
|
_objectiveExecutor.Execute(engine, world, commander, state);
|
||||||
|
|
||||||
if (FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId) is null)
|
|
||||||
{
|
|
||||||
if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-war-industry", StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
TryQueueFactionExpansionProject(world, commander, SelectGoalDrivenWarIndustryProject(world, state, commander.FactionId));
|
|
||||||
}
|
|
||||||
else if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-water-security", StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
TryQueueFactionExpansionProject(world, commander, FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateShipCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
private void UpdateShipCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
||||||
@@ -172,18 +157,30 @@ internal sealed class CommanderPlanningService
|
|||||||
TargetSystemCount = Math.Max(1, Math.Min(StationSimulationService.StrategicControlTargetSystems, world.Systems.Count)),
|
TargetSystemCount = Math.Max(1, Math.Min(StationSimulationService.StrategicControlTargetSystems, world.Systems.Count)),
|
||||||
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
||||||
OreStockpile = economy.GetCommodity("ore").OnHand,
|
OreStockpile = economy.GetCommodity("ore").OnHand,
|
||||||
RefinedMetalsStockpile = refinedMetals.OnHand,
|
RefinedMetalsAvailableStock = refinedMetals.AvailableStock,
|
||||||
RefinedMetalsProductionRate = refinedMetals.ProjectedProductionRatePerSecond,
|
RefinedMetalsUsageRate = refinedMetals.OperationalUsageRatePerSecond,
|
||||||
RefinedMetalsShortageHorizonSeconds = refinedMetals.ProjectedShortageHorizonSeconds,
|
RefinedMetalsProjectedProductionRate = refinedMetals.ProjectedProductionRatePerSecond,
|
||||||
HullpartsStockpile = hullparts.OnHand,
|
RefinedMetalsProjectedNetRate = refinedMetals.ProjectedNetRatePerSecond,
|
||||||
HullpartsProductionRate = hullparts.ProjectedProductionRatePerSecond,
|
RefinedMetalsLevelSeconds = refinedMetals.LevelSeconds,
|
||||||
HullpartsShortageHorizonSeconds = hullparts.ProjectedShortageHorizonSeconds,
|
RefinedMetalsLevel = refinedMetals.Level.ToString().ToLowerInvariant(),
|
||||||
ClaytronicsStockpile = claytronics.OnHand,
|
HullpartsAvailableStock = hullparts.AvailableStock,
|
||||||
ClaytronicsProductionRate = claytronics.ProjectedProductionRatePerSecond,
|
HullpartsUsageRate = hullparts.OperationalUsageRatePerSecond,
|
||||||
ClaytronicsShortageHorizonSeconds = claytronics.ProjectedShortageHorizonSeconds,
|
HullpartsProjectedProductionRate = hullparts.ProjectedProductionRatePerSecond,
|
||||||
WaterStockpile = water.OnHand,
|
HullpartsProjectedNetRate = hullparts.ProjectedNetRatePerSecond,
|
||||||
WaterProductionRate = water.ProjectedProductionRatePerSecond,
|
HullpartsLevelSeconds = hullparts.LevelSeconds,
|
||||||
WaterShortageHorizonSeconds = water.ProjectedShortageHorizonSeconds,
|
HullpartsLevel = hullparts.Level.ToString().ToLowerInvariant(),
|
||||||
|
ClaytronicsAvailableStock = claytronics.AvailableStock,
|
||||||
|
ClaytronicsUsageRate = claytronics.OperationalUsageRatePerSecond,
|
||||||
|
ClaytronicsProjectedProductionRate = claytronics.ProjectedProductionRatePerSecond,
|
||||||
|
ClaytronicsProjectedNetRate = claytronics.ProjectedNetRatePerSecond,
|
||||||
|
ClaytronicsLevelSeconds = claytronics.LevelSeconds,
|
||||||
|
ClaytronicsLevel = claytronics.Level.ToString().ToLowerInvariant(),
|
||||||
|
WaterAvailableStock = water.AvailableStock,
|
||||||
|
WaterUsageRate = water.OperationalUsageRatePerSecond,
|
||||||
|
WaterProjectedProductionRate = water.ProjectedProductionRatePerSecond,
|
||||||
|
WaterProjectedNetRate = water.ProjectedNetRatePerSecond,
|
||||||
|
WaterLevelSeconds = water.LevelSeconds,
|
||||||
|
WaterLevel = water.Level.ToString().ToLowerInvariant(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,16 +189,23 @@ internal sealed class CommanderPlanningService
|
|||||||
ShipRuntime ship,
|
ShipRuntime ship,
|
||||||
CommanderRuntime commander)
|
CommanderRuntime commander)
|
||||||
{
|
{
|
||||||
var factionCommander = world.Commanders.FirstOrDefault(c =>
|
var factionCommander = FindFactionCommander(world, commander.FactionId);
|
||||||
c.FactionId == commander.FactionId &&
|
|
||||||
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
var enemyTarget = SelectEnemyTarget(world, ship);
|
var enemyTarget = SelectEnemyTarget(world, ship);
|
||||||
var tradeRoute = SelectTradeRoute(world, ship.FactionId);
|
var tradeRoute = SelectTradeRoute(world, ship.FactionId);
|
||||||
|
var expansionTask = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ExpandIndustry);
|
||||||
|
var attackTask = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.AttackFactionAssets);
|
||||||
|
var shipyardExpansionTask = factionCommander?.IssuedTasks
|
||||||
|
.Where(task =>
|
||||||
|
task.Kind == FactionIssuedTaskKind.ExpandIndustry
|
||||||
|
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active
|
||||||
|
&& string.Equals(task.ModuleId, "module_gen_build_l_01", StringComparison.Ordinal))
|
||||||
|
.OrderByDescending(task => task.Priority)
|
||||||
|
.FirstOrDefault();
|
||||||
var expansionProject = FactionIndustryPlanner.GetActiveExpansionProject(world, ship.FactionId);
|
var expansionProject = FactionIndustryPlanner.GetActiveExpansionProject(world, ship.FactionId);
|
||||||
if (commander.ActiveBehavior is not null)
|
if (commander.ActiveBehavior is not null)
|
||||||
{
|
{
|
||||||
commander.ActiveBehavior.AreaSystemId = enemyTarget?.SystemId;
|
commander.ActiveBehavior.AreaSystemId = attackTask?.TargetSystemId ?? expansionTask?.TargetSystemId ?? enemyTarget?.SystemId;
|
||||||
commander.ActiveBehavior.TargetEntityId = enemyTarget?.EntityId;
|
commander.ActiveBehavior.TargetEntityId = enemyTarget?.EntityId;
|
||||||
if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal))
|
if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -209,12 +213,12 @@ internal sealed class CommanderPlanningService
|
|||||||
commander.ActiveBehavior.StationId = tradeRoute?.SourceStationId;
|
commander.ActiveBehavior.StationId = tradeRoute?.SourceStationId;
|
||||||
commander.ActiveBehavior.TargetEntityId = tradeRoute?.DestinationStationId;
|
commander.ActiveBehavior.TargetEntityId = tradeRoute?.DestinationStationId;
|
||||||
}
|
}
|
||||||
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal) && expansionProject is not null)
|
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal) && (expansionTask is not null || expansionProject is not null))
|
||||||
{
|
{
|
||||||
commander.ActiveBehavior.StationId = expansionProject.SupportStationId;
|
commander.ActiveBehavior.StationId = expansionProject?.SupportStationId;
|
||||||
commander.ActiveBehavior.TargetEntityId = expansionProject.SiteId;
|
commander.ActiveBehavior.TargetEntityId = expansionTask?.TargetSiteId ?? expansionProject?.SiteId;
|
||||||
commander.ActiveBehavior.ModuleId = expansionProject.ModuleId;
|
commander.ActiveBehavior.ModuleId = expansionTask?.ModuleId ?? expansionProject?.ModuleId;
|
||||||
commander.ActiveBehavior.AreaSystemId = expansionProject.SystemId;
|
commander.ActiveBehavior.AreaSystemId = expansionTask?.TargetSystemId ?? expansionProject?.SystemId;
|
||||||
}
|
}
|
||||||
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal))
|
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -229,15 +233,13 @@ internal sealed class CommanderPlanningService
|
|||||||
ShipKind = ship.Definition.Kind,
|
ShipKind = ship.Definition.Kind,
|
||||||
HasMiningCapability = HasShipCapabilities(ship.Definition, "mining"),
|
HasMiningCapability = HasShipCapabilities(ship.Definition, "mining"),
|
||||||
FactionWantsOre = true,
|
FactionWantsOre = true,
|
||||||
FactionWantsCombat = factionCommander?.ActiveDirectives.Contains("attack-rival", StringComparer.Ordinal) ?? false,
|
FactionWantsCombat = attackTask is not null,
|
||||||
FactionWantsExpansion = factionCommander?.ActiveDirectives
|
FactionWantsExpansion = expansionTask is not null,
|
||||||
.Contains("expand-territory", StringComparer.Ordinal) ?? false,
|
FactionNeedsShipyard = shipyardExpansionTask is not null
|
||||||
FactionNeedsShipyard = !(factionCommander?.ActiveDirectives.Contains("bootstrap-war-industry", StringComparer.Ordinal) ?? false)
|
&& !world.Stations.Any(station =>
|
||||||
? false
|
|
||||||
: !world.Stations.Any(station =>
|
|
||||||
string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)
|
string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)
|
||||||
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
||||||
TargetEnemySystemId = enemyTarget?.SystemId,
|
TargetEnemySystemId = attackTask?.TargetSystemId ?? enemyTarget?.SystemId,
|
||||||
TargetEnemyEntityId = enemyTarget?.EntityId,
|
TargetEnemyEntityId = enemyTarget?.EntityId,
|
||||||
TradeItemId = tradeRoute?.ItemId,
|
TradeItemId = tradeRoute?.ItemId,
|
||||||
TradeSourceStationId = tradeRoute?.SourceStationId,
|
TradeSourceStationId = tradeRoute?.SourceStationId,
|
||||||
@@ -245,70 +247,33 @@ internal sealed class CommanderPlanningService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<GoapAction<FactionPlanningState>> BuildFactionActions(SimulationWorld world)
|
internal static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) =>
|
||||||
{
|
|
||||||
var actions = new List<GoapAction<FactionPlanningState>>();
|
|
||||||
|
|
||||||
actions.Add(new PlanWarIndustryAction());
|
|
||||||
actions.Add(new PlanCommoditySupplyAction("water"));
|
|
||||||
|
|
||||||
foreach (var (shipId, def) in world.ShipDefinitions)
|
|
||||||
{
|
|
||||||
actions.Add(new OrderShipProductionAction(def.Kind, shipId));
|
|
||||||
}
|
|
||||||
|
|
||||||
actions.Add(new LaunchExterminationCampaignAction());
|
|
||||||
actions.Add(new ExpandToSystemAction());
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool FactionCommanderHasDirective(SimulationWorld world, string factionId, string directive) =>
|
|
||||||
world.Commanders.FirstOrDefault(c =>
|
world.Commanders.FirstOrDefault(c =>
|
||||||
c.FactionId == factionId &&
|
c.FactionId == factionId &&
|
||||||
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal))
|
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
|
||||||
?.ActiveDirectives.Contains(directive, StringComparer.Ordinal) ?? false;
|
|
||||||
|
|
||||||
private static void TryQueueFactionExpansionProject(
|
internal static bool FactionCommanderHasIssuedTask(
|
||||||
SimulationWorld world,
|
SimulationWorld world,
|
||||||
CommanderRuntime commander,
|
string factionId,
|
||||||
IndustryExpansionProject? project)
|
FactionIssuedTaskKind kind,
|
||||||
{
|
string? shipRole = null) =>
|
||||||
if (project is null)
|
FindFactionCommander(world, factionId)?
|
||||||
{
|
.IssuedTasks.Any(task =>
|
||||||
return;
|
task.Kind == kind
|
||||||
}
|
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active or FactionIssuedTaskState.Blocked
|
||||||
|
&& (shipRole is null || string.Equals(task.ShipRole, shipRole, StringComparison.Ordinal))) ?? false;
|
||||||
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
internal static FactionIssuedTaskRuntime? GetHighestPriorityIssuedTask(
|
||||||
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
|
CommanderRuntime? factionCommander,
|
||||||
}
|
FactionIssuedTaskKind kind,
|
||||||
|
string? shipRole = null) =>
|
||||||
private static IndustryExpansionProject? SelectGoalDrivenWarIndustryProject(
|
factionCommander?.IssuedTasks
|
||||||
SimulationWorld world,
|
.Where(task =>
|
||||||
FactionPlanningState state,
|
task.Kind == kind
|
||||||
string factionId)
|
&& task.State is FactionIssuedTaskState.Planned or FactionIssuedTaskState.Active or FactionIssuedTaskState.Blocked
|
||||||
{
|
&& (shipRole is null || string.Equals(task.ShipRole, shipRole, StringComparison.Ordinal)))
|
||||||
if (!state.HasRefinedMetalsProduction || state.RefinedMetalsShortageHorizonSeconds < 240f)
|
.OrderByDescending(task => task.Priority)
|
||||||
{
|
.FirstOrDefault();
|
||||||
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "refinedmetals");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.HasHullpartsProduction || state.HullpartsShortageHorizonSeconds < 240f)
|
|
||||||
{
|
|
||||||
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "hullparts");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.HasClaytronicsProduction || state.ClaytronicsShortageHorizonSeconds < 240f)
|
|
||||||
{
|
|
||||||
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "claytronics");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.HasShipFactory)
|
|
||||||
{
|
|
||||||
return FactionIndustryPlanner.CreateShipyardFoundationProject(world, factionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string EntityId, string SystemId)? SelectEnemyTarget(SimulationWorld world, ShipRuntime ship)
|
private static (string EntityId, string SystemId)? SelectEnemyTarget(SimulationWorld world, ShipRuntime ship)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,26 +16,40 @@ public sealed class FactionPlanningState
|
|||||||
public int EnemyShipCount { get; set; }
|
public int EnemyShipCount { get; set; }
|
||||||
public int EnemyStationCount { get; set; }
|
public int EnemyStationCount { get; set; }
|
||||||
public float OreStockpile { get; set; }
|
public float OreStockpile { get; set; }
|
||||||
public float RefinedMetalsStockpile { get; set; }
|
public float RefinedMetalsAvailableStock { get; set; }
|
||||||
public float RefinedMetalsProductionRate { get; set; }
|
public float RefinedMetalsUsageRate { get; set; }
|
||||||
public float RefinedMetalsShortageHorizonSeconds { get; set; }
|
public float RefinedMetalsProjectedProductionRate { get; set; }
|
||||||
public float HullpartsStockpile { get; set; }
|
public float RefinedMetalsProjectedNetRate { get; set; }
|
||||||
public float HullpartsProductionRate { get; set; }
|
public float RefinedMetalsLevelSeconds { get; set; }
|
||||||
public float HullpartsShortageHorizonSeconds { get; set; }
|
public string RefinedMetalsLevel { get; set; } = "unknown";
|
||||||
public float ClaytronicsStockpile { get; set; }
|
public float HullpartsAvailableStock { get; set; }
|
||||||
public float ClaytronicsProductionRate { get; set; }
|
public float HullpartsUsageRate { get; set; }
|
||||||
public float ClaytronicsShortageHorizonSeconds { get; set; }
|
public float HullpartsProjectedProductionRate { get; set; }
|
||||||
public float WaterStockpile { get; set; }
|
public float HullpartsProjectedNetRate { get; set; }
|
||||||
public float WaterProductionRate { get; set; }
|
public float HullpartsLevelSeconds { get; set; }
|
||||||
public float WaterShortageHorizonSeconds { get; set; }
|
public string HullpartsLevel { get; set; } = "unknown";
|
||||||
|
public float ClaytronicsAvailableStock { get; set; }
|
||||||
|
public float ClaytronicsUsageRate { get; set; }
|
||||||
|
public float ClaytronicsProjectedProductionRate { get; set; }
|
||||||
|
public float ClaytronicsProjectedNetRate { get; set; }
|
||||||
|
public float ClaytronicsLevelSeconds { get; set; }
|
||||||
|
public string ClaytronicsLevel { get; set; } = "unknown";
|
||||||
|
public float WaterAvailableStock { get; set; }
|
||||||
|
public float WaterUsageRate { get; set; }
|
||||||
|
public float WaterProjectedProductionRate { get; set; }
|
||||||
|
public float WaterProjectedNetRate { get; set; }
|
||||||
|
public float WaterLevelSeconds { get; set; }
|
||||||
|
public string WaterLevel { get; set; } = "unknown";
|
||||||
|
|
||||||
public bool HasRefinedMetalsProduction => RefinedMetalsProductionRate > 0.01f;
|
public bool HasRefinedMetalsProduction => RefinedMetalsProjectedProductionRate > 0.01f;
|
||||||
public bool HasHullpartsProduction => HullpartsProductionRate > 0.01f;
|
public bool HasHullpartsProduction => HullpartsProjectedProductionRate > 0.01f;
|
||||||
public bool HasClaytronicsProduction => ClaytronicsProductionRate > 0.01f;
|
public bool HasClaytronicsProduction => ClaytronicsProjectedProductionRate > 0.01f;
|
||||||
public bool HasWaterProduction => WaterProductionRate > 0.01f;
|
public bool HasWaterProduction => WaterProjectedProductionRate > 0.01f;
|
||||||
|
|
||||||
public bool HasWarIndustrySupplyChain =>
|
public bool HasWarIndustrySupplyChain =>
|
||||||
HasRefinedMetalsProduction && HasHullpartsProduction && HasClaytronicsProduction;
|
IsCommodityOperational(RefinedMetalsProjectedProductionRate, RefinedMetalsProjectedNetRate, RefinedMetalsLevelSeconds, RefinedMetalsLevel, 240f)
|
||||||
|
&& IsCommodityOperational(HullpartsProjectedProductionRate, HullpartsProjectedNetRate, HullpartsLevelSeconds, HullpartsLevel, 240f)
|
||||||
|
&& IsCommodityOperational(ClaytronicsProjectedProductionRate, ClaytronicsProjectedNetRate, ClaytronicsLevelSeconds, ClaytronicsLevel, 240f);
|
||||||
|
|
||||||
public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone();
|
public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone();
|
||||||
|
|
||||||
@@ -44,6 +58,39 @@ public sealed class FactionPlanningState
|
|||||||
var expansionDeficit = Math.Max(0, state.TargetSystemCount - state.ControlledSystemCount);
|
var expansionDeficit = Math.Max(0, state.TargetSystemCount - state.ControlledSystemCount);
|
||||||
return Math.Max(3, (state.ControlledSystemCount * 2) + (expansionDeficit * 3) + Math.Min(4, state.EnemyFactionCount + state.EnemyStationCount));
|
return Math.Max(3, (state.ControlledSystemCount * 2) + (expansionDeficit * 3) + Math.Min(4, state.EnemyFactionCount + state.EnemyStationCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool IsCommodityOperational(
|
||||||
|
float projectedProductionRate,
|
||||||
|
float projectedNetRate,
|
||||||
|
float levelSeconds,
|
||||||
|
string level,
|
||||||
|
float targetLevelSeconds) =>
|
||||||
|
projectedProductionRate > 0.01f
|
||||||
|
&& projectedNetRate >= -0.01f
|
||||||
|
&& levelSeconds >= targetLevelSeconds
|
||||||
|
&& (string.Equals(level, "stable", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(level, "surplus", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
internal static float ComputeCommodityNeed(
|
||||||
|
float projectedProductionRate,
|
||||||
|
float usageRate,
|
||||||
|
float projectedNetRate,
|
||||||
|
float levelSeconds,
|
||||||
|
string level,
|
||||||
|
float targetLevelSeconds)
|
||||||
|
{
|
||||||
|
var levelWeight = level switch
|
||||||
|
{
|
||||||
|
"critical" => 140f,
|
||||||
|
"low" => 80f,
|
||||||
|
"stable" => 20f,
|
||||||
|
_ => 0f,
|
||||||
|
};
|
||||||
|
var rateDeficit = MathF.Max(0f, usageRate - projectedProductionRate);
|
||||||
|
var levelDeficit = MathF.Max(0f, targetLevelSeconds - levelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||||
|
var instability = projectedNetRate < 0f ? MathF.Abs(projectedNetRate) * 80f : 0f;
|
||||||
|
return levelWeight + (rateDeficit * 140f) + (levelDeficit * 120f) + instability;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Goals ─────────────────────────────────────────────────────────────────────
|
// ─── Goals ─────────────────────────────────────────────────────────────────────
|
||||||
@@ -63,12 +110,16 @@ public sealed class EnsureWarIndustryGoal : GoapGoal<FactionPlanningState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
var missingStages =
|
var missingStages =
|
||||||
(state.HasRefinedMetalsProduction ? 0 : 1) +
|
(FactionPlanningState.IsCommodityOperational(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) ? 0 : 1) +
|
||||||
(state.HasHullpartsProduction ? 0 : 1) +
|
(FactionPlanningState.IsCommodityOperational(state.HullpartsProjectedProductionRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) ? 0 : 1) +
|
||||||
(state.HasClaytronicsProduction ? 0 : 1) +
|
(FactionPlanningState.IsCommodityOperational(state.ClaytronicsProjectedProductionRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f) ? 0 : 1) +
|
||||||
(state.HasShipFactory ? 0 : 1);
|
(state.HasShipFactory ? 0 : 1);
|
||||||
|
var supplyNeed =
|
||||||
|
FactionPlanningState.ComputeCommodityNeed(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsUsageRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f)
|
||||||
|
+ FactionPlanningState.ComputeCommodityNeed(state.HullpartsProjectedProductionRate, state.HullpartsUsageRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f)
|
||||||
|
+ FactionPlanningState.ComputeCommodityNeed(state.ClaytronicsProjectedProductionRate, state.ClaytronicsUsageRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f);
|
||||||
|
|
||||||
return missingStages <= 0 ? 0f : 125f + (missingStages * 18f);
|
return missingStages <= 0 && supplyNeed <= 0.01f ? 0f : 110f + (missingStages * 22f) + (supplyNeed * 0.18f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,21 +128,22 @@ public sealed class EnsureWaterSecurityGoal : GoapGoal<FactionPlanningState>
|
|||||||
public override string Name => "ensure-water-security";
|
public override string Name => "ensure-water-security";
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||||
state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f;
|
FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f);
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||||
{
|
{
|
||||||
if (state.HasWaterProduction && state.WaterShortageHorizonSeconds >= 300f)
|
if (FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f))
|
||||||
{
|
{
|
||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (float.IsPositiveInfinity(state.WaterShortageHorizonSeconds))
|
return 55f + FactionPlanningState.ComputeCommodityNeed(
|
||||||
{
|
state.WaterProjectedProductionRate,
|
||||||
return state.HasWaterProduction ? 0f : 85f;
|
state.WaterUsageRate,
|
||||||
}
|
state.WaterProjectedNetRate,
|
||||||
|
state.WaterLevelSeconds,
|
||||||
return 55f + MathF.Max(0f, 300f - state.WaterShortageHorizonSeconds) * 0.2f;
|
state.WaterLevel,
|
||||||
|
300f) * 0.25f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,159 +222,3 @@ public sealed class EnsureConstructionCapacityGoal : GoapGoal<FactionPlanningSta
|
|||||||
return deficit <= 0 ? 0f : 60f + (deficit * 10f);
|
return deficit <= 0 ? 0f : 60f + (deficit * 10f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Actions ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public sealed class OrderShipProductionAction : GoapAction<FactionPlanningState>
|
|
||||||
{
|
|
||||||
private readonly string shipKind;
|
|
||||||
private readonly string shipId;
|
|
||||||
|
|
||||||
public OrderShipProductionAction(string shipKind, string shipId)
|
|
||||||
{
|
|
||||||
this.shipKind = shipKind;
|
|
||||||
this.shipId = shipId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Name => $"order-{shipId}-production";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(FactionPlanningState state) =>
|
|
||||||
state.HasShipFactory && state.HasWarIndustrySupplyChain;
|
|
||||||
|
|
||||||
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
switch (shipKind)
|
|
||||||
{
|
|
||||||
case "military": state.MilitaryShipCount++; break;
|
|
||||||
case "mining": state.MinerShipCount++; break;
|
|
||||||
case "transport": state.TransportShipCount++; break;
|
|
||||||
case "construction": state.ConstructorShipCount++; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
commander.ActiveDirectives.Add($"produce-{shipKind}-ships");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlanWarIndustryAction : GoapAction<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "plan-war-industry";
|
|
||||||
public override float Cost => 2f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(FactionPlanningState state) =>
|
|
||||||
state.EnemyFactionCount > 0 && (!state.HasWarIndustrySupplyChain || !state.HasShipFactory);
|
|
||||||
|
|
||||||
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
state.RefinedMetalsProductionRate = MathF.Max(state.RefinedMetalsProductionRate, 1f);
|
|
||||||
state.HullpartsProductionRate = MathF.Max(state.HullpartsProductionRate, 1f);
|
|
||||||
state.ClaytronicsProductionRate = MathF.Max(state.ClaytronicsProductionRate, 1f);
|
|
||||||
state.HasShipFactory = true;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
commander.ActiveDirectives.Add("bootstrap-war-industry");
|
|
||||||
|
|
||||||
if (FactionIndustryPlanner.AnalyzeShipyardNeed(world, commander.FactionId) is not { } project)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
|
||||||
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlanCommoditySupplyAction : GoapAction<FactionPlanningState>
|
|
||||||
{
|
|
||||||
private readonly string commodityId;
|
|
||||||
|
|
||||||
public PlanCommoditySupplyAction(string commodityId)
|
|
||||||
{
|
|
||||||
this.commodityId = commodityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Name => $"plan-{commodityId}-supply";
|
|
||||||
public override float Cost => 2f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(FactionPlanningState state) =>
|
|
||||||
commodityId switch
|
|
||||||
{
|
|
||||||
"water" => !state.HasWaterProduction || state.WaterShortageHorizonSeconds < 300f,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
if (string.Equals(commodityId, "water", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
state.WaterProductionRate = MathF.Max(state.WaterProductionRate, 1f);
|
|
||||||
state.WaterShortageHorizonSeconds = MathF.Max(state.WaterShortageHorizonSeconds, 600f);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
if (FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId) is not { } project)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
|
||||||
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ExpandToSystemAction : GoapAction<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "expand-to-system";
|
|
||||||
public override float Cost => 3f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(FactionPlanningState state) =>
|
|
||||||
state.ConstructorShipCount > 0 && state.MilitaryShipCount >= 2 && state.HasWarIndustrySupplyChain;
|
|
||||||
|
|
||||||
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
state.ControlledSystemCount++;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
commander.ActiveDirectives.Add("expand-territory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class LaunchExterminationCampaignAction : GoapAction<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "launch-extermination-campaign";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(FactionPlanningState state) =>
|
|
||||||
state.EnemyFactionCount > 0
|
|
||||||
&& state.HasShipFactory
|
|
||||||
&& state.MilitaryShipCount >= Math.Max(2, FactionPlanningState.ComputeTargetWarships(state) / 2);
|
|
||||||
|
|
||||||
public override FactionPlanningState ApplyEffects(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
state.EnemyShipCount = 0;
|
|
||||||
state.EnemyStationCount = 0;
|
|
||||||
state.EnemyFactionCount = 0;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
commander.ActiveDirectives.Add("attack-rival");
|
|
||||||
commander.ActiveDirectives.Add("produce-military-ships");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
1169
apps/backend/Factions/AI/FactionObjectivePlanning.cs
Normal file
1169
apps/backend/Factions/AI/FactionObjectivePlanning.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
namespace SpaceGame.Api.Factions.Contracts;
|
namespace SpaceGame.Api.Factions.Contracts;
|
||||||
|
|
||||||
public sealed record FactionGoapStateSnapshot(
|
public sealed record FactionPlanningStateSnapshot(
|
||||||
int MilitaryShipCount,
|
int MilitaryShipCount,
|
||||||
int MinerShipCount,
|
int MinerShipCount,
|
||||||
int TransportShipCount,
|
int TransportShipCount,
|
||||||
@@ -9,17 +9,134 @@ public sealed record FactionGoapStateSnapshot(
|
|||||||
int TargetSystemCount,
|
int TargetSystemCount,
|
||||||
bool HasShipFactory,
|
bool HasShipFactory,
|
||||||
float OreStockpile,
|
float OreStockpile,
|
||||||
float RefinedMetalsStockpile,
|
float RefinedMetalsAvailableStock,
|
||||||
float RefinedMetalsProductionRate,
|
float RefinedMetalsUsageRate,
|
||||||
float HullpartsStockpile,
|
float RefinedMetalsProjectedProductionRate,
|
||||||
float HullpartsProductionRate,
|
float RefinedMetalsProjectedNetRate,
|
||||||
float ClaytronicsStockpile,
|
float RefinedMetalsLevelSeconds,
|
||||||
float ClaytronicsProductionRate,
|
string RefinedMetalsLevel,
|
||||||
float WaterStockpile,
|
float HullpartsAvailableStock,
|
||||||
float WaterProductionRate,
|
float HullpartsUsageRate,
|
||||||
float WaterShortageHorizonSeconds);
|
float HullpartsProjectedProductionRate,
|
||||||
|
float HullpartsProjectedNetRate,
|
||||||
|
float HullpartsLevelSeconds,
|
||||||
|
string HullpartsLevel,
|
||||||
|
float ClaytronicsAvailableStock,
|
||||||
|
float ClaytronicsUsageRate,
|
||||||
|
float ClaytronicsProjectedProductionRate,
|
||||||
|
float ClaytronicsProjectedNetRate,
|
||||||
|
float ClaytronicsLevelSeconds,
|
||||||
|
string ClaytronicsLevel,
|
||||||
|
float WaterAvailableStock,
|
||||||
|
float WaterUsageRate,
|
||||||
|
float WaterProjectedProductionRate,
|
||||||
|
float WaterProjectedNetRate,
|
||||||
|
float WaterLevelSeconds,
|
||||||
|
string WaterLevel);
|
||||||
|
|
||||||
public sealed record FactionGoapPrioritySnapshot(string GoalName, float Priority);
|
public sealed record FactionStrategicPrioritySnapshot(string GoalName, float Priority);
|
||||||
|
|
||||||
|
public sealed record FactionCommoditySignalSnapshot(
|
||||||
|
string ItemId,
|
||||||
|
float AvailableStock,
|
||||||
|
float OnHand,
|
||||||
|
float ProductionRatePerSecond,
|
||||||
|
float CommittedProductionRatePerSecond,
|
||||||
|
float UsageRatePerSecond,
|
||||||
|
float NetRatePerSecond,
|
||||||
|
float ProjectedNetRatePerSecond,
|
||||||
|
float LevelSeconds,
|
||||||
|
string Level,
|
||||||
|
float ProjectedProductionRatePerSecond,
|
||||||
|
float BuyBacklog,
|
||||||
|
float ReservedForConstruction);
|
||||||
|
|
||||||
|
public sealed record FactionThreatSignalSnapshot(
|
||||||
|
string ScopeId,
|
||||||
|
string ScopeKind,
|
||||||
|
int EnemyShipCount,
|
||||||
|
int EnemyStationCount);
|
||||||
|
|
||||||
|
public sealed record FactionBlackboardSnapshot(
|
||||||
|
int PlanCycle,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
int TargetWarshipCount,
|
||||||
|
bool HasWarIndustrySupplyChain,
|
||||||
|
bool HasShipyard,
|
||||||
|
bool HasActiveExpansionProject,
|
||||||
|
string? ActiveExpansionCommodityId,
|
||||||
|
string? ActiveExpansionModuleId,
|
||||||
|
string? ActiveExpansionSiteId,
|
||||||
|
string? ActiveExpansionSystemId,
|
||||||
|
int EnemyFactionCount,
|
||||||
|
int EnemyShipCount,
|
||||||
|
int EnemyStationCount,
|
||||||
|
int MilitaryShipCount,
|
||||||
|
int MinerShipCount,
|
||||||
|
int TransportShipCount,
|
||||||
|
int ConstructorShipCount,
|
||||||
|
int ControlledSystemCount,
|
||||||
|
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals,
|
||||||
|
IReadOnlyList<FactionThreatSignalSnapshot> ThreatSignals);
|
||||||
|
|
||||||
|
public sealed record FactionPlanStepSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
float Priority,
|
||||||
|
string? CommodityId,
|
||||||
|
string? ModuleId,
|
||||||
|
string? TargetFactionId,
|
||||||
|
string? TargetSiteId,
|
||||||
|
string? BlockingReason,
|
||||||
|
string? Notes,
|
||||||
|
int LastEvaluatedCycle,
|
||||||
|
IReadOnlyList<string> DependencyStepIds,
|
||||||
|
IReadOnlyList<string> RequiredFacts,
|
||||||
|
IReadOnlyList<string> ProducedFacts,
|
||||||
|
IReadOnlyList<string> AssignedAssets,
|
||||||
|
IReadOnlyList<string> IssuedTaskIds);
|
||||||
|
|
||||||
|
public sealed record FactionIssuedTaskSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string State,
|
||||||
|
string ObjectiveId,
|
||||||
|
string StepId,
|
||||||
|
float Priority,
|
||||||
|
string? ShipRole,
|
||||||
|
string? CommodityId,
|
||||||
|
string? ModuleId,
|
||||||
|
string? TargetFactionId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
string? TargetSiteId,
|
||||||
|
int CreatedAtCycle,
|
||||||
|
int UpdatedAtCycle,
|
||||||
|
string? BlockingReason,
|
||||||
|
string? Notes,
|
||||||
|
IReadOnlyList<string> AssignedAssets);
|
||||||
|
|
||||||
|
public sealed record FactionObjectiveSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string State,
|
||||||
|
float Priority,
|
||||||
|
string? ParentObjectiveId,
|
||||||
|
string? TargetFactionId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
string? TargetSiteId,
|
||||||
|
string? TargetRegionId,
|
||||||
|
string? CommodityId,
|
||||||
|
string? ModuleId,
|
||||||
|
int BudgetWeight,
|
||||||
|
int SlotCost,
|
||||||
|
int CreatedAtCycle,
|
||||||
|
int UpdatedAtCycle,
|
||||||
|
string? InvalidationReason,
|
||||||
|
string? BlockingReason,
|
||||||
|
IReadOnlyList<string> PrerequisiteObjectiveIds,
|
||||||
|
IReadOnlyList<string> AssignedAssets,
|
||||||
|
IReadOnlyList<FactionPlanStepSnapshot> Steps);
|
||||||
|
|
||||||
public sealed record FactionSnapshot(
|
public sealed record FactionSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -32,8 +149,11 @@ public sealed record FactionSnapshot(
|
|||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId,
|
string? DefaultPolicySetId,
|
||||||
FactionGoapStateSnapshot? GoapState,
|
FactionPlanningStateSnapshot? StrategicAssessment,
|
||||||
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
|
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
||||||
|
FactionBlackboardSnapshot? Blackboard,
|
||||||
|
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
||||||
|
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
||||||
|
|
||||||
public sealed record FactionDelta(
|
public sealed record FactionDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -46,5 +166,8 @@ public sealed record FactionDelta(
|
|||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId,
|
string? DefaultPolicySetId,
|
||||||
FactionGoapStateSnapshot? GoapState,
|
FactionPlanningStateSnapshot? StrategicAssessment,
|
||||||
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
|
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
||||||
|
FactionBlackboardSnapshot? Blackboard,
|
||||||
|
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
||||||
|
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public sealed class CommanderRuntime
|
|||||||
public string? PolicySetId { get; set; }
|
public string? PolicySetId { get; set; }
|
||||||
public string? Doctrine { get; set; }
|
public string? Doctrine { get; set; }
|
||||||
public List<string> Goals { get; } = [];
|
public List<string> Goals { get; } = [];
|
||||||
public HashSet<string> ActiveDirectives { get; } = new(StringComparer.Ordinal);
|
|
||||||
public string? ActiveGoalName { get; set; }
|
public string? ActiveGoalName { get; set; }
|
||||||
public string? ActiveActionName { get; set; }
|
public string? ActiveActionName { get; set; }
|
||||||
public float ReplanTimer { get; set; }
|
public float ReplanTimer { get; set; }
|
||||||
@@ -37,8 +36,194 @@ public sealed class CommanderRuntime
|
|||||||
public CommanderTaskRuntime? ActiveTask { get; set; }
|
public CommanderTaskRuntime? ActiveTask { get; set; }
|
||||||
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
public bool IsAlive { get; set; } = true;
|
public bool IsAlive { get; set; } = true;
|
||||||
public FactionPlanningState? LastPlanningState { get; set; }
|
public FactionPlanningState? LastStrategicAssessment { get; set; }
|
||||||
public IReadOnlyList<(string Name, float Priority)>? LastGoalPriorities { get; set; }
|
public IReadOnlyList<(string Name, float Priority)>? LastStrategicPriorities { get; set; }
|
||||||
|
public FactionBlackboardRuntime? FactionBlackboard { get; set; }
|
||||||
|
public List<FactionObjectiveRuntime> Objectives { get; } = [];
|
||||||
|
public List<FactionIssuedTaskRuntime> IssuedTasks { get; } = [];
|
||||||
|
public int PlanningCycle { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionObjectiveKind
|
||||||
|
{
|
||||||
|
DestroyFaction,
|
||||||
|
BootstrapWarIndustry,
|
||||||
|
BuildShipyard,
|
||||||
|
BuildAttackFleet,
|
||||||
|
EnsureCommoditySupply,
|
||||||
|
EnsureWaterSecurity,
|
||||||
|
EnsureMiningCapacity,
|
||||||
|
EnsureConstructionCapacity,
|
||||||
|
EnsureTransportCapacity,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionObjectiveState
|
||||||
|
{
|
||||||
|
Planned,
|
||||||
|
Active,
|
||||||
|
Blocked,
|
||||||
|
Complete,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionPlanStepKind
|
||||||
|
{
|
||||||
|
EnsureCommodityProduction,
|
||||||
|
EnsureShipyardSite,
|
||||||
|
ProduceFleet,
|
||||||
|
AttackFactionAssets,
|
||||||
|
EnsureWaterSupply,
|
||||||
|
EnsureMiningCapacity,
|
||||||
|
EnsureConstructionCapacity,
|
||||||
|
EnsureTransportCapacity,
|
||||||
|
MonitorExpansionProject,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionPlanStepStatus
|
||||||
|
{
|
||||||
|
Planned,
|
||||||
|
Ready,
|
||||||
|
Running,
|
||||||
|
Blocked,
|
||||||
|
Complete,
|
||||||
|
Failed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionIssuedTaskKind
|
||||||
|
{
|
||||||
|
ExpandIndustry,
|
||||||
|
ProduceShips,
|
||||||
|
AttackFactionAssets,
|
||||||
|
SustainWarIndustry,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FactionIssuedTaskState
|
||||||
|
{
|
||||||
|
Planned,
|
||||||
|
Active,
|
||||||
|
Blocked,
|
||||||
|
Complete,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionObjectiveRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string MergeKey { get; init; }
|
||||||
|
public required FactionObjectiveKind Kind { get; init; }
|
||||||
|
public FactionObjectiveState State { get; set; } = FactionObjectiveState.Planned;
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? ParentObjectiveId { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetSiteId { get; set; }
|
||||||
|
public string? TargetRegionId { get; set; }
|
||||||
|
public string? CommodityId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public int BudgetWeight { get; set; }
|
||||||
|
public int SlotCost { get; set; } = 1;
|
||||||
|
public int CreatedAtCycle { get; init; }
|
||||||
|
public int UpdatedAtCycle { get; set; }
|
||||||
|
public string? InvalidationReason { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
|
public HashSet<string> PrerequisiteObjectiveIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionPlanStepRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string ObjectiveId { get; init; }
|
||||||
|
public required FactionPlanStepKind Kind { get; init; }
|
||||||
|
public FactionPlanStepStatus Status { get; set; } = FactionPlanStepStatus.Planned;
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? CommodityId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public string? TargetSiteId { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public int LastEvaluatedCycle { get; set; }
|
||||||
|
public HashSet<string> DependencyStepIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> RequiredFacts { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ProducedFacts { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> IssuedTaskIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionIssuedTaskRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string MergeKey { get; init; }
|
||||||
|
public required FactionIssuedTaskKind Kind { get; init; }
|
||||||
|
public required string ObjectiveId { get; init; }
|
||||||
|
public required string StepId { get; init; }
|
||||||
|
public FactionIssuedTaskState State { get; set; } = FactionIssuedTaskState.Planned;
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? ShipRole { get; set; }
|
||||||
|
public string? CommodityId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetSiteId { get; set; }
|
||||||
|
public int CreatedAtCycle { get; init; }
|
||||||
|
public int UpdatedAtCycle { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionBlackboardRuntime
|
||||||
|
{
|
||||||
|
public int PlanCycle { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||||
|
public int TargetWarshipCount { get; set; }
|
||||||
|
public bool HasWarIndustrySupplyChain { get; set; }
|
||||||
|
public bool HasShipyard { get; set; }
|
||||||
|
public bool HasActiveExpansionProject { get; set; }
|
||||||
|
public string? ActiveExpansionCommodityId { get; set; }
|
||||||
|
public string? ActiveExpansionModuleId { get; set; }
|
||||||
|
public string? ActiveExpansionSiteId { get; set; }
|
||||||
|
public string? ActiveExpansionSystemId { get; set; }
|
||||||
|
public int EnemyFactionCount { get; set; }
|
||||||
|
public int EnemyShipCount { get; set; }
|
||||||
|
public int EnemyStationCount { get; set; }
|
||||||
|
public int MilitaryShipCount { get; set; }
|
||||||
|
public int MinerShipCount { get; set; }
|
||||||
|
public int TransportShipCount { get; set; }
|
||||||
|
public int ConstructorShipCount { get; set; }
|
||||||
|
public int ControlledSystemCount { get; set; }
|
||||||
|
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
|
||||||
|
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
|
||||||
|
public HashSet<string> AvailableShipIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionCommoditySignalRuntime
|
||||||
|
{
|
||||||
|
public required string ItemId { get; init; }
|
||||||
|
public float AvailableStock { get; set; }
|
||||||
|
public float OnHand { get; set; }
|
||||||
|
public float ProductionRatePerSecond { get; set; }
|
||||||
|
public float CommittedProductionRatePerSecond { get; set; }
|
||||||
|
public float UsageRatePerSecond { get; set; }
|
||||||
|
public float NetRatePerSecond { get; set; }
|
||||||
|
public float ProjectedNetRatePerSecond { get; set; }
|
||||||
|
public float LevelSeconds { get; set; }
|
||||||
|
public string Level { get; set; } = "unknown";
|
||||||
|
public float ProjectedProductionRatePerSecond { get; set; }
|
||||||
|
public float BuyBacklog { get; set; }
|
||||||
|
public float ReservedForConstruction { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionThreatSignalRuntime
|
||||||
|
{
|
||||||
|
public required string ScopeId { get; init; }
|
||||||
|
public required string ScopeKind { get; init; }
|
||||||
|
public int EnemyShipCount { get; set; }
|
||||||
|
public int EnemyStationCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CommanderBehaviorRuntime
|
public sealed class CommanderBehaviorRuntime
|
||||||
|
|||||||
54
apps/backend/Industry/Planning/CommodityOperationalSignal.cs
Normal file
54
apps/backend/Industry/Planning/CommodityOperationalSignal.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
namespace SpaceGame.Api.Industry.Planning;
|
||||||
|
|
||||||
|
internal static class CommodityOperationalSignal
|
||||||
|
{
|
||||||
|
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||||
|
{
|
||||||
|
var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
|
||||||
|
var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||||
|
var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
|
||||||
|
|
||||||
|
var levelWeight = commodity.Level switch
|
||||||
|
{
|
||||||
|
CommodityLevelKind.Critical => 140f,
|
||||||
|
CommodityLevelKind.Low => 80f,
|
||||||
|
CommodityLevelKind.Stable => 20f,
|
||||||
|
_ => 0f,
|
||||||
|
};
|
||||||
|
|
||||||
|
return levelWeight
|
||||||
|
+ (productionDeficit * 140f)
|
||||||
|
+ (levelDeficit * 120f)
|
||||||
|
+ backlogPressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||||
|
commodity.ProjectedProductionRatePerSecond > 0.01f
|
||||||
|
&& commodity.ProjectedNetRatePerSecond >= -0.01f
|
||||||
|
&& commodity.LevelSeconds >= targetLevelSeconds
|
||||||
|
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
|
||||||
|
|
||||||
|
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
|
||||||
|
!IsOperational(commodity, targetLevelSeconds)
|
||||||
|
|| commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
|
||||||
|
|
||||||
|
internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
|
||||||
|
{
|
||||||
|
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
|
||||||
|
{
|
||||||
|
return 0.65f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commodity.Level is CommodityLevelKind.Critical)
|
||||||
|
{
|
||||||
|
return 0.72f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
|
||||||
|
{
|
||||||
|
return 0.84f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1f;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,50 +41,27 @@ internal sealed class FactionCommoditySnapshot
|
|||||||
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
|
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||||
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
|
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
|
||||||
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
|
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
|
||||||
|
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
|
||||||
|
internal float LevelSeconds => AvailableStock <= 0.01f
|
||||||
|
? 0f
|
||||||
|
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
|
||||||
|
|
||||||
internal float ShortageHorizonSeconds
|
internal CommodityLevelKind Level =>
|
||||||
|
LevelSeconds switch
|
||||||
{
|
{
|
||||||
get
|
<= 60f => CommodityLevelKind.Critical,
|
||||||
{
|
<= 180f => CommodityLevelKind.Low,
|
||||||
if (ConsumptionRatePerSecond <= 0.01f && BuyBacklog <= 0.01f)
|
<= 480f => CommodityLevelKind.Stable,
|
||||||
{
|
_ => CommodityLevelKind.Surplus,
|
||||||
return float.PositiveInfinity;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NetRatePerSecond >= -0.01f)
|
internal enum CommodityLevelKind
|
||||||
{
|
{
|
||||||
return float.PositiveInfinity;
|
Critical,
|
||||||
}
|
Low,
|
||||||
|
Stable,
|
||||||
return AvailableStock / MathF.Max(0.01f, -NetRatePerSecond);
|
Surplus,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal float ProjectedShortageHorizonSeconds
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (ConsumptionRatePerSecond <= 0.01f && BuyBacklog <= 0.01f)
|
|
||||||
{
|
|
||||||
return float.PositiveInfinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ProjectedNetRatePerSecond >= -0.01f)
|
|
||||||
{
|
|
||||||
return float.PositiveInfinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AvailableStock / MathF.Max(0.01f, -ProjectedNetRatePerSecond);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal float PressureScore =>
|
|
||||||
MathF.Max(0f, (BuyBacklog + ReservedForConstruction) - (OnHand + Inbound))
|
|
||||||
+ MathF.Max(0f, ConsumptionRatePerSecond - ProductionRatePerSecond) * 120f;
|
|
||||||
|
|
||||||
internal float ProjectedPressureScore =>
|
|
||||||
MathF.Max(0f, (BuyBacklog + ReservedForConstruction) - (OnHand + Inbound))
|
|
||||||
+ MathF.Max(0f, ConsumptionRatePerSecond - ProjectedProductionRatePerSecond) * 120f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class FactionEconomyAnalyzer
|
internal static class FactionEconomyAnalyzer
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|||||||
|
|
||||||
internal static class FactionIndustryPlanner
|
internal static class FactionIndustryPlanner
|
||||||
{
|
{
|
||||||
|
private const float CommodityTargetLevelSeconds = 240f;
|
||||||
|
private const float WaterTargetLevelSeconds = 300f;
|
||||||
|
|
||||||
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId)
|
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId)
|
||||||
{
|
{
|
||||||
if (HasActiveExpansionProject(world, factionId))
|
if (HasActiveExpansionProject(world, factionId))
|
||||||
@@ -64,14 +67,13 @@ internal static class FactionIndustryPlanner
|
|||||||
.Select(itemId => new
|
.Select(itemId => new
|
||||||
{
|
{
|
||||||
ItemId = itemId,
|
ItemId = itemId,
|
||||||
HasProducer = FactionHasProducerForCommodity(world, factionId, itemId),
|
Commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId),
|
||||||
Pressure = GetCommodityPressure(world, factionId, itemId),
|
|
||||||
Stockpile = GetCommodityStockpile(world, factionId, itemId),
|
|
||||||
})
|
})
|
||||||
.Where(entry => !entry.HasProducer || entry.Pressure > 0.01f || entry.Stockpile < 120f)
|
.Where(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f
|
||||||
.OrderByDescending(entry => !entry.HasProducer ? 1 : 0)
|
|| CommodityOperationalSignal.IsStrained(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
||||||
.ThenByDescending(entry => entry.Pressure)
|
.OrderByDescending(entry => entry.Commodity.ProjectedProductionRatePerSecond <= 0.01f ? 1 : 0)
|
||||||
.ThenBy(entry => entry.Stockpile)
|
.ThenByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Commodity, GetTargetLevelSeconds(entry.ItemId)))
|
||||||
|
.ThenBy(entry => entry.Commodity.AvailableStock)
|
||||||
.Select(entry => entry.ItemId)
|
.Select(entry => entry.ItemId)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
@@ -301,14 +303,20 @@ internal static class FactionIndustryPlanner
|
|||||||
.GroupBy(order => order.ItemId, StringComparer.Ordinal)
|
.GroupBy(order => order.ItemId, StringComparer.Ordinal)
|
||||||
.ToDictionary(group => group.Key, group => group.Sum(order => order.RemainingAmount), StringComparer.Ordinal);
|
.ToDictionary(group => group.Key, group => group.Sum(order => order.RemainingAmount), StringComparer.Ordinal);
|
||||||
|
|
||||||
if (CommanderPlanningService.FactionCommanderHasDirective(world, factionId, "produce-military-ships"))
|
if (CommanderPlanningService.FactionCommanderHasIssuedTask(world, factionId, FactionIssuedTaskKind.ProduceShips, "military"))
|
||||||
{
|
{
|
||||||
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
||||||
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return demandByItem
|
return demandByItem
|
||||||
.Select(entry => (ItemId: ResolveBottleneckCommodity(world, factionId, entry.Key), Score: entry.Value))
|
.Select(entry =>
|
||||||
|
{
|
||||||
|
var itemId = ResolveBottleneckCommodity(world, factionId, entry.Key);
|
||||||
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
||||||
|
var score = entry.Value + CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
||||||
|
return (ItemId: itemId, Score: score);
|
||||||
|
})
|
||||||
.Where(entry => entry.ItemId is not null)
|
.Where(entry => entry.ItemId is not null)
|
||||||
.GroupBy(entry => entry.ItemId!, StringComparer.Ordinal)
|
.GroupBy(entry => entry.ItemId!, StringComparer.Ordinal)
|
||||||
.Select(group => (ItemId: group.Key, Score: group.Sum(entry => entry.Score)))
|
.Select(group => (ItemId: group.Key, Score: group.Sum(entry => entry.Score)))
|
||||||
@@ -358,7 +366,11 @@ internal static class FactionIndustryPlanner
|
|||||||
|
|
||||||
var weakestUnproducedInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
var weakestUnproducedInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
||||||
.Where(inputId => !FactionHasProducerForCommodity(world, factionId, inputId))
|
.Where(inputId => !FactionHasProducerForCommodity(world, factionId, inputId))
|
||||||
.Select(inputId => (ItemId: inputId, Score: GetCommodityPressure(world, factionId, inputId), Stockpile: GetCommodityStockpile(world, factionId, inputId)))
|
.Select(inputId =>
|
||||||
|
{
|
||||||
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
||||||
|
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)), Stockpile: commodity.AvailableStock);
|
||||||
|
})
|
||||||
.OrderByDescending(entry => entry.Score)
|
.OrderByDescending(entry => entry.Score)
|
||||||
.ThenBy(entry => entry.Stockpile)
|
.ThenBy(entry => entry.Stockpile)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -370,11 +382,15 @@ internal static class FactionIndustryPlanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
var weakestInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
var weakestInput = world.ProductionGraph.GetImmediateInputs(itemId)
|
||||||
.Select(inputId => (ItemId: inputId, Score: GetCommodityPressure(world, factionId, inputId)))
|
.Select(inputId =>
|
||||||
|
{
|
||||||
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(inputId);
|
||||||
|
return (ItemId: inputId, Score: CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(inputId)));
|
||||||
|
})
|
||||||
.OrderByDescending(entry => entry.Score)
|
.OrderByDescending(entry => entry.Score)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
return weakestInput.Score > GetCommodityPressure(world, factionId, itemId) * 0.6f
|
return weakestInput.Score > GetCommodityNeedScore(world, factionId, itemId) * 0.6f
|
||||||
? ResolveBottleneckCommodity(world, factionId, weakestInput.ItemId, visited)
|
? ResolveBottleneckCommodity(world, factionId, weakestInput.ItemId, visited)
|
||||||
: itemId;
|
: itemId;
|
||||||
}
|
}
|
||||||
@@ -419,13 +435,14 @@ internal static class FactionIndustryPlanner
|
|||||||
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
|
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
|
||||||
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
|
||||||
|
|
||||||
private static float GetCommodityPressure(SimulationWorld world, string factionId, string itemId)
|
private static float GetCommodityNeedScore(SimulationWorld world, string factionId, string itemId)
|
||||||
{
|
{
|
||||||
return FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId).ProjectedPressureScore;
|
var commodity = FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId);
|
||||||
|
return CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(itemId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float GetCommodityStockpile(SimulationWorld world, string factionId, string itemId) =>
|
private static float GetTargetLevelSeconds(string itemId) =>
|
||||||
FactionEconomyAnalyzer.Build(world, factionId).GetCommodity(itemId).AvailableStock;
|
string.Equals(itemId, "water", StringComparison.Ordinal) ? WaterTargetLevelSeconds : CommodityTargetLevelSeconds;
|
||||||
|
|
||||||
private static CelestialRuntime? SelectFoundationCelestial(SimulationWorld world, string factionId, string commodityId)
|
private static CelestialRuntime? SelectFoundationCelestial(SimulationWorld world, string factionId, string commodityId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -207,8 +207,11 @@ internal sealed class SimulationProjectionService
|
|||||||
faction.ShipsBuilt,
|
faction.ShipsBuilt,
|
||||||
faction.ShipsLost,
|
faction.ShipsLost,
|
||||||
faction.DefaultPolicySetId,
|
faction.DefaultPolicySetId,
|
||||||
faction.GoapState,
|
faction.StrategicAssessment,
|
||||||
faction.GoapPriorities)).ToList());
|
faction.StrategicPriorities,
|
||||||
|
faction.Blackboard,
|
||||||
|
faction.Objectives,
|
||||||
|
faction.IssuedTasks)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PrimeDeltaBaseline(SimulationWorld world)
|
public void PrimeDeltaBaseline(SimulationWorld world)
|
||||||
@@ -515,10 +518,18 @@ internal sealed class SimulationProjectionService
|
|||||||
|
|
||||||
private static string BuildFactionSignature(FactionRuntime faction, CommanderRuntime? commander)
|
private static string BuildFactionSignature(FactionRuntime faction, CommanderRuntime? commander)
|
||||||
{
|
{
|
||||||
var goapSig = commander?.LastGoalPriorities is { } prios
|
var prioritySig = commander?.LastStrategicPriorities is { } prios
|
||||||
? string.Join(",", prios.Select(p => $"{p.Name}:{p.Priority:0.##}"))
|
? string.Join(",", prios.Select(p => $"{p.Name}:{p.Priority:0.##}"))
|
||||||
: string.Empty;
|
: string.Empty;
|
||||||
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{goapSig}";
|
var objectiveSig = commander?.Objectives is { Count: > 0 } objectives
|
||||||
|
? string.Join(",", objectives.Select(objective =>
|
||||||
|
$"{objective.Kind}:{objective.State}:{objective.Priority:0.##}:{objective.BlockingReason}:{objective.InvalidationReason}"))
|
||||||
|
: string.Empty;
|
||||||
|
var taskSig = commander?.IssuedTasks is { Count: > 0 } tasks
|
||||||
|
? string.Join(",", tasks.Select(task =>
|
||||||
|
$"{task.Kind}:{task.State}:{task.Priority:0.##}:{task.ShipRole}:{task.CommodityId}:{task.TargetFactionId}:{task.TargetSiteId}"))
|
||||||
|
: string.Empty;
|
||||||
|
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{prioritySig}|{objectiveSig}|{taskSig}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
||||||
@@ -759,12 +770,15 @@ internal sealed class SimulationProjectionService
|
|||||||
|
|
||||||
private static FactionDelta ToFactionDelta(FactionRuntime faction, CommanderRuntime? commander)
|
private static FactionDelta ToFactionDelta(FactionRuntime faction, CommanderRuntime? commander)
|
||||||
{
|
{
|
||||||
FactionGoapStateSnapshot? goapState = null;
|
FactionPlanningStateSnapshot? strategicAssessment = null;
|
||||||
IReadOnlyList<FactionGoapPrioritySnapshot>? goapPriorities = null;
|
IReadOnlyList<FactionStrategicPrioritySnapshot>? strategicPriorities = null;
|
||||||
|
FactionBlackboardSnapshot? blackboard = null;
|
||||||
|
IReadOnlyList<FactionObjectiveSnapshot>? objectives = null;
|
||||||
|
IReadOnlyList<FactionIssuedTaskSnapshot>? issuedTasks = null;
|
||||||
|
|
||||||
if (commander?.LastPlanningState is { } ps)
|
if (commander?.LastStrategicAssessment is { } ps)
|
||||||
{
|
{
|
||||||
goapState = new FactionGoapStateSnapshot(
|
strategicAssessment = new FactionPlanningStateSnapshot(
|
||||||
ps.MilitaryShipCount,
|
ps.MilitaryShipCount,
|
||||||
ps.MinerShipCount,
|
ps.MinerShipCount,
|
||||||
ps.TransportShipCount,
|
ps.TransportShipCount,
|
||||||
@@ -773,20 +787,149 @@ internal sealed class SimulationProjectionService
|
|||||||
ps.TargetSystemCount,
|
ps.TargetSystemCount,
|
||||||
ps.HasShipFactory,
|
ps.HasShipFactory,
|
||||||
NormalizeFiniteFloat(ps.OreStockpile),
|
NormalizeFiniteFloat(ps.OreStockpile),
|
||||||
NormalizeFiniteFloat(ps.RefinedMetalsStockpile),
|
NormalizeFiniteFloat(ps.RefinedMetalsAvailableStock),
|
||||||
NormalizeFiniteFloat(ps.RefinedMetalsProductionRate),
|
NormalizeFiniteFloat(ps.RefinedMetalsUsageRate),
|
||||||
NormalizeFiniteFloat(ps.HullpartsStockpile),
|
NormalizeFiniteFloat(ps.RefinedMetalsProjectedProductionRate),
|
||||||
NormalizeFiniteFloat(ps.HullpartsProductionRate),
|
NormalizeFiniteFloat(ps.RefinedMetalsProjectedNetRate),
|
||||||
NormalizeFiniteFloat(ps.ClaytronicsStockpile),
|
NormalizeFiniteFloat(ps.RefinedMetalsLevelSeconds),
|
||||||
NormalizeFiniteFloat(ps.ClaytronicsProductionRate),
|
ps.RefinedMetalsLevel,
|
||||||
NormalizeFiniteFloat(ps.WaterStockpile),
|
NormalizeFiniteFloat(ps.HullpartsAvailableStock),
|
||||||
NormalizeFiniteFloat(ps.WaterProductionRate),
|
NormalizeFiniteFloat(ps.HullpartsUsageRate),
|
||||||
NormalizeFiniteFloat(ps.WaterShortageHorizonSeconds));
|
NormalizeFiniteFloat(ps.HullpartsProjectedProductionRate),
|
||||||
|
NormalizeFiniteFloat(ps.HullpartsProjectedNetRate),
|
||||||
|
NormalizeFiniteFloat(ps.HullpartsLevelSeconds),
|
||||||
|
ps.HullpartsLevel,
|
||||||
|
NormalizeFiniteFloat(ps.ClaytronicsAvailableStock),
|
||||||
|
NormalizeFiniteFloat(ps.ClaytronicsUsageRate),
|
||||||
|
NormalizeFiniteFloat(ps.ClaytronicsProjectedProductionRate),
|
||||||
|
NormalizeFiniteFloat(ps.ClaytronicsProjectedNetRate),
|
||||||
|
NormalizeFiniteFloat(ps.ClaytronicsLevelSeconds),
|
||||||
|
ps.ClaytronicsLevel,
|
||||||
|
NormalizeFiniteFloat(ps.WaterAvailableStock),
|
||||||
|
NormalizeFiniteFloat(ps.WaterUsageRate),
|
||||||
|
NormalizeFiniteFloat(ps.WaterProjectedProductionRate),
|
||||||
|
NormalizeFiniteFloat(ps.WaterProjectedNetRate),
|
||||||
|
NormalizeFiniteFloat(ps.WaterLevelSeconds),
|
||||||
|
ps.WaterLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commander?.LastGoalPriorities is { } prios)
|
if (commander?.LastStrategicPriorities is { } prios)
|
||||||
{
|
{
|
||||||
goapPriorities = prios.Select(p => new FactionGoapPrioritySnapshot(p.Name, p.Priority)).ToList();
|
strategicPriorities = prios.Select(p => new FactionStrategicPrioritySnapshot(p.Name, p.Priority)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commander?.FactionBlackboard is { } bb)
|
||||||
|
{
|
||||||
|
blackboard = new FactionBlackboardSnapshot(
|
||||||
|
bb.PlanCycle,
|
||||||
|
bb.UpdatedAtUtc,
|
||||||
|
bb.TargetWarshipCount,
|
||||||
|
bb.HasWarIndustrySupplyChain,
|
||||||
|
bb.HasShipyard,
|
||||||
|
bb.HasActiveExpansionProject,
|
||||||
|
bb.ActiveExpansionCommodityId,
|
||||||
|
bb.ActiveExpansionModuleId,
|
||||||
|
bb.ActiveExpansionSiteId,
|
||||||
|
bb.ActiveExpansionSystemId,
|
||||||
|
bb.EnemyFactionCount,
|
||||||
|
bb.EnemyShipCount,
|
||||||
|
bb.EnemyStationCount,
|
||||||
|
bb.MilitaryShipCount,
|
||||||
|
bb.MinerShipCount,
|
||||||
|
bb.TransportShipCount,
|
||||||
|
bb.ConstructorShipCount,
|
||||||
|
bb.ControlledSystemCount,
|
||||||
|
bb.CommoditySignals.Select(signal => new FactionCommoditySignalSnapshot(
|
||||||
|
signal.ItemId,
|
||||||
|
NormalizeFiniteFloat(signal.AvailableStock),
|
||||||
|
NormalizeFiniteFloat(signal.OnHand),
|
||||||
|
NormalizeFiniteFloat(signal.ProductionRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.CommittedProductionRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.UsageRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.NetRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.ProjectedNetRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.LevelSeconds),
|
||||||
|
signal.Level,
|
||||||
|
NormalizeFiniteFloat(signal.ProjectedProductionRatePerSecond),
|
||||||
|
NormalizeFiniteFloat(signal.BuyBacklog),
|
||||||
|
NormalizeFiniteFloat(signal.ReservedForConstruction))).ToList(),
|
||||||
|
bb.ThreatSignals.Select(signal => new FactionThreatSignalSnapshot(
|
||||||
|
signal.ScopeId,
|
||||||
|
signal.ScopeKind,
|
||||||
|
signal.EnemyShipCount,
|
||||||
|
signal.EnemyStationCount)).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commander?.Objectives is { Count: > 0 } runtimeObjectives)
|
||||||
|
{
|
||||||
|
objectives = runtimeObjectives
|
||||||
|
.OrderByDescending(objective => objective.Priority)
|
||||||
|
.Select(objective => new FactionObjectiveSnapshot(
|
||||||
|
objective.Id,
|
||||||
|
objective.Kind.ToString(),
|
||||||
|
objective.State.ToString(),
|
||||||
|
objective.Priority,
|
||||||
|
objective.ParentObjectiveId,
|
||||||
|
objective.TargetFactionId,
|
||||||
|
objective.TargetSystemId,
|
||||||
|
objective.TargetSiteId,
|
||||||
|
objective.TargetRegionId,
|
||||||
|
objective.CommodityId,
|
||||||
|
objective.ModuleId,
|
||||||
|
objective.BudgetWeight,
|
||||||
|
objective.SlotCost,
|
||||||
|
objective.CreatedAtCycle,
|
||||||
|
objective.UpdatedAtCycle,
|
||||||
|
objective.InvalidationReason,
|
||||||
|
objective.BlockingReason,
|
||||||
|
objective.PrerequisiteObjectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||||
|
objective.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||||
|
objective.Steps
|
||||||
|
.OrderByDescending(step => step.Priority)
|
||||||
|
.Select(step => new FactionPlanStepSnapshot(
|
||||||
|
step.Id,
|
||||||
|
step.Kind.ToString(),
|
||||||
|
step.Status.ToString(),
|
||||||
|
step.Priority,
|
||||||
|
step.CommodityId,
|
||||||
|
step.ModuleId,
|
||||||
|
step.TargetFactionId,
|
||||||
|
step.TargetSiteId,
|
||||||
|
step.BlockingReason,
|
||||||
|
step.Notes,
|
||||||
|
step.LastEvaluatedCycle,
|
||||||
|
step.DependencyStepIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||||
|
step.RequiredFacts.OrderBy(fact => fact, StringComparer.Ordinal).ToList(),
|
||||||
|
step.ProducedFacts.OrderBy(fact => fact, StringComparer.Ordinal).ToList(),
|
||||||
|
step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||||
|
step.IssuedTaskIds.OrderBy(id => id, StringComparer.Ordinal).ToList()))
|
||||||
|
.ToList()))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commander?.IssuedTasks is { Count: > 0 } runtimeTasks)
|
||||||
|
{
|
||||||
|
issuedTasks = runtimeTasks
|
||||||
|
.OrderByDescending(task => task.Priority)
|
||||||
|
.Select(task => new FactionIssuedTaskSnapshot(
|
||||||
|
task.Id,
|
||||||
|
task.Kind.ToString(),
|
||||||
|
task.State.ToString(),
|
||||||
|
task.ObjectiveId,
|
||||||
|
task.StepId,
|
||||||
|
task.Priority,
|
||||||
|
task.ShipRole,
|
||||||
|
task.CommodityId,
|
||||||
|
task.ModuleId,
|
||||||
|
task.TargetFactionId,
|
||||||
|
task.TargetSystemId,
|
||||||
|
task.TargetSiteId,
|
||||||
|
task.CreatedAtCycle,
|
||||||
|
task.UpdatedAtCycle,
|
||||||
|
task.BlockingReason,
|
||||||
|
task.Notes,
|
||||||
|
task.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList()))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FactionDelta(
|
return new FactionDelta(
|
||||||
@@ -800,8 +943,11 @@ internal sealed class SimulationProjectionService
|
|||||||
faction.ShipsBuilt,
|
faction.ShipsBuilt,
|
||||||
faction.ShipsLost,
|
faction.ShipsLost,
|
||||||
faction.DefaultPolicySetId,
|
faction.DefaultPolicySetId,
|
||||||
goapState,
|
strategicAssessment,
|
||||||
goapPriorities);
|
strategicPriorities,
|
||||||
|
blackboard,
|
||||||
|
objectives,
|
||||||
|
issuedTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace SpaceGame.Api.Stations.Simulation;
|
|||||||
|
|
||||||
internal sealed class InfrastructureSimulationService
|
internal sealed class InfrastructureSimulationService
|
||||||
{
|
{
|
||||||
|
private const float CommodityTargetLevelSeconds = 240f;
|
||||||
|
private const float EnergyTargetLevelSeconds = 240f;
|
||||||
|
|
||||||
internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
internal void UpdateClaims(SimulationWorld world, ICollection<SimulationEventRecord> events)
|
||||||
{
|
{
|
||||||
foreach (var claim in world.Claims)
|
foreach (var claim in world.Claims)
|
||||||
@@ -259,16 +262,19 @@ internal sealed class InfrastructureSimulationService
|
|||||||
var currentCount = CountModules(station.InstalledModules, objectiveModuleId);
|
var currentCount = CountModules(station.InstalledModules, objectiveModuleId);
|
||||||
var marginalOutputRate = EstimateMarginalOutputRate(world, station, objectiveModuleId, objectiveCommodityId);
|
var marginalOutputRate = EstimateMarginalOutputRate(world, station, objectiveModuleId, objectiveCommodityId);
|
||||||
var constructionImpact = EstimateConstructionBottleneckImpact(world, objectiveModuleId, constructionDemandByItem);
|
var constructionImpact = EstimateConstructionBottleneckImpact(world, objectiveModuleId, constructionDemandByItem);
|
||||||
var score = 90f + commodity.ProjectedPressureScore + (marginalOutputRate * 900f) + constructionImpact;
|
var score = 90f
|
||||||
|
+ CommodityOperationalSignal.ComputeNeedScore(commodity, GetTargetLevelSeconds(objectiveCommodityId))
|
||||||
|
+ (marginalOutputRate * 900f)
|
||||||
|
+ constructionImpact;
|
||||||
|
|
||||||
if (currentCount == 0)
|
if (currentCount == 0)
|
||||||
{
|
{
|
||||||
score += 80f;
|
score += 80f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!float.IsPositiveInfinity(commodity.ProjectedShortageHorizonSeconds))
|
if (commodity.LevelSeconds < GetTargetLevelSeconds(objectiveCommodityId))
|
||||||
{
|
{
|
||||||
score += MathF.Max(0f, 300f - commodity.ProjectedShortageHorizonSeconds) * 0.3f;
|
score += MathF.Max(0f, GetTargetLevelSeconds(objectiveCommodityId) - commodity.LevelSeconds) * 0.3f;
|
||||||
}
|
}
|
||||||
|
|
||||||
score *= EstimateObjectiveExpansionFeasibility(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
score *= EstimateObjectiveExpansionFeasibility(world, station, economy, objectiveModuleId, objectiveCommodityId);
|
||||||
@@ -287,16 +293,19 @@ internal sealed class InfrastructureSimulationService
|
|||||||
var currentCount = CountModules(station.InstalledModules, "module_gen_prod_energycells_01");
|
var currentCount = CountModules(station.InstalledModules, "module_gen_prod_energycells_01");
|
||||||
var constructionImpact = EstimateConstructionBottleneckImpact(world, "module_gen_prod_energycells_01", constructionDemandByItem);
|
var constructionImpact = EstimateConstructionBottleneckImpact(world, "module_gen_prod_energycells_01", constructionDemandByItem);
|
||||||
var readinessUnlock = EstimateSupportUnlockScore(world, station, economy, "module_gen_prod_energycells_01");
|
var readinessUnlock = EstimateSupportUnlockScore(world, station, economy, "module_gen_prod_energycells_01");
|
||||||
var score = 40f + energy.ProjectedPressureScore * 0.5f + constructionImpact + readinessUnlock;
|
var score = 40f
|
||||||
|
+ CommodityOperationalSignal.ComputeNeedScore(energy, EnergyTargetLevelSeconds) * 0.5f
|
||||||
|
+ constructionImpact
|
||||||
|
+ readinessUnlock;
|
||||||
|
|
||||||
if (currentCount == 0)
|
if (currentCount == 0)
|
||||||
{
|
{
|
||||||
score += 70f;
|
score += 70f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!float.IsPositiveInfinity(energy.ProjectedShortageHorizonSeconds))
|
if (energy.LevelSeconds < EnergyTargetLevelSeconds)
|
||||||
{
|
{
|
||||||
score += MathF.Max(0f, 240f - energy.ProjectedShortageHorizonSeconds) * 0.2f;
|
score += MathF.Max(0f, EnergyTargetLevelSeconds - energy.LevelSeconds) * 0.2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return score - (currentCount * 40f);
|
return score - (currentCount * 40f);
|
||||||
@@ -433,17 +442,9 @@ internal sealed class InfrastructureSimulationService
|
|||||||
foreach (var input in recipe.Inputs)
|
foreach (var input in recipe.Inputs)
|
||||||
{
|
{
|
||||||
var inputCommodity = economy.GetCommodity(input.ItemId);
|
var inputCommodity = economy.GetCommodity(input.ItemId);
|
||||||
if (inputCommodity.AvailableStock <= 0.01f && inputCommodity.ProjectedProductionRatePerSecond <= 0.01f)
|
feasibility *= CommodityOperationalSignal.ComputeFeasibilityFactor(
|
||||||
{
|
inputCommodity,
|
||||||
feasibility *= 0.65f;
|
GetTargetLevelSeconds(input.ItemId));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!float.IsPositiveInfinity(inputCommodity.ProjectedShortageHorizonSeconds)
|
|
||||||
&& inputCommodity.ProjectedShortageHorizonSeconds < 180f)
|
|
||||||
{
|
|
||||||
feasibility *= 0.82f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +506,8 @@ internal sealed class InfrastructureSimulationService
|
|||||||
{
|
{
|
||||||
inputFactor *= 0.95f + (availableStockRatio * 0.05f);
|
inputFactor *= 0.95f + (availableStockRatio * 0.05f);
|
||||||
}
|
}
|
||||||
else if (commodity.ProjectedProductionRatePerSecond > 0.01f)
|
else if (commodity.ProjectedProductionRatePerSecond > 0.01f
|
||||||
|
&& commodity.Level is not CommodityLevelKind.Critical)
|
||||||
{
|
{
|
||||||
inputFactor *= 0.82f + (availableStockRatio * 0.08f);
|
inputFactor *= 0.82f + (availableStockRatio * 0.08f);
|
||||||
}
|
}
|
||||||
@@ -719,6 +721,11 @@ internal sealed class InfrastructureSimulationService
|
|||||||
private static bool ObjectiveNeedsEnergy(SimulationWorld world, string objectiveCommodityId) =>
|
private static bool ObjectiveNeedsEnergy(SimulationWorld world, string objectiveCommodityId) =>
|
||||||
world.ProductionGraph.GetImmediateInputs(objectiveCommodityId).Contains("energycells", StringComparer.Ordinal);
|
world.ProductionGraph.GetImmediateInputs(objectiveCommodityId).Contains("energycells", StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private static float GetTargetLevelSeconds(string commodityId) =>
|
||||||
|
string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
|
||||||
|
string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
|
||||||
|
CommodityTargetLevelSeconds;
|
||||||
|
|
||||||
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
||||||
{
|
{
|
||||||
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
var nextModuleId = GetNextStationModuleToBuild(station, world);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ internal sealed class StationSimulationService
|
|||||||
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||||
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||||
&& FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships")
|
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
||||||
? 90f
|
? 90f
|
||||||
: 0f;
|
: 0f;
|
||||||
|
|
||||||
@@ -163,14 +163,33 @@ internal sealed class StationSimulationService
|
|||||||
var priority = (float)recipe.Priority;
|
var priority = (float)recipe.Priority;
|
||||||
|
|
||||||
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
|
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
|
||||||
var fleetPressure = FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships") ? 1f : 0f;
|
var fleetPressure = FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military") ? 1f : 0f;
|
||||||
priority += GetStationRecipePriorityAdjustment(station, recipe, expansionPressure, fleetPressure);
|
priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure);
|
||||||
|
|
||||||
return priority;
|
return priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float GetStationRecipePriorityAdjustment(StationRuntime station, RecipeDefinition recipe, float expansionPressure, float fleetPressure)
|
private static float GetStationRecipePriorityAdjustment(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, float expansionPressure, float fleetPressure)
|
||||||
{
|
{
|
||||||
|
if (recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition))
|
||||||
|
{
|
||||||
|
var shipPressure = GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind);
|
||||||
|
return shipDefinition.Kind switch
|
||||||
|
{
|
||||||
|
"military" => recipe.Id switch
|
||||||
|
{
|
||||||
|
"frigate-construction" => 320f * shipPressure,
|
||||||
|
"destroyer-construction" => 200f * shipPressure,
|
||||||
|
"cruiser-construction" => 120f * shipPressure,
|
||||||
|
_ => 160f * shipPressure,
|
||||||
|
},
|
||||||
|
"construction" => 260f * shipPressure,
|
||||||
|
"mining" => 250f * shipPressure,
|
||||||
|
"transport" => 230f * shipPressure,
|
||||||
|
_ => 0f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var outputItemIds = recipe.Outputs
|
var outputItemIds = recipe.Outputs
|
||||||
.Select(output => output.ItemId)
|
.Select(output => output.ItemId)
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
@@ -201,9 +220,6 @@ internal sealed class StationSimulationService
|
|||||||
{
|
{
|
||||||
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
|
"command-bridge-module-assembly" or "reactor-core-module-assembly" or "capacitor-bank-module-assembly" or "ion-drive-module-assembly" or "ftl-core-module-assembly" or "gun-turret-module-assembly"
|
||||||
=> 220f * MathF.Max(expansionPressure, fleetPressure),
|
=> 220f * MathF.Max(expansionPressure, fleetPressure),
|
||||||
"frigate-construction" => 320f * MathF.Max(expansionPressure, fleetPressure),
|
|
||||||
"destroyer-construction" => 200f * MathF.Max(expansionPressure, fleetPressure),
|
|
||||||
"cruiser-construction" => 120f * MathF.Max(expansionPressure, fleetPressure),
|
|
||||||
"ammo-fabrication" => -80f * expansionPressure,
|
"ammo-fabrication" => -80f * expansionPressure,
|
||||||
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
|
"trade-hub-assembly" or "refinery-assembly" or "farm-ring-assembly" or "manufactory-assembly" or "shipyard-assembly" or "defense-grid-assembly" or "stargate-assembly"
|
||||||
=> -120f * expansionPressure,
|
=> -120f * expansionPressure,
|
||||||
@@ -228,8 +244,7 @@ internal sealed class StationSimulationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal)
|
if (!FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, shipDefinition.Kind))
|
||||||
|| !FactionCommanderHasDirective(world, station.FactionId, "produce-military-ships"))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -431,16 +446,17 @@ internal sealed class StationSimulationService
|
|||||||
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
||||||
{
|
{
|
||||||
var commodity = economy.GetCommodity(itemId);
|
var commodity = economy.GetCommodity(itemId);
|
||||||
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
|
if (commodity.Level == CommodityLevelKind.Critical)
|
||||||
{
|
{
|
||||||
return MathF.Max(0f, baseReserve);
|
return baseReserve * 1.6f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return commodity.ShortageHorizonSeconds < 180f
|
return commodity.Level switch
|
||||||
? baseReserve * 1.5f
|
{
|
||||||
: commodity.ShortageHorizonSeconds < 360f
|
CommodityLevelKind.Low => baseReserve * 1.25f,
|
||||||
? baseReserve * 1.2f
|
CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseReserve * 1.1f,
|
||||||
: baseReserve;
|
_ => MathF.Max(0f, baseReserve),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ScaleSupplyTriggerByEconomy(FactionEconomySnapshot economy, string itemId, float baseTrigger)
|
private static float ScaleSupplyTriggerByEconomy(FactionEconomySnapshot economy, string itemId, float baseTrigger)
|
||||||
@@ -452,26 +468,38 @@ internal sealed class StationSimulationService
|
|||||||
private static float ScaleDemandValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
private static float ScaleDemandValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
||||||
{
|
{
|
||||||
var commodity = economy.GetCommodity(itemId);
|
var commodity = economy.GetCommodity(itemId);
|
||||||
if (float.IsPositiveInfinity(commodity.ShortageHorizonSeconds))
|
return commodity.Level switch
|
||||||
{
|
{
|
||||||
return commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.3f;
|
CommodityLevelKind.Critical => baseValuation * 1.6f,
|
||||||
}
|
CommodityLevelKind.Low => baseValuation * 1.3f,
|
||||||
|
CommodityLevelKind.Stable when commodity.ProjectedNetRatePerSecond < -0.01f => baseValuation * 1.15f,
|
||||||
return commodity.ShortageHorizonSeconds < 180f
|
CommodityLevelKind.Surplus when commodity.ProjectedNetRatePerSecond > 0.01f => baseValuation * 0.9f,
|
||||||
? baseValuation * 1.5f
|
_ => commodity.ProductionRatePerSecond > 0.01f ? baseValuation : baseValuation * 1.15f,
|
||||||
: commodity.ShortageHorizonSeconds < 360f
|
};
|
||||||
? baseValuation * 1.25f
|
|
||||||
: baseValuation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ScaleSupplyValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
private static float ScaleSupplyValuation(FactionEconomySnapshot economy, string itemId, float baseValuation)
|
||||||
{
|
{
|
||||||
var commodity = economy.GetCommodity(itemId);
|
var commodity = economy.GetCommodity(itemId);
|
||||||
return commodity.NetRatePerSecond > 0.01f && commodity.ShortageHorizonSeconds > 600f
|
return commodity.Level == CommodityLevelKind.Surplus && commodity.NetRatePerSecond > 0.01f
|
||||||
? baseValuation * 0.75f
|
? baseValuation * 0.75f
|
||||||
|
: commodity.Level == CommodityLevelKind.Critical
|
||||||
|
? baseValuation * 1.15f
|
||||||
: baseValuation;
|
: baseValuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind)
|
||||||
|
{
|
||||||
|
var factionCommander = FindFactionCommander(world, factionId);
|
||||||
|
var task = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ProduceShips, shipKind);
|
||||||
|
if (task is null)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.State == FactionIssuedTaskState.Blocked ? 0.4f : 1f;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||||
{
|
{
|
||||||
var totalLagrangePoints = world.Celestials.Count(node =>
|
var totalLagrangePoints = world.Celestials.Count(node =>
|
||||||
|
|||||||
31
apps/viewer/package-lock.json
generated
31
apps/viewer/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "space-game-viewer",
|
"name": "space-game-viewer",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"three": "^0.179.1",
|
"three": "^0.179.1",
|
||||||
"vue": "^3.5.21"
|
"vue": "^3.5.21"
|
||||||
@@ -1118,6 +1119,36 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/vue-table": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"three": "^0.179.1",
|
"three": "^0.179.1",
|
||||||
"vue": "^3.5.21"
|
"vue": "^3.5.21"
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { GameViewer } from "./GameViewer";
|
|||||||
import CollapsibleHudPanel from "./components/CollapsibleHudPanel.vue";
|
import CollapsibleHudPanel from "./components/CollapsibleHudPanel.vue";
|
||||||
import HtmlInfoPanel from "./components/HtmlInfoPanel.vue";
|
import HtmlInfoPanel from "./components/HtmlInfoPanel.vue";
|
||||||
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
||||||
import ViewerOpsStrip from "./components/ViewerOpsStrip.vue";
|
import GmOpsWindow from "./components/gm/GmOpsWindow.vue";
|
||||||
import { createViewerHudState } from "./viewerHudState";
|
import { createViewerHudState } from "./viewerHudState";
|
||||||
import { useViewerSelectionStore } from "./ui/stores/viewerSelection";
|
import { useViewerSelectionStore } from "./ui/stores/viewerSelection";
|
||||||
import type { Selectable } from "./viewerTypes";
|
import type { Selectable } from "./viewerTypes";
|
||||||
|
|
||||||
const canvasHostEl = ref<HTMLDivElement | null>(null);
|
const canvasHostEl = ref<HTMLDivElement | null>(null);
|
||||||
const opsStripHostEl = ref<HTMLDivElement | null>(null);
|
|
||||||
const historyLayerHostEl = ref<HTMLDivElement | null>(null);
|
const historyLayerHostEl = ref<HTMLDivElement | null>(null);
|
||||||
const marqueeEl = ref<HTMLDivElement | null>(null);
|
const marqueeEl = ref<HTMLDivElement | null>(null);
|
||||||
const hoverLabelEl = ref<HTMLDivElement | null>(null);
|
const hoverLabelEl = ref<HTMLDivElement | null>(null);
|
||||||
@@ -22,11 +21,12 @@ const selectionStore = useViewerSelectionStore();
|
|||||||
const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore);
|
const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore);
|
||||||
let viewer: GameViewer | undefined;
|
let viewer: GameViewer | undefined;
|
||||||
|
|
||||||
|
const gmOpsOpen = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (
|
if (
|
||||||
!canvasHostEl.value
|
!canvasHostEl.value
|
||||||
|| !opsStripHostEl.value
|
|
||||||
|| !historyLayerHostEl.value
|
|| !historyLayerHostEl.value
|
||||||
|| !marqueeEl.value
|
|| !marqueeEl.value
|
||||||
|| !hoverLabelEl.value
|
|| !hoverLabelEl.value
|
||||||
@@ -38,7 +38,6 @@ onMounted(async () => {
|
|||||||
viewer = new GameViewer(canvasHostEl.value, {
|
viewer = new GameViewer(canvasHostEl.value, {
|
||||||
state: hudState,
|
state: hudState,
|
||||||
selectionStore,
|
selectionStore,
|
||||||
opsStripEl: opsStripHostEl.value,
|
|
||||||
historyLayerEl: historyLayerHostEl.value,
|
historyLayerEl: historyLayerHostEl.value,
|
||||||
marqueeEl: marqueeEl.value,
|
marqueeEl: marqueeEl.value,
|
||||||
hoverLabelEl: hoverLabelEl.value,
|
hoverLabelEl: hoverLabelEl.value,
|
||||||
@@ -146,13 +145,19 @@ function onFocusSelection(selection: Selectable, cameraMode?: "follow" | "tactic
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="opsStripHostEl">
|
<button
|
||||||
<ViewerOpsStrip
|
type="button"
|
||||||
:state="hudState.opsStrip"
|
class="gm-console-toggle"
|
||||||
@history="onOpenHistory"
|
@click="gmOpsOpen = !gmOpsOpen"
|
||||||
@focus="onFocusSelection"
|
>
|
||||||
|
{{ gmOpsOpen ? "Close" : "GM Console" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<GmOpsWindow
|
||||||
|
v-if="gmOpsOpen"
|
||||||
|
@close="gmOpsOpen = false"
|
||||||
|
@focus="(id, kind) => onFocusSelection({ kind, id }, kind === 'ship' ? 'follow' : 'tactical')"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref="marqueeEl"
|
ref="marqueeEl"
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export class ViewerAppController {
|
|||||||
|
|
||||||
readonly hudState: ViewerHudState;
|
readonly hudState: ViewerHudState;
|
||||||
readonly selectionStore: ViewerSelectionStore;
|
readonly selectionStore: ViewerSelectionStore;
|
||||||
private readonly opsStripEl: HTMLDivElement;
|
|
||||||
private readonly historyLayerEl: HTMLDivElement;
|
private readonly historyLayerEl: HTMLDivElement;
|
||||||
private readonly marqueeEl: HTMLDivElement;
|
private readonly marqueeEl: HTMLDivElement;
|
||||||
private readonly hoverLabelEl: HTMLDivElement;
|
private readonly hoverLabelEl: HTMLDivElement;
|
||||||
@@ -122,7 +121,6 @@ export class ViewerAppController {
|
|||||||
this.container = container;
|
this.container = container;
|
||||||
this.hudState = hud.state;
|
this.hudState = hud.state;
|
||||||
this.selectionStore = hud.selectionStore;
|
this.selectionStore = hud.selectionStore;
|
||||||
this.opsStripEl = hud.opsStripEl;
|
|
||||||
this.historyLayerEl = hud.historyLayerEl;
|
this.historyLayerEl = hud.historyLayerEl;
|
||||||
this.marqueeEl = hud.marqueeEl;
|
this.marqueeEl = hud.marqueeEl;
|
||||||
this.hoverLabelEl = hud.hoverLabelEl;
|
this.hoverLabelEl = hud.hoverLabelEl;
|
||||||
|
|||||||
617
apps/viewer/src/components/gm/GmOpsWindow.vue
Normal file
617
apps/viewer/src/components/gm/GmOpsWindow.vue
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import {
|
||||||
|
useVueTable,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
createColumnHelper,
|
||||||
|
FlexRender,
|
||||||
|
type SortingState,
|
||||||
|
type ColumnOrderState,
|
||||||
|
type Updater,
|
||||||
|
} from "@tanstack/vue-table";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import GmWindow from "./GmWindow.vue";
|
||||||
|
import { useGmStore } from "../../ui/stores/gmStore";
|
||||||
|
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||||
|
import type { ShipSnapshot } from "../../contractsShips";
|
||||||
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
|
import type { FactionSnapshot } from "../../contractsFactions";
|
||||||
|
|
||||||
|
// ── Column ordering composable ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
function useColumnOrder(initialIds: string[]) {
|
||||||
|
const columnOrder = ref<ColumnOrderState>([...initialIds]);
|
||||||
|
const draggingId = ref<string | null>(null);
|
||||||
|
const overId = ref<string | null>(null);
|
||||||
|
|
||||||
|
function onColumnOrderChange(updater: Updater<ColumnOrderState>) {
|
||||||
|
columnOrder.value = typeof updater === "function" ? updater(columnOrder.value) : updater;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(e: DragEvent, id: string) {
|
||||||
|
draggingId.value = id;
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
e.dataTransfer.setData("text/plain", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(e: DragEvent, id: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||||
|
overId.value = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragLeave() {
|
||||||
|
overId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(targetId: string) {
|
||||||
|
if (!draggingId.value || draggingId.value === targetId) return;
|
||||||
|
const order = [...columnOrder.value];
|
||||||
|
const from = order.indexOf(draggingId.value);
|
||||||
|
const to = order.indexOf(targetId);
|
||||||
|
order.splice(from, 1);
|
||||||
|
order.splice(to, 0, draggingId.value);
|
||||||
|
columnOrder.value = order;
|
||||||
|
draggingId.value = null;
|
||||||
|
overId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
draggingId.value = null;
|
||||||
|
overId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { columnOrder, draggingId, overId, onColumnOrderChange, onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd };
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
focus: [id: string, kind: "ship" | "station"];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
type TabId = "ships" | "stations" | "factions";
|
||||||
|
const activeTab = ref<TabId>("ships");
|
||||||
|
|
||||||
|
const gmStore = useGmStore();
|
||||||
|
const selectionStore = useViewerSelectionStore();
|
||||||
|
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||||
|
|
||||||
|
// Faction name lookup
|
||||||
|
const factionMap = computed(() =>
|
||||||
|
new Map(gmStore.factions.map((f) => [f.id, f.label])),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Ships table ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type ShipRow = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
class: string;
|
||||||
|
faction: string;
|
||||||
|
system: string;
|
||||||
|
state: string;
|
||||||
|
behavior: string;
|
||||||
|
task: string;
|
||||||
|
cargo: number;
|
||||||
|
health: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shipRows = computed<ShipRow[]>(() =>
|
||||||
|
gmStore.ships.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
label: s.label,
|
||||||
|
class: s.class,
|
||||||
|
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||||
|
system: s.systemId,
|
||||||
|
state: s.state,
|
||||||
|
behavior: s.defaultBehaviorKind + (s.behaviorPhase ? ` · ${s.behaviorPhase}` : ""),
|
||||||
|
task: s.controllerTaskKind,
|
||||||
|
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||||
|
health: Math.round(s.health),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||||
|
const shipColumns = [
|
||||||
|
shipColumnHelper.accessor("label", { header: "Name" }),
|
||||||
|
shipColumnHelper.accessor("class", { header: "Class" }),
|
||||||
|
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||||
|
shipColumnHelper.accessor("system", { header: "System" }),
|
||||||
|
shipColumnHelper.accessor("state", { header: "State" }),
|
||||||
|
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
||||||
|
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||||
|
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||||
|
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const shipFilter = ref("");
|
||||||
|
const shipSorting = ref<SortingState>([]);
|
||||||
|
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "behavior", "task", "cargo", "health"]);
|
||||||
|
|
||||||
|
const shipTable = useVueTable({
|
||||||
|
get data() { return shipRows.value; },
|
||||||
|
columns: shipColumns,
|
||||||
|
state: {
|
||||||
|
get globalFilter() { return shipFilter.value; },
|
||||||
|
get sorting() { return shipSorting.value; },
|
||||||
|
get columnOrder() { return shipOrder.columnOrder.value; },
|
||||||
|
},
|
||||||
|
onGlobalFilterChange: (v) => { shipFilter.value = String(v); },
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
shipSorting.value = typeof updater === "function" ? updater(shipSorting.value) : updater;
|
||||||
|
},
|
||||||
|
onColumnOrderChange: shipOrder.onColumnOrderChange,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stations table ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type StationRow = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
category: string;
|
||||||
|
faction: string;
|
||||||
|
system: string;
|
||||||
|
docked: string;
|
||||||
|
cargo: number;
|
||||||
|
population: number;
|
||||||
|
modules: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stationRows = computed<StationRow[]>(() =>
|
||||||
|
gmStore.stations.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
label: s.label,
|
||||||
|
category: s.category,
|
||||||
|
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||||
|
system: s.systemId,
|
||||||
|
docked: `${s.dockedShips} / ${s.dockingPads}`,
|
||||||
|
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||||
|
population: Math.round(s.population),
|
||||||
|
modules: s.installedModules.length,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const stationColumnHelper = createColumnHelper<StationRow>();
|
||||||
|
const stationColumns = [
|
||||||
|
stationColumnHelper.accessor("label", { header: "Name" }),
|
||||||
|
stationColumnHelper.accessor("category", { header: "Category" }),
|
||||||
|
stationColumnHelper.accessor("faction", { header: "Faction" }),
|
||||||
|
stationColumnHelper.accessor("system", { header: "System" }),
|
||||||
|
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||||
|
stationColumnHelper.accessor("cargo", { header: "Cargo" }),
|
||||||
|
stationColumnHelper.accessor("population", { header: "Pop" }),
|
||||||
|
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const stationFilter = ref("");
|
||||||
|
const stationSorting = ref<SortingState>([]);
|
||||||
|
const stationOrder = useColumnOrder(["label", "category", "faction", "system", "docked", "cargo", "population", "modules"]);
|
||||||
|
|
||||||
|
const stationTable = useVueTable({
|
||||||
|
get data() { return stationRows.value; },
|
||||||
|
columns: stationColumns,
|
||||||
|
state: {
|
||||||
|
get globalFilter() { return stationFilter.value; },
|
||||||
|
get sorting() { return stationSorting.value; },
|
||||||
|
get columnOrder() { return stationOrder.columnOrder.value; },
|
||||||
|
},
|
||||||
|
onGlobalFilterChange: (v) => { stationFilter.value = String(v); },
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
stationSorting.value = typeof updater === "function" ? updater(stationSorting.value) : updater;
|
||||||
|
},
|
||||||
|
onColumnOrderChange: stationOrder.onColumnOrderChange,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Factions table ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type FactionRow = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
credits: number;
|
||||||
|
population: number;
|
||||||
|
military: number;
|
||||||
|
miners: number;
|
||||||
|
transport: number;
|
||||||
|
constructors: number;
|
||||||
|
systems: string;
|
||||||
|
ore: number;
|
||||||
|
shipsBuilt: number;
|
||||||
|
shipsLost: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const factionRows = computed<FactionRow[]>(() =>
|
||||||
|
gmStore.factions.map((f) => {
|
||||||
|
const gs = f.goapState;
|
||||||
|
return {
|
||||||
|
id: f.id,
|
||||||
|
label: f.label,
|
||||||
|
credits: Math.round(f.credits),
|
||||||
|
population: Math.round(f.populationTotal),
|
||||||
|
military: gs?.militaryShipCount ?? 0,
|
||||||
|
miners: gs?.minerShipCount ?? 0,
|
||||||
|
transport: gs?.transportShipCount ?? 0,
|
||||||
|
constructors: gs?.constructorShipCount ?? 0,
|
||||||
|
systems: gs ? `${gs.controlledSystemCount} / ${gs.targetSystemCount}` : "—",
|
||||||
|
ore: gs ? Math.round(gs.oreStockpile) : 0,
|
||||||
|
shipsBuilt: f.shipsBuilt,
|
||||||
|
shipsLost: f.shipsLost,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||||
|
const factionColumns = [
|
||||||
|
factionColumnHelper.accessor("label", { header: "Faction" }),
|
||||||
|
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
||||||
|
factionColumnHelper.accessor("population", { header: "Pop" }),
|
||||||
|
factionColumnHelper.accessor("military", { header: "Military" }),
|
||||||
|
factionColumnHelper.accessor("miners", { header: "Miners" }),
|
||||||
|
factionColumnHelper.accessor("transport", { header: "Transport" }),
|
||||||
|
factionColumnHelper.accessor("constructors", { header: "Constructors" }),
|
||||||
|
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
||||||
|
factionColumnHelper.accessor("ore", { header: "Ore" }),
|
||||||
|
factionColumnHelper.accessor("shipsBuilt", { header: "Built" }),
|
||||||
|
factionColumnHelper.accessor("shipsLost", { header: "Lost" }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const factionFilter = ref("");
|
||||||
|
const factionSorting = ref<SortingState>([]);
|
||||||
|
const factionOrder = useColumnOrder(["label", "credits", "population", "military", "miners", "transport", "constructors", "systems", "ore", "shipsBuilt", "shipsLost"]);
|
||||||
|
|
||||||
|
const factionTable = useVueTable({
|
||||||
|
get data() { return factionRows.value; },
|
||||||
|
columns: factionColumns,
|
||||||
|
state: {
|
||||||
|
get globalFilter() { return factionFilter.value; },
|
||||||
|
get sorting() { return factionSorting.value; },
|
||||||
|
get columnOrder() { return factionOrder.columnOrder.value; },
|
||||||
|
},
|
||||||
|
onGlobalFilterChange: (v) => { factionFilter.value = String(v); },
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
factionSorting.value = typeof updater === "function" ? updater(factionSorting.value) : updater;
|
||||||
|
},
|
||||||
|
onColumnOrderChange: factionOrder.onColumnOrderChange,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Row counts ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const tabs: { id: TabId; label: string }[] = [
|
||||||
|
{ id: "ships", label: "Ships" },
|
||||||
|
{ id: "stations", label: "Stations" },
|
||||||
|
{ id: "factions", label: "Factions" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeFilter = computed({
|
||||||
|
get: () => {
|
||||||
|
if (activeTab.value === "ships") return shipFilter.value;
|
||||||
|
if (activeTab.value === "stations") return stationFilter.value;
|
||||||
|
return factionFilter.value;
|
||||||
|
},
|
||||||
|
set: (v: string) => {
|
||||||
|
if (activeTab.value === "ships") shipFilter.value = v;
|
||||||
|
else if (activeTab.value === "stations") stationFilter.value = v;
|
||||||
|
else factionFilter.value = v;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeRowCount = computed(() => {
|
||||||
|
if (activeTab.value === "ships") return shipTable.getFilteredRowModel().rows.length;
|
||||||
|
if (activeTab.value === "stations") return stationTable.getFilteredRowModel().rows.length;
|
||||||
|
return factionTable.getFilteredRowModel().rows.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTotalCount = computed(() => {
|
||||||
|
if (activeTab.value === "ships") return gmStore.ships.length;
|
||||||
|
if (activeTab.value === "stations") return gmStore.stations.length;
|
||||||
|
return gmStore.factions.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Row interaction ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function onShipClick(row: ShipRow) {
|
||||||
|
selectionStore.selectSelection({ id: row.id, kind: "ship", label: row.label }, "ui");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShipDblClick(row: ShipRow) {
|
||||||
|
selectionStore.selectSelection({ id: row.id, kind: "ship", label: row.label }, "ui");
|
||||||
|
emit("focus", row.id, "ship");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStationClick(row: StationRow) {
|
||||||
|
selectionStore.selectSelection({ id: row.id, kind: "station", label: row.label }, "ui");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStationDblClick(row: StationRow) {
|
||||||
|
selectionStore.selectSelection({ id: row.id, kind: "station", label: row.label }, "ui");
|
||||||
|
emit("focus", row.id, "station");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShipSelected(id: string) {
|
||||||
|
return selectedEntityKind.value === "ship" && selectedEntityId.value === id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStationSelected(id: string) {
|
||||||
|
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GmWindow
|
||||||
|
title="Ships"
|
||||||
|
:initial-width="980"
|
||||||
|
:initial-height="560"
|
||||||
|
:initial-x="80"
|
||||||
|
:initial-y="80"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<!-- Tab bar + search -->
|
||||||
|
<div class="gm-toolbar flex shrink-0 items-center gap-3 px-3 py-2">
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
type="button"
|
||||||
|
class="gm-tab-btn rounded px-3 py-1 text-xs font-semibold uppercase tracking-widest transition"
|
||||||
|
:class="activeTab === tab.id ? 'gm-tab-btn--active' : ''"
|
||||||
|
@click="activeTab = tab.id"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<input
|
||||||
|
v-model="activeFilter"
|
||||||
|
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
||||||
|
placeholder="Filter…"
|
||||||
|
type="search"
|
||||||
|
/>
|
||||||
|
<svg class="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 opacity-40" width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" />
|
||||||
|
</svg>
|
||||||
|
<button
|
||||||
|
v-if="activeFilter"
|
||||||
|
type="button"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 text-[10px] opacity-40 hover:opacity-80"
|
||||||
|
@click="activeFilter = ''"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="gm-row-count shrink-0 font-mono text-xs tabular-nums opacity-60">
|
||||||
|
{{ activeRowCount }} / {{ activeTotalCount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ships table -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'ships'"
|
||||||
|
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||||
|
>
|
||||||
|
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||||
|
<thead class="sticky top-0 z-10">
|
||||||
|
<tr
|
||||||
|
v-for="headerGroup in shipTable.getHeaderGroups()"
|
||||||
|
:key="headerGroup.id"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||||
|
:class="[
|
||||||
|
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||||
|
shipOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||||
|
shipOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||||
|
]"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="shipOrder.onDragStart($event, header.column.id)"
|
||||||
|
@dragover="shipOrder.onDragOver($event, header.column.id)"
|
||||||
|
@dragleave="shipOrder.onDragLeave()"
|
||||||
|
@drop="shipOrder.onDrop(header.column.id)"
|
||||||
|
@dragend="shipOrder.onDragEnd()"
|
||||||
|
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||||
|
>
|
||||||
|
<span class="flex select-none items-center gap-1">
|
||||||
|
<FlexRender
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
<span class="text-[10px] opacity-60">
|
||||||
|
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="row in shipTable.getRowModel().rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="gm-tr cursor-pointer"
|
||||||
|
:class="isShipSelected(row.original.id) ? 'gm-tr--selected' : ''"
|
||||||
|
@click="onShipClick(row.original)"
|
||||||
|
@dblclick="onShipDblClick(row.original)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="cell.column.id === 'class'"
|
||||||
|
class="gm-badge rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
|
</span>
|
||||||
|
<FlexRender
|
||||||
|
v-else
|
||||||
|
:render="cell.column.columnDef.cell"
|
||||||
|
:props="cell.getContext()"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="shipTable.getRowModel().rows.length === 0">
|
||||||
|
<td :colspan="shipColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||||
|
No ships match the filter.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stations table -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'stations'"
|
||||||
|
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||||
|
>
|
||||||
|
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||||
|
<thead class="sticky top-0 z-10">
|
||||||
|
<tr
|
||||||
|
v-for="headerGroup in stationTable.getHeaderGroups()"
|
||||||
|
:key="headerGroup.id"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||||
|
:class="[
|
||||||
|
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||||
|
stationOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||||
|
stationOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||||
|
]"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="stationOrder.onDragStart($event, header.column.id)"
|
||||||
|
@dragover="stationOrder.onDragOver($event, header.column.id)"
|
||||||
|
@dragleave="stationOrder.onDragLeave()"
|
||||||
|
@drop="stationOrder.onDrop(header.column.id)"
|
||||||
|
@dragend="stationOrder.onDragEnd()"
|
||||||
|
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||||
|
>
|
||||||
|
<span class="flex select-none items-center gap-1">
|
||||||
|
<FlexRender
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
<span class="text-[10px] opacity-60">
|
||||||
|
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="row in stationTable.getRowModel().rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="gm-tr cursor-pointer"
|
||||||
|
:class="isStationSelected(row.original.id) ? 'gm-tr--selected' : ''"
|
||||||
|
@click="onStationClick(row.original)"
|
||||||
|
@dblclick="onStationDblClick(row.original)"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="cell.column.id === 'category'"
|
||||||
|
class="gm-badge rounded px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
|
</span>
|
||||||
|
<FlexRender
|
||||||
|
v-else
|
||||||
|
:render="cell.column.columnDef.cell"
|
||||||
|
:props="cell.getContext()"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="stationTable.getRowModel().rows.length === 0">
|
||||||
|
<td :colspan="stationColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||||
|
No stations match the filter.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Factions table -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'factions'"
|
||||||
|
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||||
|
>
|
||||||
|
<table class="gm-table w-full min-w-max border-separate border-spacing-0 text-xs">
|
||||||
|
<thead class="sticky top-0 z-10">
|
||||||
|
<tr
|
||||||
|
v-for="headerGroup in factionTable.getHeaderGroups()"
|
||||||
|
:key="headerGroup.id"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
v-for="header in headerGroup.headers"
|
||||||
|
:key="header.id"
|
||||||
|
class="gm-th whitespace-nowrap px-3 py-2 text-left"
|
||||||
|
:class="[
|
||||||
|
header.column.getCanSort() ? 'cursor-pointer' : '',
|
||||||
|
factionOrder.draggingId.value === header.column.id ? 'gm-th--dragging' : '',
|
||||||
|
factionOrder.overId.value === header.column.id ? 'gm-th--dragover' : '',
|
||||||
|
]"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="factionOrder.onDragStart($event, header.column.id)"
|
||||||
|
@dragover="factionOrder.onDragOver($event, header.column.id)"
|
||||||
|
@dragleave="factionOrder.onDragLeave()"
|
||||||
|
@drop="factionOrder.onDrop(header.column.id)"
|
||||||
|
@dragend="factionOrder.onDragEnd()"
|
||||||
|
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||||
|
>
|
||||||
|
<span class="flex select-none items-center gap-1">
|
||||||
|
<FlexRender
|
||||||
|
:render="header.column.columnDef.header"
|
||||||
|
:props="header.getContext()"
|
||||||
|
/>
|
||||||
|
<span class="text-[10px] opacity-60">
|
||||||
|
{{ header.column.getIsSorted() === "asc" ? "↑" : header.column.getIsSorted() === "desc" ? "↓" : "" }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="row in factionTable.getRowModel().rows"
|
||||||
|
:key="row.id"
|
||||||
|
class="gm-tr"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
v-for="cell in row.getVisibleCells()"
|
||||||
|
:key="cell.id"
|
||||||
|
class="gm-td whitespace-nowrap px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="factionTable.getRowModel().rows.length === 0">
|
||||||
|
<td :colspan="factionColumns.length" class="gm-td px-3 py-6 text-center opacity-40">
|
||||||
|
No factions match the filter.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GmWindow>
|
||||||
|
</template>
|
||||||
97
apps/viewer/src/components/gm/GmWindow.vue
Normal file
97
apps/viewer/src/components/gm/GmWindow.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
title: string;
|
||||||
|
initialX?: number;
|
||||||
|
initialY?: number;
|
||||||
|
initialWidth?: number;
|
||||||
|
initialHeight?: number;
|
||||||
|
}>(), {
|
||||||
|
initialX: 80,
|
||||||
|
initialY: 80,
|
||||||
|
initialWidth: 960,
|
||||||
|
initialHeight: 580,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const windowEl = ref<HTMLDivElement | null>(null);
|
||||||
|
const x = ref(props.initialX);
|
||||||
|
const y = ref(props.initialY);
|
||||||
|
const isDragging = ref(false);
|
||||||
|
let dragOffsetX = 0;
|
||||||
|
let dragOffsetY = 0;
|
||||||
|
|
||||||
|
function onTitleMouseDown(e: MouseEvent) {
|
||||||
|
if ((e.target as HTMLElement).closest("button")) return;
|
||||||
|
isDragging.value = true;
|
||||||
|
dragOffsetX = e.clientX - x.value;
|
||||||
|
dragOffsetY = e.clientY - y.value;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
x.value = e.clientX - dragOffsetX;
|
||||||
|
y.value = e.clientY - dragOffsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
isDragging.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
// Set initial size imperatively so Vue's reactive style binding never
|
||||||
|
// touches width/height — the browser's CSS resize handle owns them.
|
||||||
|
if (windowEl.value) {
|
||||||
|
windowEl.value.style.width = `${props.initialWidth}px`;
|
||||||
|
windowEl.value.style.height = `${props.initialHeight}px`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="windowEl"
|
||||||
|
class="gm-window pointer-events-auto fixed flex flex-col overflow-hidden rounded-xl border"
|
||||||
|
:style="{
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
cursor: isDragging ? 'grabbing' : 'default',
|
||||||
|
zIndex: 200,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Title bar -->
|
||||||
|
<div
|
||||||
|
class="gm-window-titlebar flex shrink-0 cursor-grab select-none items-center gap-2 px-4 py-2.5"
|
||||||
|
:style="{ cursor: isDragging ? 'grabbing' : 'grab' }"
|
||||||
|
@mousedown="onTitleMouseDown"
|
||||||
|
>
|
||||||
|
<span class="gm-window-title-badge mr-1 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-widest">GM</span>
|
||||||
|
<h2 class="flex-1 font-[Space_Grotesk] text-sm font-semibold tracking-wide">{{ title }}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gm-window-close-btn flex h-6 w-6 items-center justify-center rounded text-xs opacity-60 transition hover:opacity-100"
|
||||||
|
aria-label="Close window"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -386,3 +386,181 @@ canvas {
|
|||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── GM Windows ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.gm-window {
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
background: rgba(7, 14, 27, 0.92);
|
||||||
|
border-color: rgba(132, 196, 255, 0.18);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(127, 214, 255, 0.06) inset,
|
||||||
|
0 32px 72px rgba(0, 0, 0, 0.52);
|
||||||
|
color: var(--viewer-text);
|
||||||
|
resize: both;
|
||||||
|
min-width: 480px;
|
||||||
|
min-height: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-window-titlebar {
|
||||||
|
background: rgba(127, 214, 255, 0.04);
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-window-title-badge {
|
||||||
|
background: rgba(127, 214, 255, 0.14);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-window-close-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--viewer-text);
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-window-close-btn:hover {
|
||||||
|
background: rgba(255, 80, 60, 0.18);
|
||||||
|
border-color: rgba(255, 80, 60, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-toolbar {
|
||||||
|
background: rgba(127, 214, 255, 0.03);
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tab-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tab-btn:hover {
|
||||||
|
color: var(--viewer-text);
|
||||||
|
background: rgba(127, 214, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tab-btn--active {
|
||||||
|
background: rgba(127, 214, 255, 0.12);
|
||||||
|
border-color: rgba(127, 214, 255, 0.22);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-search-input {
|
||||||
|
background: rgba(127, 214, 255, 0.05);
|
||||||
|
border-color: rgba(132, 196, 255, 0.18);
|
||||||
|
color: var(--viewer-text);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-search-input:focus {
|
||||||
|
border-color: rgba(127, 214, 255, 0.4);
|
||||||
|
background: rgba(127, 214, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-search-input::placeholder {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-row-count {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-table-container {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(127, 214, 255, 0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-table {
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-th {
|
||||||
|
background: rgba(7, 14, 27, 0.98);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-th:hover {
|
||||||
|
color: var(--viewer-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tr {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: background 80ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tr:nth-child(even) .gm-td {
|
||||||
|
background: rgba(127, 214, 255, 0.025);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tr:hover .gm-td {
|
||||||
|
background: rgba(127, 214, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-tr--selected .gm-td {
|
||||||
|
background: rgba(255, 191, 105, 0.1) !important;
|
||||||
|
border-bottom-color: rgba(255, 191, 105, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-td {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-td:first-child {
|
||||||
|
color: var(--viewer-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-badge {
|
||||||
|
background: rgba(127, 214, 255, 0.1);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-th--dragging {
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-th--dragover {
|
||||||
|
background: rgba(127, 214, 255, 0.14);
|
||||||
|
box-shadow: inset 2px 0 0 var(--viewer-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-console-toggle {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 7px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(127, 214, 255, 0.1);
|
||||||
|
border: 1px solid rgba(127, 214, 255, 0.24);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 140ms ease, border-color 140ms ease;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-console-toggle:hover {
|
||||||
|
background: rgba(127, 214, 255, 0.18);
|
||||||
|
border-color: rgba(127, 214, 255, 0.4);
|
||||||
|
}
|
||||||
|
|||||||
23
apps/viewer/src/ui/stores/gmStore.ts
Normal file
23
apps/viewer/src/ui/stores/gmStore.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { ShipSnapshot } from "../../contractsShips";
|
||||||
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
|
import type { FactionSnapshot } from "../../contractsFactions";
|
||||||
|
|
||||||
|
export const useGmStore = defineStore("gm", {
|
||||||
|
state: () => ({
|
||||||
|
ships: [] as ShipSnapshot[],
|
||||||
|
stations: [] as StationSnapshot[],
|
||||||
|
factions: [] as FactionSnapshot[],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
updateWorld(
|
||||||
|
ships: ShipSnapshot[],
|
||||||
|
stations: StationSnapshot[],
|
||||||
|
factions: FactionSnapshot[],
|
||||||
|
) {
|
||||||
|
this.ships = ships;
|
||||||
|
this.stations = stations;
|
||||||
|
this.factions = factions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -112,7 +112,6 @@ export interface ViewerHudState {
|
|||||||
export interface ViewerHudBindings {
|
export interface ViewerHudBindings {
|
||||||
state: ViewerHudState;
|
state: ViewerHudState;
|
||||||
selectionStore: ViewerSelectionStore;
|
selectionStore: ViewerSelectionStore;
|
||||||
opsStripEl: HTMLDivElement;
|
|
||||||
historyLayerEl: HTMLDivElement;
|
historyLayerEl: HTMLDivElement;
|
||||||
marqueeEl: HTMLDivElement;
|
marqueeEl: HTMLDivElement;
|
||||||
hoverLabelEl: HTMLDivElement;
|
hoverLabelEl: HTMLDivElement;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { fetchWorldSnapshot, openWorldStream } from "./api";
|
import { fetchWorldSnapshot, openWorldStream } from "./api";
|
||||||
import type { ViewerHudState } from "./viewerHudState";
|
import type { ViewerHudState } from "./viewerHudState";
|
||||||
import { buildOpsStripState } from "./viewerOpsStrip";
|
import { buildOpsStripState } from "./viewerOpsStrip";
|
||||||
|
import { useGmStore } from "./ui/stores/gmStore";
|
||||||
|
import { viewerPinia } from "./ui/stores/pinia";
|
||||||
import { buildDetailPanelState } from "./viewerPanels";
|
import { buildDetailPanelState } from "./viewerPanels";
|
||||||
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
||||||
import type {
|
import type {
|
||||||
@@ -195,6 +197,15 @@ export class ViewerWorldLifecycle {
|
|||||||
this.context.getPovLevel(),
|
this.context.getPovLevel(),
|
||||||
this.context.getActiveSystemId(),
|
this.context.getActiveSystemId(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const world = this.context.getWorld();
|
||||||
|
if (world) {
|
||||||
|
useGmStore(viewerPinia).updateWorld(
|
||||||
|
[...world.ships.values()],
|
||||||
|
[...world.stations.values()],
|
||||||
|
[...world.factions.values()],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePanels() {
|
updatePanels() {
|
||||||
|
|||||||
Reference in New Issue
Block a user