feat: massive AI generation
This commit is contained in:
@@ -35,7 +35,11 @@ public sealed record PolicySetSnapshot(
|
|||||||
string TradeAccessPolicy,
|
string TradeAccessPolicy,
|
||||||
string DockingAccessPolicy,
|
string DockingAccessPolicy,
|
||||||
string ConstructionAccessPolicy,
|
string ConstructionAccessPolicy,
|
||||||
string OperationalRangePolicy);
|
string OperationalRangePolicy,
|
||||||
|
string CombatEngagementPolicy,
|
||||||
|
bool AvoidHostileSystems,
|
||||||
|
float FleeHullRatio,
|
||||||
|
IReadOnlyList<string> BlacklistedSystemIds);
|
||||||
|
|
||||||
public sealed record PolicySetDelta(
|
public sealed record PolicySetDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -44,4 +48,8 @@ public sealed record PolicySetDelta(
|
|||||||
string TradeAccessPolicy,
|
string TradeAccessPolicy,
|
||||||
string DockingAccessPolicy,
|
string DockingAccessPolicy,
|
||||||
string ConstructionAccessPolicy,
|
string ConstructionAccessPolicy,
|
||||||
string OperationalRangePolicy);
|
string OperationalRangePolicy,
|
||||||
|
string CombatEngagementPolicy,
|
||||||
|
bool AvoidHostileSystems,
|
||||||
|
float FleeHullRatio,
|
||||||
|
IReadOnlyList<string> BlacklistedSystemIds);
|
||||||
|
|||||||
@@ -26,5 +26,9 @@ public sealed class PolicySetRuntime
|
|||||||
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
|
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
|
||||||
public string ConstructionAccessPolicy { get; set; } = "owner-only";
|
public string ConstructionAccessPolicy { get; set; } = "owner-only";
|
||||||
public string OperationalRangePolicy { get; set; } = "unrestricted";
|
public string OperationalRangePolicy { get; set; } = "unrestricted";
|
||||||
|
public string CombatEngagementPolicy { get; set; } = "defensive";
|
||||||
|
public bool AvoidHostileSystems { get; set; } = true;
|
||||||
|
public float FleeHullRatio { get; set; } = 0.35f;
|
||||||
|
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
|
||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Factions.AI;
|
|
||||||
|
|
||||||
// ─── Planning State ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public sealed class FactionPlanningState
|
|
||||||
{
|
|
||||||
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 int TargetSystemCount { get; set; }
|
|
||||||
public bool HasShipFactory { get; set; }
|
|
||||||
public int EnemyFactionCount { get; set; }
|
|
||||||
public int EnemyShipCount { get; set; }
|
|
||||||
public int EnemyStationCount { get; set; }
|
|
||||||
public float OreStockpile { get; set; }
|
|
||||||
public float RefinedMetalsAvailableStock { get; set; }
|
|
||||||
public float RefinedMetalsUsageRate { get; set; }
|
|
||||||
public float RefinedMetalsProjectedProductionRate { get; set; }
|
|
||||||
public float RefinedMetalsProjectedNetRate { get; set; }
|
|
||||||
public float RefinedMetalsLevelSeconds { get; set; }
|
|
||||||
public string RefinedMetalsLevel { get; set; } = "unknown";
|
|
||||||
public float HullpartsAvailableStock { get; set; }
|
|
||||||
public float HullpartsUsageRate { get; set; }
|
|
||||||
public float HullpartsProjectedProductionRate { get; set; }
|
|
||||||
public float HullpartsProjectedNetRate { get; set; }
|
|
||||||
public float HullpartsLevelSeconds { 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 => RefinedMetalsProjectedProductionRate > 0.01f;
|
|
||||||
public bool HasHullpartsProduction => HullpartsProjectedProductionRate > 0.01f;
|
|
||||||
public bool HasClaytronicsProduction => ClaytronicsProjectedProductionRate > 0.01f;
|
|
||||||
public bool HasWaterProduction => WaterProjectedProductionRate > 0.01f;
|
|
||||||
|
|
||||||
public bool HasWarIndustrySupplyChain =>
|
|
||||||
IsCommodityOperational(RefinedMetalsProjectedProductionRate, RefinedMetalsProjectedNetRate, RefinedMetalsLevelSeconds, RefinedMetalsLevel, 240f)
|
|
||||||
&& IsCommodityOperational(HullpartsProjectedProductionRate, HullpartsProjectedNetRate, HullpartsLevelSeconds, HullpartsLevel, 240f)
|
|
||||||
&& IsCommodityOperational(ClaytronicsProjectedProductionRate, ClaytronicsProjectedNetRate, ClaytronicsLevelSeconds, ClaytronicsLevel, 240f);
|
|
||||||
|
|
||||||
public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone();
|
|
||||||
|
|
||||||
internal static int ComputeTargetWarships(FactionPlanningState state)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public sealed class EnsureWarIndustryGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "ensure-war-industry";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
|
||||||
state.EnemyFactionCount <= 0 || (state.HasWarIndustrySupplyChain && state.HasShipFactory);
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
if (state.EnemyFactionCount <= 0)
|
|
||||||
{
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
var missingStages =
|
|
||||||
(FactionPlanningState.IsCommodityOperational(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) ? 0 : 1) +
|
|
||||||
(FactionPlanningState.IsCommodityOperational(state.HullpartsProjectedProductionRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) ? 0 : 1) +
|
|
||||||
(FactionPlanningState.IsCommodityOperational(state.ClaytronicsProjectedProductionRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f) ? 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 && supplyNeed <= 0.01f ? 0f : 110f + (missingStages * 22f) + (supplyNeed * 0.18f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EnsureWaterSecurityGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "ensure-water-security";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
|
||||||
FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f);
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
if (FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f))
|
|
||||||
{
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 55f + FactionPlanningState.ComputeCommodityNeed(
|
|
||||||
state.WaterProjectedProductionRate,
|
|
||||||
state.WaterUsageRate,
|
|
||||||
state.WaterProjectedNetRate,
|
|
||||||
state.WaterLevelSeconds,
|
|
||||||
state.WaterLevel,
|
|
||||||
300f) * 0.25f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EnsureWarFleetGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "ensure-war-fleet";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
|
||||||
state.MilitaryShipCount >= FactionPlanningState.ComputeTargetWarships(state);
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var deficit = FactionPlanningState.ComputeTargetWarships(state) - state.MilitaryShipCount;
|
|
||||||
return deficit <= 0 ? 0f : 50f + (deficit * 10f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ExterminateRivalGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "exterminate-rival";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
|
||||||
state.EnemyFactionCount <= 0 || (state.EnemyShipCount <= 0 && state.EnemyStationCount <= 0);
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
if (state.EnemyFactionCount <= 0)
|
|
||||||
{
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 140f + (state.EnemyStationCount * 25f) + (state.EnemyShipCount * 6f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ExpandTerritoryGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "expand-territory";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
|
||||||
state.ControlledSystemCount >= state.TargetSystemCount;
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var deficit = state.TargetSystemCount - state.ControlledSystemCount;
|
|
||||||
return deficit <= 0 ? 0f : 80f + (deficit * 15f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EnsureMiningCapacityGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
private const int MinMiners = 2;
|
|
||||||
|
|
||||||
public override string Name => "ensure-mining-capacity";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) => state.MinerShipCount >= MinMiners;
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var deficit = MinMiners - state.MinerShipCount;
|
|
||||||
return deficit <= 0 ? 0f : 70f + (deficit * 12f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EnsureConstructionCapacityGoal : GoapGoal<FactionPlanningState>
|
|
||||||
{
|
|
||||||
private const int MinConstructors = 1;
|
|
||||||
|
|
||||||
public override string Name => "ensure-construction-capacity";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(FactionPlanningState state) => state.ConstructorShipCount >= MinConstructors;
|
|
||||||
|
|
||||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var deficit = MinConstructors - state.ConstructorShipCount;
|
|
||||||
return deficit <= 0 ? 0f : 60f + (deficit * 10f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,76 @@
|
|||||||
namespace SpaceGame.Api.Factions.Contracts;
|
namespace SpaceGame.Api.Factions.Contracts;
|
||||||
|
|
||||||
public sealed record FactionPlanningStateSnapshot(
|
public sealed record FactionDoctrineSnapshot(
|
||||||
int MilitaryShipCount,
|
string StrategicPosture,
|
||||||
int MinerShipCount,
|
string ExpansionPosture,
|
||||||
int TransportShipCount,
|
string MilitaryPosture,
|
||||||
int ConstructorShipCount,
|
string EconomicPosture,
|
||||||
int ControlledSystemCount,
|
int DesiredControlledSystems,
|
||||||
int TargetSystemCount,
|
int DesiredMilitaryPerFront,
|
||||||
bool HasShipFactory,
|
int DesiredMinersPerSystem,
|
||||||
float OreStockpile,
|
int DesiredTransportsPerSystem,
|
||||||
float RefinedMetalsAvailableStock,
|
int DesiredConstructors,
|
||||||
float RefinedMetalsUsageRate,
|
float ReserveCreditsRatio,
|
||||||
float RefinedMetalsProjectedProductionRate,
|
float ExpansionBudgetRatio,
|
||||||
float RefinedMetalsProjectedNetRate,
|
float WarBudgetRatio,
|
||||||
float RefinedMetalsLevelSeconds,
|
float ReserveMilitaryRatio,
|
||||||
string RefinedMetalsLevel,
|
float OffensiveReadinessThreshold,
|
||||||
float HullpartsAvailableStock,
|
float SupplySecurityBias,
|
||||||
float HullpartsUsageRate,
|
float FailureAversion,
|
||||||
float HullpartsProjectedProductionRate,
|
int ReinforcementLeadPerFront);
|
||||||
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 FactionStrategicPrioritySnapshot(string GoalName, float Priority);
|
public sealed record FactionSystemMemorySnapshot(
|
||||||
|
string SystemId,
|
||||||
|
DateTimeOffset LastSeenAtUtc,
|
||||||
|
int LastEnemyShipCount,
|
||||||
|
int LastEnemyStationCount,
|
||||||
|
bool ControlledByFaction,
|
||||||
|
string? LastRole,
|
||||||
|
float FrontierPressure,
|
||||||
|
float RouteRisk,
|
||||||
|
float HistoricalShortagePressure,
|
||||||
|
int OffensiveFailures,
|
||||||
|
int DefensiveFailures,
|
||||||
|
int OffensiveSuccesses,
|
||||||
|
int DefensiveSuccesses,
|
||||||
|
DateTimeOffset? LastContestedAtUtc,
|
||||||
|
DateTimeOffset? LastShortageAtUtc);
|
||||||
|
|
||||||
|
public sealed record FactionCommodityMemorySnapshot(
|
||||||
|
string ItemId,
|
||||||
|
float HistoricalShortageScore,
|
||||||
|
float HistoricalSurplusScore,
|
||||||
|
float LastObservedBacklog,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
DateTimeOffset? LastCriticalAtUtc);
|
||||||
|
|
||||||
|
public sealed record FactionOutcomeRecordSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Summary,
|
||||||
|
string? RelatedCampaignId,
|
||||||
|
string? RelatedObjectiveId,
|
||||||
|
DateTimeOffset OccurredAtUtc);
|
||||||
|
|
||||||
|
public sealed record FactionMemorySnapshot(
|
||||||
|
int LastPlanCycle,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
int LastObservedShipsBuilt,
|
||||||
|
int LastObservedShipsLost,
|
||||||
|
float LastObservedCredits,
|
||||||
|
IReadOnlyList<string> KnownSystemIds,
|
||||||
|
IReadOnlyList<string> KnownEnemyFactionIds,
|
||||||
|
IReadOnlyList<FactionSystemMemorySnapshot> Systems,
|
||||||
|
IReadOnlyList<FactionCommodityMemorySnapshot> Commodities,
|
||||||
|
IReadOnlyList<FactionOutcomeRecordSnapshot> RecentOutcomes);
|
||||||
|
|
||||||
|
public sealed record FactionBudgetSnapshot(
|
||||||
|
float ReservedCredits,
|
||||||
|
float ExpansionCredits,
|
||||||
|
float WarCredits,
|
||||||
|
int ReservedMilitaryAssets,
|
||||||
|
int ReservedLogisticsAssets,
|
||||||
|
int ReservedConstructionAssets);
|
||||||
|
|
||||||
public sealed record FactionCommoditySignalSnapshot(
|
public sealed record FactionCommoditySignalSnapshot(
|
||||||
string ItemId,
|
string ItemId,
|
||||||
@@ -51,96 +87,185 @@ public sealed record FactionCommoditySignalSnapshot(
|
|||||||
float BuyBacklog,
|
float BuyBacklog,
|
||||||
float ReservedForConstruction);
|
float ReservedForConstruction);
|
||||||
|
|
||||||
public sealed record FactionThreatSignalSnapshot(
|
public sealed record FactionEconomicAssessmentSnapshot(
|
||||||
string ScopeId,
|
|
||||||
string ScopeKind,
|
|
||||||
int EnemyShipCount,
|
|
||||||
int EnemyStationCount);
|
|
||||||
|
|
||||||
public sealed record FactionBlackboardSnapshot(
|
|
||||||
int PlanCycle,
|
int PlanCycle,
|
||||||
DateTimeOffset UpdatedAtUtc,
|
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 MilitaryShipCount,
|
||||||
int MinerShipCount,
|
int MinerShipCount,
|
||||||
int TransportShipCount,
|
int TransportShipCount,
|
||||||
int ConstructorShipCount,
|
int ConstructorShipCount,
|
||||||
int ControlledSystemCount,
|
int ControlledSystemCount,
|
||||||
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals,
|
int TargetMilitaryShipCount,
|
||||||
|
int TargetMinerShipCount,
|
||||||
|
int TargetTransportShipCount,
|
||||||
|
int TargetConstructorShipCount,
|
||||||
|
bool HasShipyard,
|
||||||
|
bool HasWarIndustrySupplyChain,
|
||||||
|
string? PrimaryExpansionSiteId,
|
||||||
|
string? PrimaryExpansionSystemId,
|
||||||
|
float ReplacementPressure,
|
||||||
|
float SustainmentScore,
|
||||||
|
float LogisticsSecurityScore,
|
||||||
|
int CriticalShortageCount,
|
||||||
|
string? IndustrialBottleneckItemId,
|
||||||
|
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals);
|
||||||
|
|
||||||
|
public sealed record FactionThreatSignalSnapshot(
|
||||||
|
string ScopeId,
|
||||||
|
string ScopeKind,
|
||||||
|
int EnemyShipCount,
|
||||||
|
int EnemyStationCount,
|
||||||
|
string? EnemyFactionId);
|
||||||
|
|
||||||
|
public sealed record FactionThreatAssessmentSnapshot(
|
||||||
|
int PlanCycle,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
int EnemyFactionCount,
|
||||||
|
int EnemyShipCount,
|
||||||
|
int EnemyStationCount,
|
||||||
|
string? PrimaryThreatFactionId,
|
||||||
|
string? PrimaryThreatSystemId,
|
||||||
IReadOnlyList<FactionThreatSignalSnapshot> ThreatSignals);
|
IReadOnlyList<FactionThreatSignalSnapshot> ThreatSignals);
|
||||||
|
|
||||||
|
public sealed record FactionTheaterSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string SystemId,
|
||||||
|
string Status,
|
||||||
|
float Priority,
|
||||||
|
float SupplyRisk,
|
||||||
|
float FriendlyAssetValue,
|
||||||
|
string? TargetFactionId,
|
||||||
|
string? AnchorEntityId,
|
||||||
|
Vector3Dto? AnchorPosition,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> CampaignIds);
|
||||||
|
|
||||||
public sealed record FactionPlanStepSnapshot(
|
public sealed record FactionPlanStepSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
string Kind,
|
string Kind,
|
||||||
string Status,
|
string Status,
|
||||||
float Priority,
|
string? Summary,
|
||||||
string? CommodityId,
|
string? BlockingReason);
|
||||||
string? ModuleId,
|
|
||||||
string? TargetFactionId,
|
|
||||||
string? TargetSiteId,
|
|
||||||
string? StatusReason,
|
|
||||||
string? ExecutionBindingKind,
|
|
||||||
string? ExecutionBindingTargetId,
|
|
||||||
string? ExecutionBindingSummary,
|
|
||||||
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(
|
public sealed record FactionCampaignSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
string Kind,
|
string Kind,
|
||||||
string State,
|
string Status,
|
||||||
string ObjectiveId,
|
|
||||||
string StepId,
|
|
||||||
float Priority,
|
float Priority,
|
||||||
string? ShipRole,
|
string? TheaterId,
|
||||||
string? CommodityId,
|
|
||||||
string? ModuleId,
|
|
||||||
string? TargetFactionId,
|
string? TargetFactionId,
|
||||||
string? TargetSystemId,
|
string? TargetSystemId,
|
||||||
string? TargetSiteId,
|
string? TargetEntityId,
|
||||||
int CreatedAtCycle,
|
string? CommodityId,
|
||||||
int UpdatedAtCycle,
|
string? SupportStationId,
|
||||||
string? BlockingReason,
|
int CurrentStepIndex,
|
||||||
string? Notes,
|
DateTimeOffset CreatedAtUtc,
|
||||||
IReadOnlyList<string> AssignedAssets);
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
string? Summary,
|
||||||
|
string? PauseReason,
|
||||||
|
float ContinuationScore,
|
||||||
|
float SupplyAdequacy,
|
||||||
|
float ReplacementPressure,
|
||||||
|
int FailureCount,
|
||||||
|
int SuccessCount,
|
||||||
|
string? FleetCommanderId,
|
||||||
|
bool RequiresReinforcement,
|
||||||
|
IReadOnlyList<FactionPlanStepSnapshot> Steps,
|
||||||
|
IReadOnlyList<string> ObjectiveIds);
|
||||||
|
|
||||||
public sealed record FactionObjectiveSnapshot(
|
public sealed record FactionObjectiveSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
|
string CampaignId,
|
||||||
|
string? TheaterId,
|
||||||
string Kind,
|
string Kind,
|
||||||
string State,
|
string DelegationKind,
|
||||||
|
string BehaviorKind,
|
||||||
|
string Status,
|
||||||
float Priority,
|
float Priority,
|
||||||
string? ParentObjectiveId,
|
string? CommanderId,
|
||||||
string? TargetFactionId,
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
string? TargetSystemId,
|
string? TargetSystemId,
|
||||||
string? TargetSiteId,
|
string? TargetEntityId,
|
||||||
string? TargetRegionId,
|
Vector3Dto? TargetPosition,
|
||||||
|
string? ItemId,
|
||||||
|
string? Notes,
|
||||||
|
int CurrentStepIndex,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
bool UseOrders,
|
||||||
|
string? StagingOrderKind,
|
||||||
|
int ReinforcementLevel,
|
||||||
|
IReadOnlyList<FactionPlanStepSnapshot> Steps,
|
||||||
|
IReadOnlyList<string> ReservedAssetIds);
|
||||||
|
|
||||||
|
public sealed record FactionReservationSnapshot(
|
||||||
|
string Id,
|
||||||
|
string ObjectiveId,
|
||||||
|
string? CampaignId,
|
||||||
|
string AssetKind,
|
||||||
|
string AssetId,
|
||||||
|
float Priority,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record FactionProductionProgramSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
float Priority,
|
||||||
|
string? CampaignId,
|
||||||
string? CommodityId,
|
string? CommodityId,
|
||||||
string? ModuleId,
|
string? ModuleId,
|
||||||
int BudgetWeight,
|
string? ShipKind,
|
||||||
int SlotCost,
|
string? TargetSystemId,
|
||||||
int CreatedAtCycle,
|
int TargetCount,
|
||||||
int UpdatedAtCycle,
|
int CurrentCount,
|
||||||
string? InvalidationReason,
|
string? Notes);
|
||||||
string? BlockingReason,
|
|
||||||
IReadOnlyList<string> PrerequisiteObjectiveIds,
|
public sealed record FactionDecisionLogEntrySnapshot(
|
||||||
IReadOnlyList<string> AssignedAssets,
|
string Id,
|
||||||
IReadOnlyList<FactionPlanStepSnapshot> Steps);
|
string Kind,
|
||||||
|
string Summary,
|
||||||
|
string? RelatedEntityId,
|
||||||
|
int PlanCycle,
|
||||||
|
DateTimeOffset OccurredAtUtc);
|
||||||
|
|
||||||
|
public sealed record FactionStrategicStateSnapshot(
|
||||||
|
int PlanCycle,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
string Status,
|
||||||
|
FactionBudgetSnapshot Budget,
|
||||||
|
FactionEconomicAssessmentSnapshot EconomicAssessment,
|
||||||
|
FactionThreatAssessmentSnapshot ThreatAssessment,
|
||||||
|
IReadOnlyList<FactionTheaterSnapshot> Theaters,
|
||||||
|
IReadOnlyList<FactionCampaignSnapshot> Campaigns,
|
||||||
|
IReadOnlyList<FactionObjectiveSnapshot> Objectives,
|
||||||
|
IReadOnlyList<FactionReservationSnapshot> Reservations,
|
||||||
|
IReadOnlyList<FactionProductionProgramSnapshot> ProductionPrograms);
|
||||||
|
|
||||||
|
public sealed record CommanderAssignmentSnapshot(
|
||||||
|
string CommanderId,
|
||||||
|
string Kind,
|
||||||
|
string BehaviorKind,
|
||||||
|
string Status,
|
||||||
|
string? ObjectiveId,
|
||||||
|
string? CampaignId,
|
||||||
|
string? TheaterId,
|
||||||
|
string? ParentCommanderId,
|
||||||
|
string? ControlledEntityId,
|
||||||
|
float Priority,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
string? TargetEntityId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? ItemId,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset? UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> ActiveObjectiveIds,
|
||||||
|
IReadOnlyList<string> SubordinateCommanderIds);
|
||||||
|
|
||||||
public sealed record FactionSnapshot(
|
public sealed record FactionSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -153,11 +278,11 @@ public sealed record FactionSnapshot(
|
|||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId,
|
string? DefaultPolicySetId,
|
||||||
FactionPlanningStateSnapshot? StrategicAssessment,
|
FactionDoctrineSnapshot Doctrine,
|
||||||
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
FactionMemorySnapshot Memory,
|
||||||
FactionBlackboardSnapshot? Blackboard,
|
FactionStrategicStateSnapshot StrategicState,
|
||||||
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
IReadOnlyList<FactionDecisionLogEntrySnapshot> DecisionLog,
|
||||||
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
IReadOnlyList<CommanderAssignmentSnapshot> Commanders);
|
||||||
|
|
||||||
public sealed record FactionDelta(
|
public sealed record FactionDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -170,8 +295,8 @@ public sealed record FactionDelta(
|
|||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId,
|
string? DefaultPolicySetId,
|
||||||
FactionPlanningStateSnapshot? StrategicAssessment,
|
FactionDoctrineSnapshot Doctrine,
|
||||||
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
FactionMemorySnapshot Memory,
|
||||||
FactionBlackboardSnapshot? Blackboard,
|
FactionStrategicStateSnapshot StrategicState,
|
||||||
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
IReadOnlyList<FactionDecisionLogEntrySnapshot> DecisionLog,
|
||||||
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
IReadOnlyList<CommanderAssignmentSnapshot> Commanders);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Factions.Runtime;
|
namespace SpaceGame.Api.Factions.Runtime;
|
||||||
|
|
||||||
public sealed class FactionRuntime
|
public sealed class FactionRuntime
|
||||||
@@ -14,6 +13,10 @@ public sealed class FactionRuntime
|
|||||||
public int ShipsLost { get; set; }
|
public int ShipsLost { get; set; }
|
||||||
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
public string? DefaultPolicySetId { get; set; }
|
public string? DefaultPolicySetId { get; set; }
|
||||||
|
public FactionDoctrineRuntime Doctrine { get; set; } = new();
|
||||||
|
public FactionMemoryRuntime Memory { get; set; } = new();
|
||||||
|
public FactionStrategicStateRuntime StrategicState { get; set; } = new();
|
||||||
|
public List<FactionDecisionLogEntryRuntime> DecisionLog { get; } = [];
|
||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,183 +29,296 @@ public sealed class CommanderRuntime
|
|||||||
public string? ControlledEntityId { get; set; }
|
public string? ControlledEntityId { get; set; }
|
||||||
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 string? ActiveGoalName { get; set; }
|
|
||||||
public string? ActiveActionName { get; set; }
|
|
||||||
public float ReplanTimer { get; set; }
|
public float ReplanTimer { get; set; }
|
||||||
public bool NeedsReplan { get; set; } = true;
|
public bool NeedsReplan { get; set; } = true;
|
||||||
public CommanderBehaviorRuntime? ActiveBehavior { get; set; }
|
public CommanderAssignmentRuntime? Assignment { get; set; }
|
||||||
public CommanderOrderRuntime? ActiveOrder { get; set; }
|
public CommanderSkillProfileRuntime Skills { get; set; } = new();
|
||||||
public CommanderTaskRuntime? ActiveTask { get; set; }
|
|
||||||
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ActiveObjectiveIds { get; } = new(StringComparer.Ordinal);
|
||||||
public bool IsAlive { get; set; } = true;
|
public bool IsAlive { get; set; } = true;
|
||||||
public FactionPlanningState? LastStrategicAssessment { 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 int PlanningCycle { get; set; }
|
||||||
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FactionObjectiveKind
|
public sealed class CommanderAssignmentRuntime
|
||||||
{
|
{
|
||||||
DestroyFaction,
|
public required string ObjectiveId { get; set; }
|
||||||
BootstrapWarIndustry,
|
public string? CampaignId { get; set; }
|
||||||
BuildShipyard,
|
public string? TheaterId { get; set; }
|
||||||
BuildAttackFleet,
|
public required string Kind { get; set; }
|
||||||
EnsureCommoditySupply,
|
public required string BehaviorKind { get; set; }
|
||||||
EnsureWaterSecurity,
|
public string Status { get; set; } = "active";
|
||||||
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 float Priority { get; set; }
|
||||||
public string? ParentObjectiveId { get; set; }
|
public string? HomeSystemId { get; set; }
|
||||||
public string? TargetFactionId { get; set; }
|
public string? HomeStationId { get; set; }
|
||||||
public string? TargetSystemId { get; set; }
|
public string? TargetSystemId { get; set; }
|
||||||
public string? TargetSiteId { get; set; }
|
public string? TargetEntityId { get; set; }
|
||||||
public string? TargetRegionId { get; set; }
|
public Vector3? TargetPosition { get; set; }
|
||||||
public string? CommodityId { get; set; }
|
public string? ItemId { get; set; }
|
||||||
public string? ModuleId { get; set; }
|
public string? Notes { get; set; }
|
||||||
public int BudgetWeight { get; set; }
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
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 sealed class CommanderSkillProfileRuntime
|
||||||
|
{
|
||||||
|
public int Leadership { get; set; } = 3;
|
||||||
|
public int Coordination { get; set; } = 3;
|
||||||
|
public int Strategy { get; set; } = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionDoctrineRuntime
|
||||||
|
{
|
||||||
|
public string StrategicPosture { get; set; } = "balanced";
|
||||||
|
public string ExpansionPosture { get; set; } = "measured";
|
||||||
|
public string MilitaryPosture { get; set; } = "defensive";
|
||||||
|
public string EconomicPosture { get; set; } = "self-sufficient";
|
||||||
|
public int DesiredControlledSystems { get; set; } = 3;
|
||||||
|
public int DesiredMilitaryPerFront { get; set; } = 2;
|
||||||
|
public int DesiredMinersPerSystem { get; set; } = 1;
|
||||||
|
public int DesiredTransportsPerSystem { get; set; } = 1;
|
||||||
|
public int DesiredConstructors { get; set; } = 1;
|
||||||
|
public float ReserveCreditsRatio { get; set; } = 0.2f;
|
||||||
|
public float ExpansionBudgetRatio { get; set; } = 0.25f;
|
||||||
|
public float WarBudgetRatio { get; set; } = 0.35f;
|
||||||
|
public float ReserveMilitaryRatio { get; set; } = 0.2f;
|
||||||
|
public float OffensiveReadinessThreshold { get; set; } = 0.62f;
|
||||||
|
public float SupplySecurityBias { get; set; } = 0.55f;
|
||||||
|
public float FailureAversion { get; set; } = 0.45f;
|
||||||
|
public int ReinforcementLeadPerFront { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionMemoryRuntime
|
||||||
|
{
|
||||||
|
public int LastPlanCycle { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||||
|
public int LastObservedShipsBuilt { get; set; }
|
||||||
|
public int LastObservedShipsLost { get; set; }
|
||||||
|
public float LastObservedCredits { get; set; }
|
||||||
|
public HashSet<string> KnownSystemIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> KnownEnemyFactionIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public List<FactionSystemMemoryRuntime> SystemMemories { get; } = [];
|
||||||
|
public List<FactionCommodityMemoryRuntime> CommodityMemories { get; } = [];
|
||||||
|
public List<FactionOutcomeRecordRuntime> RecentOutcomes { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionSystemMemoryRuntime
|
||||||
|
{
|
||||||
|
public required string SystemId { get; init; }
|
||||||
|
public DateTimeOffset LastSeenAtUtc { get; set; }
|
||||||
|
public int LastEnemyShipCount { get; set; }
|
||||||
|
public int LastEnemyStationCount { get; set; }
|
||||||
|
public bool ControlledByFaction { get; set; }
|
||||||
|
public string? LastRole { get; set; }
|
||||||
|
public float FrontierPressure { get; set; }
|
||||||
|
public float RouteRisk { get; set; }
|
||||||
|
public float HistoricalShortagePressure { get; set; }
|
||||||
|
public int OffensiveFailures { get; set; }
|
||||||
|
public int DefensiveFailures { get; set; }
|
||||||
|
public int OffensiveSuccesses { get; set; }
|
||||||
|
public int DefensiveSuccesses { get; set; }
|
||||||
|
public DateTimeOffset? LastContestedAtUtc { get; set; }
|
||||||
|
public DateTimeOffset? LastShortageAtUtc { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionCommodityMemoryRuntime
|
||||||
|
{
|
||||||
|
public required string ItemId { get; init; }
|
||||||
|
public float HistoricalShortageScore { get; set; }
|
||||||
|
public float HistoricalSurplusScore { get; set; }
|
||||||
|
public float LastObservedBacklog { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||||
|
public DateTimeOffset? LastCriticalAtUtc { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionOutcomeRecordRuntime
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string ObjectiveId { get; init; }
|
public required string Kind { get; set; }
|
||||||
public required FactionPlanStepKind Kind { get; init; }
|
public required string Summary { get; set; }
|
||||||
public FactionPlanStepStatus Status { get; set; } = FactionPlanStepStatus.Planned;
|
public string? RelatedCampaignId { get; set; }
|
||||||
public float Priority { get; set; }
|
public string? RelatedObjectiveId { get; set; }
|
||||||
public string? CommodityId { get; set; }
|
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
public string? ModuleId { get; set; }
|
|
||||||
public string? TargetFactionId { get; set; }
|
|
||||||
public string? TargetSiteId { get; set; }
|
|
||||||
public string? StatusReason { get; set; }
|
|
||||||
public string? ExecutionBindingKind { get; set; }
|
|
||||||
public string? ExecutionBindingTargetId { get; set; }
|
|
||||||
public string? ExecutionBindingSummary { 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 sealed class FactionStrategicStateRuntime
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public int PlanCycle { get; set; }
|
||||||
public required string MergeKey { get; init; }
|
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||||
public required FactionIssuedTaskKind Kind { get; init; }
|
public string Status { get; set; } = "stable";
|
||||||
public required string ObjectiveId { get; init; }
|
public FactionBudgetRuntime Budget { get; set; } = new();
|
||||||
public required string StepId { get; init; }
|
public FactionEconomicAssessmentRuntime EconomicAssessment { get; set; } = new();
|
||||||
public FactionIssuedTaskState State { get; set; } = FactionIssuedTaskState.Planned;
|
public FactionThreatAssessmentRuntime ThreatAssessment { get; set; } = new();
|
||||||
public float Priority { get; set; }
|
public List<FactionTheaterRuntime> Theaters { get; } = [];
|
||||||
public string? ShipRole { get; set; }
|
public List<FactionCampaignRuntime> Campaigns { get; } = [];
|
||||||
public string? CommodityId { get; set; }
|
public List<FactionOperationalObjectiveRuntime> Objectives { get; } = [];
|
||||||
public string? ModuleId { get; set; }
|
public List<FactionAssetReservationRuntime> Reservations { get; } = [];
|
||||||
public string? TargetFactionId { get; set; }
|
public List<FactionProductionProgramRuntime> ProductionPrograms { get; } = [];
|
||||||
public string? TargetSystemId { get; set; }
|
}
|
||||||
public string? TargetSiteId { get; set; }
|
|
||||||
public int CreatedAtCycle { get; init; }
|
public sealed class FactionBudgetRuntime
|
||||||
public int UpdatedAtCycle { get; set; }
|
{
|
||||||
public string? BlockingReason { get; set; }
|
public float ReservedCredits { get; set; }
|
||||||
public string? Notes { get; set; }
|
public float ExpansionCredits { get; set; }
|
||||||
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
public float WarCredits { get; set; }
|
||||||
}
|
public int ReservedMilitaryAssets { get; set; }
|
||||||
|
public int ReservedLogisticsAssets { get; set; }
|
||||||
public sealed class FactionBlackboardRuntime
|
public int ReservedConstructionAssets { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionEconomicAssessmentRuntime
|
||||||
{
|
{
|
||||||
public int PlanCycle { get; set; }
|
public int PlanCycle { get; set; }
|
||||||
public DateTimeOffset UpdatedAtUtc { 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 MilitaryShipCount { get; set; }
|
||||||
public int MinerShipCount { get; set; }
|
public int MinerShipCount { get; set; }
|
||||||
public int TransportShipCount { get; set; }
|
public int TransportShipCount { get; set; }
|
||||||
public int ConstructorShipCount { get; set; }
|
public int ConstructorShipCount { get; set; }
|
||||||
public int ControlledSystemCount { get; set; }
|
public int ControlledSystemCount { get; set; }
|
||||||
|
public int TargetMilitaryShipCount { get; set; }
|
||||||
|
public int TargetMinerShipCount { get; set; }
|
||||||
|
public int TargetTransportShipCount { get; set; }
|
||||||
|
public int TargetConstructorShipCount { get; set; }
|
||||||
|
public bool HasShipyard { get; set; }
|
||||||
|
public bool HasWarIndustrySupplyChain { get; set; }
|
||||||
|
public string? PrimaryExpansionSiteId { get; set; }
|
||||||
|
public string? PrimaryExpansionSystemId { get; set; }
|
||||||
|
public float ReplacementPressure { get; set; }
|
||||||
|
public float SustainmentScore { get; set; }
|
||||||
|
public float LogisticsSecurityScore { get; set; }
|
||||||
|
public int CriticalShortageCount { get; set; }
|
||||||
|
public string? IndustrialBottleneckItemId { get; set; }
|
||||||
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
|
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionThreatAssessmentRuntime
|
||||||
|
{
|
||||||
|
public int PlanCycle { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||||
|
public int EnemyFactionCount { get; set; }
|
||||||
|
public int EnemyShipCount { get; set; }
|
||||||
|
public int EnemyStationCount { get; set; }
|
||||||
|
public string? PrimaryThreatFactionId { get; set; }
|
||||||
|
public string? PrimaryThreatSystemId { get; set; }
|
||||||
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
|
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
|
||||||
public HashSet<string> AvailableShipIds { get; } = new(StringComparer.Ordinal);
|
}
|
||||||
|
|
||||||
|
public sealed class FactionTheaterRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public float SupplyRisk { get; set; }
|
||||||
|
public float FriendlyAssetValue { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public string? AnchorEntityId { get; set; }
|
||||||
|
public Vector3? AnchorPosition { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> CampaignIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionCampaignRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "planned";
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? TheaterId { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? CommodityId { get; set; }
|
||||||
|
public string? SupportStationId { get; set; }
|
||||||
|
public int CurrentStepIndex { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string? PauseReason { get; set; }
|
||||||
|
public float ContinuationScore { get; set; }
|
||||||
|
public float SupplyAdequacy { get; set; }
|
||||||
|
public float ReplacementPressure { get; set; }
|
||||||
|
public int FailureCount { get; set; }
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
public string? FleetCommanderId { get; set; }
|
||||||
|
public bool RequiresReinforcement { get; set; }
|
||||||
|
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||||
|
public List<string> ObjectiveIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionOperationalObjectiveRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string CampaignId { get; set; }
|
||||||
|
public string? TheaterId { get; set; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public required string DelegationKind { get; set; }
|
||||||
|
public required string BehaviorKind { get; set; }
|
||||||
|
public string Status { get; set; } = "planned";
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
|
public string? HomeSystemId { get; set; }
|
||||||
|
public string? HomeStationId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public Vector3? TargetPosition { get; set; }
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public int CurrentStepIndex { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public bool UseOrders { get; set; }
|
||||||
|
public string? StagingOrderKind { get; set; }
|
||||||
|
public int ReinforcementLevel { get; set; }
|
||||||
|
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||||
|
public List<string> ReservedAssetIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionPlanStepRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "planned";
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionAssetReservationRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string ObjectiveId { get; set; }
|
||||||
|
public string? CampaignId { get; set; }
|
||||||
|
public required string AssetKind { get; set; }
|
||||||
|
public required string AssetId { get; set; }
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionProductionProgramRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "planned";
|
||||||
|
public float Priority { get; set; }
|
||||||
|
public string? CampaignId { get; set; }
|
||||||
|
public string? CommodityId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public string? ShipKind { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public int TargetCount { get; set; }
|
||||||
|
public int CurrentCount { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FactionDecisionLogEntryRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public string? RelatedEntityId { get; set; }
|
||||||
|
public int PlanCycle { get; set; }
|
||||||
|
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class FactionCommoditySignalRuntime
|
public sealed class FactionCommoditySignalRuntime
|
||||||
@@ -228,38 +344,5 @@ public sealed class FactionThreatSignalRuntime
|
|||||||
public required string ScopeKind { get; init; }
|
public required string ScopeKind { get; init; }
|
||||||
public int EnemyShipCount { get; set; }
|
public int EnemyShipCount { get; set; }
|
||||||
public int EnemyStationCount { get; set; }
|
public int EnemyStationCount { get; set; }
|
||||||
}
|
public string? EnemyFactionId { get; set; }
|
||||||
|
|
||||||
public sealed class CommanderBehaviorRuntime
|
|
||||||
{
|
|
||||||
public required string Kind { get; set; }
|
|
||||||
public string? Phase { get; set; }
|
|
||||||
public string? TargetEntityId { get; set; }
|
|
||||||
public string? ItemId { get; set; }
|
|
||||||
public string? NodeId { get; set; }
|
|
||||||
public string? StationId { get; set; }
|
|
||||||
public string? ModuleId { get; set; }
|
|
||||||
public string? AreaSystemId { get; set; }
|
|
||||||
public int PatrolIndex { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class CommanderOrderRuntime
|
|
||||||
{
|
|
||||||
public required string Kind { get; init; }
|
|
||||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
|
||||||
public string? TargetEntityId { get; set; }
|
|
||||||
public string? DestinationNodeId { get; set; }
|
|
||||||
public required string DestinationSystemId { get; init; }
|
|
||||||
public required Vector3 DestinationPosition { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class CommanderTaskRuntime
|
|
||||||
{
|
|
||||||
public required string Kind { get; set; }
|
|
||||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
|
||||||
public string? TargetEntityId { get; set; }
|
|
||||||
public string? TargetSystemId { get; set; }
|
|
||||||
public string? TargetNodeId { get; set; }
|
|
||||||
public Vector3? TargetPosition { get; set; }
|
|
||||||
public float Threshold { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
283
apps/backend/Geopolitics/Contracts/Geopolitics.cs
Normal file
283
apps/backend/Geopolitics/Contracts/Geopolitics.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
namespace SpaceGame.Api.Geopolitics.Contracts;
|
||||||
|
|
||||||
|
public sealed record SystemRouteLinkSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SourceSystemId,
|
||||||
|
string DestinationSystemId,
|
||||||
|
float Distance,
|
||||||
|
bool IsPrimaryLane);
|
||||||
|
|
||||||
|
public sealed record DiplomaticRelationSnapshot(
|
||||||
|
string Id,
|
||||||
|
string FactionAId,
|
||||||
|
string FactionBId,
|
||||||
|
string Status,
|
||||||
|
string Posture,
|
||||||
|
float TrustScore,
|
||||||
|
float TensionScore,
|
||||||
|
float GrievanceScore,
|
||||||
|
string TradeAccessPolicy,
|
||||||
|
string MilitaryAccessPolicy,
|
||||||
|
string? WarStateId,
|
||||||
|
DateTimeOffset? CeasefireUntilUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> ActiveTreatyIds,
|
||||||
|
IReadOnlyList<string> ActiveIncidentIds);
|
||||||
|
|
||||||
|
public sealed record TreatySnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string TradeAccessPolicy,
|
||||||
|
string MilitaryAccessPolicy,
|
||||||
|
string? Summary,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> FactionIds);
|
||||||
|
|
||||||
|
public sealed record DiplomaticIncidentSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string SourceFactionId,
|
||||||
|
string TargetFactionId,
|
||||||
|
string? SystemId,
|
||||||
|
string? BorderEdgeId,
|
||||||
|
string Summary,
|
||||||
|
float Severity,
|
||||||
|
float EscalationScore,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset LastObservedAtUtc);
|
||||||
|
|
||||||
|
public sealed record BorderTensionSnapshot(
|
||||||
|
string Id,
|
||||||
|
string RelationId,
|
||||||
|
string BorderEdgeId,
|
||||||
|
string FactionAId,
|
||||||
|
string FactionBId,
|
||||||
|
string Status,
|
||||||
|
float TensionScore,
|
||||||
|
float IncidentScore,
|
||||||
|
float MilitaryPressure,
|
||||||
|
float AccessFriction,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> SystemIds);
|
||||||
|
|
||||||
|
public sealed record WarStateSnapshot(
|
||||||
|
string Id,
|
||||||
|
string RelationId,
|
||||||
|
string FactionAId,
|
||||||
|
string FactionBId,
|
||||||
|
string Status,
|
||||||
|
string WarGoal,
|
||||||
|
float EscalationScore,
|
||||||
|
DateTimeOffset StartedAtUtc,
|
||||||
|
DateTimeOffset? CeasefireUntilUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> ActiveFrontLineIds);
|
||||||
|
|
||||||
|
public sealed record DiplomaticStateSnapshot(
|
||||||
|
IReadOnlyList<DiplomaticRelationSnapshot> Relations,
|
||||||
|
IReadOnlyList<TreatySnapshot> Treaties,
|
||||||
|
IReadOnlyList<DiplomaticIncidentSnapshot> Incidents,
|
||||||
|
IReadOnlyList<BorderTensionSnapshot> BorderTensions,
|
||||||
|
IReadOnlyList<WarStateSnapshot> Wars);
|
||||||
|
|
||||||
|
public sealed record TerritoryClaimSnapshot(
|
||||||
|
string Id,
|
||||||
|
string? SourceClaimId,
|
||||||
|
string FactionId,
|
||||||
|
string SystemId,
|
||||||
|
string CelestialId,
|
||||||
|
string Status,
|
||||||
|
string ClaimKind,
|
||||||
|
float ClaimStrength,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record TerritoryInfluenceSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SystemId,
|
||||||
|
string FactionId,
|
||||||
|
float ClaimStrength,
|
||||||
|
float AssetStrength,
|
||||||
|
float LogisticsStrength,
|
||||||
|
float TotalInfluence,
|
||||||
|
bool IsContesting,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record TerritoryControlStateSnapshot(
|
||||||
|
string SystemId,
|
||||||
|
string? ControllerFactionId,
|
||||||
|
string? PrimaryClaimantFactionId,
|
||||||
|
string ControlKind,
|
||||||
|
bool IsContested,
|
||||||
|
float ControlScore,
|
||||||
|
float StrategicValue,
|
||||||
|
IReadOnlyList<string> ClaimantFactionIds,
|
||||||
|
IReadOnlyList<string> InfluencingFactionIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record SectorStrategicProfileSnapshot(
|
||||||
|
string SystemId,
|
||||||
|
string? ControllerFactionId,
|
||||||
|
string ZoneKind,
|
||||||
|
bool IsContested,
|
||||||
|
float StrategicValue,
|
||||||
|
float SecurityRating,
|
||||||
|
float TerritorialPressure,
|
||||||
|
float LogisticsValue,
|
||||||
|
string? EconomicRegionId,
|
||||||
|
string? FrontLineId,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record BorderEdgeSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SourceSystemId,
|
||||||
|
string DestinationSystemId,
|
||||||
|
string? SourceFactionId,
|
||||||
|
string? DestinationFactionId,
|
||||||
|
bool IsContested,
|
||||||
|
string? RelationId,
|
||||||
|
float TensionScore,
|
||||||
|
float CorridorImportance,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record FrontLineSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string? AnchorSystemId,
|
||||||
|
float PressureScore,
|
||||||
|
float SupplyRisk,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> FactionIds,
|
||||||
|
IReadOnlyList<string> SystemIds,
|
||||||
|
IReadOnlyList<string> BorderEdgeIds);
|
||||||
|
|
||||||
|
public sealed record TerritoryZoneSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SystemId,
|
||||||
|
string? FactionId,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string? Reason,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record TerritoryPressureSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SystemId,
|
||||||
|
string? FactionId,
|
||||||
|
string Kind,
|
||||||
|
float PressureScore,
|
||||||
|
float SecurityScore,
|
||||||
|
float HostileInfluence,
|
||||||
|
float CorridorRisk,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record TerritoryStateSnapshot(
|
||||||
|
IReadOnlyList<TerritoryClaimSnapshot> Claims,
|
||||||
|
IReadOnlyList<TerritoryInfluenceSnapshot> Influences,
|
||||||
|
IReadOnlyList<TerritoryControlStateSnapshot> ControlStates,
|
||||||
|
IReadOnlyList<SectorStrategicProfileSnapshot> StrategicProfiles,
|
||||||
|
IReadOnlyList<BorderEdgeSnapshot> BorderEdges,
|
||||||
|
IReadOnlyList<FrontLineSnapshot> FrontLines,
|
||||||
|
IReadOnlyList<TerritoryZoneSnapshot> Zones,
|
||||||
|
IReadOnlyList<TerritoryPressureSnapshot> Pressures);
|
||||||
|
|
||||||
|
public sealed record EconomicRegionSnapshot(
|
||||||
|
string Id,
|
||||||
|
string? FactionId,
|
||||||
|
string Label,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string CoreSystemId,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> SystemIds,
|
||||||
|
IReadOnlyList<string> StationIds,
|
||||||
|
IReadOnlyList<string> FrontLineIds,
|
||||||
|
IReadOnlyList<string> CorridorIds);
|
||||||
|
|
||||||
|
public sealed record SupplyNetworkSnapshot(
|
||||||
|
string Id,
|
||||||
|
string RegionId,
|
||||||
|
float ThroughputScore,
|
||||||
|
float RiskScore,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> StationIds,
|
||||||
|
IReadOnlyList<string> ProducerItemIds,
|
||||||
|
IReadOnlyList<string> ConsumerItemIds,
|
||||||
|
IReadOnlyList<string> ConstructionItemIds);
|
||||||
|
|
||||||
|
public sealed record LogisticsCorridorSnapshot(
|
||||||
|
string Id,
|
||||||
|
string? FactionId,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
float RiskScore,
|
||||||
|
float ThroughputScore,
|
||||||
|
string AccessState,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> SystemPathIds,
|
||||||
|
IReadOnlyList<string> RegionIds,
|
||||||
|
IReadOnlyList<string> BorderEdgeIds);
|
||||||
|
|
||||||
|
public sealed record RegionalProductionProfileSnapshot(
|
||||||
|
string RegionId,
|
||||||
|
string PrimaryIndustry,
|
||||||
|
int ShipyardCount,
|
||||||
|
int StationCount,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<string> ProducedItemIds,
|
||||||
|
IReadOnlyList<string> ScarceItemIds);
|
||||||
|
|
||||||
|
public sealed record RegionalTradeBalanceSnapshot(
|
||||||
|
string RegionId,
|
||||||
|
int ImportsRequiredCount,
|
||||||
|
int ExportsSurplusCount,
|
||||||
|
int CriticalShortageCount,
|
||||||
|
float NetTradeScore,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record RegionalBottleneckSnapshot(
|
||||||
|
string Id,
|
||||||
|
string RegionId,
|
||||||
|
string ItemId,
|
||||||
|
string Cause,
|
||||||
|
string Status,
|
||||||
|
float Severity,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record RegionalSecurityAssessmentSnapshot(
|
||||||
|
string RegionId,
|
||||||
|
float SupplyRisk,
|
||||||
|
float BorderPressure,
|
||||||
|
int ActiveWarCount,
|
||||||
|
int HostileRelationCount,
|
||||||
|
float AccessFriction,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record RegionalEconomicAssessmentSnapshot(
|
||||||
|
string RegionId,
|
||||||
|
float SustainmentScore,
|
||||||
|
float ProductionDepth,
|
||||||
|
float ConstructionPressure,
|
||||||
|
float CorridorDependency,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record EconomyRegionStateSnapshot(
|
||||||
|
IReadOnlyList<EconomicRegionSnapshot> Regions,
|
||||||
|
IReadOnlyList<SupplyNetworkSnapshot> SupplyNetworks,
|
||||||
|
IReadOnlyList<LogisticsCorridorSnapshot> Corridors,
|
||||||
|
IReadOnlyList<RegionalProductionProfileSnapshot> ProductionProfiles,
|
||||||
|
IReadOnlyList<RegionalTradeBalanceSnapshot> TradeBalances,
|
||||||
|
IReadOnlyList<RegionalBottleneckSnapshot> Bottlenecks,
|
||||||
|
IReadOnlyList<RegionalSecurityAssessmentSnapshot> SecurityAssessments,
|
||||||
|
IReadOnlyList<RegionalEconomicAssessmentSnapshot> EconomicAssessments);
|
||||||
|
|
||||||
|
public sealed record GeopoliticalStateSnapshot(
|
||||||
|
int Cycle,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
IReadOnlyList<SystemRouteLinkSnapshot> Routes,
|
||||||
|
DiplomaticStateSnapshot Diplomacy,
|
||||||
|
TerritoryStateSnapshot Territory,
|
||||||
|
EconomyRegionStateSnapshot EconomyRegions);
|
||||||
336
apps/backend/Geopolitics/Runtime/GeopoliticalRuntimeModels.cs
Normal file
336
apps/backend/Geopolitics/Runtime/GeopoliticalRuntimeModels.cs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
namespace SpaceGame.Api.Geopolitics.Runtime;
|
||||||
|
|
||||||
|
public sealed class GeopoliticalStateRuntime
|
||||||
|
{
|
||||||
|
public int Cycle { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<SystemRouteLinkRuntime> Routes { get; } = [];
|
||||||
|
public DiplomaticStateRuntime Diplomacy { get; set; } = new();
|
||||||
|
public TerritoryStateRuntime Territory { get; set; } = new();
|
||||||
|
public EconomyRegionStateRuntime EconomyRegions { get; set; } = new();
|
||||||
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SystemRouteLinkRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SourceSystemId { get; set; }
|
||||||
|
public required string DestinationSystemId { get; set; }
|
||||||
|
public float Distance { get; set; }
|
||||||
|
public bool IsPrimaryLane { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DiplomaticStateRuntime
|
||||||
|
{
|
||||||
|
public List<DiplomaticRelationRuntime> Relations { get; } = [];
|
||||||
|
public List<TreatyRuntime> Treaties { get; } = [];
|
||||||
|
public List<DiplomaticIncidentRuntime> Incidents { get; } = [];
|
||||||
|
public List<BorderTensionRuntime> BorderTensions { get; } = [];
|
||||||
|
public List<WarStateRuntime> Wars { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DiplomaticRelationRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string FactionAId { get; set; }
|
||||||
|
public required string FactionBId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Posture { get; set; } = "neutral";
|
||||||
|
public float TrustScore { get; set; }
|
||||||
|
public float TensionScore { get; set; }
|
||||||
|
public float GrievanceScore { get; set; }
|
||||||
|
public string TradeAccessPolicy { get; set; } = "restricted";
|
||||||
|
public string MilitaryAccessPolicy { get; set; } = "restricted";
|
||||||
|
public string? WarStateId { get; set; }
|
||||||
|
public DateTimeOffset? CeasefireUntilUtc { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> ActiveTreatyIds { get; } = [];
|
||||||
|
public List<string> ActiveIncidentIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TreatyRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string TradeAccessPolicy { get; set; } = "restricted";
|
||||||
|
public string MilitaryAccessPolicy { get; set; } = "restricted";
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> FactionIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DiplomaticIncidentRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public required string SourceFactionId { get; set; }
|
||||||
|
public required string TargetFactionId { get; set; }
|
||||||
|
public string? SystemId { get; set; }
|
||||||
|
public string? BorderEdgeId { get; set; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public float Severity { get; set; }
|
||||||
|
public float EscalationScore { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset LastObservedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BorderTensionRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string RelationId { get; set; }
|
||||||
|
public required string BorderEdgeId { get; set; }
|
||||||
|
public required string FactionAId { get; set; }
|
||||||
|
public required string FactionBId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public float TensionScore { get; set; }
|
||||||
|
public float IncidentScore { get; set; }
|
||||||
|
public float MilitaryPressure { get; set; }
|
||||||
|
public float AccessFriction { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> SystemIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WarStateRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string RelationId { get; set; }
|
||||||
|
public required string FactionAId { get; set; }
|
||||||
|
public required string FactionBId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string WarGoal { get; set; } = "territorial-pressure";
|
||||||
|
public float EscalationScore { get; set; }
|
||||||
|
public DateTimeOffset StartedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset? CeasefireUntilUtc { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> ActiveFrontLineIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryStateRuntime
|
||||||
|
{
|
||||||
|
public List<TerritoryClaimRuntime> Claims { get; } = [];
|
||||||
|
public List<TerritoryInfluenceRuntime> Influences { get; } = [];
|
||||||
|
public List<TerritoryControlStateRuntime> ControlStates { get; } = [];
|
||||||
|
public List<SectorStrategicProfileRuntime> StrategicProfiles { get; } = [];
|
||||||
|
public List<BorderEdgeRuntime> BorderEdges { get; } = [];
|
||||||
|
public List<FrontLineRuntime> FrontLines { get; } = [];
|
||||||
|
public List<TerritoryZoneRuntime> Zones { get; } = [];
|
||||||
|
public List<TerritoryPressureRuntime> Pressures { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryClaimRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public string? SourceClaimId { get; set; }
|
||||||
|
public required string FactionId { get; set; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public required string CelestialId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string ClaimKind { get; set; } = "infrastructure";
|
||||||
|
public float ClaimStrength { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryInfluenceRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public required string FactionId { get; set; }
|
||||||
|
public float ClaimStrength { get; set; }
|
||||||
|
public float AssetStrength { get; set; }
|
||||||
|
public float LogisticsStrength { get; set; }
|
||||||
|
public float TotalInfluence { get; set; }
|
||||||
|
public bool IsContesting { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryControlStateRuntime
|
||||||
|
{
|
||||||
|
public required string SystemId { get; init; }
|
||||||
|
public string? ControllerFactionId { get; set; }
|
||||||
|
public string? PrimaryClaimantFactionId { get; set; }
|
||||||
|
public string ControlKind { get; set; } = "unclaimed";
|
||||||
|
public bool IsContested { get; set; }
|
||||||
|
public float ControlScore { get; set; }
|
||||||
|
public float StrategicValue { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> ClaimantFactionIds { get; } = [];
|
||||||
|
public List<string> InfluencingFactionIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SectorStrategicProfileRuntime
|
||||||
|
{
|
||||||
|
public required string SystemId { get; init; }
|
||||||
|
public string? ControllerFactionId { get; set; }
|
||||||
|
public string ZoneKind { get; set; } = "unclaimed";
|
||||||
|
public bool IsContested { get; set; }
|
||||||
|
public float StrategicValue { get; set; }
|
||||||
|
public float SecurityRating { get; set; }
|
||||||
|
public float TerritorialPressure { get; set; }
|
||||||
|
public float LogisticsValue { get; set; }
|
||||||
|
public string? EconomicRegionId { get; set; }
|
||||||
|
public string? FrontLineId { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class BorderEdgeRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SourceSystemId { get; set; }
|
||||||
|
public required string DestinationSystemId { get; set; }
|
||||||
|
public string? SourceFactionId { get; set; }
|
||||||
|
public string? DestinationFactionId { get; set; }
|
||||||
|
public bool IsContested { get; set; }
|
||||||
|
public string? RelationId { get; set; }
|
||||||
|
public float TensionScore { get; set; }
|
||||||
|
public float CorridorImportance { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FrontLineRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public string Kind { get; set; } = "border-front";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string? AnchorSystemId { get; set; }
|
||||||
|
public float PressureScore { get; set; }
|
||||||
|
public float SupplyRisk { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> FactionIds { get; } = [];
|
||||||
|
public List<string> SystemIds { get; } = [];
|
||||||
|
public List<string> BorderEdgeIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryZoneRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public string? FactionId { get; set; }
|
||||||
|
public string Kind { get; set; } = "unclaimed";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class TerritoryPressureRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public string? FactionId { get; set; }
|
||||||
|
public string Kind { get; set; } = "border-pressure";
|
||||||
|
public float PressureScore { get; set; }
|
||||||
|
public float SecurityScore { get; set; }
|
||||||
|
public float HostileInfluence { get; set; }
|
||||||
|
public float CorridorRisk { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EconomyRegionStateRuntime
|
||||||
|
{
|
||||||
|
public List<EconomicRegionRuntime> Regions { get; } = [];
|
||||||
|
public List<SupplyNetworkRuntime> SupplyNetworks { get; } = [];
|
||||||
|
public List<LogisticsCorridorRuntime> Corridors { get; } = [];
|
||||||
|
public List<RegionalProductionProfileRuntime> ProductionProfiles { get; } = [];
|
||||||
|
public List<RegionalTradeBalanceRuntime> TradeBalances { get; } = [];
|
||||||
|
public List<RegionalBottleneckRuntime> Bottlenecks { get; } = [];
|
||||||
|
public List<RegionalSecurityAssessmentRuntime> SecurityAssessments { get; } = [];
|
||||||
|
public List<RegionalEconomicAssessmentRuntime> EconomicAssessments { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EconomicRegionRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public string? FactionId { get; set; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Kind { get; set; } = "balanced-region";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public required string CoreSystemId { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> SystemIds { get; } = [];
|
||||||
|
public List<string> StationIds { get; } = [];
|
||||||
|
public List<string> FrontLineIds { get; } = [];
|
||||||
|
public List<string> CorridorIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SupplyNetworkRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public float ThroughputScore { get; set; }
|
||||||
|
public float RiskScore { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> StationIds { get; } = [];
|
||||||
|
public List<string> ProducerItemIds { get; } = [];
|
||||||
|
public List<string> ConsumerItemIds { get; } = [];
|
||||||
|
public List<string> ConstructionItemIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LogisticsCorridorRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public string? FactionId { get; set; }
|
||||||
|
public string Kind { get; set; } = "supply-corridor";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public float RiskScore { get; set; }
|
||||||
|
public float ThroughputScore { get; set; }
|
||||||
|
public string AccessState { get; set; } = "restricted";
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> SystemPathIds { get; } = [];
|
||||||
|
public List<string> RegionIds { get; } = [];
|
||||||
|
public List<string> BorderEdgeIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RegionalProductionProfileRuntime
|
||||||
|
{
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public string PrimaryIndustry { get; set; } = "mixed";
|
||||||
|
public int ShipyardCount { get; set; }
|
||||||
|
public int StationCount { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public List<string> ProducedItemIds { get; } = [];
|
||||||
|
public List<string> ScarceItemIds { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RegionalTradeBalanceRuntime
|
||||||
|
{
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public int ImportsRequiredCount { get; set; }
|
||||||
|
public int ExportsSurplusCount { get; set; }
|
||||||
|
public int CriticalShortageCount { get; set; }
|
||||||
|
public float NetTradeScore { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RegionalBottleneckRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public required string ItemId { get; set; }
|
||||||
|
public string Cause { get; set; } = "regional-shortage";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public float Severity { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RegionalSecurityAssessmentRuntime
|
||||||
|
{
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public float SupplyRisk { get; set; }
|
||||||
|
public float BorderPressure { get; set; }
|
||||||
|
public int ActiveWarCount { get; set; }
|
||||||
|
public int HostileRelationCount { get; set; }
|
||||||
|
public float AccessFriction { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RegionalEconomicAssessmentRuntime
|
||||||
|
{
|
||||||
|
public required string RegionId { get; set; }
|
||||||
|
public float SustainmentScore { get; set; }
|
||||||
|
public float ProductionDepth { get; set; }
|
||||||
|
public float ConstructionPressure { get; set; }
|
||||||
|
public float CorridorDependency { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,923 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Geopolitics.Simulation;
|
||||||
|
|
||||||
|
internal sealed class GeopoliticalSimulationService
|
||||||
|
{
|
||||||
|
internal void Update(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||||
|
{
|
||||||
|
var state = EnsureState(world);
|
||||||
|
state.Cycle += 1;
|
||||||
|
state.UpdatedAtUtc = world.GeneratedAtUtc;
|
||||||
|
|
||||||
|
RebuildRoutes(world, state);
|
||||||
|
RebuildTerritory(world, state);
|
||||||
|
RebuildDiplomacy(world, state, events);
|
||||||
|
RebuildEconomyRegions(world, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static GeopoliticalStateRuntime EnsureState(SimulationWorld world)
|
||||||
|
{
|
||||||
|
world.Geopolitics ??= new GeopoliticalStateRuntime();
|
||||||
|
return world.Geopolitics;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static DiplomaticRelationRuntime? FindRelation(SimulationWorld world, string factionAId, string factionBId)
|
||||||
|
{
|
||||||
|
var state = EnsureState(world);
|
||||||
|
return state.Diplomacy.Relations.FirstOrDefault(relation => string.Equals(relation.Id, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static WarStateRuntime? FindWarState(SimulationWorld world, string factionAId, string factionBId) =>
|
||||||
|
EnsureState(world).Diplomacy.Wars.FirstOrDefault(war => string.Equals(war.RelationId, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal) && war.Status == "active");
|
||||||
|
|
||||||
|
internal static TerritoryControlStateRuntime? GetSystemControlState(SimulationWorld world, string systemId) =>
|
||||||
|
EnsureState(world).Territory.ControlStates.FirstOrDefault(state => string.Equals(state.SystemId, systemId, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
internal static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) =>
|
||||||
|
string.Equals(GetSystemControlState(world, systemId)?.ControllerFactionId, factionId, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
internal static IReadOnlyList<string> GetControlledSystems(SimulationWorld world, string factionId) =>
|
||||||
|
EnsureState(world).Territory.ControlStates
|
||||||
|
.Where(state => string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal))
|
||||||
|
.OrderBy(state => state.SystemId, StringComparer.Ordinal)
|
||||||
|
.Select(state => state.SystemId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
internal static float GetSystemRouteRisk(SimulationWorld world, string systemId, string? factionId = null)
|
||||||
|
{
|
||||||
|
var pressure = EnsureState(world).Territory.Pressures
|
||||||
|
.Where(entry => string.Equals(entry.SystemId, systemId, StringComparison.Ordinal)
|
||||||
|
&& (factionId is null || string.Equals(entry.FactionId, factionId, StringComparison.Ordinal)))
|
||||||
|
.OrderByDescending(entry => entry.CorridorRisk)
|
||||||
|
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||||
|
.FirstOrDefault();
|
||||||
|
return pressure?.CorridorRisk
|
||||||
|
?? EnsureState(world).Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == systemId)?.TerritorialPressure
|
||||||
|
?? 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasHostileRelation(SimulationWorld world, string factionAId, string factionBId)
|
||||||
|
{
|
||||||
|
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relation = FindRelation(world, factionAId, factionBId);
|
||||||
|
return relation is not null && relation.Posture is "hostile" or "war";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasTradeAccess(SimulationWorld world, string factionAId, string factionBId)
|
||||||
|
{
|
||||||
|
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relation = FindRelation(world, factionAId, factionBId);
|
||||||
|
return relation?.TradeAccessPolicy is "open" or "allied";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasMilitaryAccess(SimulationWorld world, string factionAId, string factionBId)
|
||||||
|
{
|
||||||
|
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relation = FindRelation(world, factionAId, factionBId);
|
||||||
|
return relation?.MilitaryAccessPolicy is "open" or "allied";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static EconomicRegionRuntime? GetPrimaryEconomicRegion(SimulationWorld world, string factionId, string systemId) =>
|
||||||
|
EnsureState(world).EconomyRegions.Regions.FirstOrDefault(region =>
|
||||||
|
string.Equals(region.FactionId, factionId, StringComparison.Ordinal)
|
||||||
|
&& region.SystemIds.Contains(systemId, StringComparer.Ordinal));
|
||||||
|
|
||||||
|
private static void RebuildRoutes(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||||
|
{
|
||||||
|
state.Routes.Clear();
|
||||||
|
if (world.Systems.Count <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var systems = world.Systems
|
||||||
|
.OrderBy(system => system.Definition.Id, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
var routeIds = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var system in systems)
|
||||||
|
{
|
||||||
|
foreach (var neighbor in systems
|
||||||
|
.Where(candidate => candidate.Definition.Id != system.Definition.Id)
|
||||||
|
.Select(candidate => new
|
||||||
|
{
|
||||||
|
candidate.Definition.Id,
|
||||||
|
Distance = system.Position.DistanceTo(candidate.Position),
|
||||||
|
})
|
||||||
|
.OrderBy(candidate => candidate.Distance)
|
||||||
|
.ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
|
||||||
|
.Take(Math.Min(3, systems.Count - 1)))
|
||||||
|
{
|
||||||
|
var routeId = BuildPairId("route", system.Definition.Id, neighbor.Id);
|
||||||
|
if (!routeIds.Add(routeId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Routes.Add(new SystemRouteLinkRuntime
|
||||||
|
{
|
||||||
|
Id = routeId,
|
||||||
|
SourceSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? system.Definition.Id : neighbor.Id,
|
||||||
|
DestinationSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? neighbor.Id : system.Definition.Id,
|
||||||
|
Distance = neighbor.Distance,
|
||||||
|
IsPrimaryLane = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildTerritory(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||||
|
{
|
||||||
|
state.Territory.Claims.Clear();
|
||||||
|
state.Territory.Influences.Clear();
|
||||||
|
state.Territory.ControlStates.Clear();
|
||||||
|
state.Territory.StrategicProfiles.Clear();
|
||||||
|
state.Territory.BorderEdges.Clear();
|
||||||
|
state.Territory.FrontLines.Clear();
|
||||||
|
state.Territory.Zones.Clear();
|
||||||
|
state.Territory.Pressures.Clear();
|
||||||
|
|
||||||
|
var nowUtc = world.GeneratedAtUtc;
|
||||||
|
foreach (var claim in world.Claims.Where(claim => claim.State != ClaimStateKinds.Destroyed))
|
||||||
|
{
|
||||||
|
state.Territory.Claims.Add(new TerritoryClaimRuntime
|
||||||
|
{
|
||||||
|
Id = $"territory-{claim.Id}",
|
||||||
|
SourceClaimId = claim.Id,
|
||||||
|
FactionId = claim.FactionId,
|
||||||
|
SystemId = claim.SystemId,
|
||||||
|
CelestialId = claim.CelestialId,
|
||||||
|
Status = claim.State,
|
||||||
|
ClaimKind = "infrastructure",
|
||||||
|
ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var influencesBySystem = new Dictionary<string, List<TerritoryInfluenceRuntime>>(StringComparer.Ordinal);
|
||||||
|
foreach (var system in world.Systems)
|
||||||
|
{
|
||||||
|
var claimsByFaction = state.Territory.Claims
|
||||||
|
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||||
|
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal);
|
||||||
|
var stationsByFaction = world.Stations
|
||||||
|
.Where(station => station.SystemId == system.Definition.Id)
|
||||||
|
.GroupBy(station => station.FactionId, StringComparer.Ordinal);
|
||||||
|
var shipsByFaction = world.Ships
|
||||||
|
.Where(ship => ship.SystemId == system.Definition.Id && ship.Health > 0f)
|
||||||
|
.GroupBy(ship => ship.FactionId, StringComparer.Ordinal);
|
||||||
|
var sitesByFaction = world.ConstructionSites
|
||||||
|
.Where(site => site.SystemId == system.Definition.Id && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed)
|
||||||
|
.GroupBy(site => site.FactionId, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var factionIds = claimsByFaction.Select(group => group.Key)
|
||||||
|
.Concat(stationsByFaction.Select(group => group.Key))
|
||||||
|
.Concat(shipsByFaction.Select(group => group.Key))
|
||||||
|
.Concat(sitesByFaction.Select(group => group.Key))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var influences = new List<TerritoryInfluenceRuntime>();
|
||||||
|
foreach (var factionId in factionIds)
|
||||||
|
{
|
||||||
|
var claimStrength = claimsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(claim => claim.ClaimStrength * 40f) ?? 0f;
|
||||||
|
var stationStrength = (stationsByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 50f;
|
||||||
|
var siteStrength = (sitesByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 18f;
|
||||||
|
var shipStrength = shipsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(ship =>
|
||||||
|
ship.Definition.Kind switch
|
||||||
|
{
|
||||||
|
"military" => 9f,
|
||||||
|
"construction" => 4f,
|
||||||
|
"transport" => 3f,
|
||||||
|
_ when ship.Definition.Kind == "mining" || ship.Definition.Kind == "miner" => 3f,
|
||||||
|
_ => 2f,
|
||||||
|
}) ?? 0f;
|
||||||
|
var logisticsStrength = MathF.Min(30f, stationStrength * 0.18f) + siteStrength;
|
||||||
|
influences.Add(new TerritoryInfluenceRuntime
|
||||||
|
{
|
||||||
|
Id = $"influence-{system.Definition.Id}-{factionId}",
|
||||||
|
SystemId = system.Definition.Id,
|
||||||
|
FactionId = factionId,
|
||||||
|
ClaimStrength = claimStrength,
|
||||||
|
AssetStrength = stationStrength + shipStrength,
|
||||||
|
LogisticsStrength = logisticsStrength,
|
||||||
|
TotalInfluence = claimStrength + stationStrength + shipStrength + logisticsStrength,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
influences.Sort((left, right) =>
|
||||||
|
{
|
||||||
|
var total = right.TotalInfluence.CompareTo(left.TotalInfluence);
|
||||||
|
return total != 0 ? total : string.Compare(left.FactionId, right.FactionId, StringComparison.Ordinal);
|
||||||
|
});
|
||||||
|
if (influences.Count > 1)
|
||||||
|
{
|
||||||
|
var lead = influences[0].TotalInfluence;
|
||||||
|
foreach (var influence in influences.Skip(1))
|
||||||
|
{
|
||||||
|
influence.IsContesting = influence.TotalInfluence >= (lead * 0.7f);
|
||||||
|
}
|
||||||
|
|
||||||
|
influences[0].IsContesting = influences[1].TotalInfluence >= (lead * 0.7f);
|
||||||
|
}
|
||||||
|
|
||||||
|
influencesBySystem[system.Definition.Id] = influences;
|
||||||
|
state.Territory.Influences.AddRange(influences);
|
||||||
|
|
||||||
|
var top = influences.FirstOrDefault();
|
||||||
|
var second = influences.Skip(1).FirstOrDefault();
|
||||||
|
var contested = top is not null && second is not null && second.TotalInfluence >= (top.TotalInfluence * 0.7f);
|
||||||
|
var controllerFactionId = top is not null && (!contested || top.TotalInfluence >= second!.TotalInfluence + 20f)
|
||||||
|
? top.FactionId
|
||||||
|
: null;
|
||||||
|
var primaryClaimantFactionId = state.Territory.Claims
|
||||||
|
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||||
|
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal)
|
||||||
|
.OrderByDescending(group => group.Sum(claim => claim.ClaimStrength))
|
||||||
|
.ThenBy(group => group.Key, StringComparer.Ordinal)
|
||||||
|
.Select(group => group.Key)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
var strategicValue = EstimateSystemStrategicValue(world, system.Definition.Id);
|
||||||
|
var controlState = new TerritoryControlStateRuntime
|
||||||
|
{
|
||||||
|
SystemId = system.Definition.Id,
|
||||||
|
ControllerFactionId = controllerFactionId,
|
||||||
|
PrimaryClaimantFactionId = primaryClaimantFactionId,
|
||||||
|
ControlKind = contested
|
||||||
|
? "contested"
|
||||||
|
: controllerFactionId is not null
|
||||||
|
? "controlled"
|
||||||
|
: primaryClaimantFactionId is not null
|
||||||
|
? "claimed"
|
||||||
|
: "unclaimed",
|
||||||
|
IsContested = contested,
|
||||||
|
ControlScore = top?.TotalInfluence ?? 0f,
|
||||||
|
StrategicValue = strategicValue,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
controlState.ClaimantFactionIds.AddRange(state.Territory.Claims
|
||||||
|
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||||
|
.Select(claim => claim.FactionId)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
controlState.InfluencingFactionIds.AddRange(influences
|
||||||
|
.Select(influence => influence.FactionId)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
state.Territory.ControlStates.Add(controlState);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var route in state.Routes)
|
||||||
|
{
|
||||||
|
var left = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.SourceSystemId);
|
||||||
|
var right = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.DestinationSystemId);
|
||||||
|
var differentControllers = !string.Equals(left.ControllerFactionId, right.ControllerFactionId, StringComparison.Ordinal);
|
||||||
|
var contested = left.IsContested || right.IsContested || differentControllers;
|
||||||
|
if (!contested && left.ControllerFactionId is null && right.ControllerFactionId is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Territory.BorderEdges.Add(new BorderEdgeRuntime
|
||||||
|
{
|
||||||
|
Id = $"border-{route.Id}",
|
||||||
|
SourceSystemId = route.SourceSystemId,
|
||||||
|
DestinationSystemId = route.DestinationSystemId,
|
||||||
|
SourceFactionId = left.ControllerFactionId ?? left.PrimaryClaimantFactionId,
|
||||||
|
DestinationFactionId = right.ControllerFactionId ?? right.PrimaryClaimantFactionId,
|
||||||
|
IsContested = contested,
|
||||||
|
TensionScore = MathF.Min(1f, MathF.Abs((left.ControlScore - right.ControlScore) / MathF.Max(50f, left.ControlScore + right.ControlScore))),
|
||||||
|
CorridorImportance = route.Distance <= 0.01f ? 0f : Math.Clamp((left.StrategicValue + right.StrategicValue) / MathF.Max(route.Distance, 1f), 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var control in state.Territory.ControlStates)
|
||||||
|
{
|
||||||
|
var adjacentBorders = state.Territory.BorderEdges.Where(edge => edge.SourceSystemId == control.SystemId || edge.DestinationSystemId == control.SystemId).ToList();
|
||||||
|
var hostileBorderCount = adjacentBorders.Count(edge => edge.IsContested);
|
||||||
|
var corridorImportance = adjacentBorders.Sum(edge => edge.CorridorImportance);
|
||||||
|
var zoneKind = control.IsContested
|
||||||
|
? "contested"
|
||||||
|
: control.ControllerFactionId is null && control.PrimaryClaimantFactionId is not null
|
||||||
|
? "buffer"
|
||||||
|
: control.ControllerFactionId is not null && hostileBorderCount == 0
|
||||||
|
? "core"
|
||||||
|
: control.ControllerFactionId is not null && corridorImportance > 1.1f
|
||||||
|
? "corridor"
|
||||||
|
: control.ControllerFactionId is not null
|
||||||
|
? "frontier"
|
||||||
|
: "unclaimed";
|
||||||
|
state.Territory.Zones.Add(new TerritoryZoneRuntime
|
||||||
|
{
|
||||||
|
Id = $"zone-{control.SystemId}",
|
||||||
|
SystemId = control.SystemId,
|
||||||
|
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
|
||||||
|
Kind = zoneKind,
|
||||||
|
Status = "active",
|
||||||
|
Reason = zoneKind == "corridor" ? "high-corridor-importance" : zoneKind == "frontier" ? "hostile-border-contact" : zoneKind,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
state.Territory.StrategicProfiles.Add(new SectorStrategicProfileRuntime
|
||||||
|
{
|
||||||
|
SystemId = control.SystemId,
|
||||||
|
ControllerFactionId = control.ControllerFactionId,
|
||||||
|
ZoneKind = zoneKind,
|
||||||
|
IsContested = control.IsContested,
|
||||||
|
StrategicValue = control.StrategicValue,
|
||||||
|
SecurityRating = Math.Clamp(1f - (hostileBorderCount * 0.22f), 0f, 1f),
|
||||||
|
TerritorialPressure = Math.Clamp(hostileBorderCount * 0.25f, 0f, 1f),
|
||||||
|
LogisticsValue = Math.Clamp(corridorImportance, 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
state.Territory.Pressures.Add(new TerritoryPressureRuntime
|
||||||
|
{
|
||||||
|
Id = $"pressure-{control.SystemId}",
|
||||||
|
SystemId = control.SystemId,
|
||||||
|
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
|
||||||
|
Kind = control.IsContested ? "contested-pressure" : "territorial-pressure",
|
||||||
|
PressureScore = Math.Clamp(hostileBorderCount * 0.28f, 0f, 1f),
|
||||||
|
SecurityScore = Math.Clamp(1f - (hostileBorderCount * 0.2f), 0f, 1f),
|
||||||
|
HostileInfluence = influencesBySystem.GetValueOrDefault(control.SystemId)?.Skip(control.ControllerFactionId is null ? 0 : 1).Sum(entry => entry.TotalInfluence) ?? 0f,
|
||||||
|
CorridorRisk = Math.Clamp(corridorImportance > 0.8f && hostileBorderCount > 0 ? 0.7f : hostileBorderCount * 0.2f, 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildDiplomacy(SimulationWorld world, GeopoliticalStateRuntime state, ICollection<SimulationEventRecord> events)
|
||||||
|
{
|
||||||
|
state.Diplomacy.Relations.Clear();
|
||||||
|
state.Diplomacy.Treaties.Clear();
|
||||||
|
state.Diplomacy.BorderTensions.Clear();
|
||||||
|
state.Diplomacy.Wars.Clear();
|
||||||
|
|
||||||
|
var nowUtc = world.GeneratedAtUtc;
|
||||||
|
var factionPairs = world.Factions
|
||||||
|
.OrderBy(faction => faction.Id, StringComparer.Ordinal)
|
||||||
|
.SelectMany((left, index) => world.Factions.Skip(index + 1).Select(right => (left, right)));
|
||||||
|
|
||||||
|
foreach (var (leftFaction, rightFaction) in factionPairs)
|
||||||
|
{
|
||||||
|
var borderEdges = state.Territory.BorderEdges
|
||||||
|
.Where(edge =>
|
||||||
|
(string.Equals(edge.SourceFactionId, leftFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, rightFaction.Id, StringComparison.Ordinal))
|
||||||
|
|| (string.Equals(edge.SourceFactionId, rightFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, leftFaction.Id, StringComparison.Ordinal)))
|
||||||
|
.OrderBy(edge => edge.Id, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
var sharedBorderPressure = borderEdges.Sum(edge => edge.TensionScore + (edge.IsContested ? 0.25f : 0f));
|
||||||
|
var conflictSystems = borderEdges.SelectMany(edge => new[] { edge.SourceSystemId, edge.DestinationSystemId }).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
var hostilePresence = world.Ships.Count(ship =>
|
||||||
|
ship.Health > 0f
|
||||||
|
&& ((ship.FactionId == leftFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))
|
||||||
|
|| (ship.FactionId == rightFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))));
|
||||||
|
var incidentSeverity = Math.Clamp(sharedBorderPressure + (hostilePresence * 0.03f), 0f, 1.6f);
|
||||||
|
var relationId = BuildRelationId(leftFaction.Id, rightFaction.Id);
|
||||||
|
var posture = incidentSeverity switch
|
||||||
|
{
|
||||||
|
>= 1.1f => "war",
|
||||||
|
>= 0.65f => "hostile",
|
||||||
|
>= 0.3f => "wary",
|
||||||
|
_ => "neutral",
|
||||||
|
};
|
||||||
|
|
||||||
|
var relation = new DiplomaticRelationRuntime
|
||||||
|
{
|
||||||
|
Id = relationId,
|
||||||
|
FactionAId = leftFaction.Id,
|
||||||
|
FactionBId = rightFaction.Id,
|
||||||
|
Status = "active",
|
||||||
|
Posture = posture,
|
||||||
|
TrustScore = Math.Clamp(0.7f - incidentSeverity, 0f, 1f),
|
||||||
|
TensionScore = Math.Clamp(incidentSeverity, 0f, 1f),
|
||||||
|
GrievanceScore = Math.Clamp(sharedBorderPressure, 0f, 1f),
|
||||||
|
TradeAccessPolicy = posture is "war" or "hostile" ? "restricted" : "open",
|
||||||
|
MilitaryAccessPolicy = posture == "neutral" ? "transit" : posture == "wary" ? "restricted" : "denied",
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (relation.Posture == "neutral")
|
||||||
|
{
|
||||||
|
var treaty = new TreatyRuntime
|
||||||
|
{
|
||||||
|
Id = $"treaty-open-trade-{relationId}",
|
||||||
|
Kind = "trade-understanding",
|
||||||
|
Status = "active",
|
||||||
|
TradeAccessPolicy = "open",
|
||||||
|
MilitaryAccessPolicy = "restricted",
|
||||||
|
Summary = $"Open civilian trade between {leftFaction.Label} and {rightFaction.Label}.",
|
||||||
|
CreatedAtUtc = nowUtc,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
treaty.FactionIds.Add(leftFaction.Id);
|
||||||
|
treaty.FactionIds.Add(rightFaction.Id);
|
||||||
|
state.Diplomacy.Treaties.Add(treaty);
|
||||||
|
relation.ActiveTreatyIds.Add(treaty.Id);
|
||||||
|
relation.TradeAccessPolicy = "open";
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Diplomacy.Relations.Add(relation);
|
||||||
|
|
||||||
|
foreach (var borderEdge in borderEdges)
|
||||||
|
{
|
||||||
|
borderEdge.RelationId = relation.Id;
|
||||||
|
borderEdge.TensionScore = Math.Clamp(borderEdge.TensionScore + (relation.TensionScore * 0.35f), 0f, 1f);
|
||||||
|
var tension = new BorderTensionRuntime
|
||||||
|
{
|
||||||
|
Id = $"tension-{borderEdge.Id}",
|
||||||
|
RelationId = relation.Id,
|
||||||
|
BorderEdgeId = borderEdge.Id,
|
||||||
|
FactionAId = leftFaction.Id,
|
||||||
|
FactionBId = rightFaction.Id,
|
||||||
|
Status = relation.Posture is "war" or "hostile" ? "escalating" : "stable",
|
||||||
|
TensionScore = relation.TensionScore,
|
||||||
|
IncidentScore = incidentSeverity,
|
||||||
|
MilitaryPressure = Math.Clamp(hostilePresence * 0.05f, 0f, 1f),
|
||||||
|
AccessFriction = relation.TradeAccessPolicy == "open" ? 0.15f : 0.75f,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
tension.SystemIds.Add(borderEdge.SourceSystemId);
|
||||||
|
tension.SystemIds.Add(borderEdge.DestinationSystemId);
|
||||||
|
state.Diplomacy.BorderTensions.Add(tension);
|
||||||
|
|
||||||
|
if (tension.TensionScore >= 0.35f)
|
||||||
|
{
|
||||||
|
var incidentId = $"incident-border-{relationId}-{borderEdge.Id}";
|
||||||
|
var incident = new DiplomaticIncidentRuntime
|
||||||
|
{
|
||||||
|
Id = incidentId,
|
||||||
|
Kind = borderEdge.IsContested ? "border-clash" : "border-friction",
|
||||||
|
Status = relation.Posture == "war" ? "escalated" : "active",
|
||||||
|
SourceFactionId = leftFaction.Id,
|
||||||
|
TargetFactionId = rightFaction.Id,
|
||||||
|
SystemId = borderEdge.SourceSystemId,
|
||||||
|
BorderEdgeId = borderEdge.Id,
|
||||||
|
Summary = $"{leftFaction.Label} and {rightFaction.Label} are under pressure on {borderEdge.SourceSystemId}/{borderEdge.DestinationSystemId}.",
|
||||||
|
Severity = tension.TensionScore,
|
||||||
|
EscalationScore = tension.IncidentScore,
|
||||||
|
CreatedAtUtc = nowUtc,
|
||||||
|
LastObservedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
state.Diplomacy.Incidents.Add(incident);
|
||||||
|
relation.ActiveIncidentIds.Add(incident.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relation.Posture == "war")
|
||||||
|
{
|
||||||
|
var warId = $"war-{relationId}";
|
||||||
|
var war = new WarStateRuntime
|
||||||
|
{
|
||||||
|
Id = warId,
|
||||||
|
RelationId = relation.Id,
|
||||||
|
FactionAId = leftFaction.Id,
|
||||||
|
FactionBId = rightFaction.Id,
|
||||||
|
Status = "active",
|
||||||
|
WarGoal = "border-dominance",
|
||||||
|
EscalationScore = relation.TensionScore,
|
||||||
|
StartedAtUtc = nowUtc,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
relation.WarStateId = war.Id;
|
||||||
|
state.Diplomacy.Wars.Add(war);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildFrontLines(state, nowUtc, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildFrontLines(GeopoliticalStateRuntime state, DateTimeOffset nowUtc, ICollection<SimulationEventRecord> events)
|
||||||
|
{
|
||||||
|
foreach (var group in state.Diplomacy.BorderTensions
|
||||||
|
.Where(tension => tension.TensionScore >= 0.35f)
|
||||||
|
.GroupBy(tension => BuildPairId("front", tension.FactionAId, tension.FactionBId), StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var tensions = group.OrderByDescending(tension => tension.TensionScore).ThenBy(tension => tension.Id, StringComparer.Ordinal).ToList();
|
||||||
|
var front = new FrontLineRuntime
|
||||||
|
{
|
||||||
|
Id = group.Key,
|
||||||
|
Kind = state.Diplomacy.Wars.Any(war => war.RelationId == tensions[0].RelationId && war.Status == "active") ? "war-front" : "border-front",
|
||||||
|
Status = "active",
|
||||||
|
AnchorSystemId = tensions.SelectMany(tension => tension.SystemIds).GroupBy(systemId => systemId, StringComparer.Ordinal).OrderByDescending(entry => entry.Count()).ThenBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => entry.Key).FirstOrDefault(),
|
||||||
|
PressureScore = Math.Clamp(tensions.Sum(tension => tension.TensionScore) / tensions.Count, 0f, 1f),
|
||||||
|
SupplyRisk = Math.Clamp(tensions.Sum(tension => tension.AccessFriction) / tensions.Count, 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
front.FactionIds.Add(tensions[0].FactionAId);
|
||||||
|
front.FactionIds.Add(tensions[0].FactionBId);
|
||||||
|
front.SystemIds.AddRange(tensions.SelectMany(tension => tension.SystemIds).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
front.BorderEdgeIds.AddRange(tensions.Select(tension => tension.BorderEdgeId).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
state.Territory.FrontLines.Add(front);
|
||||||
|
|
||||||
|
foreach (var war in state.Diplomacy.Wars.Where(war => string.Equals(war.RelationId, tensions[0].RelationId, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
war.ActiveFrontLineIds.Add(front.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
events.Add(new SimulationEventRecord("front-line", front.Id, "front-updated", $"Front {front.Id} pressure {front.PressureScore.ToString("0.00", CultureInfo.InvariantCulture)}.", nowUtc, "geopolitics"));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var profile in state.Territory.StrategicProfiles)
|
||||||
|
{
|
||||||
|
profile.FrontLineId = state.Territory.FrontLines.FirstOrDefault(front => front.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RebuildEconomyRegions(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||||
|
{
|
||||||
|
state.EconomyRegions.Regions.Clear();
|
||||||
|
state.EconomyRegions.SupplyNetworks.Clear();
|
||||||
|
state.EconomyRegions.Corridors.Clear();
|
||||||
|
state.EconomyRegions.ProductionProfiles.Clear();
|
||||||
|
state.EconomyRegions.TradeBalances.Clear();
|
||||||
|
state.EconomyRegions.Bottlenecks.Clear();
|
||||||
|
state.EconomyRegions.SecurityAssessments.Clear();
|
||||||
|
state.EconomyRegions.EconomicAssessments.Clear();
|
||||||
|
|
||||||
|
var nowUtc = world.GeneratedAtUtc;
|
||||||
|
foreach (var faction in world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var factionSystems = state.Territory.ControlStates
|
||||||
|
.Where(control => string.Equals(control.ControllerFactionId ?? control.PrimaryClaimantFactionId, faction.Id, StringComparison.Ordinal))
|
||||||
|
.Select(control => control.SystemId)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(systemId => systemId, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
if (factionSystems.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var connectedComponents = BuildConnectedComponents(factionSystems, state.Routes);
|
||||||
|
foreach (var component in connectedComponents)
|
||||||
|
{
|
||||||
|
var coreSystemId = component
|
||||||
|
.OrderByDescending(systemId => world.Stations.Count(station => station.FactionId == faction.Id && station.SystemId == systemId))
|
||||||
|
.ThenBy(systemId => systemId, StringComparer.Ordinal)
|
||||||
|
.First();
|
||||||
|
var regionId = $"region-{faction.Id}-{coreSystemId}";
|
||||||
|
var stations = world.Stations
|
||||||
|
.Where(station => station.FactionId == faction.Id && component.Contains(station.SystemId, StringComparer.Ordinal))
|
||||||
|
.OrderBy(station => station.Id, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
var economy = BuildRegionalEconomy(world, faction.Id, component);
|
||||||
|
var regionKind = ResolveRegionKind(stations, economy);
|
||||||
|
var frontLineIds = state.Territory.FrontLines
|
||||||
|
.Where(front => front.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal)))
|
||||||
|
.Select(front => front.Id)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var region = new EconomicRegionRuntime
|
||||||
|
{
|
||||||
|
Id = regionId,
|
||||||
|
FactionId = faction.Id,
|
||||||
|
Label = $"{faction.Label} {coreSystemId}",
|
||||||
|
Kind = regionKind,
|
||||||
|
Status = "active",
|
||||||
|
CoreSystemId = coreSystemId,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
region.SystemIds.AddRange(component.OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
region.StationIds.AddRange(stations.Select(station => station.Id));
|
||||||
|
region.FrontLineIds.AddRange(frontLineIds);
|
||||||
|
state.EconomyRegions.Regions.Add(region);
|
||||||
|
|
||||||
|
var producerItems = economy.Commodities
|
||||||
|
.Where(entry => entry.Value.ProductionRatePerSecond > 0.01f)
|
||||||
|
.OrderByDescending(entry => entry.Value.ProductionRatePerSecond)
|
||||||
|
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
|
||||||
|
.Take(8)
|
||||||
|
.Select(entry => entry.Key)
|
||||||
|
.ToList();
|
||||||
|
var scarceItems = economy.Commodities
|
||||||
|
.Where(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low)
|
||||||
|
.OrderByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Value, 240f))
|
||||||
|
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
|
||||||
|
.Take(8)
|
||||||
|
.Select(entry => entry.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var supplyNetwork = new SupplyNetworkRuntime
|
||||||
|
{
|
||||||
|
Id = $"network-{regionId}",
|
||||||
|
RegionId = regionId,
|
||||||
|
ThroughputScore = Math.Clamp(stations.Count * 0.18f, 0f, 1f),
|
||||||
|
RiskScore = Math.Clamp(frontLineIds.Count * 0.24f, 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
supplyNetwork.StationIds.AddRange(stations.Select(station => station.Id));
|
||||||
|
supplyNetwork.ProducerItemIds.AddRange(producerItems);
|
||||||
|
supplyNetwork.ConsumerItemIds.AddRange(scarceItems);
|
||||||
|
supplyNetwork.ConstructionItemIds.AddRange(world.ConstructionSites
|
||||||
|
.Where(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal))
|
||||||
|
.SelectMany(site => site.RequiredItems.Keys)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal));
|
||||||
|
state.EconomyRegions.SupplyNetworks.Add(supplyNetwork);
|
||||||
|
|
||||||
|
var productionProfile = new RegionalProductionProfileRuntime
|
||||||
|
{
|
||||||
|
RegionId = regionId,
|
||||||
|
PrimaryIndustry = regionKind,
|
||||||
|
ShipyardCount = stations.Count(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
||||||
|
StationCount = stations.Count,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
productionProfile.ProducedItemIds.AddRange(producerItems);
|
||||||
|
productionProfile.ScarceItemIds.AddRange(scarceItems);
|
||||||
|
state.EconomyRegions.ProductionProfiles.Add(productionProfile);
|
||||||
|
|
||||||
|
state.EconomyRegions.TradeBalances.Add(new RegionalTradeBalanceRuntime
|
||||||
|
{
|
||||||
|
RegionId = regionId,
|
||||||
|
ImportsRequiredCount = economy.Commodities.Count(entry => entry.Value.BuyBacklog > 0.01f),
|
||||||
|
ExportsSurplusCount = economy.Commodities.Count(entry => entry.Value.SellBacklog > 0.01f || entry.Value.Level == CommodityLevelKind.Surplus),
|
||||||
|
CriticalShortageCount = scarceItems.Count,
|
||||||
|
NetTradeScore = Math.Clamp((economy.Commodities.Sum(entry => entry.Value.ProjectedNetRatePerSecond) + 5f) / 10f, -1f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scarceItems.FirstOrDefault() is { } bottleneckItemId)
|
||||||
|
{
|
||||||
|
state.EconomyRegions.Bottlenecks.Add(new RegionalBottleneckRuntime
|
||||||
|
{
|
||||||
|
Id = $"bottleneck-{regionId}-{bottleneckItemId}",
|
||||||
|
RegionId = regionId,
|
||||||
|
ItemId = bottleneckItemId,
|
||||||
|
Cause = "regional-shortage",
|
||||||
|
Status = "active",
|
||||||
|
Severity = Math.Clamp(CommodityOperationalSignal.ComputeNeedScore(economy.GetCommodity(bottleneckItemId), 240f), 0f, 10f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var supplyRisk = Math.Clamp(frontLineIds.Count * 0.2f, 0f, 1f);
|
||||||
|
state.EconomyRegions.SecurityAssessments.Add(new RegionalSecurityAssessmentRuntime
|
||||||
|
{
|
||||||
|
RegionId = regionId,
|
||||||
|
SupplyRisk = supplyRisk,
|
||||||
|
BorderPressure = Math.Clamp(frontLineIds.Count * 0.22f, 0f, 1f),
|
||||||
|
ActiveWarCount = state.Diplomacy.Wars.Count(war => war.ActiveFrontLineIds.Intersect(frontLineIds, StringComparer.Ordinal).Any()),
|
||||||
|
HostileRelationCount = state.Diplomacy.Relations.Count(relation => relation.Posture is "hostile" or "war"),
|
||||||
|
AccessFriction = Math.Clamp(state.Diplomacy.BorderTensions.Where(tension => tension.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal))).DefaultIfEmpty().Average(tension => tension?.AccessFriction ?? 0f), 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.EconomyRegions.EconomicAssessments.Add(new RegionalEconomicAssessmentRuntime
|
||||||
|
{
|
||||||
|
RegionId = regionId,
|
||||||
|
SustainmentScore = Math.Clamp(1f - (scarceItems.Count * 0.12f) - (supplyRisk * 0.35f), 0f, 1f),
|
||||||
|
ProductionDepth = Math.Clamp(producerItems.Count / 8f, 0f, 1f),
|
||||||
|
ConstructionPressure = Math.Clamp(world.ConstructionSites.Count(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal)) * 0.22f, 0f, 1f),
|
||||||
|
CorridorDependency = Math.Clamp(frontLineIds.Count * 0.18f, 0f, 1f),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildCorridors(world, state, nowUtc);
|
||||||
|
foreach (var profile in state.Territory.StrategicProfiles)
|
||||||
|
{
|
||||||
|
profile.EconomicRegionId = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildCorridors(SimulationWorld world, GeopoliticalStateRuntime state, DateTimeOffset nowUtc)
|
||||||
|
{
|
||||||
|
foreach (var route in state.Routes)
|
||||||
|
{
|
||||||
|
var sourceRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.SourceSystemId, StringComparer.Ordinal));
|
||||||
|
var destinationRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.DestinationSystemId, StringComparer.Ordinal));
|
||||||
|
if (sourceRegion is null && destinationRegion is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var borderEdge = state.Territory.BorderEdges.FirstOrDefault(edge =>
|
||||||
|
(edge.SourceSystemId == route.SourceSystemId && edge.DestinationSystemId == route.DestinationSystemId)
|
||||||
|
|| (edge.SourceSystemId == route.DestinationSystemId && edge.DestinationSystemId == route.SourceSystemId));
|
||||||
|
var risk = borderEdge?.TensionScore ?? 0f;
|
||||||
|
var corridor = new LogisticsCorridorRuntime
|
||||||
|
{
|
||||||
|
Id = $"corridor-{route.Id}",
|
||||||
|
FactionId = sourceRegion?.FactionId ?? destinationRegion?.FactionId,
|
||||||
|
Kind = borderEdge?.IsContested == true ? "frontier-corridor" : "supply-corridor",
|
||||||
|
Status = borderEdge?.IsContested == true ? "risky" : "active",
|
||||||
|
RiskScore = Math.Clamp(risk + ((sourceRegion is not null && destinationRegion is not null && sourceRegion.Id != destinationRegion.Id) ? 0.15f : 0f), 0f, 1f),
|
||||||
|
ThroughputScore = Math.Clamp(((sourceRegion?.StationIds.Count ?? 0) + (destinationRegion?.StationIds.Count ?? 0)) / 10f, 0f, 1f),
|
||||||
|
AccessState = ResolveCorridorAccessState(world, borderEdge, sourceRegion, destinationRegion),
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
corridor.SystemPathIds.Add(route.SourceSystemId);
|
||||||
|
corridor.SystemPathIds.Add(route.DestinationSystemId);
|
||||||
|
if (sourceRegion is not null)
|
||||||
|
{
|
||||||
|
corridor.RegionIds.Add(sourceRegion.Id);
|
||||||
|
}
|
||||||
|
if (destinationRegion is not null && !corridor.RegionIds.Contains(destinationRegion.Id, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
corridor.RegionIds.Add(destinationRegion.Id);
|
||||||
|
}
|
||||||
|
if (borderEdge is not null)
|
||||||
|
{
|
||||||
|
corridor.BorderEdgeIds.Add(borderEdge.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.EconomyRegions.Corridors.Add(corridor);
|
||||||
|
if (sourceRegion is not null && !sourceRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
sourceRegion.CorridorIds.Add(corridor.Id);
|
||||||
|
}
|
||||||
|
if (destinationRegion is not null && !destinationRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
destinationRegion.CorridorIds.Add(corridor.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveCorridorAccessState(
|
||||||
|
SimulationWorld world,
|
||||||
|
BorderEdgeRuntime? borderEdge,
|
||||||
|
EconomicRegionRuntime? sourceRegion,
|
||||||
|
EconomicRegionRuntime? destinationRegion)
|
||||||
|
{
|
||||||
|
if (sourceRegion?.FactionId is null || destinationRegion?.FactionId is null)
|
||||||
|
{
|
||||||
|
return borderEdge?.IsContested == true ? "restricted" : "open";
|
||||||
|
}
|
||||||
|
|
||||||
|
var relation = FindRelation(world, sourceRegion.FactionId, destinationRegion.FactionId);
|
||||||
|
if (relation is null)
|
||||||
|
{
|
||||||
|
return "restricted";
|
||||||
|
}
|
||||||
|
|
||||||
|
return relation.Posture switch
|
||||||
|
{
|
||||||
|
"war" => "denied",
|
||||||
|
"hostile" => "restricted",
|
||||||
|
_ => relation.TradeAccessPolicy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FactionEconomySnapshot BuildRegionalEconomy(SimulationWorld world, string factionId, IReadOnlyCollection<string> systemIds)
|
||||||
|
{
|
||||||
|
var snapshot = new FactionEconomySnapshot();
|
||||||
|
foreach (var station in world.Stations.Where(station => station.FactionId == factionId && systemIds.Contains(station.SystemId, StringComparer.Ordinal)))
|
||||||
|
{
|
||||||
|
foreach (var (itemId, amount) in station.Inventory)
|
||||||
|
{
|
||||||
|
snapshot.GetCommodity(itemId).OnHand += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
|
||||||
|
{
|
||||||
|
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
|
||||||
|
if (recipe is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
|
||||||
|
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
|
||||||
|
foreach (var input in recipe.Inputs)
|
||||||
|
{
|
||||||
|
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
|
||||||
|
}
|
||||||
|
foreach (var output in recipe.Outputs)
|
||||||
|
{
|
||||||
|
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var order in world.MarketOrders.Where(order => order.FactionId == factionId))
|
||||||
|
{
|
||||||
|
var relatedSystemId = world.Stations.FirstOrDefault(station => station.Id == order.StationId)?.SystemId
|
||||||
|
?? world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId)?.SystemId;
|
||||||
|
if (relatedSystemId is null || !systemIds.Contains(relatedSystemId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commodity = snapshot.GetCommodity(order.ItemId);
|
||||||
|
if (order.Kind == MarketOrderKinds.Buy)
|
||||||
|
{
|
||||||
|
commodity.BuyBacklog += order.RemainingAmount;
|
||||||
|
}
|
||||||
|
else if (order.Kind == MarketOrderKinds.Sell)
|
||||||
|
{
|
||||||
|
commodity.SellBacklog += order.RemainingAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var site in world.ConstructionSites.Where(site => site.FactionId == factionId && systemIds.Contains(site.SystemId, StringComparer.Ordinal)))
|
||||||
|
{
|
||||||
|
foreach (var required in site.RequiredItems)
|
||||||
|
{
|
||||||
|
var remaining = MathF.Max(0f, required.Value - (site.DeliveredItems.TryGetValue(required.Key, out var delivered) ? delivered : 0f));
|
||||||
|
if (remaining > 0.01f)
|
||||||
|
{
|
||||||
|
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<string>> BuildConnectedComponents(IReadOnlyCollection<string> systems, IReadOnlyCollection<SystemRouteLinkRuntime> routes)
|
||||||
|
{
|
||||||
|
var remaining = systems.ToHashSet(StringComparer.Ordinal);
|
||||||
|
var adjacency = routes
|
||||||
|
.SelectMany(route => new[]
|
||||||
|
{
|
||||||
|
(route.SourceSystemId, route.DestinationSystemId),
|
||||||
|
(route.DestinationSystemId, route.SourceSystemId),
|
||||||
|
})
|
||||||
|
.GroupBy(entry => entry.Item1, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(group => group.Key, group => group.Select(entry => entry.Item2).ToList(), StringComparer.Ordinal);
|
||||||
|
var components = new List<List<string>>();
|
||||||
|
|
||||||
|
while (remaining.Count > 0)
|
||||||
|
{
|
||||||
|
var start = remaining.OrderBy(id => id, StringComparer.Ordinal).First();
|
||||||
|
var frontier = new Queue<string>();
|
||||||
|
frontier.Enqueue(start);
|
||||||
|
remaining.Remove(start);
|
||||||
|
var component = new List<string>();
|
||||||
|
|
||||||
|
while (frontier.Count > 0)
|
||||||
|
{
|
||||||
|
var current = frontier.Dequeue();
|
||||||
|
component.Add(current);
|
||||||
|
foreach (var neighbor in adjacency.GetValueOrDefault(current, []))
|
||||||
|
{
|
||||||
|
if (remaining.Remove(neighbor))
|
||||||
|
{
|
||||||
|
frontier.Enqueue(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
components.Add(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveRegionKind(IReadOnlyCollection<StationRuntime> stations, FactionEconomySnapshot economy)
|
||||||
|
{
|
||||||
|
if (stations.Any(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)))
|
||||||
|
{
|
||||||
|
return "shipbuilding-region";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stations.Count(station => StationSimulationService.DetermineStationRole(station) == "refinery") >= 2)
|
||||||
|
{
|
||||||
|
return "industrial-core";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (economy.Commodities.Any(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low))
|
||||||
|
{
|
||||||
|
return "frontier-sustainment";
|
||||||
|
}
|
||||||
|
|
||||||
|
return stations.Count <= 2 ? "extraction-region" : "balanced-region";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float EstimateSystemStrategicValue(SimulationWorld world, string systemId)
|
||||||
|
{
|
||||||
|
var stationValue = world.Stations.Count(station => station.SystemId == systemId) * 30f;
|
||||||
|
var constructionValue = world.ConstructionSites.Count(site => site.SystemId == systemId && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed) * 18f;
|
||||||
|
var nodeValue = world.Nodes.Count(node => node.SystemId == systemId) * 8f;
|
||||||
|
return stationValue + constructionValue + nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildRelationId(string factionAId, string factionBId) =>
|
||||||
|
BuildPairId("relation", factionAId, factionBId);
|
||||||
|
|
||||||
|
private static string BuildPairId(string prefix, string leftId, string rightId)
|
||||||
|
{
|
||||||
|
return string.Compare(leftId, rightId, StringComparison.Ordinal) <= 0
|
||||||
|
? $"{prefix}-{leftId}-{rightId}"
|
||||||
|
: $"{prefix}-{rightId}-{leftId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,15 @@ global using SpaceGame.Api.Economy.Runtime;
|
|||||||
global using SpaceGame.Api.Factions.AI;
|
global using SpaceGame.Api.Factions.AI;
|
||||||
global using SpaceGame.Api.Factions.Contracts;
|
global using SpaceGame.Api.Factions.Contracts;
|
||||||
global using SpaceGame.Api.Factions.Runtime;
|
global using SpaceGame.Api.Factions.Runtime;
|
||||||
|
global using SpaceGame.Api.Geopolitics.Contracts;
|
||||||
|
global using SpaceGame.Api.Geopolitics.Runtime;
|
||||||
|
global using SpaceGame.Api.Geopolitics.Simulation;
|
||||||
global using SpaceGame.Api.Industry.Planning;
|
global using SpaceGame.Api.Industry.Planning;
|
||||||
global using SpaceGame.Api.Shared.AI;
|
global using SpaceGame.Api.PlayerFaction.Contracts;
|
||||||
|
global using SpaceGame.Api.PlayerFaction.Runtime;
|
||||||
|
global using SpaceGame.Api.PlayerFaction.Simulation;
|
||||||
global using SpaceGame.Api.Shared.Contracts;
|
global using SpaceGame.Api.Shared.Contracts;
|
||||||
global using SpaceGame.Api.Shared.Runtime;
|
global using SpaceGame.Api.Shared.Runtime;
|
||||||
global using SpaceGame.Api.Ships.AI;
|
|
||||||
global using SpaceGame.Api.Ships.Contracts;
|
global using SpaceGame.Api.Ships.Contracts;
|
||||||
global using SpaceGame.Api.Ships.Runtime;
|
global using SpaceGame.Api.Ships.Runtime;
|
||||||
global using SpaceGame.Api.Ships.Simulation;
|
global using SpaceGame.Api.Ships.Simulation;
|
||||||
|
|||||||
@@ -219,6 +219,11 @@ internal static class FactionIndustryPlanner
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CanEstablishExpansionSite(world, factionId, project))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var nowUtc = DateTimeOffset.UtcNow;
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
var claimId = $"claim-{factionId}-{project.CelestialId}";
|
var claimId = $"claim-{factionId}-{project.CelestialId}";
|
||||||
if (world.Claims.All(candidate => candidate.Id != claimId))
|
if (world.Claims.All(candidate => candidate.Id != claimId))
|
||||||
@@ -303,7 +308,8 @@ 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.FactionCommanderHasIssuedTask(world, factionId, FactionIssuedTaskKind.ProduceShips, "military"))
|
var threatAssessment = CommanderPlanningService.FindFactionThreatAssessment(world, factionId);
|
||||||
|
if (threatAssessment is not null && threatAssessment.EnemyFactionCount > 0)
|
||||||
{
|
{
|
||||||
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
||||||
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
||||||
@@ -451,7 +457,8 @@ internal static class FactionIndustryPlanner
|
|||||||
.Where(celestial =>
|
.Where(celestial =>
|
||||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||||
&& celestial.OccupyingStructureId is null
|
&& celestial.OccupyingStructureId is null
|
||||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed))
|
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||||
|
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||||
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems))
|
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
@@ -462,7 +469,8 @@ internal static class FactionIndustryPlanner
|
|||||||
.Where(celestial =>
|
.Where(celestial =>
|
||||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||||
&& celestial.OccupyingStructureId is null
|
&& celestial.OccupyingStructureId is null
|
||||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed))
|
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||||
|
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||||
.OrderByDescending(celestial => world.Stations.Count(station =>
|
.OrderByDescending(celestial => world.Stations.Count(station =>
|
||||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)))
|
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)))
|
||||||
@@ -482,7 +490,80 @@ internal static class FactionIndustryPlanner
|
|||||||
var factionPresence = world.Stations.Count(station =>
|
var factionPresence = world.Stations.Count(station =>
|
||||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal));
|
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal));
|
||||||
return resourceScore + (factionPresence * 5_000f);
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, celestial.SystemId);
|
||||||
|
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, celestial.SystemId);
|
||||||
|
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == celestial.SystemId);
|
||||||
|
var pressure = world.Geopolitics?.Territory.Pressures
|
||||||
|
.Where(entry => entry.SystemId == celestial.SystemId && entry.FactionId == factionId)
|
||||||
|
.OrderByDescending(entry => entry.HostileInfluence)
|
||||||
|
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||||
|
.FirstOrDefault();
|
||||||
|
var controlBias = string.Equals(controlState?.ControllerFactionId, factionId, StringComparison.Ordinal)
|
||||||
|
? 12_000f
|
||||||
|
: string.Equals(controlState?.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal)
|
||||||
|
? 4_000f
|
||||||
|
: 0f;
|
||||||
|
var regionBias = region is null
|
||||||
|
? 0f
|
||||||
|
: region.Kind switch
|
||||||
|
{
|
||||||
|
"core-industry" => 4_500f,
|
||||||
|
"shipbuilding" => 3_250f,
|
||||||
|
"trade-hub" => 2_250f,
|
||||||
|
"corridor" => 1_500f,
|
||||||
|
_ => 1_000f,
|
||||||
|
};
|
||||||
|
var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f)
|
||||||
|
+ ((strategicProfile?.TerritorialPressure ?? 0f) * 9f)
|
||||||
|
+ ((world.Geopolitics is null ? 0f : GeopoliticalSimulationService.GetSystemRouteRisk(world, celestial.SystemId, factionId)) * 250f);
|
||||||
|
return resourceScore
|
||||||
|
+ (factionPresence * 5_000f)
|
||||||
|
+ controlBias
|
||||||
|
+ regionBias
|
||||||
|
- securityPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsExpansionSystemEligible(SimulationWorld world, string factionId, string systemId)
|
||||||
|
{
|
||||||
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId);
|
||||||
|
if (controlState is null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authorityFactionId = controlState.ControllerFactionId ?? controlState.PrimaryClaimantFactionId;
|
||||||
|
if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanEstablishExpansionSite(SimulationWorld world, string factionId, IndustryExpansionProject project)
|
||||||
|
{
|
||||||
|
if (!IsExpansionSystemEligible(world, factionId, project.SystemId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, project.SystemId);
|
||||||
|
if (controlState?.IsContested == true)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pressure = world.Geopolitics?.Territory.Pressures
|
||||||
|
.Where(entry => entry.SystemId == project.SystemId && entry.FactionId == factionId)
|
||||||
|
.OrderByDescending(entry => entry.CorridorRisk + entry.HostileInfluence)
|
||||||
|
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||||
|
.FirstOrDefault();
|
||||||
|
return pressure is null || (pressure.CorridorRisk < 0.8f && pressure.HostileInfluence < 1.2f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StationRuntime? SelectSupportStation(SimulationWorld world, string factionId, string moduleId, string targetSystemId)
|
private static StationRuntime? SelectSupportStation(SimulationWorld world, string factionId, string moduleId, string targetSystemId)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class CreatePlayerOrganizationHandler(WorldService worldService) : Endpoint<PlayerOrganizationCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/organizations");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = worldService.CreatePlayerOrganization(request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(cancellation: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class DeletePlayerDirectiveRequest
|
||||||
|
{
|
||||||
|
public string DirectiveId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : Endpoint<DeletePlayerDirectiveRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Delete("/api/player-faction/directives/{directiveId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshot = worldService.DeletePlayerDirective(request.DirectiveId);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class DeletePlayerOrganizationRequest
|
||||||
|
{
|
||||||
|
public string OrganizationId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : Endpoint<DeletePlayerOrganizationRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Delete("/api/player-faction/organizations/{organizationId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = worldService.DeletePlayerOrganization(request.OrganizationId);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(cancellation: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs
Normal file
24
apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class GetPlayerFactionHandler(WorldService worldService) : EndpointWithoutRequest<PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/player-faction");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshot = worldService.GetPlayerFaction();
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService worldService) : Endpoint<PlayerOrganizationMembershipCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/player-faction/organizations/{organizationId}/membership");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var organizationId = Route<string>("organizationId");
|
||||||
|
if (string.IsNullOrWhiteSpace(organizationId))
|
||||||
|
{
|
||||||
|
AddError("organizationId route parameter is required.");
|
||||||
|
await SendErrorsAsync(cancellation: cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = worldService.UpdatePlayerOrganizationMembership(organizationId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(cancellation: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService) : Endpoint<PlayerStrategicIntentCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/player-faction/strategic-intent");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshot = worldService.UpdatePlayerStrategicIntent(request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : Endpoint<PlayerAssetAssignmentCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/player-faction/assets/{assetId}/assignment");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var assetId = Route<string>("assetId");
|
||||||
|
if (string.IsNullOrWhiteSpace(assetId))
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = worldService.UpsertPlayerAssignment(assetId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldService) : Endpoint<PlayerAutomationPolicyCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/automation-policies");
|
||||||
|
Put("/api/player-faction/automation-policies/{automationPolicyId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var automationPolicyId = Route<string?>("automationPolicyId");
|
||||||
|
var snapshot = worldService.UpsertPlayerAutomationPolicy(string.IsNullOrWhiteSpace(automationPolicyId) ? null : automationPolicyId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : Endpoint<PlayerDirectiveCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/directives");
|
||||||
|
Put("/api/player-faction/directives/{directiveId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var directiveId = Route<string?>("directiveId");
|
||||||
|
var snapshot = worldService.UpsertPlayerDirective(string.IsNullOrWhiteSpace(directiveId) ? null : directiveId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs
Normal file
26
apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpoint<PlayerPolicyCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/policies");
|
||||||
|
Put("/api/player-faction/policies/{policyId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var policyId = Route<string?>("policyId");
|
||||||
|
var snapshot = worldService.UpsertPlayerPolicy(string.IsNullOrWhiteSpace(policyId) ? null : policyId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerProductionProgramHandler(WorldService worldService) : Endpoint<PlayerProductionProgramCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/production-programs");
|
||||||
|
Put("/api/player-faction/production-programs/{productionProgramId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var productionProgramId = Route<string?>("productionProgramId");
|
||||||
|
var snapshot = worldService.UpsertPlayerProductionProgram(string.IsNullOrWhiteSpace(productionProgramId) ? null : productionProgramId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||||
|
|
||||||
|
public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldService) : Endpoint<PlayerReinforcementPolicyCommandRequest, PlayerFactionSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/player-faction/reinforcement-policies");
|
||||||
|
Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var reinforcementPolicyId = Route<string?>("reinforcementPolicyId");
|
||||||
|
var snapshot = worldService.UpsertPlayerReinforcementPolicy(string.IsNullOrWhiteSpace(reinforcementPolicyId) ? null : reinforcementPolicyId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
271
apps/backend/PlayerFaction/Contracts/PlayerFaction.cs
Normal file
271
apps/backend/PlayerFaction/Contracts/PlayerFaction.cs
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
namespace SpaceGame.Api.PlayerFaction.Contracts;
|
||||||
|
|
||||||
|
public sealed record PlayerAssetRegistrySnapshot(
|
||||||
|
IReadOnlyList<string> ShipIds,
|
||||||
|
IReadOnlyList<string> StationIds,
|
||||||
|
IReadOnlyList<string> CommanderIds,
|
||||||
|
IReadOnlyList<string> ClaimIds,
|
||||||
|
IReadOnlyList<string> ConstructionSiteIds,
|
||||||
|
IReadOnlyList<string> PolicySetIds,
|
||||||
|
IReadOnlyList<string> MarketOrderIds,
|
||||||
|
IReadOnlyList<string> FleetIds,
|
||||||
|
IReadOnlyList<string> TaskForceIds,
|
||||||
|
IReadOnlyList<string> StationGroupIds,
|
||||||
|
IReadOnlyList<string> EconomicRegionIds,
|
||||||
|
IReadOnlyList<string> FrontIds,
|
||||||
|
IReadOnlyList<string> ReserveIds);
|
||||||
|
|
||||||
|
public sealed record PlayerStrategicIntentSnapshot(
|
||||||
|
string StrategicPosture,
|
||||||
|
string EconomicPosture,
|
||||||
|
string MilitaryPosture,
|
||||||
|
string LogisticsPosture,
|
||||||
|
float DesiredReserveRatio,
|
||||||
|
bool AllowDelegatedCombatAutomation,
|
||||||
|
bool AllowDelegatedEconomicAutomation,
|
||||||
|
string? Notes);
|
||||||
|
|
||||||
|
public sealed record PlayerFleetSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Role,
|
||||||
|
string? CommanderId,
|
||||||
|
string? FrontId,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string? ReinforcementPolicyId,
|
||||||
|
IReadOnlyList<string> AssetIds,
|
||||||
|
IReadOnlyList<string> TaskForceIds,
|
||||||
|
IReadOnlyList<string> DirectiveIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerTaskForceSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Role,
|
||||||
|
string? FleetId,
|
||||||
|
string? CommanderId,
|
||||||
|
string? FrontId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
IReadOnlyList<string> AssetIds,
|
||||||
|
IReadOnlyList<string> DirectiveIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerStationGroupSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Role,
|
||||||
|
string? EconomicRegionId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
IReadOnlyList<string> StationIds,
|
||||||
|
IReadOnlyList<string> DirectiveIds,
|
||||||
|
IReadOnlyList<string> FocusItemIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerEconomicRegionSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Role,
|
||||||
|
string? SharedEconomicRegionId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
IReadOnlyList<string> SystemIds,
|
||||||
|
IReadOnlyList<string> StationGroupIds,
|
||||||
|
IReadOnlyList<string> DirectiveIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerFrontSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
float Priority,
|
||||||
|
string Posture,
|
||||||
|
string? SharedFrontLineId,
|
||||||
|
string? TargetFactionId,
|
||||||
|
IReadOnlyList<string> SystemIds,
|
||||||
|
IReadOnlyList<string> FleetIds,
|
||||||
|
IReadOnlyList<string> ReserveIds,
|
||||||
|
IReadOnlyList<string> DirectiveIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerReserveGroupSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string ReserveKind,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? PolicyId,
|
||||||
|
IReadOnlyList<string> AssetIds,
|
||||||
|
IReadOnlyList<string> FrontIds,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerFactionPolicySnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
string? PolicySetId,
|
||||||
|
bool AllowDelegatedCombat,
|
||||||
|
bool AllowDelegatedTrade,
|
||||||
|
float ReserveCreditsRatio,
|
||||||
|
float ReserveMilitaryRatio,
|
||||||
|
string TradeAccessPolicy,
|
||||||
|
string DockingAccessPolicy,
|
||||||
|
string ConstructionAccessPolicy,
|
||||||
|
string OperationalRangePolicy,
|
||||||
|
string CombatEngagementPolicy,
|
||||||
|
bool AvoidHostileSystems,
|
||||||
|
float FleeHullRatio,
|
||||||
|
IReadOnlyList<string> BlacklistedSystemIds,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerAutomationPolicySnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
bool Enabled,
|
||||||
|
string BehaviorKind,
|
||||||
|
bool UseOrders,
|
||||||
|
string? StagingOrderKind,
|
||||||
|
int MaxSystemRange,
|
||||||
|
bool KnownStationsOnly,
|
||||||
|
float Radius,
|
||||||
|
float WaitSeconds,
|
||||||
|
string? PreferredItemId,
|
||||||
|
string? Notes,
|
||||||
|
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerReinforcementPolicySnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
string ShipKind,
|
||||||
|
int DesiredAssetCount,
|
||||||
|
int MinimumReserveCount,
|
||||||
|
bool AutoTransferReserves,
|
||||||
|
bool AutoQueueProduction,
|
||||||
|
string? SourceReserveId,
|
||||||
|
string? TargetFrontId,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerProductionProgramSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Kind,
|
||||||
|
string? TargetShipKind,
|
||||||
|
string? TargetModuleId,
|
||||||
|
string? TargetItemId,
|
||||||
|
int TargetCount,
|
||||||
|
int CurrentCount,
|
||||||
|
string? StationGroupId,
|
||||||
|
string? ReinforcementPolicyId,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerDirectiveSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string Status,
|
||||||
|
string Kind,
|
||||||
|
string ScopeKind,
|
||||||
|
string ScopeId,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string BehaviorKind,
|
||||||
|
bool UseOrders,
|
||||||
|
string? StagingOrderKind,
|
||||||
|
string? ItemId,
|
||||||
|
string? PreferredNodeId,
|
||||||
|
string? PreferredConstructionSiteId,
|
||||||
|
string? PreferredModuleId,
|
||||||
|
int Priority,
|
||||||
|
float Radius,
|
||||||
|
float WaitSeconds,
|
||||||
|
int MaxSystemRange,
|
||||||
|
bool KnownStationsOnly,
|
||||||
|
IReadOnlyList<Vector3Dto> PatrolPoints,
|
||||||
|
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerAssignmentSnapshot(
|
||||||
|
string Id,
|
||||||
|
string AssetKind,
|
||||||
|
string AssetId,
|
||||||
|
string? FleetId,
|
||||||
|
string? TaskForceId,
|
||||||
|
string? StationGroupId,
|
||||||
|
string? EconomicRegionId,
|
||||||
|
string? FrontId,
|
||||||
|
string? ReserveId,
|
||||||
|
string? DirectiveId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string Role,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerDecisionLogEntrySnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Summary,
|
||||||
|
string? RelatedEntityKind,
|
||||||
|
string? RelatedEntityId,
|
||||||
|
DateTimeOffset OccurredAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerAlertSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Severity,
|
||||||
|
string Summary,
|
||||||
|
string? AssetKind,
|
||||||
|
string? AssetId,
|
||||||
|
string? RelatedDirectiveId,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset CreatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record PlayerFactionSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Label,
|
||||||
|
string SovereignFactionId,
|
||||||
|
string Status,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
PlayerAssetRegistrySnapshot AssetRegistry,
|
||||||
|
PlayerStrategicIntentSnapshot StrategicIntent,
|
||||||
|
IReadOnlyList<PlayerFleetSnapshot> Fleets,
|
||||||
|
IReadOnlyList<PlayerTaskForceSnapshot> TaskForces,
|
||||||
|
IReadOnlyList<PlayerStationGroupSnapshot> StationGroups,
|
||||||
|
IReadOnlyList<PlayerEconomicRegionSnapshot> EconomicRegions,
|
||||||
|
IReadOnlyList<PlayerFrontSnapshot> Fronts,
|
||||||
|
IReadOnlyList<PlayerReserveGroupSnapshot> Reserves,
|
||||||
|
IReadOnlyList<PlayerFactionPolicySnapshot> Policies,
|
||||||
|
IReadOnlyList<PlayerAutomationPolicySnapshot> AutomationPolicies,
|
||||||
|
IReadOnlyList<PlayerReinforcementPolicySnapshot> ReinforcementPolicies,
|
||||||
|
IReadOnlyList<PlayerProductionProgramSnapshot> ProductionPrograms,
|
||||||
|
IReadOnlyList<PlayerDirectiveSnapshot> Directives,
|
||||||
|
IReadOnlyList<PlayerAssignmentSnapshot> Assignments,
|
||||||
|
IReadOnlyList<PlayerDecisionLogEntrySnapshot> DecisionLog,
|
||||||
|
IReadOnlyList<PlayerAlertSnapshot> Alerts);
|
||||||
140
apps/backend/PlayerFaction/Contracts/PlayerFactionCommands.cs
Normal file
140
apps/backend/PlayerFaction/Contracts/PlayerFactionCommands.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
namespace SpaceGame.Api.PlayerFaction.Contracts;
|
||||||
|
|
||||||
|
public sealed record PlayerOrganizationCommandRequest(
|
||||||
|
string Kind,
|
||||||
|
string Label,
|
||||||
|
string? ParentOrganizationId,
|
||||||
|
string? FrontId,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string? ReinforcementPolicyId,
|
||||||
|
string? TargetFactionId,
|
||||||
|
float? Priority,
|
||||||
|
string? Role,
|
||||||
|
string? ReserveKind,
|
||||||
|
IReadOnlyList<string>? SystemIds,
|
||||||
|
IReadOnlyList<string>? FocusItemIds,
|
||||||
|
string? Notes);
|
||||||
|
|
||||||
|
public sealed record PlayerOrganizationMembershipCommandRequest(
|
||||||
|
IReadOnlyList<string>? AssetIds,
|
||||||
|
IReadOnlyList<string>? ChildOrganizationIds,
|
||||||
|
IReadOnlyList<string>? SystemIds,
|
||||||
|
IReadOnlyList<string>? FrontIds,
|
||||||
|
bool Replace = false);
|
||||||
|
|
||||||
|
public sealed record PlayerDirectiveCommandRequest(
|
||||||
|
string Label,
|
||||||
|
string Kind,
|
||||||
|
string ScopeKind,
|
||||||
|
string ScopeId,
|
||||||
|
string BehaviorKind,
|
||||||
|
bool UseOrders,
|
||||||
|
string? StagingOrderKind,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string? ItemId,
|
||||||
|
string? PreferredNodeId,
|
||||||
|
string? PreferredConstructionSiteId,
|
||||||
|
string? PreferredModuleId,
|
||||||
|
int Priority,
|
||||||
|
float? Radius,
|
||||||
|
float? WaitSeconds,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool? KnownStationsOnly,
|
||||||
|
IReadOnlyList<Vector3Dto>? PatrolPoints,
|
||||||
|
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string? Notes);
|
||||||
|
|
||||||
|
public sealed record PlayerPolicyCommandRequest(
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
string? PolicySetId,
|
||||||
|
bool AllowDelegatedCombat,
|
||||||
|
bool AllowDelegatedTrade,
|
||||||
|
float ReserveCreditsRatio,
|
||||||
|
float ReserveMilitaryRatio,
|
||||||
|
string? Notes,
|
||||||
|
string? TradeAccessPolicy,
|
||||||
|
string? DockingAccessPolicy,
|
||||||
|
string? ConstructionAccessPolicy,
|
||||||
|
string? OperationalRangePolicy,
|
||||||
|
string? CombatEngagementPolicy,
|
||||||
|
bool? AvoidHostileSystems,
|
||||||
|
float? FleeHullRatio,
|
||||||
|
IReadOnlyList<string>? BlacklistedSystemIds);
|
||||||
|
|
||||||
|
public sealed record PlayerAutomationPolicyCommandRequest(
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
bool Enabled,
|
||||||
|
string BehaviorKind,
|
||||||
|
bool UseOrders,
|
||||||
|
string? StagingOrderKind,
|
||||||
|
int MaxSystemRange,
|
||||||
|
bool KnownStationsOnly,
|
||||||
|
float Radius,
|
||||||
|
float WaitSeconds,
|
||||||
|
string? PreferredItemId,
|
||||||
|
string? Notes,
|
||||||
|
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders);
|
||||||
|
|
||||||
|
public sealed record PlayerReinforcementPolicyCommandRequest(
|
||||||
|
string Label,
|
||||||
|
string ScopeKind,
|
||||||
|
string? ScopeId,
|
||||||
|
string ShipKind,
|
||||||
|
int DesiredAssetCount,
|
||||||
|
int MinimumReserveCount,
|
||||||
|
bool AutoTransferReserves,
|
||||||
|
bool AutoQueueProduction,
|
||||||
|
string? SourceReserveId,
|
||||||
|
string? TargetFrontId,
|
||||||
|
string? Notes);
|
||||||
|
|
||||||
|
public sealed record PlayerProductionProgramCommandRequest(
|
||||||
|
string Label,
|
||||||
|
string Kind,
|
||||||
|
string? TargetShipKind,
|
||||||
|
string? TargetModuleId,
|
||||||
|
string? TargetItemId,
|
||||||
|
int TargetCount,
|
||||||
|
string? StationGroupId,
|
||||||
|
string? ReinforcementPolicyId,
|
||||||
|
string? Notes);
|
||||||
|
|
||||||
|
public sealed record PlayerAssetAssignmentCommandRequest(
|
||||||
|
string AssetKind,
|
||||||
|
string AssetId,
|
||||||
|
string? FleetId,
|
||||||
|
string? TaskForceId,
|
||||||
|
string? StationGroupId,
|
||||||
|
string? EconomicRegionId,
|
||||||
|
string? FrontId,
|
||||||
|
string? ReserveId,
|
||||||
|
string? DirectiveId,
|
||||||
|
string? PolicyId,
|
||||||
|
string? AutomationPolicyId,
|
||||||
|
string Role,
|
||||||
|
bool ClearConflicts = true);
|
||||||
|
|
||||||
|
public sealed record PlayerStrategicIntentCommandRequest(
|
||||||
|
string StrategicPosture,
|
||||||
|
string EconomicPosture,
|
||||||
|
string MilitaryPosture,
|
||||||
|
string LogisticsPosture,
|
||||||
|
float DesiredReserveRatio,
|
||||||
|
bool AllowDelegatedCombatAutomation,
|
||||||
|
bool AllowDelegatedEconomicAutomation,
|
||||||
|
string? Notes);
|
||||||
306
apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs
Normal file
306
apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
namespace SpaceGame.Api.PlayerFaction.Runtime;
|
||||||
|
|
||||||
|
public sealed class PlayerFactionRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public required string SovereignFactionId { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new();
|
||||||
|
public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new();
|
||||||
|
public List<PlayerFleetRuntime> Fleets { get; } = [];
|
||||||
|
public List<PlayerTaskForceRuntime> TaskForces { get; } = [];
|
||||||
|
public List<PlayerStationGroupRuntime> StationGroups { get; } = [];
|
||||||
|
public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = [];
|
||||||
|
public List<PlayerFrontRuntime> Fronts { get; } = [];
|
||||||
|
public List<PlayerReserveGroupRuntime> Reserves { get; } = [];
|
||||||
|
public List<PlayerFactionPolicyRuntime> Policies { get; } = [];
|
||||||
|
public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = [];
|
||||||
|
public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = [];
|
||||||
|
public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = [];
|
||||||
|
public List<PlayerDirectiveRuntime> Directives { get; } = [];
|
||||||
|
public List<PlayerAssignmentRuntime> Assignments { get; } = [];
|
||||||
|
public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = [];
|
||||||
|
public List<PlayerAlertRuntime> Alerts { get; } = [];
|
||||||
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerAssetRegistryRuntime
|
||||||
|
{
|
||||||
|
public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerStrategicIntentRuntime
|
||||||
|
{
|
||||||
|
public string StrategicPosture { get; set; } = "balanced";
|
||||||
|
public string EconomicPosture { get; set; } = "delegated";
|
||||||
|
public string MilitaryPosture { get; set; } = "layered-defense";
|
||||||
|
public string LogisticsPosture { get; set; } = "stable";
|
||||||
|
public float DesiredReserveRatio { get; set; } = 0.2f;
|
||||||
|
public bool AllowDelegatedCombatAutomation { get; set; } = true;
|
||||||
|
public bool AllowDelegatedEconomicAutomation { get; set; } = true;
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerFleetRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Role { get; set; } = "general-purpose";
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
|
public string? FrontId { get; set; }
|
||||||
|
public string? HomeSystemId { get; set; }
|
||||||
|
public string? HomeStationId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public string? ReinforcementPolicyId { get; set; }
|
||||||
|
public List<string> AssetIds { get; } = [];
|
||||||
|
public List<string> TaskForceIds { get; } = [];
|
||||||
|
public List<string> DirectiveIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerTaskForceRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Role { get; set; } = "task-force";
|
||||||
|
public string? FleetId { get; set; }
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
|
public string? FrontId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public List<string> AssetIds { get; } = [];
|
||||||
|
public List<string> DirectiveIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerStationGroupRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Role { get; set; } = "industrial-group";
|
||||||
|
public string? EconomicRegionId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public List<string> StationIds { get; } = [];
|
||||||
|
public List<string> DirectiveIds { get; } = [];
|
||||||
|
public List<string> FocusItemIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerEconomicRegionRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Role { get; set; } = "balanced-region";
|
||||||
|
public string? SharedEconomicRegionId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public List<string> SystemIds { get; } = [];
|
||||||
|
public List<string> StationGroupIds { get; } = [];
|
||||||
|
public List<string> DirectiveIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerFrontRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public float Priority { get; set; } = 50f;
|
||||||
|
public string Posture { get; set; } = "hold";
|
||||||
|
public string? SharedFrontLineId { get; set; }
|
||||||
|
public string? TargetFactionId { get; set; }
|
||||||
|
public List<string> SystemIds { get; } = [];
|
||||||
|
public List<string> FleetIds { get; } = [];
|
||||||
|
public List<string> ReserveIds { get; } = [];
|
||||||
|
public List<string> DirectiveIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerReserveGroupRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "ready";
|
||||||
|
public string ReserveKind { get; set; } = "military";
|
||||||
|
public string? HomeSystemId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public List<string> AssetIds { get; } = [];
|
||||||
|
public List<string> FrontIds { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerFactionPolicyRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string ScopeKind { get; set; } = "player-faction";
|
||||||
|
public string? ScopeId { get; set; }
|
||||||
|
public string? PolicySetId { get; set; }
|
||||||
|
public bool AllowDelegatedCombat { get; set; } = true;
|
||||||
|
public bool AllowDelegatedTrade { get; set; } = true;
|
||||||
|
public float ReserveCreditsRatio { get; set; } = 0.2f;
|
||||||
|
public float ReserveMilitaryRatio { get; set; } = 0.2f;
|
||||||
|
public string TradeAccessPolicy { get; set; } = "owner-and-allies";
|
||||||
|
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
|
||||||
|
public string ConstructionAccessPolicy { get; set; } = "owner-only";
|
||||||
|
public string OperationalRangePolicy { get; set; } = "unrestricted";
|
||||||
|
public string CombatEngagementPolicy { get; set; } = "defensive";
|
||||||
|
public bool AvoidHostileSystems { get; set; } = true;
|
||||||
|
public float FleeHullRatio { get; set; } = 0.35f;
|
||||||
|
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerAutomationPolicyRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string ScopeKind { get; set; } = "player-faction";
|
||||||
|
public string? ScopeId { get; set; }
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string BehaviorKind { get; set; } = "idle";
|
||||||
|
public bool UseOrders { get; set; }
|
||||||
|
public string? StagingOrderKind { get; set; }
|
||||||
|
public int MaxSystemRange { get; set; }
|
||||||
|
public bool KnownStationsOnly { get; set; }
|
||||||
|
public float Radius { get; set; } = 24f;
|
||||||
|
public float WaitSeconds { get; set; } = 3f;
|
||||||
|
public string? PreferredItemId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerReinforcementPolicyRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string ScopeKind { get; set; } = "player-faction";
|
||||||
|
public string? ScopeId { get; set; }
|
||||||
|
public string ShipKind { get; set; } = "military";
|
||||||
|
public int DesiredAssetCount { get; set; }
|
||||||
|
public int MinimumReserveCount { get; set; }
|
||||||
|
public bool AutoTransferReserves { get; set; } = true;
|
||||||
|
public bool AutoQueueProduction { get; set; } = true;
|
||||||
|
public string? SourceReserveId { get; set; }
|
||||||
|
public string? TargetFrontId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerProductionProgramRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Kind { get; set; } = "ship-production";
|
||||||
|
public string? TargetShipKind { get; set; }
|
||||||
|
public string? TargetModuleId { get; set; }
|
||||||
|
public string? TargetItemId { get; set; }
|
||||||
|
public int TargetCount { get; set; }
|
||||||
|
public int CurrentCount { get; set; }
|
||||||
|
public string? StationGroupId { get; set; }
|
||||||
|
public string? ReinforcementPolicyId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerDirectiveRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Label { get; set; }
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public string Kind { get; set; } = "hold";
|
||||||
|
public string ScopeKind { get; set; } = "asset";
|
||||||
|
public string ScopeId { get; set; } = string.Empty;
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public Vector3? TargetPosition { get; set; }
|
||||||
|
public string? HomeSystemId { get; set; }
|
||||||
|
public string? HomeStationId { get; set; }
|
||||||
|
public string? SourceStationId { get; set; }
|
||||||
|
public string? DestinationStationId { get; set; }
|
||||||
|
public string BehaviorKind { get; set; } = "idle";
|
||||||
|
public bool UseOrders { get; set; }
|
||||||
|
public string? StagingOrderKind { get; set; }
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public string? PreferredNodeId { get; set; }
|
||||||
|
public string? PreferredConstructionSiteId { get; set; }
|
||||||
|
public string? PreferredModuleId { get; set; }
|
||||||
|
public int Priority { get; set; } = 50;
|
||||||
|
public float Radius { get; set; } = 24f;
|
||||||
|
public float WaitSeconds { get; set; } = 3f;
|
||||||
|
public int MaxSystemRange { get; set; }
|
||||||
|
public bool KnownStationsOnly { get; set; }
|
||||||
|
public List<Vector3> PatrolPoints { get; } = [];
|
||||||
|
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerAssignmentRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string AssetKind { get; set; }
|
||||||
|
public required string AssetId { get; set; }
|
||||||
|
public string? FleetId { get; set; }
|
||||||
|
public string? TaskForceId { get; set; }
|
||||||
|
public string? StationGroupId { get; set; }
|
||||||
|
public string? EconomicRegionId { get; set; }
|
||||||
|
public string? FrontId { get; set; }
|
||||||
|
public string? ReserveId { get; set; }
|
||||||
|
public string? DirectiveId { get; set; }
|
||||||
|
public string? PolicyId { get; set; }
|
||||||
|
public string? AutomationPolicyId { get; set; }
|
||||||
|
public string Role { get; set; } = "line";
|
||||||
|
public string Status { get; set; } = "active";
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerDecisionLogEntryRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public string? RelatedEntityKind { get; set; }
|
||||||
|
public string? RelatedEntityId { get; set; }
|
||||||
|
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerAlertRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public required string Severity { get; set; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public string? AssetKind { get; set; }
|
||||||
|
public string? AssetId { get; set; }
|
||||||
|
public string? RelatedDirectiveId { get; set; }
|
||||||
|
public string Status { get; set; } = "open";
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
2441
apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs
Normal file
2441
apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
|
using FastEndpoints.Swagger;
|
||||||
using SpaceGame.Api.Universe.Simulation;
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.WebHost.UseUrls("http://127.0.0.1:5079");
|
|
||||||
builder.Services.AddCors((options) =>
|
builder.Services.AddCors((options) =>
|
||||||
{
|
{
|
||||||
options.AddDefaultPolicy((policy) =>
|
options.AddDefaultPolicy((policy) =>
|
||||||
@@ -17,6 +17,7 @@ builder.Services.AddCors((options) =>
|
|||||||
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
||||||
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
||||||
builder.Services.AddFastEndpoints();
|
builder.Services.AddFastEndpoints();
|
||||||
|
builder.Services.SwaggerDocument();
|
||||||
builder.Services.AddSingleton<WorldService>();
|
builder.Services.AddSingleton<WorldService>();
|
||||||
builder.Services.AddSingleton<TelemetryService>();
|
builder.Services.AddSingleton<TelemetryService>();
|
||||||
builder.Services.AddHostedService<SimulationHostedService>();
|
builder.Services.AddHostedService<SimulationHostedService>();
|
||||||
@@ -25,5 +26,6 @@ var app = builder.Build();
|
|||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
app.UseFastEndpoints();
|
app.UseFastEndpoints();
|
||||||
|
app.UseSwaggerGen();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -5,16 +5,7 @@
|
|||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "http://localhost:0",
|
"applicationUrl": "http://0.0.0.0:5079",
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"launchBrowser": false,
|
|
||||||
"applicationUrl": "https://localhost:0;http://localhost:0",
|
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Shared.AI;
|
|
||||||
|
|
||||||
public abstract class GoapAction<TState>
|
|
||||||
{
|
|
||||||
public abstract string Name { get; }
|
|
||||||
public abstract float Cost { get; }
|
|
||||||
public abstract bool CheckPreconditions(TState state);
|
|
||||||
public abstract TState ApplyEffects(TState state);
|
|
||||||
public abstract void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class GoapGoal<TState>
|
|
||||||
{
|
|
||||||
public abstract string Name { get; }
|
|
||||||
public abstract bool IsSatisfied(TState state);
|
|
||||||
public abstract float ComputePriority(TState state, SimulationWorld world, CommanderRuntime commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GoapPlan<TState>
|
|
||||||
{
|
|
||||||
public static readonly GoapPlan<TState> Empty = new() { Actions = [], TotalCost = 0f };
|
|
||||||
|
|
||||||
public required IReadOnlyList<GoapAction<TState>> Actions { get; init; }
|
|
||||||
public required float TotalCost { get; init; }
|
|
||||||
public int CurrentStep { get; set; }
|
|
||||||
|
|
||||||
public GoapAction<TState>? CurrentAction => CurrentStep < Actions.Count ? Actions[CurrentStep] : null;
|
|
||||||
public bool IsComplete => CurrentStep >= Actions.Count;
|
|
||||||
public void Advance() => CurrentStep++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GoapPlanner<TState>
|
|
||||||
{
|
|
||||||
private readonly Func<TState, TState> cloneState;
|
|
||||||
|
|
||||||
public GoapPlanner(Func<TState, TState> cloneState)
|
|
||||||
{
|
|
||||||
this.cloneState = cloneState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GoapPlan<TState>? Plan(
|
|
||||||
TState initialState,
|
|
||||||
GoapGoal<TState> goal,
|
|
||||||
IReadOnlyList<GoapAction<TState>> availableActions)
|
|
||||||
{
|
|
||||||
if (goal.IsSatisfied(initialState))
|
|
||||||
{
|
|
||||||
return GoapPlan<TState>.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var openSet = new PriorityQueue<PlanNode, float>();
|
|
||||||
openSet.Enqueue(new PlanNode(cloneState(initialState), [], 0f), 0f);
|
|
||||||
|
|
||||||
const int MaxIterations = 256;
|
|
||||||
var iterations = 0;
|
|
||||||
|
|
||||||
while (openSet.Count > 0 && iterations++ < MaxIterations)
|
|
||||||
{
|
|
||||||
var current = openSet.Dequeue();
|
|
||||||
|
|
||||||
if (goal.IsSatisfied(current.State))
|
|
||||||
{
|
|
||||||
return new GoapPlan<TState>
|
|
||||||
{
|
|
||||||
Actions = current.Actions,
|
|
||||||
TotalCost = current.Cost,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var action in availableActions)
|
|
||||||
{
|
|
||||||
if (!action.CheckPreconditions(current.State))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newState = action.ApplyEffects(cloneState(current.State));
|
|
||||||
var newCost = current.Cost + action.Cost;
|
|
||||||
var newActions = new List<GoapAction<TState>>(current.Actions) { action };
|
|
||||||
openSet.Enqueue(new PlanNode(newState, newActions, newCost), newCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record PlanNode(
|
|
||||||
TState State,
|
|
||||||
IReadOnlyList<GoapAction<TState>> Actions,
|
|
||||||
float Cost);
|
|
||||||
}
|
|
||||||
@@ -12,14 +12,47 @@ public enum WorkStatus
|
|||||||
{
|
{
|
||||||
Pending,
|
Pending,
|
||||||
Active,
|
Active,
|
||||||
|
Blocked,
|
||||||
Completed,
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Interrupted,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum OrderStatus
|
public enum OrderStatus
|
||||||
{
|
{
|
||||||
Queued,
|
Queued,
|
||||||
Accepted,
|
Active,
|
||||||
Completed,
|
Completed,
|
||||||
|
Cancelled,
|
||||||
|
Failed,
|
||||||
|
Interrupted,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AiPlanStatus
|
||||||
|
{
|
||||||
|
Planned,
|
||||||
|
Running,
|
||||||
|
Blocked,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Interrupted,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AiPlanStepStatus
|
||||||
|
{
|
||||||
|
Planned,
|
||||||
|
Running,
|
||||||
|
Blocked,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Interrupted,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AiPlanSourceKind
|
||||||
|
{
|
||||||
|
Rule,
|
||||||
|
Order,
|
||||||
|
DefaultBehavior,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ShipState
|
public enum ShipState
|
||||||
@@ -49,22 +82,8 @@ public enum ShipState
|
|||||||
Blocked,
|
Blocked,
|
||||||
Undocking,
|
Undocking,
|
||||||
EngagingTarget,
|
EngagingTarget,
|
||||||
}
|
HoldingPosition,
|
||||||
|
Fleeing,
|
||||||
public enum ControllerTaskKind
|
|
||||||
{
|
|
||||||
Idle,
|
|
||||||
Travel,
|
|
||||||
Extract,
|
|
||||||
Dock,
|
|
||||||
Load,
|
|
||||||
Unload,
|
|
||||||
DeliverConstruction,
|
|
||||||
BuildConstructionSite,
|
|
||||||
AttackTarget,
|
|
||||||
|
|
||||||
ConstructModule,
|
|
||||||
Undock,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SpaceLayerKinds
|
public static class SpaceLayerKinds
|
||||||
@@ -95,37 +114,39 @@ public static class CommanderKind
|
|||||||
|
|
||||||
public static class ShipTaskKinds
|
public static class ShipTaskKinds
|
||||||
{
|
{
|
||||||
public const string Idle = "idle";
|
public const string HoldPosition = "hold-position";
|
||||||
public const string LocalMove = "local-move";
|
public const string Travel = "travel";
|
||||||
public const string WarpToNode = "warp-to-node";
|
public const string FollowTarget = "follow-target";
|
||||||
public const string UseStargate = "use-stargate";
|
public const string MineNode = "mine-node";
|
||||||
public const string UseFtl = "use-ftl";
|
|
||||||
public const string Dock = "dock";
|
public const string Dock = "dock";
|
||||||
public const string Undock = "undock";
|
public const string Undock = "undock";
|
||||||
public const string LoadCargo = "load-cargo";
|
public const string LoadCargo = "load-cargo";
|
||||||
public const string UnloadCargo = "unload-cargo";
|
public const string UnloadCargo = "unload-cargo";
|
||||||
|
public const string TransferCargoToShip = "transfer-cargo-to-ship";
|
||||||
public const string MineNode = "mine-node";
|
public const string SalvageWreck = "salvage-wreck";
|
||||||
public const string HarvestGas = "harvest-gas";
|
public const string DeliverConstruction = "deliver-construction";
|
||||||
public const string DeliverToStation = "deliver-to-station";
|
public const string ConstructModule = "construct-module";
|
||||||
public const string ClaimLagrangePoint = "claim-lagrange-point";
|
|
||||||
public const string BuildConstructionSite = "build-construction-site";
|
public const string BuildConstructionSite = "build-construction-site";
|
||||||
public const string EscortTarget = "escort-target";
|
|
||||||
public const string AttackTarget = "attack-target";
|
public const string AttackTarget = "attack-target";
|
||||||
public const string DefendCelestial = "defend-celestial";
|
public const string Flee = "flee";
|
||||||
public const string Retreat = "retreat";
|
public const string Wait = "wait";
|
||||||
public const string HoldPosition = "hold-position";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ShipOrderKinds
|
public static class ShipOrderKinds
|
||||||
{
|
{
|
||||||
public const string DirectMove = "direct-move";
|
public const string Move = "move";
|
||||||
public const string TravelToNode = "travel-to-node";
|
|
||||||
public const string DockAtStation = "dock-at-station";
|
public const string DockAtStation = "dock-at-station";
|
||||||
public const string DeliverCargo = "deliver-cargo";
|
public const string DockAndWait = "dock-and-wait";
|
||||||
|
public const string FlyAndWait = "fly-and-wait";
|
||||||
|
public const string FlyToObject = "fly-to-object";
|
||||||
|
public const string FollowShip = "follow-ship";
|
||||||
|
public const string TradeRoute = "trade-route";
|
||||||
|
public const string MineAndDeliver = "mine-and-deliver";
|
||||||
public const string BuildAtSite = "build-at-site";
|
public const string BuildAtSite = "build-at-site";
|
||||||
public const string AttackTarget = "attack-target";
|
public const string AttackTarget = "attack-target";
|
||||||
public const string HoldPosition = "hold-position";
|
public const string HoldPosition = "hold-position";
|
||||||
|
public const string RepeatOrders = "repeat-orders";
|
||||||
|
public const string Flee = "flee";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ClaimStateKinds
|
public static class ClaimStateKinds
|
||||||
@@ -174,18 +195,54 @@ public static class SimulationEnumMappings
|
|||||||
{
|
{
|
||||||
WorkStatus.Pending => "pending",
|
WorkStatus.Pending => "pending",
|
||||||
WorkStatus.Active => "active",
|
WorkStatus.Active => "active",
|
||||||
|
WorkStatus.Blocked => "blocked",
|
||||||
WorkStatus.Completed => "completed",
|
WorkStatus.Completed => "completed",
|
||||||
|
WorkStatus.Failed => "failed",
|
||||||
|
WorkStatus.Interrupted => "interrupted",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ToContractValue(this OrderStatus status) => status switch
|
public static string ToContractValue(this OrderStatus status) => status switch
|
||||||
{
|
{
|
||||||
OrderStatus.Queued => "queued",
|
OrderStatus.Queued => "queued",
|
||||||
OrderStatus.Accepted => "accepted",
|
OrderStatus.Active => "active",
|
||||||
OrderStatus.Completed => "completed",
|
OrderStatus.Completed => "completed",
|
||||||
|
OrderStatus.Cancelled => "cancelled",
|
||||||
|
OrderStatus.Failed => "failed",
|
||||||
|
OrderStatus.Interrupted => "interrupted",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static string ToContractValue(this AiPlanStatus status) => status switch
|
||||||
|
{
|
||||||
|
AiPlanStatus.Planned => "planned",
|
||||||
|
AiPlanStatus.Running => "running",
|
||||||
|
AiPlanStatus.Blocked => "blocked",
|
||||||
|
AiPlanStatus.Completed => "completed",
|
||||||
|
AiPlanStatus.Failed => "failed",
|
||||||
|
AiPlanStatus.Interrupted => "interrupted",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToContractValue(this AiPlanStepStatus status) => status switch
|
||||||
|
{
|
||||||
|
AiPlanStepStatus.Planned => "planned",
|
||||||
|
AiPlanStepStatus.Running => "running",
|
||||||
|
AiPlanStepStatus.Blocked => "blocked",
|
||||||
|
AiPlanStepStatus.Completed => "completed",
|
||||||
|
AiPlanStepStatus.Failed => "failed",
|
||||||
|
AiPlanStepStatus.Interrupted => "interrupted",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToContractValue(this AiPlanSourceKind kind) => kind switch
|
||||||
|
{
|
||||||
|
AiPlanSourceKind.Rule => "rule",
|
||||||
|
AiPlanSourceKind.Order => "order",
|
||||||
|
AiPlanSourceKind.DefaultBehavior => "default-behavior",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||||
|
};
|
||||||
|
|
||||||
public static string ToContractValue(this ShipState state) => state switch
|
public static string ToContractValue(this ShipState state) => state switch
|
||||||
{
|
{
|
||||||
ShipState.Idle => "idle",
|
ShipState.Idle => "idle",
|
||||||
@@ -213,23 +270,8 @@ public static class SimulationEnumMappings
|
|||||||
ShipState.Blocked => "blocked",
|
ShipState.Blocked => "blocked",
|
||||||
ShipState.Undocking => "undocking",
|
ShipState.Undocking => "undocking",
|
||||||
ShipState.EngagingTarget => "engaging-target",
|
ShipState.EngagingTarget => "engaging-target",
|
||||||
|
ShipState.HoldingPosition => "holding-position",
|
||||||
|
ShipState.Fleeing => "fleeing",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ToContractValue(this ControllerTaskKind kind) => kind switch
|
|
||||||
{
|
|
||||||
ControllerTaskKind.Idle => "idle",
|
|
||||||
ControllerTaskKind.Travel => "travel",
|
|
||||||
ControllerTaskKind.Extract => "extract",
|
|
||||||
ControllerTaskKind.Dock => "dock",
|
|
||||||
ControllerTaskKind.Load => "load",
|
|
||||||
ControllerTaskKind.Unload => "unload",
|
|
||||||
ControllerTaskKind.DeliverConstruction => "deliver-construction",
|
|
||||||
ControllerTaskKind.BuildConstructionSite => "build-construction-site",
|
|
||||||
ControllerTaskKind.AttackTarget => "attack-target",
|
|
||||||
|
|
||||||
ControllerTaskKind.ConstructModule => "construct-module",
|
|
||||||
ControllerTaskKind.Undock => "undock",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Ships.AI;
|
|
||||||
|
|
||||||
internal interface IShipBehaviorState
|
|
||||||
{
|
|
||||||
string Kind { get; }
|
|
||||||
|
|
||||||
void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world);
|
|
||||||
|
|
||||||
void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Ships.AI;
|
|
||||||
|
|
||||||
internal sealed class ShipBehaviorStateMachine
|
|
||||||
{
|
|
||||||
private readonly IReadOnlyDictionary<string, IShipBehaviorState> states;
|
|
||||||
private readonly IShipBehaviorState fallbackState;
|
|
||||||
|
|
||||||
private ShipBehaviorStateMachine(IReadOnlyDictionary<string, IShipBehaviorState> states, IShipBehaviorState fallbackState)
|
|
||||||
{
|
|
||||||
this.states = states;
|
|
||||||
this.fallbackState = fallbackState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ShipBehaviorStateMachine CreateDefault()
|
|
||||||
{
|
|
||||||
var idleState = new IdleShipBehaviorState();
|
|
||||||
var knownStates = new IShipBehaviorState[]
|
|
||||||
{
|
|
||||||
idleState,
|
|
||||||
new PatrolShipBehaviorState(),
|
|
||||||
new AttackTargetShipBehaviorState(),
|
|
||||||
new TradeHaulShipBehaviorState(),
|
|
||||||
new ResourceHarvestShipBehaviorState("auto-mine", null, "mining"),
|
|
||||||
new ConstructStationShipBehaviorState(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return new ShipBehaviorStateMachine(
|
|
||||||
knownStates.ToDictionary(state => state.Kind, StringComparer.Ordinal),
|
|
||||||
idleState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
Resolve(ship.DefaultBehavior.Kind).Plan(engine, ship, world);
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) =>
|
|
||||||
Resolve(ship.DefaultBehavior.Kind).ApplyEvent(engine, ship, world, controllerEvent);
|
|
||||||
|
|
||||||
private IShipBehaviorState Resolve(string kind) =>
|
|
||||||
states.TryGetValue(kind, out var state) ? state : fallbackState;
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Ships.AI;
|
|
||||||
|
|
||||||
internal sealed class IdleShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
public string Kind => "idle";
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Idle,
|
|
||||||
Threshold = world.Balance.ArrivalThreshold,
|
|
||||||
Status = WorkStatus.Pending,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class PatrolShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
public string Kind => "patrol";
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
|
|
||||||
{
|
|
||||||
ship.DefaultBehavior.Kind = "idle";
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Idle,
|
|
||||||
Threshold = world.Balance.ArrivalThreshold,
|
|
||||||
Status = WorkStatus.Pending,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
|
|
||||||
TargetSystemId = ship.SystemId,
|
|
||||||
Threshold = 18f,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0)
|
|
||||||
{
|
|
||||||
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
private readonly string? resourceItemId;
|
|
||||||
private readonly string requiredModule;
|
|
||||||
|
|
||||||
public ResourceHarvestShipBehaviorState(string kind, string? resourceItemId, string requiredModule)
|
|
||||||
{
|
|
||||||
Kind = kind;
|
|
||||||
this.resourceItemId = resourceItemId;
|
|
||||||
this.requiredModule = requiredModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Kind { get; }
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
|
||||||
{
|
|
||||||
case ("travel-to-node", "arrived"):
|
|
||||||
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
|
|
||||||
break;
|
|
||||||
case ("extract", "cargo-full"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
|
||||||
break;
|
|
||||||
case ("extract", "node-depleted"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
|
||||||
ship.DefaultBehavior.NodeId = null;
|
|
||||||
break;
|
|
||||||
case ("travel-to-station", "arrived"):
|
|
||||||
ship.DefaultBehavior.Phase = "dock";
|
|
||||||
break;
|
|
||||||
case ("dock", "docked"):
|
|
||||||
ship.DefaultBehavior.Phase = "unload";
|
|
||||||
break;
|
|
||||||
case ("unload", "unloaded"):
|
|
||||||
ship.DefaultBehavior.Phase = "undock";
|
|
||||||
break;
|
|
||||||
case ("undock", "undocked"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-node";
|
|
||||||
ship.DefaultBehavior.NodeId = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
public string Kind => "construct-station";
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
engine.PlanStationConstruction(ship, world);
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
|
||||||
{
|
|
||||||
case ("travel-to-station", "arrived"):
|
|
||||||
ship.DefaultBehavior.Phase = "deliver-to-site";
|
|
||||||
break;
|
|
||||||
case ("deliver-to-site", "construction-delivered"):
|
|
||||||
ship.DefaultBehavior.Phase = "build-site";
|
|
||||||
break;
|
|
||||||
case ("construct-module", "module-constructed"):
|
|
||||||
case ("build-site", "site-constructed"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-station";
|
|
||||||
ship.DefaultBehavior.ModuleId = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class AttackTargetShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
public string Kind => "attack-target";
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
engine.PlanAttackTarget(ship, world);
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
if (controllerEvent is "target-destroyed" or "target-lost")
|
|
||||||
{
|
|
||||||
ship.DefaultBehavior.TargetEntityId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class TradeHaulShipBehaviorState : IShipBehaviorState
|
|
||||||
{
|
|
||||||
public string Kind => "trade-haul";
|
|
||||||
|
|
||||||
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
engine.PlanTransportHaul(ship, world);
|
|
||||||
|
|
||||||
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
switch (ship.DefaultBehavior.Phase, controllerEvent)
|
|
||||||
{
|
|
||||||
case ("travel-to-source", "arrived"):
|
|
||||||
ship.DefaultBehavior.Phase = "dock-source";
|
|
||||||
break;
|
|
||||||
case ("dock-source", "docked"):
|
|
||||||
ship.DefaultBehavior.Phase = "load";
|
|
||||||
break;
|
|
||||||
case ("load", "loaded"):
|
|
||||||
ship.DefaultBehavior.Phase = "undock-from-source";
|
|
||||||
break;
|
|
||||||
case ("undock-from-source", "undocked"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-destination";
|
|
||||||
break;
|
|
||||||
case ("travel-to-destination", "arrived"):
|
|
||||||
ship.DefaultBehavior.Phase = "dock-destination";
|
|
||||||
break;
|
|
||||||
case ("dock-destination", "docked"):
|
|
||||||
ship.DefaultBehavior.Phase = "unload";
|
|
||||||
break;
|
|
||||||
case ("unload", "unloaded"):
|
|
||||||
ship.DefaultBehavior.Phase = "undock-from-destination";
|
|
||||||
break;
|
|
||||||
case ("undock-from-destination", "undocked"):
|
|
||||||
ship.DefaultBehavior.Phase = "travel-to-source";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Ships.AI;
|
|
||||||
|
|
||||||
// ─── Planning State ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public sealed class ShipPlanningState
|
|
||||||
{
|
|
||||||
public string ShipKind { get; set; } = string.Empty;
|
|
||||||
public bool HasMiningCapability { get; set; }
|
|
||||||
public bool FactionWantsOre { get; set; }
|
|
||||||
public bool FactionWantsExpansion { get; set; }
|
|
||||||
public bool FactionWantsCombat { get; set; }
|
|
||||||
public bool FactionNeedsShipyard { get; set; }
|
|
||||||
public string? TargetEnemySystemId { get; set; }
|
|
||||||
public string? TargetEnemyEntityId { get; set; }
|
|
||||||
public string? TradeItemId { get; set; }
|
|
||||||
public string? TradeSourceStationId { get; set; }
|
|
||||||
public string? TradeDestinationStationId { get; set; }
|
|
||||||
public string? CurrentObjective { get; set; }
|
|
||||||
|
|
||||||
public ShipPlanningState Clone() => (ShipPlanningState)MemberwiseClone();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Goals ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// A ship should always have an assigned objective. The planner picks the best one.
|
|
||||||
public sealed class AssignObjectiveGoal : GoapGoal<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "assign-objective";
|
|
||||||
|
|
||||||
public override bool IsSatisfied(ShipPlanningState state) => state.CurrentObjective is not null;
|
|
||||||
|
|
||||||
public override float ComputePriority(ShipPlanningState state, SimulationWorld world, CommanderRuntime commander) =>
|
|
||||||
100f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Actions ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public sealed class SetMiningObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-mining-objective";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
||||||
state.HasMiningCapability && state.FactionWantsOre;
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "auto-mine";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "auto-mine", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "auto-mine";
|
|
||||||
ship.DefaultBehavior.Phase = null;
|
|
||||||
ship.DefaultBehavior.NodeId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetPatrolObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-patrol-objective";
|
|
||||||
public override float Cost => 2f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
||||||
string.Equals(state.ShipKind, "military", StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "patrol";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "patrol", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
|
|
||||||
{
|
|
||||||
var station = world.Stations.FirstOrDefault(s =>
|
|
||||||
s.FactionId == ship.FactionId &&
|
|
||||||
string.Equals(s.SystemId, ship.SystemId, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (station is not null)
|
|
||||||
{
|
|
||||||
var radius = station.Radius + 90f;
|
|
||||||
ship.DefaultBehavior.PatrolPoints.AddRange(
|
|
||||||
[
|
|
||||||
new Vector3(station.Position.X + radius, station.Position.Y, station.Position.Z),
|
|
||||||
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + radius),
|
|
||||||
new Vector3(station.Position.X - radius, station.Position.Y, station.Position.Z),
|
|
||||||
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - radius),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "patrol";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetAttackObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-attack-objective";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
||||||
string.Equals(state.ShipKind, "military", StringComparison.Ordinal)
|
|
||||||
&& state.FactionWantsCombat
|
|
||||||
&& state.TargetEnemyEntityId is not null;
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "attack-target";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "attack-target";
|
|
||||||
ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior?.AreaSystemId ?? ship.DefaultBehavior.AreaSystemId;
|
|
||||||
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
|
|
||||||
ship.DefaultBehavior.Phase = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetConstructionObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-construction-objective";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
||||||
string.Equals(state.ShipKind, "construction", StringComparison.Ordinal)
|
|
||||||
&& (state.FactionWantsExpansion || state.FactionNeedsShipyard);
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "construct-station";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "construct-station", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "construct-station";
|
|
||||||
ship.DefaultBehavior.Phase = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetTradeObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-trade-objective";
|
|
||||||
public override float Cost => 1f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) =>
|
|
||||||
string.Equals(state.ShipKind, "transport", StringComparison.Ordinal)
|
|
||||||
&& state.TradeItemId is not null
|
|
||||||
&& state.TradeSourceStationId is not null
|
|
||||||
&& state.TradeDestinationStationId is not null;
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "trade-haul";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null || commander.ActiveBehavior is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "trade-haul";
|
|
||||||
ship.DefaultBehavior.ItemId = commander.ActiveBehavior.ItemId;
|
|
||||||
ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId;
|
|
||||||
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior.TargetEntityId;
|
|
||||||
ship.DefaultBehavior.Phase ??= "travel-to-source";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SetIdleObjectiveAction : GoapAction<ShipPlanningState>
|
|
||||||
{
|
|
||||||
public override string Name => "set-idle-objective";
|
|
||||||
public override float Cost => 10f;
|
|
||||||
|
|
||||||
public override bool CheckPreconditions(ShipPlanningState state) => true;
|
|
||||||
|
|
||||||
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
|
|
||||||
{
|
|
||||||
state.CurrentObjective = "idle";
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
|
|
||||||
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "idle", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DefaultBehavior.Kind = "idle";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
apps/backend/Ships/Api/EnqueueShipOrderHandler.cs
Normal file
39
apps/backend/Ships/Api/EnqueueShipOrderHandler.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Ships.Api;
|
||||||
|
|
||||||
|
public sealed class EnqueueShipOrderHandler(WorldService worldService) : Endpoint<ShipOrderCommandRequest, ShipSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/ships/{shipId}/orders");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var shipId = Route<string>("shipId");
|
||||||
|
if (string.IsNullOrWhiteSpace(shipId))
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = worldService.EnqueueShipOrder(shipId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(cancellation: cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
apps/backend/Ships/Api/RemoveShipOrderHandler.cs
Normal file
30
apps/backend/Ships/Api/RemoveShipOrderHandler.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Ships.Api;
|
||||||
|
|
||||||
|
public sealed class RemoveShipOrderRequest
|
||||||
|
{
|
||||||
|
public string ShipId { get; set; } = string.Empty;
|
||||||
|
public string OrderId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint<RemoveShipOrderRequest, ShipSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Delete("/api/ships/{shipId}/orders/{orderId}");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(RemoveShipOrderRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshot = worldService.RemoveShipOrder(request.ShipId, request.OrderId);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs
Normal file
31
apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Ships.Api;
|
||||||
|
|
||||||
|
public sealed class UpdateShipDefaultBehaviorHandler(WorldService worldService) : Endpoint<ShipDefaultBehaviorCommandRequest, ShipSnapshot>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/ships/{shipId}/default-behavior");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var shipId = Route<string>("shipId");
|
||||||
|
if (string.IsNullOrWhiteSpace(shipId))
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = worldService.UpdateShipDefaultBehavior(shipId, request);
|
||||||
|
if (snapshot is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendOkAsync(snapshot, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
apps/backend/Ships/Contracts/ShipCommands.cs
Normal file
55
apps/backend/Ships/Contracts/ShipCommands.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace SpaceGame.Api.Ships.Contracts;
|
||||||
|
|
||||||
|
public sealed record ShipOrderCommandRequest(
|
||||||
|
string Kind,
|
||||||
|
int Priority,
|
||||||
|
bool InterruptCurrentPlan,
|
||||||
|
string? Label,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string? ItemId,
|
||||||
|
string? NodeId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string? ModuleId,
|
||||||
|
float? WaitSeconds,
|
||||||
|
float? Radius,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool? KnownStationsOnly);
|
||||||
|
|
||||||
|
public sealed record ShipOrderTemplateCommandRequest(
|
||||||
|
string Kind,
|
||||||
|
string? Label,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string? ItemId,
|
||||||
|
string? NodeId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string? ModuleId,
|
||||||
|
float? WaitSeconds,
|
||||||
|
float? Radius,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool? KnownStationsOnly);
|
||||||
|
|
||||||
|
public sealed record ShipDefaultBehaviorCommandRequest(
|
||||||
|
string Kind,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? AreaSystemId,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? PreferredItemId,
|
||||||
|
string? PreferredNodeId,
|
||||||
|
string? PreferredConstructionSiteId,
|
||||||
|
string? PreferredModuleId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
float? WaitSeconds,
|
||||||
|
float? Radius,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool? KnownStationsOnly,
|
||||||
|
IReadOnlyList<Vector3Dto>? PatrolPoints,
|
||||||
|
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders);
|
||||||
@@ -1,5 +1,132 @@
|
|||||||
namespace SpaceGame.Api.Ships.Contracts;
|
namespace SpaceGame.Api.Ships.Contracts;
|
||||||
|
|
||||||
|
public sealed record ShipSkillProfileSnapshot(
|
||||||
|
int Navigation,
|
||||||
|
int Trade,
|
||||||
|
int Mining,
|
||||||
|
int Combat,
|
||||||
|
int Construction);
|
||||||
|
|
||||||
|
public sealed record ShipOrderSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
int Priority,
|
||||||
|
bool InterruptCurrentPlan,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
string? Label,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string? ItemId,
|
||||||
|
string? NodeId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string? ModuleId,
|
||||||
|
float WaitSeconds,
|
||||||
|
float Radius,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool KnownStationsOnly,
|
||||||
|
string? FailureReason);
|
||||||
|
|
||||||
|
public sealed record ShipOrderTemplateSnapshot(
|
||||||
|
string Kind,
|
||||||
|
string? Label,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? SourceStationId,
|
||||||
|
string? DestinationStationId,
|
||||||
|
string? ItemId,
|
||||||
|
string? NodeId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string? ModuleId,
|
||||||
|
float WaitSeconds,
|
||||||
|
float Radius,
|
||||||
|
int? MaxSystemRange,
|
||||||
|
bool KnownStationsOnly);
|
||||||
|
|
||||||
|
public sealed record DefaultBehaviorSnapshot(
|
||||||
|
string Kind,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? AreaSystemId,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? PreferredItemId,
|
||||||
|
string? PreferredNodeId,
|
||||||
|
string? PreferredConstructionSiteId,
|
||||||
|
string? PreferredModuleId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
float WaitSeconds,
|
||||||
|
float Radius,
|
||||||
|
int MaxSystemRange,
|
||||||
|
bool KnownStationsOnly,
|
||||||
|
IReadOnlyList<Vector3Dto> PatrolPoints,
|
||||||
|
int PatrolIndex,
|
||||||
|
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||||
|
int RepeatIndex);
|
||||||
|
|
||||||
|
public sealed record ShipAssignmentSnapshot(
|
||||||
|
string CommanderId,
|
||||||
|
string? ParentCommanderId,
|
||||||
|
string Kind,
|
||||||
|
string BehaviorKind,
|
||||||
|
string Status,
|
||||||
|
string? ObjectiveId,
|
||||||
|
string? CampaignId,
|
||||||
|
string? TheaterId,
|
||||||
|
float Priority,
|
||||||
|
string? HomeSystemId,
|
||||||
|
string? HomeStationId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
string? TargetEntityId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? ItemId,
|
||||||
|
string? Notes,
|
||||||
|
DateTimeOffset? UpdatedAtUtc);
|
||||||
|
|
||||||
|
public sealed record ShipSubTaskSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string Summary,
|
||||||
|
string? TargetEntityId,
|
||||||
|
string? TargetSystemId,
|
||||||
|
string? TargetNodeId,
|
||||||
|
Vector3Dto? TargetPosition,
|
||||||
|
string? ItemId,
|
||||||
|
string? ModuleId,
|
||||||
|
float Threshold,
|
||||||
|
float Amount,
|
||||||
|
float Progress,
|
||||||
|
float ElapsedSeconds,
|
||||||
|
float TotalSeconds,
|
||||||
|
string? BlockingReason);
|
||||||
|
|
||||||
|
public sealed record ShipPlanStepSnapshot(
|
||||||
|
string Id,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string Summary,
|
||||||
|
string? BlockingReason,
|
||||||
|
int CurrentSubTaskIndex,
|
||||||
|
IReadOnlyList<ShipSubTaskSnapshot> SubTasks);
|
||||||
|
|
||||||
|
public sealed record ShipPlanSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SourceKind,
|
||||||
|
string SourceId,
|
||||||
|
string Kind,
|
||||||
|
string Status,
|
||||||
|
string Summary,
|
||||||
|
int CurrentStepIndex,
|
||||||
|
DateTimeOffset CreatedAtUtc,
|
||||||
|
DateTimeOffset UpdatedAtUtc,
|
||||||
|
string? InterruptReason,
|
||||||
|
string? FailureReason,
|
||||||
|
IReadOnlyList<ShipPlanStepSnapshot> Steps);
|
||||||
|
|
||||||
public sealed record ShipSnapshot(
|
public sealed record ShipSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
string Label,
|
string Label,
|
||||||
@@ -10,24 +137,29 @@ public sealed record ShipSnapshot(
|
|||||||
Vector3Dto LocalVelocity,
|
Vector3Dto LocalVelocity,
|
||||||
Vector3Dto TargetLocalPosition,
|
Vector3Dto TargetLocalPosition,
|
||||||
string State,
|
string State,
|
||||||
string? OrderKind,
|
IReadOnlyList<ShipOrderSnapshot> OrderQueue,
|
||||||
string DefaultBehaviorKind,
|
DefaultBehaviorSnapshot DefaultBehavior,
|
||||||
string? BehaviorPhase,
|
ShipAssignmentSnapshot? Assignment,
|
||||||
string ControllerTaskKind,
|
ShipSkillProfileSnapshot Skills,
|
||||||
string? CommanderObjective,
|
ShipPlanSnapshot? ActivePlan,
|
||||||
|
string? CurrentStepId,
|
||||||
|
IReadOnlyList<ShipSubTaskSnapshot> ActiveSubTasks,
|
||||||
|
string ControlSourceKind,
|
||||||
|
string? ControlSourceId,
|
||||||
|
string? ControlReason,
|
||||||
|
string? LastReplanReason,
|
||||||
|
string? LastAccessFailureReason,
|
||||||
string? CelestialId,
|
string? CelestialId,
|
||||||
string? DockedStationId,
|
string? DockedStationId,
|
||||||
string? CommanderId,
|
string? CommanderId,
|
||||||
string? PolicySetId,
|
string? PolicySetId,
|
||||||
float CargoCapacity,
|
float CargoCapacity,
|
||||||
|
|
||||||
float TravelSpeed,
|
float TravelSpeed,
|
||||||
string TravelSpeedUnit,
|
string TravelSpeedUnit,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
IReadOnlyList<string> History,
|
IReadOnlyList<string> History,
|
||||||
ShipActionProgressSnapshot? CurrentAction,
|
|
||||||
ShipSpatialStateSnapshot SpatialState);
|
ShipSpatialStateSnapshot SpatialState);
|
||||||
|
|
||||||
public sealed record ShipDelta(
|
public sealed record ShipDelta(
|
||||||
@@ -40,30 +172,31 @@ public sealed record ShipDelta(
|
|||||||
Vector3Dto LocalVelocity,
|
Vector3Dto LocalVelocity,
|
||||||
Vector3Dto TargetLocalPosition,
|
Vector3Dto TargetLocalPosition,
|
||||||
string State,
|
string State,
|
||||||
string? OrderKind,
|
IReadOnlyList<ShipOrderSnapshot> OrderQueue,
|
||||||
string DefaultBehaviorKind,
|
DefaultBehaviorSnapshot DefaultBehavior,
|
||||||
string? BehaviorPhase,
|
ShipAssignmentSnapshot? Assignment,
|
||||||
string ControllerTaskKind,
|
ShipSkillProfileSnapshot Skills,
|
||||||
string? CommanderObjective,
|
ShipPlanSnapshot? ActivePlan,
|
||||||
|
string? CurrentStepId,
|
||||||
|
IReadOnlyList<ShipSubTaskSnapshot> ActiveSubTasks,
|
||||||
|
string ControlSourceKind,
|
||||||
|
string? ControlSourceId,
|
||||||
|
string? ControlReason,
|
||||||
|
string? LastReplanReason,
|
||||||
|
string? LastAccessFailureReason,
|
||||||
string? CelestialId,
|
string? CelestialId,
|
||||||
string? DockedStationId,
|
string? DockedStationId,
|
||||||
string? CommanderId,
|
string? CommanderId,
|
||||||
string? PolicySetId,
|
string? PolicySetId,
|
||||||
float CargoCapacity,
|
float CargoCapacity,
|
||||||
|
|
||||||
float TravelSpeed,
|
float TravelSpeed,
|
||||||
string TravelSpeedUnit,
|
string TravelSpeedUnit,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
IReadOnlyList<string> History,
|
IReadOnlyList<string> History,
|
||||||
ShipActionProgressSnapshot? CurrentAction,
|
|
||||||
ShipSpatialStateSnapshot SpatialState);
|
ShipSpatialStateSnapshot SpatialState);
|
||||||
|
|
||||||
public sealed record ShipActionProgressSnapshot(
|
|
||||||
string Label,
|
|
||||||
float Progress);
|
|
||||||
|
|
||||||
public sealed record ShipSpatialStateSnapshot(
|
public sealed record ShipSpatialStateSnapshot(
|
||||||
string SpaceLayer,
|
string SpaceLayer,
|
||||||
string CurrentSystemId,
|
string CurrentSystemId,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Runtime;
|
namespace SpaceGame.Api.Ships.Runtime;
|
||||||
|
|
||||||
public sealed class ShipRuntime
|
public sealed class ShipRuntime
|
||||||
@@ -12,56 +11,147 @@ public sealed class ShipRuntime
|
|||||||
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||||
public ShipState State { get; set; } = ShipState.Idle;
|
public ShipState State { get; set; } = ShipState.Idle;
|
||||||
public ShipOrderRuntime? Order { get; set; }
|
|
||||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
public List<ShipOrderRuntime> OrderQueue { get; } = [];
|
||||||
public float ActionTimer { get; set; }
|
public ShipPlanRuntime? ActivePlan { get; set; }
|
||||||
|
public required ShipSkillProfileRuntime Skills { get; set; }
|
||||||
|
public bool NeedsReplan { get; set; } = true;
|
||||||
|
public float ReplanCooldownSeconds { get; set; }
|
||||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public string? DockedStationId { get; set; }
|
public string? DockedStationId { get; set; }
|
||||||
public int? AssignedDockingPadIndex { get; set; }
|
public int? AssignedDockingPadIndex { get; set; }
|
||||||
public string? CommanderId { get; set; }
|
public string? CommanderId { get; set; }
|
||||||
public string? PolicySetId { get; set; }
|
public string? PolicySetId { get; set; }
|
||||||
|
public string ControlSourceKind { get; set; } = "unassigned";
|
||||||
|
public string? ControlSourceId { get; set; }
|
||||||
|
public string? ControlReason { get; set; }
|
||||||
|
public string? LastReplanReason { get; set; }
|
||||||
|
public string? LastAccessFailureReason { get; set; }
|
||||||
public float Health { get; set; }
|
public float Health { get; set; }
|
||||||
public string? TrackedActionKey { get; set; }
|
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
|
||||||
public float TrackedActionTotal { get; set; }
|
|
||||||
public List<string> History { get; } = [];
|
public List<string> History { get; } = [];
|
||||||
public string LastSignature { get; set; } = string.Empty;
|
public string LastSignature { get; set; } = string.Empty;
|
||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ShipSkillProfileRuntime
|
||||||
|
{
|
||||||
|
public int Navigation { get; set; }
|
||||||
|
public int Trade { get; set; }
|
||||||
|
public int Mining { get; set; }
|
||||||
|
public int Combat { get; set; }
|
||||||
|
public int Construction { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ShipOrderRuntime
|
public sealed class ShipOrderRuntime
|
||||||
{
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
public required string Kind { get; init; }
|
public required string Kind { get; init; }
|
||||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
public OrderStatus Status { get; set; } = OrderStatus.Queued;
|
||||||
public required string DestinationSystemId { get; init; }
|
public int Priority { get; set; }
|
||||||
public required Vector3 DestinationPosition { get; init; }
|
public bool InterruptCurrentPlan { get; set; } = true;
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public Vector3? TargetPosition { get; set; }
|
||||||
|
public string? SourceStationId { get; set; }
|
||||||
|
public string? DestinationStationId { get; set; }
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
|
public string? ConstructionSiteId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public float WaitSeconds { get; set; }
|
||||||
|
public float Radius { get; set; }
|
||||||
|
public int? MaxSystemRange { get; set; }
|
||||||
|
public bool KnownStationsOnly { get; set; }
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DefaultBehaviorRuntime
|
public sealed class DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
public required string Kind { get; set; }
|
public required string Kind { get; set; }
|
||||||
|
public string? HomeSystemId { get; set; }
|
||||||
|
public string? HomeStationId { get; set; }
|
||||||
public string? AreaSystemId { get; set; }
|
public string? AreaSystemId { get; set; }
|
||||||
public string? TargetEntityId { get; set; }
|
public string? TargetEntityId { get; set; }
|
||||||
public string? ItemId { get; set; }
|
public string? PreferredItemId { get; set; }
|
||||||
public string? StationId { get; set; }
|
public string? PreferredNodeId { get; set; }
|
||||||
public string? RefineryId { get; set; }
|
public string? PreferredConstructionSiteId { get; set; }
|
||||||
public string? NodeId { get; set; }
|
public string? PreferredModuleId { get; set; }
|
||||||
public string? ModuleId { get; set; }
|
public Vector3? TargetPosition { get; set; }
|
||||||
public string? Phase { get; set; }
|
public float WaitSeconds { get; set; } = 3f;
|
||||||
|
public float Radius { get; set; } = 24f;
|
||||||
|
public int MaxSystemRange { get; set; }
|
||||||
|
public bool KnownStationsOnly { get; set; }
|
||||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||||
public int PatrolIndex { get; set; }
|
public int PatrolIndex { get; set; }
|
||||||
|
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
|
||||||
|
public int RepeatIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ControllerTaskRuntime
|
public sealed class ShipOrderTemplateRuntime
|
||||||
{
|
{
|
||||||
public required ControllerTaskKind Kind { get; set; }
|
public required string Kind { get; init; }
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public Vector3? TargetPosition { get; set; }
|
||||||
|
public string? SourceStationId { get; set; }
|
||||||
|
public string? DestinationStationId { get; set; }
|
||||||
|
public string? ItemId { get; set; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
|
public string? ConstructionSiteId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public float WaitSeconds { get; set; }
|
||||||
|
public float Radius { get; set; }
|
||||||
|
public int? MaxSystemRange { get; set; }
|
||||||
|
public bool KnownStationsOnly { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShipPlanRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required AiPlanSourceKind SourceKind { get; init; }
|
||||||
|
public required string SourceId { get; init; }
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned;
|
||||||
|
public int CurrentStepIndex { get; set; }
|
||||||
|
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||||
|
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
public string? InterruptReason { get; set; }
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
public List<ShipPlanStepRuntime> Steps { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShipPlanStepRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
|
public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned;
|
||||||
|
public int CurrentSubTaskIndex { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
|
public List<ShipSubTaskRuntime> SubTasks { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShipSubTaskRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
public required string Summary { get; set; }
|
||||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||||
public string? CommanderId { get; set; }
|
|
||||||
public string? TargetEntityId { get; set; }
|
public string? TargetEntityId { get; set; }
|
||||||
public string? TargetSystemId { get; set; }
|
public string? TargetSystemId { get; set; }
|
||||||
public string? TargetNodeId { get; set; }
|
public string? TargetNodeId { get; set; }
|
||||||
public Vector3? TargetPosition { get; set; }
|
public Vector3? TargetPosition { get; set; }
|
||||||
public float Threshold { get; set; }
|
|
||||||
public string? ItemId { get; set; }
|
public string? ItemId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public float Threshold { get; set; }
|
||||||
|
public float Amount { get; set; }
|
||||||
|
public float ElapsedSeconds { get; set; }
|
||||||
|
public float TotalSeconds { get; set; }
|
||||||
|
public float Progress { get; set; }
|
||||||
|
public string? BlockingReason { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
2708
apps/backend/Ships/Simulation/ShipAiService.cs
Normal file
2708
apps/backend/Ships/Simulation/ShipAiService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,880 +0,0 @@
|
|||||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Simulation;
|
|
||||||
|
|
||||||
internal sealed class ShipControlService
|
|
||||||
{
|
|
||||||
private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
|
|
||||||
|
|
||||||
private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) =>
|
|
||||||
ship.CommanderId is null
|
|
||||||
? null
|
|
||||||
: world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId && candidate.Kind == CommanderKind.Ship);
|
|
||||||
|
|
||||||
private static void SyncCommanderToShip(ShipRuntime ship, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
if (commander.ActiveBehavior is not null)
|
|
||||||
{
|
|
||||||
ship.DefaultBehavior.Kind = commander.ActiveBehavior.Kind;
|
|
||||||
ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior.AreaSystemId;
|
|
||||||
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior.TargetEntityId;
|
|
||||||
ship.DefaultBehavior.ItemId = commander.ActiveBehavior.ItemId;
|
|
||||||
ship.DefaultBehavior.ModuleId = commander.ActiveBehavior.ModuleId;
|
|
||||||
ship.DefaultBehavior.NodeId = commander.ActiveBehavior.NodeId;
|
|
||||||
ship.DefaultBehavior.Phase = commander.ActiveBehavior.Phase;
|
|
||||||
ship.DefaultBehavior.PatrolIndex = commander.ActiveBehavior.PatrolIndex;
|
|
||||||
ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commander.ActiveOrder is null)
|
|
||||||
{
|
|
||||||
ship.Order = null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ship.Order = new ShipOrderRuntime
|
|
||||||
{
|
|
||||||
Kind = commander.ActiveOrder.Kind,
|
|
||||||
Status = commander.ActiveOrder.Status,
|
|
||||||
DestinationSystemId = commander.ActiveOrder.DestinationSystemId,
|
|
||||||
DestinationPosition = commander.ActiveOrder.DestinationPosition,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commander.ActiveTask is not null)
|
|
||||||
{
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ParseControllerTaskKind(commander.ActiveTask.Kind),
|
|
||||||
Status = commander.ActiveTask.Status,
|
|
||||||
CommanderId = commander.Id,
|
|
||||||
TargetEntityId = commander.ActiveTask.TargetEntityId,
|
|
||||||
TargetNodeId = commander.ActiveTask.TargetNodeId,
|
|
||||||
TargetPosition = commander.ActiveTask.TargetPosition,
|
|
||||||
TargetSystemId = commander.ActiveTask.TargetSystemId,
|
|
||||||
Threshold = commander.ActiveTask.Threshold,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SyncShipToCommander(ShipRuntime ship, CommanderRuntime commander)
|
|
||||||
{
|
|
||||||
commander.ActiveBehavior ??= new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind };
|
|
||||||
commander.ActiveBehavior.Kind = ship.DefaultBehavior.Kind;
|
|
||||||
commander.ActiveBehavior.AreaSystemId = ship.DefaultBehavior.AreaSystemId;
|
|
||||||
commander.ActiveBehavior.TargetEntityId = ship.DefaultBehavior.TargetEntityId;
|
|
||||||
commander.ActiveBehavior.ItemId = ship.DefaultBehavior.ItemId;
|
|
||||||
commander.ActiveBehavior.ModuleId = ship.DefaultBehavior.ModuleId;
|
|
||||||
commander.ActiveBehavior.NodeId = ship.DefaultBehavior.NodeId;
|
|
||||||
commander.ActiveBehavior.Phase = ship.DefaultBehavior.Phase;
|
|
||||||
commander.ActiveBehavior.PatrolIndex = ship.DefaultBehavior.PatrolIndex;
|
|
||||||
commander.ActiveBehavior.StationId = ship.DefaultBehavior.StationId;
|
|
||||||
|
|
||||||
if (ship.Order is null)
|
|
||||||
{
|
|
||||||
commander.ActiveOrder = null;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
commander.ActiveOrder ??= new CommanderOrderRuntime
|
|
||||||
{
|
|
||||||
Kind = ship.Order.Kind,
|
|
||||||
DestinationSystemId = ship.Order.DestinationSystemId,
|
|
||||||
DestinationPosition = ship.Order.DestinationPosition,
|
|
||||||
};
|
|
||||||
commander.ActiveOrder.Status = ship.Order.Status;
|
|
||||||
commander.ActiveOrder.TargetEntityId = ship.ControllerTask.TargetEntityId;
|
|
||||||
commander.ActiveOrder.DestinationNodeId = ship.ControllerTask.TargetNodeId ?? ship.SpatialState.DestinationNodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
commander.ActiveTask ??= new CommanderTaskRuntime { Kind = ship.ControllerTask.Kind.ToContractValue() };
|
|
||||||
commander.ActiveTask.Kind = ship.ControllerTask.Kind.ToContractValue();
|
|
||||||
commander.ActiveTask.Status = ship.ControllerTask.Status;
|
|
||||||
commander.ActiveTask.TargetEntityId = ship.ControllerTask.TargetEntityId;
|
|
||||||
commander.ActiveTask.TargetNodeId = ship.ControllerTask.TargetNodeId;
|
|
||||||
commander.ActiveTask.TargetPosition = ship.ControllerTask.TargetPosition;
|
|
||||||
commander.ActiveTask.TargetSystemId = ship.ControllerTask.TargetSystemId;
|
|
||||||
commander.ActiveTask.Threshold = ship.ControllerTask.Threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void RefreshControlLayers(ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
var commander = GetShipCommander(world, ship);
|
|
||||||
if (commander is not null)
|
|
||||||
{
|
|
||||||
SyncCommanderToShip(ship, commander);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ship.Order is not null && ship.Order.Status == OrderStatus.Queued)
|
|
||||||
{
|
|
||||||
ship.Order.Status = OrderStatus.Accepted;
|
|
||||||
if (commander?.ActiveOrder is not null)
|
|
||||||
{
|
|
||||||
commander.ActiveOrder.Status = ship.Order.Status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commander is not null)
|
|
||||||
{
|
|
||||||
SyncShipToCommander(ship, commander);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PlanControllerTask(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
var commander = GetShipCommander(world, ship);
|
|
||||||
if (ship.Order is not null)
|
|
||||||
{
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
Status = WorkStatus.Active,
|
|
||||||
CommanderId = commander?.Id,
|
|
||||||
TargetSystemId = ship.Order.DestinationSystemId,
|
|
||||||
TargetNodeId = ship.SpatialState.DestinationNodeId,
|
|
||||||
TargetPosition = ship.Order.DestinationPosition,
|
|
||||||
Threshold = world.Balance.ArrivalThreshold,
|
|
||||||
};
|
|
||||||
SyncCommanderTask(commander, ship.ControllerTask);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_shipBehaviorStateMachine.Plan(engine, ship, world);
|
|
||||||
SyncCommanderTask(commander, ship.ControllerTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PlanAttackTarget(ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
var behavior = ship.DefaultBehavior;
|
|
||||||
var target = ResolveAttackTarget(ship, world);
|
|
||||||
if (target is null)
|
|
||||||
{
|
|
||||||
behavior.Kind = "idle";
|
|
||||||
behavior.TargetEntityId = null;
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.TargetEntityId = target.EntityId;
|
|
||||||
behavior.AreaSystemId = target.SystemId;
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.AttackTarget,
|
|
||||||
TargetEntityId = target.EntityId,
|
|
||||||
TargetSystemId = target.SystemId,
|
|
||||||
TargetPosition = target.Position,
|
|
||||||
Threshold = target.AttackRange,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PlanTransportHaul(ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
var behavior = ship.DefaultBehavior;
|
|
||||||
var sourceStation = behavior.StationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId);
|
|
||||||
var destinationStation = behavior.TargetEntityId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.TargetEntityId);
|
|
||||||
if (sourceStation is null || destinationStation is null || string.IsNullOrWhiteSpace(behavior.ItemId))
|
|
||||||
{
|
|
||||||
behavior.Kind = "idle";
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var carryingCargo = GetShipCargoAmount(ship) > 0.01f;
|
|
||||||
if (carryingCargo)
|
|
||||||
{
|
|
||||||
if (ship.DockedStationId == destinationStation.Id)
|
|
||||||
{
|
|
||||||
behavior.Phase = "unload";
|
|
||||||
}
|
|
||||||
else if (ship.DockedStationId is not null)
|
|
||||||
{
|
|
||||||
behavior.Phase = "undock-from-source";
|
|
||||||
}
|
|
||||||
else if (behavior.Phase is not "travel-to-destination" and not "dock-destination" and not "unload")
|
|
||||||
{
|
|
||||||
behavior.Phase = "travel-to-destination";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (ship.DockedStationId == sourceStation.Id)
|
|
||||||
{
|
|
||||||
var available = GetInventoryAmount(sourceStation.Inventory, behavior.ItemId);
|
|
||||||
behavior.Phase = available > 0.01f ? "load" : "wait-source";
|
|
||||||
}
|
|
||||||
else if (ship.DockedStationId == destinationStation.Id)
|
|
||||||
{
|
|
||||||
behavior.Phase = "undock-from-destination";
|
|
||||||
}
|
|
||||||
else if (behavior.Phase is not "travel-to-source" and not "dock-source" and not "load")
|
|
||||||
{
|
|
||||||
behavior.Phase = "travel-to-source";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.ControllerTask = behavior.Phase switch
|
|
||||||
{
|
|
||||||
"travel-to-source" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = sourceStation.Id,
|
|
||||||
TargetSystemId = sourceStation.SystemId,
|
|
||||||
TargetPosition = sourceStation.Position,
|
|
||||||
Threshold = sourceStation.Radius + 8f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"dock-source" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Dock,
|
|
||||||
TargetEntityId = sourceStation.Id,
|
|
||||||
TargetSystemId = sourceStation.SystemId,
|
|
||||||
TargetPosition = sourceStation.Position,
|
|
||||||
Threshold = sourceStation.Radius + 4f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"load" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Load,
|
|
||||||
TargetEntityId = sourceStation.Id,
|
|
||||||
TargetSystemId = sourceStation.SystemId,
|
|
||||||
TargetPosition = sourceStation.Position,
|
|
||||||
Threshold = 0f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"undock-from-source" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Undock,
|
|
||||||
TargetEntityId = sourceStation.Id,
|
|
||||||
TargetSystemId = sourceStation.SystemId,
|
|
||||||
TargetPosition = new Vector3(sourceStation.Position.X + world.Balance.UndockDistance, sourceStation.Position.Y, sourceStation.Position.Z),
|
|
||||||
Threshold = 8f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"travel-to-destination" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = destinationStation.Id,
|
|
||||||
TargetSystemId = destinationStation.SystemId,
|
|
||||||
TargetPosition = destinationStation.Position,
|
|
||||||
Threshold = destinationStation.Radius + 8f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"dock-destination" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Dock,
|
|
||||||
TargetEntityId = destinationStation.Id,
|
|
||||||
TargetSystemId = destinationStation.SystemId,
|
|
||||||
TargetPosition = destinationStation.Position,
|
|
||||||
Threshold = destinationStation.Radius + 4f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"unload" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Unload,
|
|
||||||
TargetEntityId = destinationStation.Id,
|
|
||||||
TargetSystemId = destinationStation.SystemId,
|
|
||||||
TargetPosition = destinationStation.Position,
|
|
||||||
Threshold = 0f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
"undock-from-destination" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Undock,
|
|
||||||
TargetEntityId = destinationStation.Id,
|
|
||||||
TargetSystemId = destinationStation.SystemId,
|
|
||||||
TargetPosition = new Vector3(destinationStation.Position.X + world.Balance.UndockDistance, destinationStation.Position.Y, destinationStation.Position.Z),
|
|
||||||
Threshold = 8f,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
},
|
|
||||||
_ => CreateIdleTask(world.Balance.ArrivalThreshold),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule)
|
|
||||||
{
|
|
||||||
var behavior = ship.DefaultBehavior;
|
|
||||||
var cargoItemId = ship.Inventory.Keys.FirstOrDefault();
|
|
||||||
var targetResourceItemId = SelectMiningResourceItem(world, ship, cargoItemId ?? behavior.ItemId ?? resourceItemId);
|
|
||||||
if (string.IsNullOrWhiteSpace(targetResourceItemId))
|
|
||||||
{
|
|
||||||
behavior.Phase = null;
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(behavior.ItemId, targetResourceItemId, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
behavior.ItemId = targetResourceItemId;
|
|
||||||
behavior.NodeId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var refinery = SelectBestBuyStation(world, ship, targetResourceItemId, behavior.StationId);
|
|
||||||
behavior.StationId = refinery?.Id;
|
|
||||||
var node = behavior.NodeId is null
|
|
||||||
? world.Nodes
|
|
||||||
.Where(candidate =>
|
|
||||||
candidate.ItemId == targetResourceItemId &&
|
|
||||||
candidate.OreRemaining > 0.01f &&
|
|
||||||
CanShipMineItem(world, ship, candidate.ItemId))
|
|
||||||
.OrderByDescending(candidate => candidate.SystemId == behavior.AreaSystemId ? 1 : 0)
|
|
||||||
.ThenByDescending(candidate => candidate.OreRemaining)
|
|
||||||
.FirstOrDefault()
|
|
||||||
: world.Nodes.FirstOrDefault(candidate =>
|
|
||||||
candidate.Id == behavior.NodeId &&
|
|
||||||
string.Equals(candidate.ItemId, targetResourceItemId, StringComparison.Ordinal) &&
|
|
||||||
candidate.OreRemaining > 0.01f);
|
|
||||||
|
|
||||||
if (node is not null)
|
|
||||||
{
|
|
||||||
behavior.AreaSystemId = node.SystemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refinery is null || node is null || !HasShipCapabilities(ship.Definition, requiredModule))
|
|
||||||
{
|
|
||||||
if (refinery is null && GetShipCargoAmount(ship) > 0.01f)
|
|
||||||
{
|
|
||||||
ship.Inventory.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.Phase = null;
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.NodeId ??= node.Id;
|
|
||||||
|
|
||||||
if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|
|
||||||
&& behavior.Phase is "travel-to-node" or "extract")
|
|
||||||
{
|
|
||||||
behavior.Phase = "travel-to-station";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ship.DockedStationId == refinery.Id)
|
|
||||||
{
|
|
||||||
if (GetShipCargoAmount(ship) > 0.01f)
|
|
||||||
{
|
|
||||||
behavior.Phase = "unload";
|
|
||||||
}
|
|
||||||
else if (behavior.Phase is "dock" or "unload")
|
|
||||||
{
|
|
||||||
behavior.Phase = "undock";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract")
|
|
||||||
{
|
|
||||||
behavior.Phase = "travel-to-station";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (behavior.Phase)
|
|
||||||
{
|
|
||||||
case "extract":
|
|
||||||
var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f);
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Extract,
|
|
||||||
TargetEntityId = node.Id,
|
|
||||||
TargetSystemId = node.SystemId,
|
|
||||||
TargetPosition = extractionPosition,
|
|
||||||
Threshold = 5f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "travel-to-station":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = refinery.Id,
|
|
||||||
TargetSystemId = refinery.SystemId,
|
|
||||||
TargetPosition = refinery.Position,
|
|
||||||
Threshold = refinery.Radius + 8f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "dock":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Dock,
|
|
||||||
TargetEntityId = refinery.Id,
|
|
||||||
TargetSystemId = refinery.SystemId,
|
|
||||||
TargetPosition = refinery.Position,
|
|
||||||
Threshold = refinery.Radius + 4f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "unload":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Unload,
|
|
||||||
TargetEntityId = refinery.Id,
|
|
||||||
TargetSystemId = refinery.SystemId,
|
|
||||||
TargetPosition = refinery.Position,
|
|
||||||
Threshold = 0f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "undock":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Undock,
|
|
||||||
TargetEntityId = refinery.Id,
|
|
||||||
TargetSystemId = refinery.SystemId,
|
|
||||||
TargetPosition = new Vector3(refinery.Position.X + world.Balance.UndockDistance, refinery.Position.Y, refinery.Position.Z),
|
|
||||||
Threshold = 8f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = node.Id,
|
|
||||||
TargetSystemId = node.SystemId,
|
|
||||||
TargetPosition = node.Position,
|
|
||||||
Threshold = 18f,
|
|
||||||
};
|
|
||||||
behavior.Phase = "travel-to-node";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string? fallbackItemId)
|
|
||||||
{
|
|
||||||
var candidateItemId = world.MarketOrders
|
|
||||||
.Where(order =>
|
|
||||||
string.Equals(order.FactionId, ship.FactionId, StringComparison.Ordinal)
|
|
||||||
&& order.Kind == MarketOrderKinds.Buy
|
|
||||||
&& order.ConstructionSiteId is null
|
|
||||||
&& order.State != MarketOrderStateKinds.Cancelled
|
|
||||||
&& order.RemainingAmount > 0.01f)
|
|
||||||
.Select(order => new
|
|
||||||
{
|
|
||||||
ItemId = order.ItemId,
|
|
||||||
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
|
|
||||||
})
|
|
||||||
.Where(entry => CanShipMineItem(world, ship, entry.ItemId))
|
|
||||||
.Where(entry => world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
|
||||||
.GroupBy(entry => entry.ItemId, StringComparer.Ordinal)
|
|
||||||
.Select(group => new
|
|
||||||
{
|
|
||||||
ItemId = group.Key,
|
|
||||||
Score = group.Sum(entry => entry.Score) + (string.Equals(group.Key, ship.DefaultBehavior.ItemId, StringComparison.Ordinal) ? 15f : 0f),
|
|
||||||
})
|
|
||||||
.OrderByDescending(entry => entry.Score)
|
|
||||||
.Select(entry => entry.ItemId)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(candidateItemId))
|
|
||||||
{
|
|
||||||
return candidateItemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(fallbackItemId)
|
|
||||||
&& CanShipMineItem(world, ship, fallbackItemId)
|
|
||||||
&& world.Nodes.Any(node => string.Equals(node.ItemId, fallbackItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
|
||||||
{
|
|
||||||
return fallbackItemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return world.Nodes
|
|
||||||
.Where(node => node.OreRemaining > 0.01f && CanShipMineItem(world, ship, node.ItemId))
|
|
||||||
.OrderByDescending(node => node.OreRemaining)
|
|
||||||
.Select(node => node.ItemId)
|
|
||||||
.FirstOrDefault() ?? fallbackItemId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanShipMineItem(SimulationWorld world, ShipRuntime ship, string itemId) =>
|
|
||||||
world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)
|
|
||||||
&& string.Equals(itemDefinition.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal)
|
|
||||||
&& HasShipCapabilities(ship.Definition, "mining");
|
|
||||||
|
|
||||||
internal static StationRuntime? SelectBestBuyStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId)
|
|
||||||
{
|
|
||||||
var preferred = preferredStationId is null
|
|
||||||
? null
|
|
||||||
: world.Stations.FirstOrDefault(station => station.Id == preferredStationId);
|
|
||||||
|
|
||||||
var bestOrder = world.MarketOrders
|
|
||||||
.Where(order =>
|
|
||||||
order.Kind == MarketOrderKinds.Buy &&
|
|
||||||
order.ConstructionSiteId is null &&
|
|
||||||
order.State != MarketOrderStateKinds.Cancelled &&
|
|
||||||
order.ItemId == itemId &&
|
|
||||||
order.RemainingAmount > 0.01f)
|
|
||||||
.Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId)))
|
|
||||||
.Where(entry => entry.Station is not null && string.Equals(entry.Station.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
||||||
.Where(entry => CanStationReceiveItem(world, entry.Station!, itemId))
|
|
||||||
.OrderByDescending(entry =>
|
|
||||||
{
|
|
||||||
var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f;
|
|
||||||
return entry.Order.Valuation - distancePenalty;
|
|
||||||
})
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
return bestOrder.Station ?? (preferred is not null && CanStationReceiveItem(world, preferred, itemId) ? preferred : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanStationReceiveItem(SimulationWorld world, StationRuntime station, string itemId)
|
|
||||||
{
|
|
||||||
if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
|
|
||||||
return requiredModule is null || station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) =>
|
|
||||||
phase switch
|
|
||||||
{
|
|
||||||
"dock" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Dock,
|
|
||||||
TargetEntityId = station.Id,
|
|
||||||
TargetSystemId = station.SystemId,
|
|
||||||
TargetPosition = station.Position,
|
|
||||||
Threshold = 8f,
|
|
||||||
},
|
|
||||||
"load" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Load,
|
|
||||||
TargetEntityId = station.Id,
|
|
||||||
TargetSystemId = station.SystemId,
|
|
||||||
TargetPosition = station.Position,
|
|
||||||
Threshold = 8f,
|
|
||||||
},
|
|
||||||
"unload" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Unload,
|
|
||||||
TargetEntityId = station.Id,
|
|
||||||
TargetSystemId = station.SystemId,
|
|
||||||
TargetPosition = station.Position,
|
|
||||||
Threshold = 8f,
|
|
||||||
},
|
|
||||||
"undock" => new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Undock,
|
|
||||||
TargetEntityId = station.Id,
|
|
||||||
TargetSystemId = station.SystemId,
|
|
||||||
TargetPosition = new Vector3(station.Position.X + world.Balance.UndockDistance, station.Position.Y, station.Position.Z),
|
|
||||||
Threshold = 8f,
|
|
||||||
},
|
|
||||||
_ => CreateIdleTask(world.Balance.ArrivalThreshold),
|
|
||||||
};
|
|
||||||
|
|
||||||
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
var behavior = ship.DefaultBehavior;
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId);
|
|
||||||
var site = !string.IsNullOrWhiteSpace(behavior.TargetEntityId)
|
|
||||||
? world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == behavior.TargetEntityId)
|
|
||||||
: station is null ? null : GetConstructionSiteForStation(world, station.Id);
|
|
||||||
if (station is null)
|
|
||||||
{
|
|
||||||
behavior.Kind = "idle";
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site is null && !string.IsNullOrWhiteSpace(behavior.TargetEntityId))
|
|
||||||
{
|
|
||||||
behavior.TargetEntityId = null;
|
|
||||||
behavior.ModuleId = null;
|
|
||||||
site = GetConstructionSiteForStation(world, station.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world);
|
|
||||||
behavior.ModuleId = moduleId;
|
|
||||||
if (moduleId is null)
|
|
||||||
{
|
|
||||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ship.DockedStationId is not null)
|
|
||||||
{
|
|
||||||
var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
||||||
if (dockedStation is not null)
|
|
||||||
{
|
|
||||||
dockedStation.DockedShipIds.Remove(ship.Id);
|
|
||||||
ReleaseDockingPad(dockedStation, ship.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DockedStationId = null;
|
|
||||||
ship.AssignedDockingPadIndex = null;
|
|
||||||
ship.Position = ResolveConstructionHoldPosition(ship, station, site, world);
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
}
|
|
||||||
|
|
||||||
var constructionHoldPosition = ResolveConstructionHoldPosition(ship, station, site, world);
|
|
||||||
var targetSystemId = site?.SystemId ?? station.SystemId;
|
|
||||||
var targetCelestialId = site?.CelestialId ?? station.CelestialId;
|
|
||||||
var isAtTargetCelestial = !string.IsNullOrWhiteSpace(targetCelestialId)
|
|
||||||
&& string.Equals(ship.SpatialState.CurrentCelestialId, targetCelestialId, StringComparison.Ordinal);
|
|
||||||
var isAtConstructionHold = ship.SystemId == targetSystemId
|
|
||||||
&& (ship.Position.DistanceTo(constructionHoldPosition) <= 10f || isAtTargetCelestial);
|
|
||||||
|
|
||||||
if (isAtConstructionHold)
|
|
||||||
{
|
|
||||||
if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
|
|
||||||
{
|
|
||||||
behavior.Phase = "deliver-to-site";
|
|
||||||
}
|
|
||||||
else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site))
|
|
||||||
{
|
|
||||||
behavior.Phase = "build-site";
|
|
||||||
}
|
|
||||||
else if (site is not null)
|
|
||||||
{
|
|
||||||
behavior.Phase = "wait-for-materials";
|
|
||||||
}
|
|
||||||
else if (CanStartModuleConstruction(station, world.ModuleRecipes[moduleId]))
|
|
||||||
{
|
|
||||||
behavior.Phase = "construct-module";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
behavior.Phase = "wait-for-materials";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (behavior.Phase != "travel-to-station")
|
|
||||||
{
|
|
||||||
behavior.Phase = "travel-to-station";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (behavior.Phase)
|
|
||||||
{
|
|
||||||
case "construct-module":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.ConstructModule,
|
|
||||||
TargetEntityId = station.Id,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = constructionHoldPosition,
|
|
||||||
Threshold = 10f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "deliver-to-site":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.DeliverConstruction,
|
|
||||||
TargetEntityId = site?.Id,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = constructionHoldPosition,
|
|
||||||
Threshold = 10f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "build-site":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.BuildConstructionSite,
|
|
||||||
TargetEntityId = site?.Id,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = constructionHoldPosition,
|
|
||||||
Threshold = 10f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "wait-for-materials":
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Idle,
|
|
||||||
TargetEntityId = site?.Id ?? station.Id,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = constructionHoldPosition,
|
|
||||||
Threshold = 0f,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ship.ControllerTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = site?.Id ?? station.Id,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = constructionHoldPosition,
|
|
||||||
Threshold = 10f,
|
|
||||||
};
|
|
||||||
behavior.Phase = "travel-to-station";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AdvanceControlState(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
|
||||||
{
|
|
||||||
var commander = GetShipCommander(world, ship);
|
|
||||||
if (ship.Order is not null && controllerEvent == "arrived")
|
|
||||||
{
|
|
||||||
ship.Order = null;
|
|
||||||
ship.ControllerTask.Kind = ControllerTaskKind.Idle;
|
|
||||||
if (commander is not null)
|
|
||||||
{
|
|
||||||
commander.ActiveOrder = null;
|
|
||||||
commander.ActiveTask = new CommanderTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ShipTaskKinds.Idle,
|
|
||||||
Status = WorkStatus.Completed,
|
|
||||||
TargetSystemId = ship.SystemId,
|
|
||||||
Threshold = 0f,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_shipBehaviorStateMachine.ApplyEvent(engine, ship, world, controllerEvent);
|
|
||||||
if (commander is not null)
|
|
||||||
{
|
|
||||||
SyncShipToCommander(ship, commander);
|
|
||||||
if (commander.ActiveTask is not null)
|
|
||||||
{
|
|
||||||
commander.ActiveTask.Status = controllerEvent == "none" ? WorkStatus.Active : WorkStatus.Completed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void TrackHistory(ShipRuntime ship, string controllerEvent)
|
|
||||||
{
|
|
||||||
var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}";
|
|
||||||
if (signature == ship.LastSignature)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.LastSignature = signature;
|
|
||||||
var target = ship.ControllerTask.TargetEntityId
|
|
||||||
?? ship.ControllerTask.TargetSystemId
|
|
||||||
?? "none";
|
|
||||||
var eventSummary = controllerEvent == "none" ? string.Empty : $" event={controllerEvent}";
|
|
||||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind.ToContractValue()} target={target} cargo={GetShipCargoAmount(ship):0.#}{eventSummary}");
|
|
||||||
if (ship.History.Count > 18)
|
|
||||||
{
|
|
||||||
ship.History.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void EmitShipStateEvents(
|
|
||||||
ShipRuntime ship,
|
|
||||||
ShipState previousState,
|
|
||||||
string previousBehavior,
|
|
||||||
ControllerTaskKind previousTask,
|
|
||||||
string controllerEvent,
|
|
||||||
ICollection<SimulationEventRecord> events)
|
|
||||||
{
|
|
||||||
var occurredAtUtc = DateTimeOffset.UtcNow;
|
|
||||||
|
|
||||||
if (previousState != ship.State)
|
|
||||||
{
|
|
||||||
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousBehavior != ship.DefaultBehavior.Kind)
|
|
||||||
{
|
|
||||||
events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousTask != ship.ControllerTask.Kind)
|
|
||||||
{
|
|
||||||
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controllerEvent != "none")
|
|
||||||
{
|
|
||||||
events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static ControllerTaskRuntime CreateIdleTask(float threshold) =>
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Idle,
|
|
||||||
Threshold = threshold,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static ControllerTaskKind ParseControllerTaskKind(string kind) => kind switch
|
|
||||||
{
|
|
||||||
"travel" => ControllerTaskKind.Travel,
|
|
||||||
"extract" => ControllerTaskKind.Extract,
|
|
||||||
"dock" => ControllerTaskKind.Dock,
|
|
||||||
"load" => ControllerTaskKind.Load,
|
|
||||||
"unload" => ControllerTaskKind.Unload,
|
|
||||||
"deliver-construction" => ControllerTaskKind.DeliverConstruction,
|
|
||||||
"build-construction-site" => ControllerTaskKind.BuildConstructionSite,
|
|
||||||
"attack-target" => ControllerTaskKind.AttackTarget,
|
|
||||||
|
|
||||||
"construct-module" => ControllerTaskKind.ConstructModule,
|
|
||||||
"undock" => ControllerTaskKind.Undock,
|
|
||||||
_ => ControllerTaskKind.Idle,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task)
|
|
||||||
{
|
|
||||||
if (commander is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commander.ActiveTask = new CommanderTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = task.Kind.ToContractValue(),
|
|
||||||
Status = task.Status,
|
|
||||||
TargetEntityId = task.TargetEntityId,
|
|
||||||
TargetNodeId = task.TargetNodeId,
|
|
||||||
TargetPosition = task.TargetPosition,
|
|
||||||
TargetSystemId = task.TargetSystemId,
|
|
||||||
Threshold = task.Threshold,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3 ResolveConstructionHoldPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world)
|
|
||||||
{
|
|
||||||
if (site is null || site.StationId is not null)
|
|
||||||
{
|
|
||||||
return GetConstructionHoldPosition(station, ship.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
|
|
||||||
var anchorPosition = anchor?.Position ?? station.Position;
|
|
||||||
return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AttackTargetCandidate? ResolveAttackTarget(ShipRuntime ship, SimulationWorld world)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(ship.DefaultBehavior.TargetEntityId))
|
|
||||||
{
|
|
||||||
var direct = ResolveAttackTargetCandidate(world, ship.DefaultBehavior.TargetEntityId!);
|
|
||||||
if (direct is not null && !string.Equals(direct.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return direct;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostileShips = world.Ships
|
|
||||||
.Where(candidate => candidate.Health > 0f && !string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
||||||
.Select(candidate => new AttackTargetCandidate(candidate.Id, candidate.FactionId, candidate.SystemId, candidate.Position, 26f))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var hostileStations = world.Stations
|
|
||||||
.Where(candidate => !string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
||||||
.Select(candidate => new AttackTargetCandidate(candidate.Id, candidate.FactionId, candidate.SystemId, candidate.Position, candidate.Radius + 18f))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var preferredSystemId = ship.DefaultBehavior.AreaSystemId;
|
|
||||||
return hostileShips
|
|
||||||
.Concat(hostileStations)
|
|
||||||
.OrderBy(candidate => preferredSystemId is null || candidate.SystemId == preferredSystemId ? 0 : 1)
|
|
||||||
.ThenBy(candidate => candidate.SystemId == ship.SystemId ? 0 : 1)
|
|
||||||
.ThenBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AttackTargetCandidate? ResolveAttackTargetCandidate(SimulationWorld world, string entityId)
|
|
||||||
{
|
|
||||||
var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == entityId && candidate.Health > 0f);
|
|
||||||
if (ship is not null)
|
|
||||||
{
|
|
||||||
return new AttackTargetCandidate(ship.Id, ship.FactionId, ship.SystemId, ship.Position, 26f);
|
|
||||||
}
|
|
||||||
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == entityId);
|
|
||||||
return station is null
|
|
||||||
? null
|
|
||||||
: new AttackTargetCandidate(station.Id, station.FactionId, station.SystemId, station.Position, station.Radius + 18f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record AttackTargetCandidate(string EntityId, string FactionId, string SystemId, Vector3 Position, float AttackRange);
|
|
||||||
}
|
|
||||||
@@ -1,592 +0,0 @@
|
|||||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
||||||
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
|
||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Simulation;
|
|
||||||
|
|
||||||
internal sealed partial class ShipTaskExecutionService
|
|
||||||
{
|
|
||||||
private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds)
|
|
||||||
{
|
|
||||||
ship.ActionTimer += deltaSeconds;
|
|
||||||
if (ship.ActionTimer < requiredSeconds)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total)
|
|
||||||
{
|
|
||||||
if (ship.TrackedActionKey == actionKey)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TrackedActionKey = actionKey;
|
|
||||||
ship.TrackedActionTotal = MathF.Max(total, 0.01f);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static float GetShipCargoAmount(ShipRuntime ship) =>
|
|
||||||
SimulationRuntimeSupport.GetShipCargoAmount(ship);
|
|
||||||
|
|
||||||
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node, world))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var cargoAmount = GetShipCargoAmount(ship);
|
|
||||||
if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.CargoFull;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "cargo-full";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = task.TargetPosition.Value;
|
|
||||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
|
||||||
if (distance > task.Threshold)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
|
|
||||||
ship.State = ShipState.MiningApproach;
|
|
||||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.Mining;
|
|
||||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds))
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount);
|
|
||||||
var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity);
|
|
||||||
mined = MathF.Min(mined, node.OreRemaining);
|
|
||||||
if (mined <= 0.01f)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full";
|
|
||||||
}
|
|
||||||
|
|
||||||
AddInventory(ship.Inventory, node.ItemId, mined);
|
|
||||||
|
|
||||||
node.OreRemaining -= mined;
|
|
||||||
node.OreRemaining = MathF.Max(0f, node.OreRemaining);
|
|
||||||
|
|
||||||
return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (station is null || task.TargetPosition is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id);
|
|
||||||
if (padIndex is null)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.AwaitingDock;
|
|
||||||
ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
|
|
||||||
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
|
|
||||||
if (waitDistance > 4f)
|
|
||||||
{
|
|
||||||
ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.AssignedDockingPadIndex = padIndex;
|
|
||||||
var padPosition = GetDockingPadPosition(station, padIndex.Value);
|
|
||||||
ship.TargetPosition = padPosition;
|
|
||||||
var distance = ship.Position.DistanceTo(padPosition);
|
|
||||||
if (distance > 4f)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
|
|
||||||
ship.State = ShipState.DockingApproach;
|
|
||||||
ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.Docking;
|
|
||||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration))
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.Docked;
|
|
||||||
ship.DockedStationId = station.Id;
|
|
||||||
station.DockedShipIds.Add(ship.Id);
|
|
||||||
ship.Position = padPosition;
|
|
||||||
ship.TargetPosition = padPosition;
|
|
||||||
return "docked";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
if (ship.DockedStationId is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
||||||
if (station is null)
|
|
||||||
{
|
|
||||||
ship.DockedStationId = null;
|
|
||||||
ship.AssignedDockingPadIndex = null;
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = GetShipDockedPosition(ship, station);
|
|
||||||
ship.Position = ship.TargetPosition;
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.Transferring;
|
|
||||||
BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship));
|
|
||||||
|
|
||||||
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId);
|
|
||||||
var transferredAny = false;
|
|
||||||
foreach (var (itemId, amount) in ship.Inventory.ToList())
|
|
||||||
{
|
|
||||||
var moved = MathF.Min(amount, world.Balance.TransferRate * deltaSeconds);
|
|
||||||
var accepted = TryAddStationInventory(world, station, itemId, moved);
|
|
||||||
transferredAny |= accepted > 0.01f;
|
|
||||||
RemoveInventory(ship.Inventory, itemId, accepted);
|
|
||||||
if (faction is not null && string.Equals(itemId, "ore", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
faction.OreMined += accepted;
|
|
||||||
faction.Credits += accepted * 0.4f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transferredAny && GetShipCargoAmount(ship) > 0.01f && HasShipCapabilities(ship.Definition, "mining"))
|
|
||||||
{
|
|
||||||
ship.Inventory.Clear();
|
|
||||||
return "unloaded";
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetShipCargoAmount(ship) <= 0.01f ? "unloaded" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
if (ship.DockedStationId is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
||||||
if (station is null)
|
|
||||||
{
|
|
||||||
ship.DockedStationId = null;
|
|
||||||
ship.AssignedDockingPadIndex = null;
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = GetShipDockedPosition(ship, station);
|
|
||||||
ship.Position = ship.TargetPosition;
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.Loading;
|
|
||||||
var itemId = ship.ControllerTask.ItemId;
|
|
||||||
BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)));
|
|
||||||
var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
|
|
||||||
var moved = itemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, itemId));
|
|
||||||
if (itemId is not null && moved > 0.01f)
|
|
||||||
{
|
|
||||||
RemoveInventory(station.Inventory, itemId, moved);
|
|
||||||
AddInventory(ship.Inventory, itemId, moved);
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemId is null
|
|
||||||
|| GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|
|
||||||
|| GetInventoryAmount(station.Inventory, itemId) <= 0.01f
|
|
||||||
? "loaded"
|
|
||||||
: "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var station = ResolveShipSupportStation(ship, world);
|
|
||||||
if (station is null || ship.DefaultBehavior.ModuleId is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe))
|
|
||||||
{
|
|
||||||
ship.AssignedDockingPadIndex = null;
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportPosition = ResolveShipSupportPosition(ship, station, null, world);
|
|
||||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.LocalFlight;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id))
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.WaitingMaterials;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.ConstructionBlocked;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.TargetPosition;
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.Constructing;
|
|
||||||
station.ActiveConstruction.ProgressSeconds += deltaSeconds;
|
|
||||||
if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds)
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
AddStationModule(world, station, station.ActiveConstruction.ModuleId);
|
|
||||||
station.ActiveConstruction = null;
|
|
||||||
return "module-constructed";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var station = ResolveShipSupportStation(ship, world);
|
|
||||||
if (station is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
|
|
||||||
if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportPosition = ResolveShipSupportPosition(ship, station, site, world);
|
|
||||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.LocalFlight;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.TargetPosition;
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.DeliveringConstruction;
|
|
||||||
BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site));
|
|
||||||
|
|
||||||
if (site.StationId is not null)
|
|
||||||
{
|
|
||||||
foreach (var required in site.RequiredItems)
|
|
||||||
{
|
|
||||||
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
|
|
||||||
var remaining = MathF.Max(0f, required.Value - delivered);
|
|
||||||
if (remaining <= 0.01f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
|
||||||
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
|
||||||
moved = MathF.Min(moved, available);
|
|
||||||
if (moved <= 0.01f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveInventory(station.Inventory, required.Key, moved);
|
|
||||||
AddInventory(site.Inventory, required.Key, moved);
|
|
||||||
AddInventory(site.DeliveredItems, required.Key, moved);
|
|
||||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var required in site.RequiredItems)
|
|
||||||
{
|
|
||||||
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
|
|
||||||
var remaining = MathF.Max(0f, required.Value - delivered);
|
|
||||||
if (remaining <= 0.01f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
|
||||||
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
|
||||||
moved = MathF.Min(moved, available);
|
|
||||||
if (moved <= 0.01f)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveInventory(station.Inventory, required.Key, moved);
|
|
||||||
AddInventory(site.Inventory, required.Key, moved);
|
|
||||||
AddInventory(site.DeliveredItems, required.Key, moved);
|
|
||||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var station = ResolveShipSupportStation(ship, world);
|
|
||||||
if (station is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
|
|
||||||
if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportPosition = ResolveShipSupportPosition(ship, station, site, world);
|
|
||||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.LocalFlight;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.WaitingMaterials;
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.TargetPosition = supportPosition;
|
|
||||||
ship.Position = ship.TargetPosition;
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.Constructing;
|
|
||||||
site.AssignedConstructorShipIds.Add(ship.Id);
|
|
||||||
site.Progress += deltaSeconds;
|
|
||||||
if (site.Progress < recipe.Duration)
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site.StationId is null)
|
|
||||||
{
|
|
||||||
CompleteStationFoundation(world, station, site);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AddStationModule(world, station, site.BlueprintId);
|
|
||||||
PrepareNextConstructionSiteStep(world, station, site);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "site-constructed";
|
|
||||||
}
|
|
||||||
|
|
||||||
private StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
ship.DockedStationId is not null
|
|
||||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId)
|
|
||||||
: ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null
|
|
||||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world)
|
|
||||||
{
|
|
||||||
if (ship.DockedStationId is not null)
|
|
||||||
{
|
|
||||||
return GetShipDockedPosition(ship, station);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site?.StationId is null && site is not null)
|
|
||||||
{
|
|
||||||
var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position;
|
|
||||||
return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetConstructionHoldPosition(station, ship.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) =>
|
|
||||||
ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f);
|
|
||||||
|
|
||||||
|
|
||||||
private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
if (ship.DockedStationId is null || task.TargetPosition is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
|
||||||
var undockTarget = station is null
|
|
||||||
? task.TargetPosition.Value
|
|
||||||
: GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
|
||||||
ship.TargetPosition = undockTarget;
|
|
||||||
|
|
||||||
ship.State = ShipState.Undocking;
|
|
||||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration))
|
|
||||||
{
|
|
||||||
if (station is not null)
|
|
||||||
{
|
|
||||||
ship.Position = GetShipDockedPosition(ship, station);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance);
|
|
||||||
if (ship.Position.DistanceTo(undockTarget) > task.Threshold)
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station is not null)
|
|
||||||
{
|
|
||||||
station.DockedShipIds.Remove(ship.Id);
|
|
||||||
ReleaseDockingPad(station, ship.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.DockedStationId = null;
|
|
||||||
ship.AssignedDockingPadIndex = null;
|
|
||||||
return "undocked";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) =>
|
|
||||||
site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key)));
|
|
||||||
|
|
||||||
private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site)
|
|
||||||
{
|
|
||||||
var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
|
|
||||||
if (anchor is null || site.BlueprintId is null)
|
|
||||||
{
|
|
||||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var station = new StationRuntime
|
|
||||||
{
|
|
||||||
Id = $"station-{world.Stations.Count + 1}",
|
|
||||||
SystemId = site.SystemId,
|
|
||||||
Label = BuildFoundedStationLabel(site.TargetDefinitionId),
|
|
||||||
Category = "station",
|
|
||||||
Objective = DetermineFoundationObjective(site.TargetDefinitionId),
|
|
||||||
Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color,
|
|
||||||
Position = anchor.Position,
|
|
||||||
FactionId = site.FactionId,
|
|
||||||
CelestialId = site.CelestialId,
|
|
||||||
Health = 600f,
|
|
||||||
MaxHealth = 600f,
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var moduleId in GetFoundationModules(world, site.BlueprintId))
|
|
||||||
{
|
|
||||||
AddStationModule(world, station, moduleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
world.Stations.Add(station);
|
|
||||||
StationLifecycleService.EnsureStationCommander(world, station);
|
|
||||||
anchor.OccupyingStructureId = station.Id;
|
|
||||||
site.StationId = station.Id;
|
|
||||||
PrepareNextConstructionSiteStep(world, station, site);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<string> GetFoundationModules(SimulationWorld world, string primaryModuleId)
|
|
||||||
{
|
|
||||||
var modules = new List<string> { "module_arg_dock_m_01_lowtech" };
|
|
||||||
foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, []))
|
|
||||||
{
|
|
||||||
if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
|
||||||
{
|
|
||||||
var storageModule = GetStorageRequirement(itemDefinition.CargoKind);
|
|
||||||
if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
modules.Add(storageModule);
|
|
||||||
}
|
|
||||||
else if (storageModule is null && !modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
modules.Add("module_arg_stor_container_m_01");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
modules.Add("module_arg_stor_container_m_01");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
modules.Add("module_gen_prod_energycells_01");
|
|
||||||
}
|
|
||||||
|
|
||||||
modules.Add(primaryModuleId);
|
|
||||||
return modules.Distinct(StringComparer.Ordinal).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DetermineFoundationObjective(string commodityId) =>
|
|
||||||
commodityId switch
|
|
||||||
{
|
|
||||||
"energycells" => "power",
|
|
||||||
"water" => "water",
|
|
||||||
"refinedmetals" => "refinery",
|
|
||||||
"hullparts" => "hullparts",
|
|
||||||
"claytronics" => "claytronics",
|
|
||||||
"shipyard" => "shipyard",
|
|
||||||
_ => "general",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string BuildFoundedStationLabel(string commodityId) =>
|
|
||||||
$"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry";
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
|
||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Simulation;
|
|
||||||
|
|
||||||
internal sealed partial class ShipTaskExecutionService
|
|
||||||
{
|
|
||||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
|
||||||
private const float FrigateDps = 7f;
|
|
||||||
private const float DestroyerDps = 12f;
|
|
||||||
private const float CruiserDps = 18f;
|
|
||||||
private const float CapitalDps = 26f;
|
|
||||||
|
|
||||||
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
|
||||||
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
|
||||||
|
|
||||||
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
|
||||||
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
|
||||||
|
|
||||||
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
|
||||||
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
|
||||||
?? Vector3.Zero;
|
|
||||||
|
|
||||||
internal string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
return task.Kind switch
|
|
||||||
{
|
|
||||||
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.AttackTarget => UpdateAttackTarget(ship, world, deltaSeconds),
|
|
||||||
|
|
||||||
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
|
|
||||||
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
|
|
||||||
_ => UpdateIdle(ship, world, deltaSeconds),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
return UpdateTravel(ship, world, deltaSeconds, task);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ControllerTaskRuntime task)
|
|
||||||
{
|
|
||||||
if (task.TargetPosition is null || task.TargetSystemId is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve live position each frame — entities like stations orbit celestials and move every tick
|
|
||||||
var targetPosition = ResolveCurrentTargetPosition(world, task);
|
|
||||||
var targetCelestial = ResolveTravelTargetCelestial(world, task, targetPosition);
|
|
||||||
var distance = ship.Position.DistanceTo(targetPosition);
|
|
||||||
ship.TargetPosition = targetPosition;
|
|
||||||
|
|
||||||
if (ship.SystemId != task.TargetSystemId)
|
|
||||||
{
|
|
||||||
if (!HasShipCapabilities(ship.Definition, "ftl"))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, task.TargetSystemId);
|
|
||||||
var destinationEntryPosition = destinationEntryCelestial?.Position ?? Vector3.Zero;
|
|
||||||
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryCelestial);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentCelestial = ResolveCurrentCelestial(world, ship);
|
|
||||||
if (targetCelestial is not null && currentCelestial is not null && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (!HasShipCapabilities(ship.Definition, "warp"))
|
|
||||||
{
|
|
||||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetCelestial is not null
|
|
||||||
&& distance > WarpEngageDistanceKilometers
|
|
||||||
&& HasShipCapabilities(ship.Definition, "warp"))
|
|
||||||
{
|
|
||||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
|
||||||
}
|
|
||||||
|
|
||||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateAttackTarget(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
|
||||||
{
|
|
||||||
var task = ship.ControllerTask;
|
|
||||||
if (string.IsNullOrWhiteSpace(task.TargetEntityId))
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "target-lost";
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId && candidate.Health > 0f);
|
|
||||||
var hostileStation = hostileShip is null
|
|
||||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if ((hostileShip is not null && string.Equals(hostileShip.FactionId, ship.FactionId, StringComparison.Ordinal))
|
|
||||||
|| (hostileStation is not null && string.Equals(hostileStation.FactionId, ship.FactionId, StringComparison.Ordinal)))
|
|
||||||
{
|
|
||||||
return "target-lost";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostileShip is null && hostileStation is null)
|
|
||||||
{
|
|
||||||
ship.State = ShipState.Idle;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
return "target-lost";
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId;
|
|
||||||
var targetPosition = hostileShip?.Position ?? hostileStation!.Position;
|
|
||||||
var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f;
|
|
||||||
var attackTask = new ControllerTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ControllerTaskKind.Travel,
|
|
||||||
TargetEntityId = task.TargetEntityId,
|
|
||||||
TargetSystemId = targetSystemId,
|
|
||||||
TargetPosition = targetPosition,
|
|
||||||
Threshold = attackRange,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange)
|
|
||||||
{
|
|
||||||
return UpdateTravel(ship, world, deltaSeconds, attackTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.EngagingTarget;
|
|
||||||
ship.TargetPosition = targetPosition;
|
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f));
|
|
||||||
var damage = GetShipDamagePerSecond(ship) * deltaSeconds;
|
|
||||||
|
|
||||||
if (hostileShip is not null)
|
|
||||||
{
|
|
||||||
hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage);
|
|
||||||
return hostileShip.Health <= 0f ? "target-destroyed" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
hostileStation!.Health = MathF.Max(0f, hostileStation.Health - damage * 0.6f);
|
|
||||||
return hostileStation.Health <= 0f ? "target-destroyed" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ControllerTaskRuntime task)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
|
||||||
{
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (station is not null)
|
|
||||||
{
|
|
||||||
return station.Position;
|
|
||||||
}
|
|
||||||
|
|
||||||
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (celestial is not null)
|
|
||||||
{
|
|
||||||
return celestial.Position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return task.TargetPosition!.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
|
||||||
{
|
|
||||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (station?.CelestialId is not null)
|
|
||||||
{
|
|
||||||
return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
|
||||||
if (celestial is not null)
|
|
||||||
{
|
|
||||||
return celestial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return world.Celestials
|
|
||||||
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
|
|
||||||
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
|
|
||||||
{
|
|
||||||
if (ship.SpatialState.CurrentCelestialId is not null)
|
|
||||||
{
|
|
||||||
return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return world.Celestials
|
|
||||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
|
||||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
|
|
||||||
world.Celestials.FirstOrDefault(candidate =>
|
|
||||||
candidate.SystemId == systemId &&
|
|
||||||
candidate.Kind == SpatialNodeKind.Star);
|
|
||||||
|
|
||||||
private string UpdateLocalTravel(
|
|
||||||
ShipRuntime ship,
|
|
||||||
SimulationWorld world,
|
|
||||||
float deltaSeconds,
|
|
||||||
string targetSystemId,
|
|
||||||
Vector3 targetPosition,
|
|
||||||
CelestialRuntime? targetCelestial,
|
|
||||||
float threshold)
|
|
||||||
{
|
|
||||||
var distance = ship.Position.DistanceTo(targetPosition);
|
|
||||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
||||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
||||||
ship.SpatialState.Transit = null;
|
|
||||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
||||||
|
|
||||||
if (distance <= threshold)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.Position = targetPosition;
|
|
||||||
ship.TargetPosition = ship.Position;
|
|
||||||
ship.SystemId = targetSystemId;
|
|
||||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
|
||||||
ship.State = ShipState.Arriving;
|
|
||||||
return "arrived";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.State = ShipState.LocalFlight;
|
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, CelestialRuntime targetCelestial)
|
|
||||||
{
|
|
||||||
var transit = ship.SpatialState.Transit;
|
|
||||||
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id)
|
|
||||||
{
|
|
||||||
transit = new ShipTransitRuntime
|
|
||||||
{
|
|
||||||
Regime = MovementRegimeKinds.Warp,
|
|
||||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
|
||||||
DestinationNodeId = targetCelestial.Id,
|
|
||||||
StartedAtUtc = world.GeneratedAtUtc,
|
|
||||||
};
|
|
||||||
ship.SpatialState.Transit = transit;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
|
|
||||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
|
|
||||||
ship.SpatialState.CurrentCelestialId = null;
|
|
||||||
ship.SpatialState.DestinationNodeId = targetCelestial.Id;
|
|
||||||
|
|
||||||
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
|
||||||
if (ship.State != ShipState.Warping)
|
|
||||||
{
|
|
||||||
if (ship.State != ShipState.SpoolingWarp)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.SpoolingWarp;
|
|
||||||
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.Warping;
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
|
||||||
? ship.Position.DistanceTo(targetPosition)
|
|
||||||
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
|
||||||
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
|
||||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
|
||||||
return ship.Position.DistanceTo(targetPosition) <= 18f
|
|
||||||
? CompleteTransitArrival(ship, targetCelestial.SystemId, targetPosition, targetCelestial)
|
|
||||||
: "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
|
||||||
{
|
|
||||||
var destinationNodeId = targetCelestial?.Id;
|
|
||||||
var transit = ship.SpatialState.Transit;
|
|
||||||
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
|
||||||
{
|
|
||||||
transit = new ShipTransitRuntime
|
|
||||||
{
|
|
||||||
Regime = MovementRegimeKinds.FtlTransit,
|
|
||||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
|
||||||
DestinationNodeId = destinationNodeId,
|
|
||||||
StartedAtUtc = world.GeneratedAtUtc,
|
|
||||||
};
|
|
||||||
ship.SpatialState.Transit = transit;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
|
|
||||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
|
|
||||||
ship.SpatialState.CurrentCelestialId = null;
|
|
||||||
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
|
||||||
|
|
||||||
if (ship.State != ShipState.Ftl)
|
|
||||||
{
|
|
||||||
if (ship.State != ShipState.SpoolingFtl)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.SpoolingFtl;
|
|
||||||
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
|
|
||||||
{
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.State = ShipState.Ftl;
|
|
||||||
}
|
|
||||||
|
|
||||||
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
|
||||||
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
|
||||||
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
|
||||||
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
|
||||||
return transit.Progress >= 0.999f
|
|
||||||
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetCelestial)
|
|
||||||
: "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.Position = targetPosition;
|
|
||||||
ship.TargetPosition = targetPosition;
|
|
||||||
ship.SystemId = targetSystemId;
|
|
||||||
ship.SpatialState.Transit = null;
|
|
||||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
||||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
||||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
|
||||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
||||||
ship.State = ShipState.Arriving;
|
|
||||||
return "arrived";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
|
||||||
{
|
|
||||||
ship.ActionTimer = 0f;
|
|
||||||
ship.Position = targetPosition;
|
|
||||||
ship.TargetPosition = targetPosition;
|
|
||||||
ship.SystemId = targetSystemId;
|
|
||||||
ship.SpatialState.Transit = null;
|
|
||||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
|
||||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
|
||||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
|
||||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
|
||||||
ship.State = ShipState.Arriving;
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float GetShipDamagePerSecond(ShipRuntime ship) =>
|
|
||||||
ship.Definition.Class switch
|
|
||||||
{
|
|
||||||
"frigate" => FrigateDps,
|
|
||||||
"destroyer" => DestroyerDps,
|
|
||||||
"cruiser" => CruiserDps,
|
|
||||||
"capital" => CapitalDps,
|
|
||||||
_ => 4f,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,12 @@ public sealed class SimulationEngine
|
|||||||
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
||||||
private readonly OrbitalStateUpdater _orbitalStateUpdater;
|
private readonly OrbitalStateUpdater _orbitalStateUpdater;
|
||||||
private readonly InfrastructureSimulationService _infrastructureSimulation;
|
private readonly InfrastructureSimulationService _infrastructureSimulation;
|
||||||
|
private readonly GeopoliticalSimulationService _geopolitics;
|
||||||
private readonly CommanderPlanningService _commanderPlanning;
|
private readonly CommanderPlanningService _commanderPlanning;
|
||||||
|
private readonly PlayerFactionService _playerFaction;
|
||||||
private readonly StationSimulationService _stationSimulation;
|
private readonly StationSimulationService _stationSimulation;
|
||||||
private readonly StationLifecycleService _stationLifecycle;
|
private readonly StationLifecycleService _stationLifecycle;
|
||||||
private readonly ShipControlService _shipControl;
|
private readonly ShipAiService _shipAi;
|
||||||
private readonly ShipTaskExecutionService _shipTaskExecution;
|
|
||||||
private readonly SimulationProjectionService _projection;
|
private readonly SimulationProjectionService _projection;
|
||||||
|
|
||||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||||
@@ -18,11 +19,12 @@ public sealed class SimulationEngine
|
|||||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||||
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
|
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
|
||||||
_infrastructureSimulation = new InfrastructureSimulationService();
|
_infrastructureSimulation = new InfrastructureSimulationService();
|
||||||
|
_geopolitics = new GeopoliticalSimulationService();
|
||||||
_commanderPlanning = new CommanderPlanningService();
|
_commanderPlanning = new CommanderPlanningService();
|
||||||
|
_playerFaction = new PlayerFactionService();
|
||||||
_stationSimulation = new StationSimulationService();
|
_stationSimulation = new StationSimulationService();
|
||||||
_stationLifecycle = new StationLifecycleService(_stationSimulation);
|
_stationLifecycle = new StationLifecycleService(_stationSimulation);
|
||||||
_shipControl = new ShipControlService();
|
_shipAi = new ShipAiService();
|
||||||
_shipTaskExecution = new ShipTaskExecutionService();
|
|
||||||
_projection = new SimulationProjectionService(_orbitalSimulation);
|
_projection = new SimulationProjectionService(_orbitalSimulation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +33,16 @@ public sealed class SimulationEngine
|
|||||||
var nowUtc = DateTimeOffset.UtcNow;
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
var events = new List<SimulationEventRecord>();
|
var events = new List<SimulationEventRecord>();
|
||||||
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
|
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
|
||||||
|
world.GeneratedAtUtc = nowUtc;
|
||||||
|
|
||||||
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||||
|
|
||||||
_orbitalStateUpdater.Update(world);
|
_orbitalStateUpdater.Update(world);
|
||||||
_infrastructureSimulation.UpdateClaims(world, events);
|
_infrastructureSimulation.UpdateClaims(world, events);
|
||||||
_infrastructureSimulation.UpdateConstructionSites(world, events);
|
_infrastructureSimulation.UpdateConstructionSites(world, events);
|
||||||
_commanderPlanning.UpdateCommanders(this, world, simulationDeltaSeconds, events);
|
_geopolitics.Update(world, simulationDeltaSeconds, events);
|
||||||
|
_commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events);
|
||||||
|
_playerFaction.Update(world, simulationDeltaSeconds, events);
|
||||||
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
|
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
|
||||||
|
|
||||||
foreach (var ship in world.Ships.ToList())
|
foreach (var ship in world.Ships.ToList())
|
||||||
@@ -48,25 +53,12 @@ public sealed class SimulationEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
var previousPosition = ship.Position;
|
var previousPosition = ship.Position;
|
||||||
var previousState = ship.State;
|
_shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events);
|
||||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
|
||||||
var previousTask = ship.ControllerTask.Kind;
|
|
||||||
|
|
||||||
_shipControl.RefreshControlLayers(ship, world);
|
|
||||||
_shipControl.PlanControllerTask(this, ship, world);
|
|
||||||
|
|
||||||
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, simulationDeltaSeconds);
|
|
||||||
|
|
||||||
_shipControl.AdvanceControlState(this, ship, world, controllerEvent);
|
|
||||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
|
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
|
||||||
_shipControl.TrackHistory(ship, controllerEvent);
|
|
||||||
_shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_orbitalStateUpdater.SyncSpatialState(world);
|
_orbitalStateUpdater.SyncSpatialState(world);
|
||||||
CleanupDestroyedEntities(world, events);
|
CleanupDestroyedEntities(world, events);
|
||||||
world.GeneratedAtUtc = nowUtc;
|
|
||||||
|
|
||||||
return _projection.BuildDelta(world, sequence, events);
|
return _projection.BuildDelta(world, sequence, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,18 +68,6 @@ public sealed class SimulationEngine
|
|||||||
public void PrimeDeltaBaseline(SimulationWorld world) =>
|
public void PrimeDeltaBaseline(SimulationWorld world) =>
|
||||||
_projection.PrimeDeltaBaseline(world);
|
_projection.PrimeDeltaBaseline(world);
|
||||||
|
|
||||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule) =>
|
|
||||||
_shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
|
||||||
|
|
||||||
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
_shipControl.PlanStationConstruction(ship, world);
|
|
||||||
|
|
||||||
internal void PlanAttackTarget(ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
_shipControl.PlanAttackTarget(ship, world);
|
|
||||||
|
|
||||||
internal void PlanTransportHaul(ShipRuntime ship, SimulationWorld world) =>
|
|
||||||
_shipControl.PlanTransportHaul(ship, world);
|
|
||||||
|
|
||||||
internal static float GetShipCargoAmount(ShipRuntime ship) =>
|
internal static float GetShipCargoAmount(ShipRuntime ship) =>
|
||||||
SimulationRuntimeSupport.GetShipCargoAmount(ship);
|
SimulationRuntimeSupport.GetShipCargoAmount(ship);
|
||||||
|
|
||||||
@@ -95,6 +75,7 @@ public sealed class SimulationEngine
|
|||||||
{
|
{
|
||||||
foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
|
foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
|
||||||
{
|
{
|
||||||
|
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f));
|
||||||
world.Ships.Remove(ship);
|
world.Ships.Remove(ship);
|
||||||
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
|
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
|
||||||
{
|
{
|
||||||
@@ -117,6 +98,7 @@ public sealed class SimulationEngine
|
|||||||
|
|
||||||
foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())
|
foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())
|
||||||
{
|
{
|
||||||
|
CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
|
||||||
world.Stations.Remove(station);
|
world.Stations.Remove(station);
|
||||||
|
|
||||||
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
|
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
|
||||||
@@ -138,4 +120,29 @@ public sealed class SimulationEngine
|
|||||||
events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow));
|
events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount)
|
||||||
|
{
|
||||||
|
var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
|
||||||
|
? "scrapmetal"
|
||||||
|
: world.ItemDefinitions.ContainsKey("rawscrap")
|
||||||
|
? "rawscrap"
|
||||||
|
: world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
|
||||||
|
if (itemId is null || amount <= 0.01f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
world.Wrecks.Add(new WreckRuntime
|
||||||
|
{
|
||||||
|
Id = $"wreck-{sourceKind}-{sourceEntityId}",
|
||||||
|
SourceKind = sourceKind,
|
||||||
|
SourceEntityId = sourceEntityId,
|
||||||
|
SystemId = systemId,
|
||||||
|
Position = position,
|
||||||
|
ItemId = itemId,
|
||||||
|
RemainingAmount = amount,
|
||||||
|
MaxAmount = amount,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FastEndpoints" Version="6.*" />
|
<PackageReference Include="FastEndpoints" Version="6.*" />
|
||||||
|
<PackageReference Include="FastEndpoints.Swagger" Version="6.*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using static SpaceGame.Api.Ships.Simulation.ShipControlService;
|
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Stations.Simulation;
|
namespace SpaceGame.Api.Stations.Simulation;
|
||||||
@@ -80,7 +79,7 @@ internal sealed class StationLifecycleService
|
|||||||
TargetPosition = spawnPosition,
|
TargetPosition = spawnPosition,
|
||||||
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
||||||
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
||||||
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
|
Skills = WorldSeedingService.CreateSkills(definition),
|
||||||
Health = definition.MaxHealth,
|
Health = definition.MaxHealth,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,13 +108,22 @@ internal sealed class StationLifecycleService
|
|||||||
{
|
{
|
||||||
if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
|
if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return new DefaultBehaviorRuntime { Kind = "idle" };
|
return new DefaultBehaviorRuntime
|
||||||
|
{
|
||||||
|
Kind = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle",
|
||||||
|
HomeSystemId = station.SystemId,
|
||||||
|
HomeStationId = station.Id,
|
||||||
|
MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var patrolRadius = station.Radius + 90f;
|
var patrolRadius = station.Radius + 90f;
|
||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "patrol",
|
Kind = "patrol",
|
||||||
|
HomeSystemId = station.SystemId,
|
||||||
|
HomeStationId = station.Id,
|
||||||
|
AreaSystemId = station.SystemId,
|
||||||
PatrolPoints =
|
PatrolPoints =
|
||||||
[
|
[
|
||||||
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
|
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
|
||||||
@@ -150,7 +158,13 @@ internal sealed class StationLifecycleService
|
|||||||
ParentCommanderId = factionCommander.Id,
|
ParentCommanderId = factionCommander.Id,
|
||||||
ControlledEntityId = station.Id,
|
ControlledEntityId = station.Id,
|
||||||
PolicySetId = factionCommander.PolicySetId,
|
PolicySetId = factionCommander.PolicySetId,
|
||||||
Doctrine = "station-default",
|
Doctrine = "station-control",
|
||||||
|
Skills = new CommanderSkillProfileRuntime
|
||||||
|
{
|
||||||
|
Leadership = 3,
|
||||||
|
Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5),
|
||||||
|
Strategy = 3,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
station.CommanderId = commander.Id;
|
station.CommanderId = commander.Id;
|
||||||
@@ -179,25 +193,12 @@ internal sealed class StationLifecycleService
|
|||||||
ParentCommanderId = factionCommander.Id,
|
ParentCommanderId = factionCommander.Id,
|
||||||
ControlledEntityId = ship.Id,
|
ControlledEntityId = ship.Id,
|
||||||
PolicySetId = factionCommander.PolicySetId,
|
PolicySetId = factionCommander.PolicySetId,
|
||||||
Doctrine = "ship-default",
|
Doctrine = "ship-control",
|
||||||
ActiveBehavior = new CommanderBehaviorRuntime
|
Skills = new CommanderSkillProfileRuntime
|
||||||
{
|
{
|
||||||
Kind = ship.DefaultBehavior.Kind,
|
Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5),
|
||||||
AreaSystemId = ship.DefaultBehavior.AreaSystemId,
|
Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5),
|
||||||
TargetEntityId = ship.DefaultBehavior.TargetEntityId,
|
Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5),
|
||||||
ItemId = ship.DefaultBehavior.ItemId,
|
|
||||||
StationId = ship.DefaultBehavior.StationId,
|
|
||||||
ModuleId = ship.DefaultBehavior.ModuleId,
|
|
||||||
NodeId = ship.DefaultBehavior.NodeId,
|
|
||||||
Phase = ship.DefaultBehavior.Phase,
|
|
||||||
PatrolIndex = ship.DefaultBehavior.PatrolIndex,
|
|
||||||
},
|
|
||||||
ActiveTask = new CommanderTaskRuntime
|
|
||||||
{
|
|
||||||
Kind = ShipTaskKinds.Idle,
|
|
||||||
Status = WorkStatus.Pending,
|
|
||||||
TargetSystemId = ship.SystemId,
|
|
||||||
Threshold = 0f,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ internal sealed class StationSimulationService
|
|||||||
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
|
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
|
||||||
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
|
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
|
||||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||||
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
|
||||||
? 90f
|
? 90f
|
||||||
: 0f;
|
: 0f;
|
||||||
|
|
||||||
@@ -104,6 +104,7 @@ internal sealed class StationSimulationService
|
|||||||
AddSupplyOrder(desiredOrders, station, "superfluidcoolant", ScaleSupplyTriggerByEconomy(economy, "superfluidcoolant", MathF.Max(superfluidCoolantReserve * 1.35f, superfluidCoolantReserve + 30f)), reserveFloor: superfluidCoolantReserve, valuationBase: ScaleSupplyValuation(economy, "superfluidcoolant", 0.9f));
|
AddSupplyOrder(desiredOrders, station, "superfluidcoolant", ScaleSupplyTriggerByEconomy(economy, "superfluidcoolant", MathF.Max(superfluidCoolantReserve * 1.35f, superfluidCoolantReserve + 30f)), reserveFloor: superfluidCoolantReserve, valuationBase: ScaleSupplyValuation(economy, "superfluidcoolant", 0.9f));
|
||||||
AddSupplyOrder(desiredOrders, station, "quantumtubes", ScaleSupplyTriggerByEconomy(economy, "quantumtubes", MathF.Max(quantumTubesReserve * 1.35f, quantumTubesReserve + 30f)), reserveFloor: quantumTubesReserve, valuationBase: ScaleSupplyValuation(economy, "quantumtubes", 0.9f));
|
AddSupplyOrder(desiredOrders, station, "quantumtubes", ScaleSupplyTriggerByEconomy(economy, "quantumtubes", MathF.Max(quantumTubesReserve * 1.35f, quantumTubesReserve + 30f)), reserveFloor: quantumTubesReserve, valuationBase: ScaleSupplyValuation(economy, "quantumtubes", 0.9f));
|
||||||
|
|
||||||
|
desiredOrders = ApplyRegionalMarketModifiers(world, station, desiredOrders);
|
||||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ internal sealed class StationSimulationService
|
|||||||
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
||||||
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
||||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||||
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
|
||||||
? 90f
|
? 90f
|
||||||
: 0f;
|
: 0f;
|
||||||
|
|
||||||
@@ -257,8 +258,9 @@ 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 = FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military") ? 1f : 0f;
|
var fleetPressure = GetShipProductionPressure(world, station.FactionId, "military");
|
||||||
priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure);
|
priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure);
|
||||||
|
priority += GetStrategicRecipeBias(world, station, recipe);
|
||||||
|
|
||||||
return priority;
|
return priority;
|
||||||
}
|
}
|
||||||
@@ -321,6 +323,52 @@ internal sealed class StationSimulationService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float GetStrategicRecipeBias(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||||
|
{
|
||||||
|
var commander = station.CommanderId is null
|
||||||
|
? null
|
||||||
|
: world.Commanders.FirstOrDefault(candidate => candidate.Id == station.CommanderId);
|
||||||
|
var assignment = commander?.Assignment;
|
||||||
|
if (assignment is null)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputItemIds = recipe.Outputs
|
||||||
|
.Select(output => output.ItemId)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (string.Equals(assignment.Kind, "ship-production-focus", StringComparison.Ordinal)
|
||||||
|
&& recipe.ShipOutputId is not null
|
||||||
|
&& world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)
|
||||||
|
&& string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return 260f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(assignment.Kind, "commodity-focus", StringComparison.Ordinal)
|
||||||
|
&& assignment.ItemId is not null
|
||||||
|
&& outputItemIds.Contains(assignment.ItemId))
|
||||||
|
{
|
||||||
|
return 220f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(assignment.Kind, "expansion-support", StringComparison.Ordinal)
|
||||||
|
&& outputItemIds.Overlaps(["energycells", "refinedmetals", "hullparts", "claytronics"]))
|
||||||
|
{
|
||||||
|
return 180f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(assignment.Kind, "station-oversight", StringComparison.Ordinal)
|
||||||
|
&& assignment.ItemId is not null
|
||||||
|
&& outputItemIds.Contains(assignment.ItemId))
|
||||||
|
{
|
||||||
|
return 90f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
internal static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||||
{
|
{
|
||||||
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
||||||
@@ -338,7 +386,7 @@ internal sealed class StationSimulationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, shipDefinition.Kind))
|
if (GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind) <= 0.05f)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -559,12 +607,20 @@ internal sealed class StationSimulationService
|
|||||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||||
var deficit = Math.Max(0, targetSystems - controlledSystems);
|
var deficit = Math.Max(0, targetSystems - controlledSystems);
|
||||||
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
|
var contestedSystems = world.Geopolitics?.Territory.ControlStates.Count(state =>
|
||||||
|
state.IsContested
|
||||||
|
&& (string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(state.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal)
|
||||||
|
|| state.ClaimantFactionIds.Contains(factionId, StringComparer.Ordinal))) ?? 0;
|
||||||
|
var frontierSystems = world.Geopolitics?.Territory.Zones.Count(zone =>
|
||||||
|
string.Equals(zone.FactionId, factionId, StringComparison.Ordinal)
|
||||||
|
&& zone.Kind is "frontier" or "corridor" or "contested") ?? 0;
|
||||||
|
return Math.Clamp((deficit / (float)targetSystems) + (contestedSystems * 0.12f) + (frontierSystems * 0.04f), 0f, 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
internal static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
||||||
{
|
{
|
||||||
return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id));
|
return GeopoliticalSimulationService.GetControlledSystems(world, factionId).Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
||||||
@@ -612,34 +668,66 @@ internal sealed class StationSimulationService
|
|||||||
: baseValuation;
|
: baseValuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<DesiredMarketOrder> ApplyRegionalMarketModifiers(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
|
||||||
|
{
|
||||||
|
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, station.FactionId, station.SystemId);
|
||||||
|
if (region is null)
|
||||||
|
{
|
||||||
|
return desiredOrders.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var security = world.Geopolitics?.EconomyRegions.SecurityAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal));
|
||||||
|
var economic = world.Geopolitics?.EconomyRegions.EconomicAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal));
|
||||||
|
var bottlenecks = world.Geopolitics?.EconomyRegions.Bottlenecks
|
||||||
|
.Where(bottleneck => string.Equals(bottleneck.RegionId, region.Id, StringComparison.Ordinal))
|
||||||
|
.ToDictionary(bottleneck => bottleneck.ItemId, StringComparer.Ordinal) ?? new Dictionary<string, RegionalBottleneckRuntime>(StringComparer.Ordinal);
|
||||||
|
var riskMultiplier = 1f + ((security?.SupplyRisk ?? 0f) * 0.3f) + ((security?.AccessFriction ?? 0f) * 0.2f);
|
||||||
|
var sustainmentFloor = 1f + MathF.Max(0f, 0.55f - (economic?.SustainmentScore ?? 1f));
|
||||||
|
|
||||||
|
return desiredOrders
|
||||||
|
.Select(order =>
|
||||||
|
{
|
||||||
|
bottlenecks.TryGetValue(order.ItemId, out var bottleneck);
|
||||||
|
var severity = bottleneck?.Severity ?? 0f;
|
||||||
|
var buyBias = order.Kind == MarketOrderKinds.Buy ? 1f + (severity * 0.08f) : 1f;
|
||||||
|
var sellBias = order.Kind == MarketOrderKinds.Sell && severity > 0f ? MathF.Max(0.35f, 1f - (severity * 0.07f)) : 1f;
|
||||||
|
var amount = order.Amount * (order.Kind == MarketOrderKinds.Buy ? riskMultiplier * buyBias * sustainmentFloor : sellBias);
|
||||||
|
var valuation = order.Valuation * (order.Kind == MarketOrderKinds.Buy
|
||||||
|
? 1f + (severity * 0.06f) + ((security?.SupplyRisk ?? 0f) * 0.18f)
|
||||||
|
: 1f + (severity * 0.04f));
|
||||||
|
float? reserveThreshold = order.ReserveThreshold.HasValue
|
||||||
|
? order.ReserveThreshold.Value * (1f + ((security?.SupplyRisk ?? 0f) * 0.15f))
|
||||||
|
: null;
|
||||||
|
return new DesiredMarketOrder(order.Kind, order.ItemId, amount, valuation, reserveThreshold);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind)
|
private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind)
|
||||||
{
|
{
|
||||||
var factionCommander = FindFactionCommander(world, factionId);
|
var economic = FindFactionEconomicAssessment(world, factionId);
|
||||||
var task = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ProduceShips, shipKind);
|
var threat = FindFactionThreatAssessment(world, factionId);
|
||||||
if (task is null)
|
if (economic is null || threat is null)
|
||||||
{
|
{
|
||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return task.State == FactionIssuedTaskState.Blocked ? 0.4f : 1f;
|
return shipKind switch
|
||||||
|
{
|
||||||
|
"military" => threat.EnemyFactionCount > 0
|
||||||
|
? economic.MilitaryShipCount < Math.Max(4, economic.ControlledSystemCount * 2) ? 1f : 0.25f
|
||||||
|
: 0.1f,
|
||||||
|
"construction" => economic.PrimaryExpansionSiteId is not null
|
||||||
|
? economic.ConstructorShipCount < 1 ? 1f : 0.35f
|
||||||
|
: economic.ConstructorShipCount < 1 ? 0.5f : 0f,
|
||||||
|
"transport" => economic.TransportShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.8f : 0.2f,
|
||||||
|
_ when shipKind == "mining" || shipKind == "miner" => economic.MinerShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.85f : 0.2f,
|
||||||
|
_ => 0.15f,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||||
{
|
=> GeopoliticalSimulationService.FactionControlsSystem(world, factionId, systemId);
|
||||||
var totalLagrangePoints = world.Celestials.Count(node =>
|
|
||||||
node.SystemId == systemId &&
|
|
||||||
node.Kind == SpatialNodeKind.LagrangePoint);
|
|
||||||
if (totalLagrangePoints == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedLocations = world.Claims.Count(claim =>
|
|
||||||
claim.SystemId == systemId &&
|
|
||||||
claim.FactionId == factionId &&
|
|
||||||
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active);
|
|
||||||
return ownedLocations > (totalLagrangePoints / 2f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
|
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ public sealed record WorldSnapshot(
|
|||||||
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
|
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
|
||||||
IReadOnlyList<PolicySetSnapshot> Policies,
|
IReadOnlyList<PolicySetSnapshot> Policies,
|
||||||
IReadOnlyList<ShipSnapshot> Ships,
|
IReadOnlyList<ShipSnapshot> Ships,
|
||||||
IReadOnlyList<FactionSnapshot> Factions);
|
IReadOnlyList<FactionSnapshot> Factions,
|
||||||
|
PlayerFactionSnapshot? PlayerFaction,
|
||||||
|
GeopoliticalStateSnapshot? Geopolitics);
|
||||||
|
|
||||||
public sealed record WorldDelta(
|
public sealed record WorldDelta(
|
||||||
long Sequence,
|
long Sequence,
|
||||||
@@ -36,6 +38,8 @@ public sealed record WorldDelta(
|
|||||||
IReadOnlyList<PolicySetDelta> Policies,
|
IReadOnlyList<PolicySetDelta> Policies,
|
||||||
IReadOnlyList<ShipDelta> Ships,
|
IReadOnlyList<ShipDelta> Ships,
|
||||||
IReadOnlyList<FactionDelta> Factions,
|
IReadOnlyList<FactionDelta> Factions,
|
||||||
|
PlayerFactionSnapshot? PlayerFaction,
|
||||||
|
GeopoliticalStateSnapshot? Geopolitics,
|
||||||
ObserverScope? Scope = null);
|
ObserverScope? Scope = null);
|
||||||
|
|
||||||
public sealed record SimulationEventRecord(
|
public sealed record SimulationEventRecord(
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ public sealed class SimulationWorld
|
|||||||
public required List<SystemRuntime> Systems { get; init; }
|
public required List<SystemRuntime> Systems { get; init; }
|
||||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||||
public required List<CelestialRuntime> Celestials { get; init; }
|
public required List<CelestialRuntime> Celestials { get; init; }
|
||||||
|
public required List<WreckRuntime> Wrecks { get; init; }
|
||||||
public required List<StationRuntime> Stations { get; init; }
|
public required List<StationRuntime> Stations { get; init; }
|
||||||
public required List<ShipRuntime> Ships { get; init; }
|
public required List<ShipRuntime> Ships { get; init; }
|
||||||
public required List<FactionRuntime> Factions { get; init; }
|
public required List<FactionRuntime> Factions { get; init; }
|
||||||
|
public PlayerFactionRuntime? PlayerFaction { get; set; }
|
||||||
|
public GeopoliticalStateRuntime? Geopolitics { get; set; }
|
||||||
public required List<CommanderRuntime> Commanders { get; init; }
|
public required List<CommanderRuntime> Commanders { get; init; }
|
||||||
public required List<ClaimRuntime> Claims { get; init; }
|
public required List<ClaimRuntime> Claims { get; init; }
|
||||||
public required List<ConstructionSiteRuntime> ConstructionSites { get; init; }
|
public required List<ConstructionSiteRuntime> ConstructionSites { get; init; }
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ public sealed class CelestialRuntime
|
|||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WreckRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SourceKind { get; init; }
|
||||||
|
public required string SourceEntityId { get; init; }
|
||||||
|
public required string SystemId { get; set; }
|
||||||
|
public required Vector3 Position { get; set; }
|
||||||
|
public required string ItemId { get; set; }
|
||||||
|
public float RemainingAmount { get; set; }
|
||||||
|
public float MaxAmount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ShipSpatialStateRuntime
|
public sealed class ShipSpatialStateRuntime
|
||||||
{
|
{
|
||||||
public string SpaceLayer { get; set; } = SpaceLayerKinds.LocalSpace;
|
public string SpaceLayer { get; set; } = SpaceLayerKinds.LocalSpace;
|
||||||
|
|||||||
@@ -15,10 +15,16 @@ internal sealed class WorldBuilder(
|
|||||||
var systems = generationService.ExpandSystems(
|
var systems = generationService.ExpandSystems(
|
||||||
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
|
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
|
||||||
worldGeneration.TargetSystemCount);
|
worldGeneration.TargetSystemCount);
|
||||||
|
|
||||||
|
Console.WriteLine("TEST");
|
||||||
|
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
|
||||||
|
|
||||||
var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
|
var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
|
||||||
catalog.Scenario,
|
catalog.Scenario,
|
||||||
systems.Select(system => system.Id).ToList());
|
systems.Select(system => system.Id).ToList());
|
||||||
|
|
||||||
|
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
|
||||||
|
|
||||||
var systemRuntimes = systems
|
var systemRuntimes = systems
|
||||||
.Select(definition => new SystemRuntime
|
.Select(definition => new SystemRuntime
|
||||||
{
|
{
|
||||||
@@ -42,11 +48,29 @@ internal sealed class WorldBuilder(
|
|||||||
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
||||||
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
|
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
|
||||||
|
|
||||||
|
if (worldGeneration.AiControllerFactionCount < int.MaxValue)
|
||||||
|
{
|
||||||
|
var aiFactionIds = stations
|
||||||
|
.Select(s => s.FactionId)
|
||||||
|
.Concat(ships.Select(s => s.FactionId))
|
||||||
|
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.OrderBy(id => id, StringComparer.Ordinal)
|
||||||
|
.Take(worldGeneration.AiControllerFactionCount)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
aiFactionIds.Add(DefaultFactionId);
|
||||||
|
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||||
|
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
var factions = seedingService.CreateFactions(stations, ships);
|
var factions = seedingService.CreateFactions(stations, ships);
|
||||||
seedingService.BootstrapFactionEconomy(factions, stations);
|
seedingService.BootstrapFactionEconomy(factions, stations);
|
||||||
var policies = seedingService.CreatePolicies(factions);
|
var policies = seedingService.CreatePolicies(factions);
|
||||||
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
||||||
var nowUtc = DateTimeOffset.UtcNow;
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
|
var playerFaction = worldGeneration.GeneratePlayerFaction
|
||||||
|
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
||||||
|
: null;
|
||||||
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
||||||
var bootstrapWorld = new SimulationWorld
|
var bootstrapWorld = new SimulationWorld
|
||||||
{
|
{
|
||||||
@@ -56,9 +80,11 @@ internal sealed class WorldBuilder(
|
|||||||
Systems = systemRuntimes,
|
Systems = systemRuntimes,
|
||||||
Celestials = spatialLayout.Celestials,
|
Celestials = spatialLayout.Celestials,
|
||||||
Nodes = spatialLayout.Nodes,
|
Nodes = spatialLayout.Nodes,
|
||||||
|
Wrecks = [],
|
||||||
Stations = stations,
|
Stations = stations,
|
||||||
Ships = ships,
|
Ships = ships,
|
||||||
Factions = factions,
|
Factions = factions,
|
||||||
|
PlayerFaction = playerFaction,
|
||||||
Commanders = commanders,
|
Commanders = commanders,
|
||||||
Claims = claims,
|
Claims = claims,
|
||||||
ConstructionSites = [],
|
ConstructionSites = [],
|
||||||
@@ -75,7 +101,7 @@ internal sealed class WorldBuilder(
|
|||||||
};
|
};
|
||||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
|
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
|
||||||
|
|
||||||
return new SimulationWorld
|
var world = new SimulationWorld
|
||||||
{
|
{
|
||||||
Label = "Split Viewer / Simulation World",
|
Label = "Split Viewer / Simulation World",
|
||||||
Seed = WorldSeed,
|
Seed = WorldSeed,
|
||||||
@@ -83,9 +109,12 @@ internal sealed class WorldBuilder(
|
|||||||
Systems = systemRuntimes,
|
Systems = systemRuntimes,
|
||||||
Celestials = spatialLayout.Celestials,
|
Celestials = spatialLayout.Celestials,
|
||||||
Nodes = spatialLayout.Nodes,
|
Nodes = spatialLayout.Nodes,
|
||||||
|
Wrecks = [],
|
||||||
Stations = stations,
|
Stations = stations,
|
||||||
Ships = ships,
|
Ships = ships,
|
||||||
Factions = factions,
|
Factions = factions,
|
||||||
|
PlayerFaction = playerFaction,
|
||||||
|
Geopolitics = null,
|
||||||
Commanders = commanders,
|
Commanders = commanders,
|
||||||
Claims = claims,
|
Claims = claims,
|
||||||
ConstructionSites = constructionSites,
|
ConstructionSites = constructionSites,
|
||||||
@@ -100,6 +129,10 @@ internal sealed class WorldBuilder(
|
|||||||
OrbitalTimeSeconds = WorldSeed * 97d,
|
OrbitalTimeSeconds = WorldSeed * 97d,
|
||||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var geopolitics = new GeopoliticalSimulationService();
|
||||||
|
geopolitics.Update(world, 0f, []);
|
||||||
|
return world;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<StationRuntime> CreateStations(
|
private static List<StationRuntime> CreateStations(
|
||||||
@@ -291,7 +324,7 @@ internal sealed class WorldBuilder(
|
|||||||
patrolRoutes,
|
patrolRoutes,
|
||||||
stations,
|
stations,
|
||||||
refinery),
|
refinery),
|
||||||
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
|
Skills = WorldSeedingService.CreateSkills(definition),
|
||||||
Health = definition.MaxHealth,
|
Health = definition.MaxHealth,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -286,16 +286,9 @@ internal sealed class WorldSeedingService
|
|||||||
FactionId = faction.Id,
|
FactionId = faction.Id,
|
||||||
ControlledEntityId = faction.Id,
|
ControlledEntityId = faction.Id,
|
||||||
PolicySetId = faction.DefaultPolicySetId,
|
PolicySetId = faction.DefaultPolicySetId,
|
||||||
Doctrine = "strategic-expansionist",
|
Doctrine = "strategic-control",
|
||||||
};
|
};
|
||||||
|
|
||||||
commander.Goals.Add("control-all-systems");
|
|
||||||
commander.Goals.Add("control-five-systems-fast");
|
|
||||||
commander.Goals.Add("expand-industrial-base");
|
|
||||||
commander.Goals.Add("grow-war-fleet");
|
|
||||||
commander.Goals.Add("deter-pirate-harassment");
|
|
||||||
commander.Goals.Add("contest-rival-expansion");
|
|
||||||
|
|
||||||
commanders.Add(commander);
|
commanders.Add(commander);
|
||||||
factionCommanders[faction.Id] = commander;
|
factionCommanders[faction.Id] = commander;
|
||||||
faction.CommanderIds.Add(commander.Id);
|
faction.CommanderIds.Add(commander.Id);
|
||||||
@@ -316,7 +309,7 @@ internal sealed class WorldSeedingService
|
|||||||
ParentCommanderId = parentCommander.Id,
|
ParentCommanderId = parentCommander.Id,
|
||||||
ControlledEntityId = station.Id,
|
ControlledEntityId = station.Id,
|
||||||
PolicySetId = parentCommander.PolicySetId,
|
PolicySetId = parentCommander.PolicySetId,
|
||||||
Doctrine = "station-default",
|
Doctrine = "station-control",
|
||||||
};
|
};
|
||||||
|
|
||||||
station.CommanderId = commander.Id;
|
station.CommanderId = commander.Id;
|
||||||
@@ -341,16 +334,9 @@ internal sealed class WorldSeedingService
|
|||||||
ParentCommanderId = parentCommander.Id,
|
ParentCommanderId = parentCommander.Id,
|
||||||
ControlledEntityId = ship.Id,
|
ControlledEntityId = ship.Id,
|
||||||
PolicySetId = parentCommander.PolicySetId,
|
PolicySetId = parentCommander.PolicySetId,
|
||||||
Doctrine = "ship-default",
|
Doctrine = "ship-control",
|
||||||
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
|
|
||||||
ActiveTask = CopyTask(ship.ControllerTask, null),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ship.Order is not null)
|
|
||||||
{
|
|
||||||
commander.ActiveOrder = CopyOrder(ship.Order);
|
|
||||||
}
|
|
||||||
|
|
||||||
ship.CommanderId = commander.Id;
|
ship.CommanderId = commander.Id;
|
||||||
ship.PolicySetId = parentCommander.PolicySetId;
|
ship.PolicySetId = parentCommander.PolicySetId;
|
||||||
parentCommander.SubordinateCommanderIds.Add(commander.Id);
|
parentCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||||
@@ -361,6 +347,93 @@ internal sealed class WorldSeedingService
|
|||||||
return commanders;
|
return commanders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal PlayerFactionRuntime CreatePlayerFaction(
|
||||||
|
IReadOnlyCollection<FactionRuntime> factions,
|
||||||
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
|
IReadOnlyCollection<ShipRuntime> ships,
|
||||||
|
IReadOnlyCollection<CommanderRuntime> commanders,
|
||||||
|
IReadOnlyCollection<PolicySetRuntime> policies,
|
||||||
|
DateTimeOffset nowUtc)
|
||||||
|
{
|
||||||
|
var sovereignFaction = factions.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
|
||||||
|
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
|
||||||
|
|
||||||
|
var player = new PlayerFactionRuntime
|
||||||
|
{
|
||||||
|
Id = "player-faction",
|
||||||
|
Label = $"{sovereignFaction.Label} Command",
|
||||||
|
SovereignFactionId = sovereignFaction.Id,
|
||||||
|
CreatedAtUtc = nowUtc,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var shipId in ships.Where(ship => ship.FactionId == sovereignFaction.Id).Select(ship => ship.Id))
|
||||||
|
{
|
||||||
|
player.AssetRegistry.ShipIds.Add(shipId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var stationId in stations.Where(station => station.FactionId == sovereignFaction.Id).Select(station => station.Id))
|
||||||
|
{
|
||||||
|
player.AssetRegistry.StationIds.Add(stationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var commanderId in commanders.Where(commander => commander.FactionId == sovereignFaction.Id).Select(commander => commander.Id))
|
||||||
|
{
|
||||||
|
player.AssetRegistry.CommanderIds.Add(commanderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var policy in policies.Where(policy => string.Equals(policy.OwnerId, sovereignFaction.Id, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
player.AssetRegistry.PolicySetIds.Add(policy.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.Policies.Add(new PlayerFactionPolicyRuntime
|
||||||
|
{
|
||||||
|
Id = "player-core-policy",
|
||||||
|
Label = "Core Empire Policy",
|
||||||
|
ScopeKind = "player-faction",
|
||||||
|
ScopeId = player.Id,
|
||||||
|
PolicySetId = sovereignFaction.DefaultPolicySetId,
|
||||||
|
TradeAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.TradeAccessPolicy ?? "owner-and-allies",
|
||||||
|
DockingAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.DockingAccessPolicy ?? "owner-and-allies",
|
||||||
|
ConstructionAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.ConstructionAccessPolicy ?? "owner-only",
|
||||||
|
OperationalRangePolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.OperationalRangePolicy ?? "unrestricted",
|
||||||
|
CombatEngagementPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.CombatEngagementPolicy ?? "defensive",
|
||||||
|
AvoidHostileSystems = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.AvoidHostileSystems ?? true,
|
||||||
|
FleeHullRatio = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.FleeHullRatio ?? 0.35f,
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId) is { } defaultPolicy)
|
||||||
|
{
|
||||||
|
foreach (var systemId in defaultPolicy.BlacklistedSystemIds)
|
||||||
|
{
|
||||||
|
player.Policies[0].BlacklistedSystemIds.Add(systemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AutomationPolicies.Add(new PlayerAutomationPolicyRuntime
|
||||||
|
{
|
||||||
|
Id = "player-core-automation",
|
||||||
|
Label = "Core Automation",
|
||||||
|
ScopeKind = "player-faction",
|
||||||
|
ScopeId = player.Id,
|
||||||
|
BehaviorKind = "idle",
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
|
||||||
|
player.Reserves.Add(new PlayerReserveGroupRuntime
|
||||||
|
{
|
||||||
|
Id = "player-core-reserve",
|
||||||
|
Label = "Strategic Reserve",
|
||||||
|
ReserveKind = "military",
|
||||||
|
UpdatedAtUtc = nowUtc,
|
||||||
|
});
|
||||||
|
player.AssetRegistry.ReserveIds.Add("player-core-reserve");
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
internal static DefaultBehaviorRuntime CreateBehavior(
|
internal static DefaultBehaviorRuntime CreateBehavior(
|
||||||
ShipDefinition definition,
|
ShipDefinition definition,
|
||||||
string systemId,
|
string systemId,
|
||||||
@@ -381,22 +454,32 @@ internal sealed class WorldSeedingService
|
|||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "construct-station",
|
Kind = "construct-station",
|
||||||
StationId = homeStation.Id,
|
HomeSystemId = homeStation.SystemId,
|
||||||
Phase = "travel-to-station",
|
HomeStationId = homeStation.Id,
|
||||||
|
PreferredConstructionSiteId = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasCapabilities(definition, "mining") && homeStation is not null)
|
if (HasCapabilities(definition, "mining") && homeStation is not null)
|
||||||
{
|
{
|
||||||
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, homeStation.Id);
|
return new DefaultBehaviorRuntime
|
||||||
|
{
|
||||||
|
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||||
|
HomeSystemId = homeStation.SystemId,
|
||||||
|
HomeStationId = homeStation.Id,
|
||||||
|
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||||
|
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "trade-haul",
|
Kind = "advanced-auto-trade",
|
||||||
Phase = "travel-to-source",
|
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||||
|
HomeStationId = homeStation?.Id,
|
||||||
|
MaxSystemRange = 2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +488,9 @@ internal sealed class WorldSeedingService
|
|||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "patrol",
|
Kind = "patrol",
|
||||||
StationId = homeStation?.Id,
|
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||||
|
HomeStationId = homeStation?.Id,
|
||||||
|
AreaSystemId = systemId,
|
||||||
PatrolPoints = route,
|
PatrolPoints = route,
|
||||||
PatrolIndex = 0,
|
PatrolIndex = 0,
|
||||||
};
|
};
|
||||||
@@ -414,6 +499,20 @@ internal sealed class WorldSeedingService
|
|||||||
return new DefaultBehaviorRuntime
|
return new DefaultBehaviorRuntime
|
||||||
{
|
{
|
||||||
Kind = "idle",
|
Kind = "idle",
|
||||||
|
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||||
|
HomeStationId = homeStation?.Id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
|
||||||
|
{
|
||||||
|
return definition.Kind switch
|
||||||
|
{
|
||||||
|
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
|
||||||
|
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
|
||||||
|
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
|
||||||
|
_ when HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
|
||||||
|
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,43 +570,4 @@ internal sealed class WorldSeedingService
|
|||||||
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DefaultBehaviorRuntime CreateResourceHarvestBehavior(string kind, string areaSystemId, string stationId) => new()
|
|
||||||
{
|
|
||||||
Kind = kind,
|
|
||||||
AreaSystemId = areaSystemId,
|
|
||||||
StationId = stationId,
|
|
||||||
Phase = "travel-to-node",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
|
|
||||||
{
|
|
||||||
Kind = behavior.Kind,
|
|
||||||
AreaSystemId = behavior.AreaSystemId,
|
|
||||||
TargetEntityId = behavior.TargetEntityId,
|
|
||||||
ItemId = behavior.ItemId,
|
|
||||||
ModuleId = behavior.ModuleId,
|
|
||||||
NodeId = behavior.NodeId,
|
|
||||||
Phase = behavior.Phase,
|
|
||||||
PatrolIndex = behavior.PatrolIndex,
|
|
||||||
StationId = behavior.StationId,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
|
|
||||||
{
|
|
||||||
Kind = order.Kind,
|
|
||||||
Status = order.Status,
|
|
||||||
DestinationSystemId = order.DestinationSystemId,
|
|
||||||
DestinationPosition = order.DestinationPosition,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
|
|
||||||
{
|
|
||||||
Kind = task.Kind.ToContractValue(),
|
|
||||||
Status = task.Status,
|
|
||||||
TargetEntityId = task.TargetEntityId,
|
|
||||||
TargetNodeId = targetNodeId ?? task.TargetNodeId,
|
|
||||||
TargetPosition = task.TargetPosition,
|
|
||||||
TargetSystemId = task.TargetSystemId,
|
|
||||||
Threshold = task.Threshold,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace SpaceGame.Api.Universe.Simulation;
|
|||||||
|
|
||||||
public sealed class WorldGenerationOptions
|
public sealed class WorldGenerationOptions
|
||||||
{
|
{
|
||||||
public int TargetSystemCount { get; init; } = 160;
|
public int TargetSystemCount { get; init; }
|
||||||
|
public int AiControllerFactionCount { get; init; }
|
||||||
|
public bool GeneratePlayerFaction { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class WorldService(
|
|||||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||||
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
|
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
|
||||||
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
||||||
|
private readonly PlayerFactionService _playerFaction = new();
|
||||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||||
private readonly Queue<WorldDelta> _history = [];
|
private readonly Queue<WorldDelta> _history = [];
|
||||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
||||||
@@ -74,6 +75,156 @@ public sealed class WorldService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request);
|
||||||
|
if (ship is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetShipSnapshotUnsafe(ship.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShipSnapshot? RemoveShipOrder(string shipId, string orderId)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId);
|
||||||
|
if (ship is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetShipSnapshotUnsafe(ship.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request);
|
||||||
|
if (ship is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetShipSnapshotUnsafe(ship.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? GetPlayerFaction()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.EnsureDomain(_world);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.CreateOrganization(_world, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.DeleteOrganization(_world, organizationId);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpdateOrganizationMembership(_world, organizationId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertDirective(_world, directiveId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.DeleteDirective(_world, directiveId);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertPolicy(_world, policyId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertProductionProgram(_world, productionProgramId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpsertAssignment(_world, assetId, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_playerFaction.UpdateStrategicIntent(_world, request);
|
||||||
|
return GetPlayerFactionSnapshotUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||||
@@ -158,7 +309,9 @@ public sealed class WorldService(
|
|||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
[]);
|
[],
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
|
||||||
_history.Enqueue(resetDelta);
|
_history.Enqueue(resetDelta);
|
||||||
foreach (var subscriber in _subscribers.Values.ToList())
|
foreach (var subscriber in _subscribers.Values.ToList())
|
||||||
@@ -203,6 +356,12 @@ public sealed class WorldService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
|
||||||
|
_engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId);
|
||||||
|
|
||||||
|
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
|
||||||
|
_engine.BuildSnapshot(_world, _sequence).PlayerFaction;
|
||||||
|
|
||||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||||
delta.RequiresSnapshotRefresh
|
delta.RequiresSnapshotRefresh
|
||||||
|| delta.Events.Count > 0
|
|| delta.Events.Count > 0
|
||||||
@@ -214,7 +373,9 @@ public sealed class WorldService(
|
|||||||
|| delta.MarketOrders.Count > 0
|
|| delta.MarketOrders.Count > 0
|
||||||
|| delta.Policies.Count > 0
|
|| delta.Policies.Count > 0
|
||||||
|| delta.Ships.Count > 0
|
|| delta.Ships.Count > 0
|
||||||
|| delta.Factions.Count > 0;
|
|| delta.Factions.Count > 0
|
||||||
|
|| delta.PlayerFaction is not null
|
||||||
|
|| delta.Geopolitics is not null;
|
||||||
|
|
||||||
private void Unsubscribe(Guid subscriberId)
|
private void Unsubscribe(Guid subscriberId)
|
||||||
{
|
{
|
||||||
@@ -261,6 +422,8 @@ public sealed class WorldService(
|
|||||||
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
|
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
|
||||||
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
|
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
|
||||||
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
|
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
|
||||||
|
PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null,
|
||||||
|
Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null,
|
||||||
Scope = scope,
|
Scope = scope,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WorldGeneration": {
|
"WorldGeneration": {
|
||||||
"TargetSystemCount": 10,
|
"TargetSystemCount": 2,
|
||||||
"IncludeSolSystem": true
|
"IncludeSolSystem": true,
|
||||||
|
"AiControllerFactionCount": 0,
|
||||||
|
"GeneratePlayerFaction": false
|
||||||
},
|
},
|
||||||
"OrbitalSimulation": {
|
"OrbitalSimulation": {
|
||||||
"SimulatedSecondsPerRealSecond": 0
|
"SimulatedSecondsPerRealSecond": 0
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
||||||
import type { TelemetrySnapshot } from "./contractsTelemetry";
|
import type { TelemetrySnapshot } from "./contractsTelemetry";
|
||||||
import type { BalanceSettings } from "./contractsBalance";
|
import type { BalanceSettings } from "./contractsBalance";
|
||||||
|
import type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||||
|
import type { ShipSnapshot } from "./contractsShips";
|
||||||
|
import type {
|
||||||
|
PlayerAssetAssignmentCommandRequest,
|
||||||
|
PlayerAutomationPolicyCommandRequest,
|
||||||
|
PlayerDirectiveCommandRequest,
|
||||||
|
PlayerOrganizationCommandRequest,
|
||||||
|
PlayerOrganizationMembershipCommandRequest,
|
||||||
|
PlayerPolicyCommandRequest,
|
||||||
|
PlayerStrategicIntentCommandRequest,
|
||||||
|
} from "./playerFactionCommands";
|
||||||
|
import type {
|
||||||
|
ShipDefaultBehaviorCommandRequest,
|
||||||
|
ShipOrderCommandRequest,
|
||||||
|
} from "./shipCommands";
|
||||||
|
|
||||||
export interface WorldStreamScope {
|
export interface WorldStreamScope {
|
||||||
scopeKind?: string;
|
scopeKind?: string;
|
||||||
@@ -8,12 +23,16 @@ export interface WorldStreamScope {
|
|||||||
bubbleId?: string | null;
|
bubbleId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch("/api/world", { signal });
|
const response = await fetch(input, init);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`World request failed with ${response.status}`);
|
throw new Error(`${init?.method ?? "GET"} ${typeof input === "string" ? input : input.toString()} failed with ${response.status}`);
|
||||||
}
|
}
|
||||||
return response.json() as Promise<WorldSnapshot>;
|
return response.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
||||||
|
return fetchJson<WorldSnapshot>("/api/world", { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openWorldStream(
|
export function openWorldStream(
|
||||||
@@ -52,39 +71,114 @@ export function openWorldStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTelemetry(signal?: AbortSignal) {
|
export async function fetchTelemetry(signal?: AbortSignal) {
|
||||||
const response = await fetch("/api/telemetry", { signal });
|
return fetchJson<TelemetrySnapshot>("/api/telemetry", { signal });
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Telemetry request failed with ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json() as Promise<TelemetrySnapshot>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchBalance(signal?: AbortSignal) {
|
export async function fetchBalance(signal?: AbortSignal) {
|
||||||
const response = await fetch("/api/balance", { signal });
|
return fetchJson<BalanceSettings>("/api/balance", { signal });
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Balance request failed with ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json() as Promise<BalanceSettings>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateBalance(settings: BalanceSettings) {
|
export async function updateBalance(settings: BalanceSettings) {
|
||||||
const response = await fetch("/api/balance", {
|
return fetchJson<BalanceSettings>("/api/balance", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(settings),
|
body: JSON.stringify(settings),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Balance update failed with ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json() as Promise<BalanceSettings>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetWorld() {
|
export async function resetWorld() {
|
||||||
const response = await fetch("/api/world/reset", {
|
return fetchJson<WorldSnapshot>("/api/world/reset", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Reset request failed with ${response.status}`);
|
|
||||||
}
|
}
|
||||||
return response.json() as Promise<WorldSnapshot>;
|
|
||||||
|
export async function fetchPlayerFaction(signal?: AbortSignal) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction", { signal });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPlayerOrganization(request: PlayerOrganizationCommandRequest) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/organizations", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePlayerOrganization(organizationId: string) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePlayerOrganizationMembership(organizationId: string, request: PlayerOrganizationMembershipCommandRequest) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}/membership`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertPlayerDirective(request: PlayerDirectiveCommandRequest, directiveId?: string | null) {
|
||||||
|
const path = directiveId ? `/api/player-faction/directives/${directiveId}` : "/api/player-faction/directives";
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||||
|
method: directiveId ? "PUT" : "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePlayerDirective(directiveId: string) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/directives/${directiveId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertPlayerAssignment(assetId: string, request: PlayerAssetAssignmentCommandRequest) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/assignments/${assetId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertPlayerPolicy(request: PlayerPolicyCommandRequest, policyId?: string | null) {
|
||||||
|
const path = policyId ? `/api/player-faction/policies/${policyId}` : "/api/player-faction/policies";
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||||
|
method: policyId ? "PUT" : "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertPlayerAutomationPolicy(request: PlayerAutomationPolicyCommandRequest, automationPolicyId?: string | null) {
|
||||||
|
const path = automationPolicyId ? `/api/player-faction/automation-policies/${automationPolicyId}` : "/api/player-faction/automation-policies";
|
||||||
|
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||||
|
method: automationPolicyId ? "PUT" : "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePlayerStrategicIntent(request: PlayerStrategicIntentCommandRequest) {
|
||||||
|
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/strategic-intent", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enqueueShipOrder(shipId: string, request: ShipOrderCommandRequest) {
|
||||||
|
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/orders`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateShipDefaultBehavior(shipId: string, request: ShipDefaultBehaviorCommandRequest) {
|
||||||
|
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/default-behavior`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
206
apps/viewer/src/components/gm/GmGeopoliticsPanel.vue
Normal file
206
apps/viewer/src/components/gm/GmGeopoliticsPanel.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useGmStore } from "../../ui/stores/gmStore";
|
||||||
|
|
||||||
|
const gmStore = useGmStore();
|
||||||
|
|
||||||
|
const factionLabelById = computed(() =>
|
||||||
|
new Map(gmStore.factions.map((faction) => [faction.id, faction.label])),
|
||||||
|
);
|
||||||
|
|
||||||
|
function factionLabel(factionId?: string | null) {
|
||||||
|
if (!factionId) return "—";
|
||||||
|
return factionLabelById.value.get(factionId) ?? factionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function titleCase(value?: string | null) {
|
||||||
|
if (!value) return "—";
|
||||||
|
return value
|
||||||
|
.replace(/[-_]+/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function percent(value?: number | null) {
|
||||||
|
if (value == null || Number.isNaN(value)) return "—";
|
||||||
|
return `${Math.round(value * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relations = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.diplomacy.relations ?? [])]
|
||||||
|
.sort((left, right) => right.tensionScore - left.tensionScore || left.id.localeCompare(right.id))
|
||||||
|
.slice(0, 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
const incidents = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.diplomacy.incidents ?? [])]
|
||||||
|
.sort((left, right) => right.lastObservedAtUtc.localeCompare(left.lastObservedAtUtc))
|
||||||
|
.slice(0, 8),
|
||||||
|
);
|
||||||
|
|
||||||
|
const contestedSystems = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.territory.controlStates ?? [])]
|
||||||
|
.filter((state) => state.isContested)
|
||||||
|
.sort((left, right) => right.strategicValue - left.strategicValue || left.systemId.localeCompare(right.systemId))
|
||||||
|
.slice(0, 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
const frontLines = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.territory.frontLines ?? [])]
|
||||||
|
.sort((left, right) => right.pressureScore - left.pressureScore || left.id.localeCompare(right.id))
|
||||||
|
.slice(0, 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
const regions = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.economyRegions.regions ?? [])]
|
||||||
|
.sort((left, right) => left.label.localeCompare(right.label))
|
||||||
|
.slice(0, 16),
|
||||||
|
);
|
||||||
|
|
||||||
|
const bottlenecks = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.economyRegions.bottlenecks ?? [])]
|
||||||
|
.sort((left, right) => right.severity - left.severity || left.id.localeCompare(right.id))
|
||||||
|
.slice(0, 12),
|
||||||
|
);
|
||||||
|
|
||||||
|
const corridors = computed(() =>
|
||||||
|
[...(gmStore.geopolitics?.economyRegions.corridors ?? [])]
|
||||||
|
.sort((left, right) => right.riskScore - left.riskScore || left.id.localeCompare(right.id))
|
||||||
|
.slice(0, 12),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-5 p-4 text-xs text-white/90">
|
||||||
|
<div v-if="!gmStore.geopolitics" class="rounded border border-white/10 bg-white/5 p-4 text-white/60">
|
||||||
|
No geopolitical state loaded.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<section class="grid gap-3 md:grid-cols-4">
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||||
|
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Diplomacy</div>
|
||||||
|
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.diplomacy.relations.length }}</div>
|
||||||
|
<div class="text-white/60">relations</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||||
|
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Wars</div>
|
||||||
|
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.diplomacy.wars.length }}</div>
|
||||||
|
<div class="text-white/60">active conflicts</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||||
|
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Contested</div>
|
||||||
|
<div class="mt-2 text-lg font-semibold">{{ contestedSystems.length }}</div>
|
||||||
|
<div class="text-white/60">systems</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||||
|
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Regions</div>
|
||||||
|
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.economyRegions.regions.length }}</div>
|
||||||
|
<div class="text-white/60">economic regions</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-5 xl:grid-cols-2">
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Relations</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="relation in relations" :key="relation.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ factionLabel(relation.factionAId) }} vs {{ factionLabel(relation.factionBId) }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
{{ titleCase(relation.posture) }} · tension {{ percent(relation.tensionScore) }} · grievance {{ percent(relation.grievanceScore) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
Trade {{ relation.tradeAccessPolicy }} · Military {{ relation.militaryAccessPolicy }} · treaties {{ relation.activeTreatyIds.length }} · incidents {{ relation.activeIncidentIds.length }}<span v-if="relation.warStateId"> · war {{ relation.warStateId }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Incidents</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="incident in incidents" :key="incident.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ titleCase(incident.kind) }} · {{ incident.systemId ?? "no-system" }}</div>
|
||||||
|
<div class="mt-1 text-white/70">{{ incident.summary }}</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
Severity {{ incident.severity.toFixed(2) }} · Escalation {{ incident.escalationScore.toFixed(2) }} · {{ factionLabel(incident.sourceFactionId) }} → {{ factionLabel(incident.targetFactionId) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-5 xl:grid-cols-2">
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Territory</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="state in contestedSystems" :key="state.systemId" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ state.systemId }} · {{ titleCase(state.controlKind) }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
Control {{ state.controlScore.toFixed(1) }} · strategic {{ state.strategicValue.toFixed(1) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
Controller {{ factionLabel(state.controllerFactionId) }} · Claimant {{ factionLabel(state.primaryClaimantFactionId) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Front Lines</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="front in frontLines" :key="front.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ front.id }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
{{ titleCase(front.kind) }} · pressure {{ percent(front.pressureScore) }} · supply {{ percent(front.supplyRisk) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
{{ front.factionIds.map((id) => factionLabel(id)).join(" vs ") }}<span v-if="front.anchorSystemId"> · anchor {{ front.anchorSystemId }}</span><br>
|
||||||
|
{{ front.systemIds.join(", ") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid gap-5 xl:grid-cols-2">
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Economic Regions</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="region in regions" :key="region.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ region.label }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
{{ titleCase(region.kind) }} · core {{ region.coreSystemId }} · systems {{ region.systemIds.length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
Faction {{ factionLabel(region.factionId) }} · fronts {{ region.frontLineIds.length }} · corridors {{ region.corridorIds.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||||
|
<h3 class="text-sm font-semibold">Bottlenecks And Corridors</h3>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<div v-for="bottleneck in bottlenecks" :key="bottleneck.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ bottleneck.itemId }} · {{ bottleneck.regionId }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
{{ titleCase(bottleneck.cause) }} · severity {{ bottleneck.severity.toFixed(2) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="corridor in corridors" :key="corridor.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||||
|
<div class="font-medium">{{ corridor.id }}</div>
|
||||||
|
<div class="mt-1 text-white/70">
|
||||||
|
{{ titleCase(corridor.kind) }} · {{ titleCase(corridor.accessState) }} · risk {{ percent(corridor.riskScore) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-white/55">
|
||||||
|
Path {{ corridor.systemPathIds.join(" → ") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
} from "@tanstack/vue-table";
|
} from "@tanstack/vue-table";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import GmWindow from "./GmWindow.vue";
|
import GmWindow from "./GmWindow.vue";
|
||||||
|
import GmPlayerFactionPanel from "./GmPlayerFactionPanel.vue";
|
||||||
|
import GmGeopoliticsPanel from "./GmGeopoliticsPanel.vue";
|
||||||
import { useGmStore } from "../../ui/stores/gmStore";
|
import { useGmStore } from "../../ui/stores/gmStore";
|
||||||
|
import { usePlayerFactionStore } from "../../ui/stores/playerFactionStore";
|
||||||
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||||
import type { ShipSnapshot } from "../../contractsShips";
|
import type { ShipSnapshot } from "../../contractsShips";
|
||||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
@@ -74,10 +77,11 @@ const emit = defineEmits<{
|
|||||||
focus: [id: string, kind: "ship" | "station"];
|
focus: [id: string, kind: "ship" | "station"];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type TabId = "ships" | "stations" | "factions";
|
type TabId = "ships" | "stations" | "factions" | "player" | "geopolitics";
|
||||||
const activeTab = ref<TabId>("ships");
|
const activeTab = ref<TabId>("ships");
|
||||||
|
|
||||||
const gmStore = useGmStore();
|
const gmStore = useGmStore();
|
||||||
|
const playerFactionStore = usePlayerFactionStore();
|
||||||
const selectionStore = useViewerSelectionStore();
|
const selectionStore = useViewerSelectionStore();
|
||||||
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||||
|
|
||||||
@@ -128,62 +132,51 @@ function formatCargoAmount(value: number | null | undefined) {
|
|||||||
return value.toFixed(2).replace(/\.?0+$/, "");
|
return value.toFixed(2).replace(/\.?0+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPercent(value: number | null | undefined) {
|
||||||
|
if (value == null || Number.isNaN(value)) return "—";
|
||||||
|
return `${Math.round(value * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLeadCampaign(faction: FactionSnapshot) {
|
||||||
|
return [...faction.strategicState.campaigns]
|
||||||
|
.sort((left, right) => right.priority - left.priority)
|
||||||
|
.find((campaign) => campaign.status !== "completed" && campaign.status !== "cancelled")
|
||||||
|
?? faction.strategicState.campaigns[0];
|
||||||
|
}
|
||||||
|
|
||||||
function getLeadObjective(faction: FactionSnapshot) {
|
function getLeadObjective(faction: FactionSnapshot) {
|
||||||
return [...(faction.objectives ?? [])]
|
return [...faction.strategicState.objectives]
|
||||||
.sort((left, right) => right.priority - left.priority)
|
.sort((left, right) => right.priority - left.priority)
|
||||||
.find((objective) => objective.state !== "Complete" && objective.state !== "Cancelled")
|
.find((objective) => objective.status !== "completed" && objective.status !== "cancelled")
|
||||||
?? faction.objectives?.[0];
|
?? faction.strategicState.objectives[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLeadStep(faction: FactionSnapshot) {
|
function getLatestDecision(faction: FactionSnapshot) {
|
||||||
const objective = getLeadObjective(faction);
|
return [...faction.decisionLog]
|
||||||
return [...(objective?.steps ?? [])]
|
.sort((left, right) => right.occurredAtUtc.localeCompare(left.occurredAtUtc))[0];
|
||||||
.sort((left, right) => right.priority - left.priority)
|
|
||||||
.find((step) => step.status !== "Complete" && step.status !== "Cancelled")
|
|
||||||
?? objective?.steps?.[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLeadTask(faction: FactionSnapshot) {
|
|
||||||
return [...(faction.issuedTasks ?? [])]
|
|
||||||
.sort((left, right) => right.priority - left.priority)
|
|
||||||
.find((task) => task.state !== "Complete" && task.state !== "Cancelled")
|
|
||||||
?? faction.issuedTasks?.[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeCommodityState(faction: FactionSnapshot, itemId: string, shortLabel: string) {
|
function describeCommodityState(faction: FactionSnapshot, itemId: string, shortLabel: string) {
|
||||||
const signal = faction.blackboard?.commoditySignals.find((entry) => entry.itemId === itemId);
|
const signal = faction.strategicState.economicAssessment.commoditySignals.find((entry) => entry.itemId === itemId);
|
||||||
if (!signal) return `${shortLabel} —`;
|
if (!signal) return `${shortLabel} —`;
|
||||||
return `${shortLabel} ${titleCaseToken(signal.level)} ${compactRate(signal.projectedNetRatePerSecond)}`;
|
return `${shortLabel} ${titleCaseToken(signal.level)} ${compactRate(signal.projectedNetRatePerSecond)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeFactionStrategicState(faction: FactionSnapshot) {
|
function describeFactionStrategicState(faction: FactionSnapshot) {
|
||||||
|
const campaign = getLeadCampaign(faction);
|
||||||
const objective = getLeadObjective(faction);
|
const objective = getLeadObjective(faction);
|
||||||
if (!objective) return "No objectives";
|
if (!campaign && !objective) return "No campaigns";
|
||||||
return `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.state)}`;
|
if (!campaign) return `${titleCaseToken(objective?.kind)} · ${titleCaseToken(objective?.status)}`;
|
||||||
}
|
return `${titleCaseToken(campaign.kind)} · ${titleCaseToken(campaign.status)}`;
|
||||||
|
|
||||||
function describeFactionLeadStep(faction: FactionSnapshot) {
|
|
||||||
const step = getLeadStep(faction);
|
|
||||||
if (!step) return "No steps";
|
|
||||||
const target = step.commodityId ?? step.moduleId ?? step.targetFactionId ?? step.targetSiteId;
|
|
||||||
return target
|
|
||||||
? `${titleCaseToken(step.kind)} · ${titleCaseToken(step.status)} · ${target}`
|
|
||||||
: `${titleCaseToken(step.kind)} · ${titleCaseToken(step.status)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeFactionLeadTask(faction: FactionSnapshot) {
|
function describeFactionLeadTask(faction: FactionSnapshot) {
|
||||||
const task = getLeadTask(faction);
|
const objective = getLeadObjective(faction);
|
||||||
if (!task) return "No tasks";
|
if (!objective) return "No objectives";
|
||||||
const target = task.shipRole ?? task.commodityId ?? task.moduleId ?? task.targetFactionId ?? task.targetSiteId;
|
const target = objective.itemId ?? objective.targetEntityId ?? objective.targetSystemId ?? objective.homeStationId;
|
||||||
return target
|
return target
|
||||||
? `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)} · ${target}`
|
? `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)} · ${target}`
|
||||||
: `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)}`;
|
: `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)}`;
|
||||||
}
|
|
||||||
|
|
||||||
function describeFactionPriority(faction: FactionSnapshot) {
|
|
||||||
const priority = [...(faction.strategicPriorities ?? [])]
|
|
||||||
.sort((left, right) => right.priority - left.priority)[0];
|
|
||||||
return priority ? `${titleCaseToken(priority.goalName)} · ${compactNumber(priority.priority, 0)}` : "—";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeFactionEconomy(faction: FactionSnapshot) {
|
function describeFactionEconomy(faction: FactionSnapshot) {
|
||||||
@@ -195,9 +188,57 @@ function describeFactionEconomy(faction: FactionSnapshot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function describeFactionThreat(faction: FactionSnapshot) {
|
function describeFactionThreat(faction: FactionSnapshot) {
|
||||||
const blackboard = faction.blackboard;
|
const threat = faction.strategicState.threatAssessment;
|
||||||
if (!blackboard) return "—";
|
return `Enemy ships ${threat.enemyShipCount} · stations ${threat.enemyStationCount}`;
|
||||||
return `Enemy ships ${blackboard.enemyShipCount} · stations ${blackboard.enemyStationCount}`;
|
}
|
||||||
|
|
||||||
|
function describeFactionCommitments(faction: FactionSnapshot) {
|
||||||
|
const economic = faction.strategicState.economicAssessment;
|
||||||
|
return `Mil ${economic.militaryShipCount}/${economic.targetMilitaryShipCount} · Min ${economic.minerShipCount}/${economic.targetMinerShipCount} · Tr ${economic.transportShipCount}/${economic.targetTransportShipCount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionReserves(faction: FactionSnapshot) {
|
||||||
|
const budget = faction.strategicState.budget;
|
||||||
|
return `Assets ${budget.reservedMilitaryAssets}/${budget.reservedLogisticsAssets}/${budget.reservedConstructionAssets} · Credits ${compactNumber(budget.reservedCredits, 0)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionBottleneck(faction: FactionSnapshot) {
|
||||||
|
const economic = faction.strategicState.economicAssessment;
|
||||||
|
if (!economic.industrialBottleneckItemId) {
|
||||||
|
return `None · sustain ${formatPercent(economic.sustainmentScore)}`;
|
||||||
|
}
|
||||||
|
return `${economic.industrialBottleneckItemId} · sustain ${formatPercent(economic.sustainmentScore)} · replace ${formatPercent(economic.replacementPressure)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionIntent(faction: FactionSnapshot) {
|
||||||
|
const latestDecision = getLatestDecision(faction);
|
||||||
|
const leadCampaign = getLeadCampaign(faction);
|
||||||
|
if (!leadCampaign) return latestDecision?.summary ?? "—";
|
||||||
|
const pause = leadCampaign.pauseReason ? ` · ${leadCampaign.pauseReason}` : "";
|
||||||
|
return `${titleCaseToken(leadCampaign.kind)} · ${titleCaseToken(leadCampaign.status)}${pause}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionMemory(faction: FactionSnapshot) {
|
||||||
|
const topSystem = [...faction.memory.systems]
|
||||||
|
.sort((left, right) => (right.frontierPressure + right.routeRisk + right.historicalShortagePressure)
|
||||||
|
- (left.frontierPressure + left.routeRisk + left.historicalShortagePressure))[0];
|
||||||
|
const topCommodity = [...faction.memory.commodities]
|
||||||
|
.sort((left, right) => right.historicalShortageScore - left.historicalShortageScore)[0];
|
||||||
|
if (!topSystem && !topCommodity) return "—";
|
||||||
|
return `${topSystem ? `${topSystem.systemId} fp ${compactNumber(topSystem.frontierPressure, 1)}` : "no-front"}${topCommodity ? ` · ${topCommodity.itemId} hs ${compactNumber(topCommodity.historicalShortageScore, 1)}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionDecision(faction: FactionSnapshot) {
|
||||||
|
const latestDecision = getLatestDecision(faction);
|
||||||
|
return latestDecision ? `${titleCaseToken(latestDecision.kind)} · ${latestDecision.summary}` : "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeFactionFronts(faction: FactionSnapshot) {
|
||||||
|
const activeTheaters = faction.strategicState.theaters.filter((theater) => theater.status === "active");
|
||||||
|
const defense = activeTheaters.filter((theater) => theater.kind.includes("defense")).length;
|
||||||
|
const offense = activeTheaters.filter((theater) => theater.kind.includes("offense")).length;
|
||||||
|
const economy = activeTheaters.filter((theater) => theater.kind.includes("economic")).length;
|
||||||
|
return `${activeTheaters.length} active · D ${defense} · O ${offense} · E ${economy}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ships table ────────────────────────────────────────────────────────────
|
// ── Ships table ────────────────────────────────────────────────────────────
|
||||||
@@ -210,17 +251,23 @@ type ShipRow = {
|
|||||||
faction: string;
|
faction: string;
|
||||||
system: string;
|
system: string;
|
||||||
state: string;
|
state: string;
|
||||||
objective: string;
|
assignment: string;
|
||||||
behavior: string;
|
behavior: string;
|
||||||
phase: string;
|
orders: string;
|
||||||
action: string;
|
plan: string;
|
||||||
task: string;
|
step: string;
|
||||||
|
subtask: string;
|
||||||
cargo: number;
|
cargo: number;
|
||||||
health: number;
|
health: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const shipRows = computed<ShipRow[]>(() =>
|
const shipRows = computed<ShipRow[]>(() =>
|
||||||
gmStore.ships.map((s) => ({
|
gmStore.ships.map((s) => {
|
||||||
|
const topOrder = [...s.orderQueue]
|
||||||
|
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||||
|
const currentStep = s.activePlan?.steps[s.activePlan.currentStepIndex];
|
||||||
|
const currentSubTask = s.activeSubTasks[0];
|
||||||
|
return {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
label: s.label,
|
label: s.label,
|
||||||
class: s.class,
|
class: s.class,
|
||||||
@@ -228,14 +275,16 @@ const shipRows = computed<ShipRow[]>(() =>
|
|||||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||||
system: s.systemId,
|
system: s.systemId,
|
||||||
state: titleCaseToken(s.state),
|
state: titleCaseToken(s.state),
|
||||||
objective: s.commanderObjective ? titleCaseToken(s.commanderObjective) : "—",
|
assignment: s.assignment ? titleCaseToken(s.assignment.kind) : "—",
|
||||||
behavior: titleCaseToken(s.defaultBehaviorKind),
|
behavior: titleCaseToken(s.defaultBehavior.kind),
|
||||||
phase: s.behaviorPhase ? titleCaseToken(s.behaviorPhase) : "—",
|
orders: topOrder ? `${titleCaseToken(topOrder.kind)} · ${s.orderQueue.length}` : "—",
|
||||||
action: s.currentAction ? `${s.currentAction.label} ${Math.round(s.currentAction.progress * 100)}%` : "—",
|
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
|
||||||
task: titleCaseToken(s.controllerTaskKind),
|
step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—",
|
||||||
|
subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—",
|
||||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||||
health: Math.round(s.health),
|
health: Math.round(s.health),
|
||||||
})),
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const shipColumnHelper = createColumnHelper<ShipRow>();
|
const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||||
@@ -250,11 +299,12 @@ const shipColumns = [
|
|||||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||||
shipColumnHelper.accessor("system", { header: "System" }),
|
shipColumnHelper.accessor("system", { header: "System" }),
|
||||||
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
||||||
shipColumnHelper.accessor("objective", { header: "Commander Objective" }),
|
shipColumnHelper.accessor("assignment", { header: "Assignment" }),
|
||||||
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
||||||
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
shipColumnHelper.accessor("orders", { header: "Orders" }),
|
||||||
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
shipColumnHelper.accessor("plan", { header: "Plan" }),
|
||||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
shipColumnHelper.accessor("step", { header: "Current Step" }),
|
||||||
|
shipColumnHelper.accessor("subtask", { header: "SubTask" }),
|
||||||
shipColumnHelper.accessor("cargo", {
|
shipColumnHelper.accessor("cargo", {
|
||||||
header: "Cargo",
|
header: "Cargo",
|
||||||
cell: (info) => formatCargoAmount(info.getValue()),
|
cell: (info) => formatCargoAmount(info.getValue()),
|
||||||
@@ -264,7 +314,7 @@ const shipColumns = [
|
|||||||
|
|
||||||
const shipFilter = ref("");
|
const shipFilter = ref("");
|
||||||
const shipSorting = ref<SortingState>([]);
|
const shipSorting = ref<SortingState>([]);
|
||||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask", "cargo", "health"]);
|
||||||
|
|
||||||
const shipTable = useVueTable({
|
const shipTable = useVueTable({
|
||||||
get data() { return shipRows.value; },
|
get data() { return shipRows.value; },
|
||||||
@@ -383,14 +433,18 @@ type FactionRow = {
|
|||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
planCycle: number;
|
planCycle: number;
|
||||||
priority: string;
|
posture: string;
|
||||||
strategicState: string;
|
fronts: string;
|
||||||
leadStep: string;
|
leadCampaign: string;
|
||||||
leadTask: string;
|
leadObjective: string;
|
||||||
warReadiness: string;
|
commitments: string;
|
||||||
|
reserves: string;
|
||||||
|
bottleneck: string;
|
||||||
|
intent: string;
|
||||||
|
decision: string;
|
||||||
|
memory: string;
|
||||||
economy: string;
|
economy: string;
|
||||||
threat: string;
|
threat: string;
|
||||||
fleets: string;
|
|
||||||
systems: string;
|
systems: string;
|
||||||
credits: number;
|
credits: number;
|
||||||
population: number;
|
population: number;
|
||||||
@@ -399,29 +453,29 @@ type FactionRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const factionRows = computed<FactionRow[]>(() =>
|
const factionRows = computed<FactionRow[]>(() =>
|
||||||
gmStore.factions.map((f) => {
|
gmStore.factions.map((f) => ({
|
||||||
const assessment = f.strategicAssessment;
|
|
||||||
const blackboard = f.blackboard;
|
|
||||||
return {
|
|
||||||
id: f.id,
|
id: f.id,
|
||||||
label: f.label,
|
label: f.label,
|
||||||
color: f.color,
|
color: f.color,
|
||||||
planCycle: blackboard?.planCycle ?? 0,
|
planCycle: f.strategicState.planCycle,
|
||||||
priority: describeFactionPriority(f),
|
posture: `${titleCaseToken(f.doctrine.strategicPosture)} · ${titleCaseToken(f.doctrine.militaryPosture)} · ${titleCaseToken(f.doctrine.economicPosture)}`,
|
||||||
strategicState: describeFactionStrategicState(f),
|
fronts: describeFactionFronts(f),
|
||||||
leadStep: describeFactionLeadStep(f),
|
leadCampaign: describeFactionStrategicState(f),
|
||||||
leadTask: describeFactionLeadTask(f),
|
leadObjective: describeFactionLeadTask(f),
|
||||||
warReadiness: `Industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"} · Shipyard ${blackboard?.hasShipyard ? "yes" : "no"}${blackboard?.hasActiveExpansionProject ? ` · Expanding ${blackboard.activeExpansionCommodityId ?? blackboard.activeExpansionModuleId ?? "site"}` : ""}`,
|
commitments: describeFactionCommitments(f),
|
||||||
|
reserves: describeFactionReserves(f),
|
||||||
|
bottleneck: describeFactionBottleneck(f),
|
||||||
|
intent: describeFactionIntent(f),
|
||||||
|
decision: describeFactionDecision(f),
|
||||||
|
memory: describeFactionMemory(f),
|
||||||
economy: describeFactionEconomy(f),
|
economy: describeFactionEconomy(f),
|
||||||
threat: describeFactionThreat(f),
|
threat: describeFactionThreat(f),
|
||||||
fleets: assessment ? `M ${assessment.militaryShipCount}/${blackboard?.targetWarshipCount ?? 0} · Mn ${assessment.minerShipCount} · Tr ${assessment.transportShipCount} · Cn ${assessment.constructorShipCount}` : "—",
|
systems: `${f.strategicState.economicAssessment.controlledSystemCount} / ${f.doctrine.desiredControlledSystems}`,
|
||||||
systems: assessment ? `${assessment.controlledSystemCount} / ${assessment.targetSystemCount}` : "—",
|
|
||||||
credits: Math.round(f.credits),
|
credits: Math.round(f.credits),
|
||||||
population: Math.round(f.populationTotal),
|
population: Math.round(f.populationTotal),
|
||||||
shipsBuilt: f.shipsBuilt,
|
shipsBuilt: f.shipsBuilt,
|
||||||
shipsLost: f.shipsLost,
|
shipsLost: f.shipsLost,
|
||||||
};
|
})),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||||
@@ -432,14 +486,18 @@ const factionColumns = [
|
|||||||
cell: (info) => renderColorCell(info.getValue()),
|
cell: (info) => renderColorCell(info.getValue()),
|
||||||
}),
|
}),
|
||||||
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
||||||
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
|
factionColumnHelper.accessor("posture", { header: "Posture" }),
|
||||||
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
|
factionColumnHelper.accessor("fronts", { header: "Fronts" }),
|
||||||
factionColumnHelper.accessor("leadStep", { header: "Lead Step" }),
|
factionColumnHelper.accessor("leadCampaign", { header: "Lead Campaign" }),
|
||||||
factionColumnHelper.accessor("leadTask", { header: "Issued Task" }),
|
factionColumnHelper.accessor("leadObjective", { header: "Lead Objective" }),
|
||||||
factionColumnHelper.accessor("warReadiness", { header: "Campaign State" }),
|
factionColumnHelper.accessor("commitments", { header: "Commitments" }),
|
||||||
|
factionColumnHelper.accessor("reserves", { header: "Reserves" }),
|
||||||
|
factionColumnHelper.accessor("bottleneck", { header: "Bottleneck" }),
|
||||||
|
factionColumnHelper.accessor("intent", { header: "Strategic Intent" }),
|
||||||
|
factionColumnHelper.accessor("decision", { header: "Recent Decision" }),
|
||||||
|
factionColumnHelper.accessor("memory", { header: "Memory" }),
|
||||||
factionColumnHelper.accessor("economy", { header: "Economy" }),
|
factionColumnHelper.accessor("economy", { header: "Economy" }),
|
||||||
factionColumnHelper.accessor("threat", { header: "Threat" }),
|
factionColumnHelper.accessor("threat", { header: "Threat" }),
|
||||||
factionColumnHelper.accessor("fleets", { header: "Fleets" }),
|
|
||||||
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
||||||
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
||||||
factionColumnHelper.accessor("population", { header: "Pop" }),
|
factionColumnHelper.accessor("population", { header: "Pop" }),
|
||||||
@@ -449,7 +507,7 @@ const factionColumns = [
|
|||||||
|
|
||||||
const factionFilter = ref("");
|
const factionFilter = ref("");
|
||||||
const factionSorting = ref<SortingState>([]);
|
const factionSorting = ref<SortingState>([]);
|
||||||
const factionOrder = useColumnOrder(["label", "color", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
const factionOrder = useColumnOrder(["label", "color", "planCycle", "posture", "fronts", "leadCampaign", "leadObjective", "commitments", "reserves", "bottleneck", "intent", "decision", "memory", "economy", "threat", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||||
|
|
||||||
const factionTable = useVueTable({
|
const factionTable = useVueTable({
|
||||||
get data() { return factionRows.value; },
|
get data() { return factionRows.value; },
|
||||||
@@ -472,6 +530,8 @@ const factionTable = useVueTable({
|
|||||||
// ── Row counts ─────────────────────────────────────────────────────────────
|
// ── Row counts ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const tabs: { id: TabId; label: string }[] = [
|
const tabs: { id: TabId; label: string }[] = [
|
||||||
|
{ id: "player", label: "Player" },
|
||||||
|
{ id: "geopolitics", label: "Geopolitics" },
|
||||||
{ id: "ships", label: "Ships" },
|
{ id: "ships", label: "Ships" },
|
||||||
{ id: "stations", label: "Stations" },
|
{ id: "stations", label: "Stations" },
|
||||||
{ id: "factions", label: "Factions" },
|
{ id: "factions", label: "Factions" },
|
||||||
@@ -479,11 +539,15 @@ const tabs: { id: TabId; label: string }[] = [
|
|||||||
|
|
||||||
const activeFilter = computed({
|
const activeFilter = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
|
if (activeTab.value === "player") return "";
|
||||||
|
if (activeTab.value === "geopolitics") return "";
|
||||||
if (activeTab.value === "ships") return shipFilter.value;
|
if (activeTab.value === "ships") return shipFilter.value;
|
||||||
if (activeTab.value === "stations") return stationFilter.value;
|
if (activeTab.value === "stations") return stationFilter.value;
|
||||||
return factionFilter.value;
|
return factionFilter.value;
|
||||||
},
|
},
|
||||||
set: (v: string) => {
|
set: (v: string) => {
|
||||||
|
if (activeTab.value === "player") return;
|
||||||
|
if (activeTab.value === "geopolitics") return;
|
||||||
if (activeTab.value === "ships") shipFilter.value = v;
|
if (activeTab.value === "ships") shipFilter.value = v;
|
||||||
else if (activeTab.value === "stations") stationFilter.value = v;
|
else if (activeTab.value === "stations") stationFilter.value = v;
|
||||||
else factionFilter.value = v;
|
else factionFilter.value = v;
|
||||||
@@ -491,12 +555,24 @@ const activeFilter = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const activeRowCount = computed(() => {
|
const activeRowCount = computed(() => {
|
||||||
|
if (activeTab.value === "player") {
|
||||||
|
return (playerFactionStore.playerFaction?.assetRegistry.shipIds.length ?? 0)
|
||||||
|
+ (playerFactionStore.playerFaction?.assetRegistry.stationIds.length ?? 0);
|
||||||
|
}
|
||||||
|
if (activeTab.value === "geopolitics") {
|
||||||
|
const geopolitics = gmStore.geopolitics;
|
||||||
|
return (geopolitics?.diplomacy.relations.length ?? 0)
|
||||||
|
+ (geopolitics?.territory.controlStates.length ?? 0)
|
||||||
|
+ (geopolitics?.economyRegions.regions.length ?? 0);
|
||||||
|
}
|
||||||
if (activeTab.value === "ships") return shipTable.getFilteredRowModel().rows.length;
|
if (activeTab.value === "ships") return shipTable.getFilteredRowModel().rows.length;
|
||||||
if (activeTab.value === "stations") return stationTable.getFilteredRowModel().rows.length;
|
if (activeTab.value === "stations") return stationTable.getFilteredRowModel().rows.length;
|
||||||
return factionTable.getFilteredRowModel().rows.length;
|
return factionTable.getFilteredRowModel().rows.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const activeTotalCount = computed(() => {
|
const activeTotalCount = computed(() => {
|
||||||
|
if (activeTab.value === "player") return activeRowCount.value;
|
||||||
|
if (activeTab.value === "geopolitics") return activeRowCount.value;
|
||||||
if (activeTab.value === "ships") return gmStore.ships.length;
|
if (activeTab.value === "ships") return gmStore.ships.length;
|
||||||
if (activeTab.value === "stations") return gmStore.stations.length;
|
if (activeTab.value === "stations") return gmStore.stations.length;
|
||||||
return gmStore.factions.length;
|
return gmStore.factions.length;
|
||||||
@@ -558,7 +634,7 @@ function hideOrdersTooltip() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<GmWindow
|
<GmWindow
|
||||||
title="AI States"
|
title="Empire / AI States"
|
||||||
:initial-width="980"
|
:initial-width="980"
|
||||||
:initial-height="560"
|
:initial-height="560"
|
||||||
:initial-x="80"
|
:initial-x="80"
|
||||||
@@ -581,7 +657,7 @@ function hideOrdersTooltip() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex-1">
|
<div v-if="activeTab !== 'player' && activeTab !== 'geopolitics'" class="relative flex-1">
|
||||||
<input
|
<input
|
||||||
v-model="activeFilter"
|
v-model="activeFilter"
|
||||||
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
||||||
@@ -600,12 +676,30 @@ function hideOrdersTooltip() {
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="flex-1 text-xs opacity-60">
|
||||||
|
{{ activeTab === "player" ? "Player empire control, policy, and observability." : "Diplomacy, territory, and regional economy observability." }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="gm-row-count shrink-0 font-mono text-xs tabular-nums opacity-60">
|
<span class="gm-row-count shrink-0 font-mono text-xs tabular-nums opacity-60">
|
||||||
{{ activeRowCount }} / {{ activeTotalCount }}
|
{{ activeRowCount }} / {{ activeTotalCount }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Ships table -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'player'"
|
||||||
|
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||||
|
>
|
||||||
|
<GmPlayerFactionPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'geopolitics'"
|
||||||
|
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||||
|
>
|
||||||
|
<GmGeopoliticsPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ships table -->
|
<!-- Ships table -->
|
||||||
<div
|
<div
|
||||||
v-show="activeTab === 'ships'"
|
v-show="activeTab === 'ships'"
|
||||||
|
|||||||
1162
apps/viewer/src/components/gm/GmPlayerFactionPanel.vue
Normal file
1162
apps/viewer/src/components/gm/GmPlayerFactionPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,3 +37,5 @@ export type {
|
|||||||
ShipTransitSnapshot,
|
ShipTransitSnapshot,
|
||||||
} from "./contractsShips";
|
} from "./contractsShips";
|
||||||
export type { FactionSnapshot, FactionDelta } from "./contractsFactions";
|
export type { FactionSnapshot, FactionDelta } from "./contractsFactions";
|
||||||
|
export type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||||
|
export type { GeopoliticalStateSnapshot } from "./contractsGeopolitics";
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export interface PolicySetSnapshot {
|
|||||||
dockingAccessPolicy: string;
|
dockingAccessPolicy: string;
|
||||||
constructionAccessPolicy: string;
|
constructionAccessPolicy: string;
|
||||||
operationalRangePolicy: string;
|
operationalRangePolicy: string;
|
||||||
|
combatEngagementPolicy: string;
|
||||||
|
avoidHostileSystems: boolean;
|
||||||
|
fleeHullRatio: number;
|
||||||
|
blacklistedSystemIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PolicySetDelta extends PolicySetSnapshot {}
|
export interface PolicySetDelta extends PolicySetSnapshot {}
|
||||||
|
|||||||
@@ -1,41 +1,81 @@
|
|||||||
export interface FactionPlanningStateSnapshot {
|
import type { Vector3Dto } from "./contractsCommon";
|
||||||
militaryShipCount: number;
|
|
||||||
minerShipCount: number;
|
export interface FactionDoctrineSnapshot {
|
||||||
transportShipCount: number;
|
strategicPosture: string;
|
||||||
constructorShipCount: number;
|
expansionPosture: string;
|
||||||
controlledSystemCount: number;
|
militaryPosture: string;
|
||||||
targetSystemCount: number;
|
economicPosture: string;
|
||||||
hasShipFactory: boolean;
|
desiredControlledSystems: number;
|
||||||
oreStockpile: number;
|
desiredMilitaryPerFront: number;
|
||||||
refinedMetalsAvailableStock: number;
|
desiredMinersPerSystem: number;
|
||||||
refinedMetalsUsageRate: number;
|
desiredTransportsPerSystem: number;
|
||||||
refinedMetalsProjectedProductionRate: number;
|
desiredConstructors: number;
|
||||||
refinedMetalsProjectedNetRate: number;
|
reserveCreditsRatio: number;
|
||||||
refinedMetalsLevelSeconds: number;
|
expansionBudgetRatio: number;
|
||||||
refinedMetalsLevel: string;
|
warBudgetRatio: number;
|
||||||
hullpartsAvailableStock: number;
|
reserveMilitaryRatio: number;
|
||||||
hullpartsUsageRate: number;
|
offensiveReadinessThreshold: number;
|
||||||
hullpartsProjectedProductionRate: number;
|
supplySecurityBias: number;
|
||||||
hullpartsProjectedNetRate: number;
|
failureAversion: number;
|
||||||
hullpartsLevelSeconds: number;
|
reinforcementLeadPerFront: number;
|
||||||
hullpartsLevel: string;
|
|
||||||
claytronicsAvailableStock: number;
|
|
||||||
claytronicsUsageRate: number;
|
|
||||||
claytronicsProjectedProductionRate: number;
|
|
||||||
claytronicsProjectedNetRate: number;
|
|
||||||
claytronicsLevelSeconds: number;
|
|
||||||
claytronicsLevel: string;
|
|
||||||
waterAvailableStock: number;
|
|
||||||
waterUsageRate: number;
|
|
||||||
waterProjectedProductionRate: number;
|
|
||||||
waterProjectedNetRate: number;
|
|
||||||
waterLevelSeconds: number;
|
|
||||||
waterLevel: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionStrategicPrioritySnapshot {
|
export interface FactionSystemMemorySnapshot {
|
||||||
goalName: string;
|
systemId: string;
|
||||||
priority: number;
|
lastSeenAtUtc: string;
|
||||||
|
lastEnemyShipCount: number;
|
||||||
|
lastEnemyStationCount: number;
|
||||||
|
controlledByFaction: boolean;
|
||||||
|
lastRole?: string | null;
|
||||||
|
frontierPressure: number;
|
||||||
|
routeRisk: number;
|
||||||
|
historicalShortagePressure: number;
|
||||||
|
offensiveFailures: number;
|
||||||
|
defensiveFailures: number;
|
||||||
|
offensiveSuccesses: number;
|
||||||
|
defensiveSuccesses: number;
|
||||||
|
lastContestedAtUtc?: string | null;
|
||||||
|
lastShortageAtUtc?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionCommodityMemorySnapshot {
|
||||||
|
itemId: string;
|
||||||
|
historicalShortageScore: number;
|
||||||
|
historicalSurplusScore: number;
|
||||||
|
lastObservedBacklog: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
lastCriticalAtUtc?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionOutcomeRecordSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
summary: string;
|
||||||
|
relatedCampaignId?: string | null;
|
||||||
|
relatedObjectiveId?: string | null;
|
||||||
|
occurredAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionMemorySnapshot {
|
||||||
|
lastPlanCycle: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
lastObservedShipsBuilt: number;
|
||||||
|
lastObservedShipsLost: number;
|
||||||
|
lastObservedCredits: number;
|
||||||
|
knownSystemIds: string[];
|
||||||
|
knownEnemyFactionIds: string[];
|
||||||
|
systems: FactionSystemMemorySnapshot[];
|
||||||
|
commodities: FactionCommodityMemorySnapshot[];
|
||||||
|
recentOutcomes: FactionOutcomeRecordSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionBudgetSnapshot {
|
||||||
|
reservedCredits: number;
|
||||||
|
expansionCredits: number;
|
||||||
|
warCredits: number;
|
||||||
|
reservedMilitaryAssets: number;
|
||||||
|
reservedLogisticsAssets: number;
|
||||||
|
reservedConstructionAssets: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionCommoditySignalSnapshot {
|
export interface FactionCommoditySignalSnapshot {
|
||||||
@@ -54,96 +94,196 @@ export interface FactionCommoditySignalSnapshot {
|
|||||||
reservedForConstruction: number;
|
reservedForConstruction: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionThreatSignalSnapshot {
|
export interface FactionEconomicAssessmentSnapshot {
|
||||||
scopeId: string;
|
|
||||||
scopeKind: string;
|
|
||||||
enemyShipCount: number;
|
|
||||||
enemyStationCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FactionBlackboardSnapshot {
|
|
||||||
planCycle: number;
|
planCycle: number;
|
||||||
updatedAtUtc: string;
|
updatedAtUtc: string;
|
||||||
targetWarshipCount: number;
|
|
||||||
hasWarIndustrySupplyChain: boolean;
|
|
||||||
hasShipyard: boolean;
|
|
||||||
hasActiveExpansionProject: boolean;
|
|
||||||
activeExpansionCommodityId?: string | null;
|
|
||||||
activeExpansionModuleId?: string | null;
|
|
||||||
activeExpansionSiteId?: string | null;
|
|
||||||
activeExpansionSystemId?: string | null;
|
|
||||||
enemyFactionCount: number;
|
|
||||||
enemyShipCount: number;
|
|
||||||
enemyStationCount: number;
|
|
||||||
militaryShipCount: number;
|
militaryShipCount: number;
|
||||||
minerShipCount: number;
|
minerShipCount: number;
|
||||||
transportShipCount: number;
|
transportShipCount: number;
|
||||||
constructorShipCount: number;
|
constructorShipCount: number;
|
||||||
controlledSystemCount: number;
|
controlledSystemCount: number;
|
||||||
|
targetMilitaryShipCount: number;
|
||||||
|
targetMinerShipCount: number;
|
||||||
|
targetTransportShipCount: number;
|
||||||
|
targetConstructorShipCount: number;
|
||||||
|
hasShipyard: boolean;
|
||||||
|
hasWarIndustrySupplyChain: boolean;
|
||||||
|
primaryExpansionSiteId?: string | null;
|
||||||
|
primaryExpansionSystemId?: string | null;
|
||||||
|
replacementPressure: number;
|
||||||
|
sustainmentScore: number;
|
||||||
|
logisticsSecurityScore: number;
|
||||||
|
criticalShortageCount: number;
|
||||||
|
industrialBottleneckItemId?: string | null;
|
||||||
commoditySignals: FactionCommoditySignalSnapshot[];
|
commoditySignals: FactionCommoditySignalSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionThreatSignalSnapshot {
|
||||||
|
scopeId: string;
|
||||||
|
scopeKind: string;
|
||||||
|
enemyShipCount: number;
|
||||||
|
enemyStationCount: number;
|
||||||
|
enemyFactionId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionThreatAssessmentSnapshot {
|
||||||
|
planCycle: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
enemyFactionCount: number;
|
||||||
|
enemyShipCount: number;
|
||||||
|
enemyStationCount: number;
|
||||||
|
primaryThreatFactionId?: string | null;
|
||||||
|
primaryThreatSystemId?: string | null;
|
||||||
threatSignals: FactionThreatSignalSnapshot[];
|
threatSignals: FactionThreatSignalSnapshot[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FactionTheaterSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
systemId: string;
|
||||||
|
status: string;
|
||||||
|
priority: number;
|
||||||
|
supplyRisk: number;
|
||||||
|
friendlyAssetValue: number;
|
||||||
|
targetFactionId?: string | null;
|
||||||
|
anchorEntityId?: string | null;
|
||||||
|
anchorPosition?: Vector3Dto | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
campaignIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface FactionPlanStepSnapshot {
|
export interface FactionPlanStepSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
status: string;
|
status: string;
|
||||||
priority: number;
|
summary?: string | null;
|
||||||
commodityId?: string | null;
|
|
||||||
moduleId?: string | null;
|
|
||||||
targetFactionId?: string | null;
|
|
||||||
targetSiteId?: string | null;
|
|
||||||
blockingReason?: string | null;
|
blockingReason?: string | null;
|
||||||
notes?: string | null;
|
|
||||||
lastEvaluatedCycle: number;
|
|
||||||
dependencyStepIds: string[];
|
|
||||||
requiredFacts: string[];
|
|
||||||
producedFacts: string[];
|
|
||||||
assignedAssets: string[];
|
|
||||||
issuedTaskIds: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionIssuedTaskSnapshot {
|
export interface FactionCampaignSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
state: string;
|
status: string;
|
||||||
objectiveId: string;
|
|
||||||
stepId: string;
|
|
||||||
priority: number;
|
priority: number;
|
||||||
shipRole?: string | null;
|
theaterId?: string | null;
|
||||||
commodityId?: string | null;
|
|
||||||
moduleId?: string | null;
|
|
||||||
targetFactionId?: string | null;
|
targetFactionId?: string | null;
|
||||||
targetSystemId?: string | null;
|
targetSystemId?: string | null;
|
||||||
targetSiteId?: string | null;
|
targetEntityId?: string | null;
|
||||||
createdAtCycle: number;
|
commodityId?: string | null;
|
||||||
updatedAtCycle: number;
|
supportStationId?: string | null;
|
||||||
blockingReason?: string | null;
|
currentStepIndex: number;
|
||||||
notes?: string | null;
|
createdAtUtc: string;
|
||||||
assignedAssets: string[];
|
updatedAtUtc: string;
|
||||||
|
summary?: string | null;
|
||||||
|
pauseReason?: string | null;
|
||||||
|
continuationScore: number;
|
||||||
|
supplyAdequacy: number;
|
||||||
|
replacementPressure: number;
|
||||||
|
failureCount: number;
|
||||||
|
successCount: number;
|
||||||
|
fleetCommanderId?: string | null;
|
||||||
|
requiresReinforcement: boolean;
|
||||||
|
steps: FactionPlanStepSnapshot[];
|
||||||
|
objectiveIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionObjectiveSnapshot {
|
export interface FactionObjectiveSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
|
campaignId: string;
|
||||||
|
theaterId?: string | null;
|
||||||
kind: string;
|
kind: string;
|
||||||
state: string;
|
delegationKind: string;
|
||||||
|
behaviorKind: string;
|
||||||
|
status: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
parentObjectiveId?: string | null;
|
commanderId?: string | null;
|
||||||
targetFactionId?: string | null;
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
targetSystemId?: string | null;
|
targetSystemId?: string | null;
|
||||||
targetSiteId?: string | null;
|
targetEntityId?: string | null;
|
||||||
targetRegionId?: string | null;
|
targetPosition?: Vector3Dto | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
currentStepIndex: number;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
useOrders: boolean;
|
||||||
|
stagingOrderKind?: string | null;
|
||||||
|
reinforcementLevel: number;
|
||||||
|
steps: FactionPlanStepSnapshot[];
|
||||||
|
reservedAssetIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionReservationSnapshot {
|
||||||
|
id: string;
|
||||||
|
objectiveId: string;
|
||||||
|
campaignId?: string | null;
|
||||||
|
assetKind: string;
|
||||||
|
assetId: string;
|
||||||
|
priority: number;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionProductionProgramSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
priority: number;
|
||||||
|
campaignId?: string | null;
|
||||||
commodityId?: string | null;
|
commodityId?: string | null;
|
||||||
moduleId?: string | null;
|
moduleId?: string | null;
|
||||||
budgetWeight: number;
|
shipKind?: string | null;
|
||||||
slotCost: number;
|
targetSystemId?: string | null;
|
||||||
createdAtCycle: number;
|
targetCount: number;
|
||||||
updatedAtCycle: number;
|
currentCount: number;
|
||||||
invalidationReason?: string | null;
|
notes?: string | null;
|
||||||
blockingReason?: string | null;
|
}
|
||||||
prerequisiteObjectiveIds: string[];
|
|
||||||
assignedAssets: string[];
|
export interface FactionDecisionLogEntrySnapshot {
|
||||||
steps: FactionPlanStepSnapshot[];
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
summary: string;
|
||||||
|
relatedEntityId?: string | null;
|
||||||
|
planCycle: number;
|
||||||
|
occurredAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionStrategicStateSnapshot {
|
||||||
|
planCycle: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
status: string;
|
||||||
|
budget: FactionBudgetSnapshot;
|
||||||
|
economicAssessment: FactionEconomicAssessmentSnapshot;
|
||||||
|
threatAssessment: FactionThreatAssessmentSnapshot;
|
||||||
|
theaters: FactionTheaterSnapshot[];
|
||||||
|
campaigns: FactionCampaignSnapshot[];
|
||||||
|
objectives: FactionObjectiveSnapshot[];
|
||||||
|
reservations: FactionReservationSnapshot[];
|
||||||
|
productionPrograms: FactionProductionProgramSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommanderAssignmentSnapshot {
|
||||||
|
commanderId: string;
|
||||||
|
kind: string;
|
||||||
|
behaviorKind: string;
|
||||||
|
status: string;
|
||||||
|
objectiveId?: string | null;
|
||||||
|
campaignId?: string | null;
|
||||||
|
theaterId?: string | null;
|
||||||
|
parentCommanderId?: string | null;
|
||||||
|
controlledEntityId?: string | null;
|
||||||
|
priority: number;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
updatedAtUtc?: string | null;
|
||||||
|
activeObjectiveIds: string[];
|
||||||
|
subordinateCommanderIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionSnapshot {
|
export interface FactionSnapshot {
|
||||||
@@ -157,11 +297,11 @@ export interface FactionSnapshot {
|
|||||||
shipsBuilt: number;
|
shipsBuilt: number;
|
||||||
shipsLost: number;
|
shipsLost: number;
|
||||||
defaultPolicySetId?: string | null;
|
defaultPolicySetId?: string | null;
|
||||||
strategicAssessment?: FactionPlanningStateSnapshot | null;
|
doctrine: FactionDoctrineSnapshot;
|
||||||
strategicPriorities?: FactionStrategicPrioritySnapshot[] | null;
|
memory: FactionMemorySnapshot;
|
||||||
blackboard?: FactionBlackboardSnapshot | null;
|
strategicState: FactionStrategicStateSnapshot;
|
||||||
objectives?: FactionObjectiveSnapshot[] | null;
|
decisionLog: FactionDecisionLogEntrySnapshot[];
|
||||||
issuedTasks?: FactionIssuedTaskSnapshot[] | null;
|
commanders: CommanderAssignmentSnapshot[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionDelta extends FactionSnapshot {}
|
export interface FactionDelta extends FactionSnapshot {}
|
||||||
|
|||||||
307
apps/viewer/src/contractsGeopolitics.ts
Normal file
307
apps/viewer/src/contractsGeopolitics.ts
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
export interface SystemRouteLinkSnapshot {
|
||||||
|
id: string;
|
||||||
|
sourceSystemId: string;
|
||||||
|
destinationSystemId: string;
|
||||||
|
distance: number;
|
||||||
|
isPrimaryLane: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiplomaticRelationSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionAId: string;
|
||||||
|
factionBId: string;
|
||||||
|
status: string;
|
||||||
|
posture: string;
|
||||||
|
trustScore: number;
|
||||||
|
tensionScore: number;
|
||||||
|
grievanceScore: number;
|
||||||
|
tradeAccessPolicy: string;
|
||||||
|
militaryAccessPolicy: string;
|
||||||
|
warStateId?: string | null;
|
||||||
|
ceasefireUntilUtc?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
activeTreatyIds: string[];
|
||||||
|
activeIncidentIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreatySnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
tradeAccessPolicy: string;
|
||||||
|
militaryAccessPolicy: string;
|
||||||
|
summary?: string | null;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
factionIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiplomaticIncidentSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
sourceFactionId: string;
|
||||||
|
targetFactionId: string;
|
||||||
|
systemId?: string | null;
|
||||||
|
borderEdgeId?: string | null;
|
||||||
|
summary: string;
|
||||||
|
severity: number;
|
||||||
|
escalationScore: number;
|
||||||
|
createdAtUtc: string;
|
||||||
|
lastObservedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BorderTensionSnapshot {
|
||||||
|
id: string;
|
||||||
|
relationId: string;
|
||||||
|
borderEdgeId: string;
|
||||||
|
factionAId: string;
|
||||||
|
factionBId: string;
|
||||||
|
status: string;
|
||||||
|
tensionScore: number;
|
||||||
|
incidentScore: number;
|
||||||
|
militaryPressure: number;
|
||||||
|
accessFriction: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
systemIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarStateSnapshot {
|
||||||
|
id: string;
|
||||||
|
relationId: string;
|
||||||
|
factionAId: string;
|
||||||
|
factionBId: string;
|
||||||
|
status: string;
|
||||||
|
warGoal: string;
|
||||||
|
escalationScore: number;
|
||||||
|
startedAtUtc: string;
|
||||||
|
ceasefireUntilUtc?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
activeFrontLineIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiplomaticStateSnapshot {
|
||||||
|
relations: DiplomaticRelationSnapshot[];
|
||||||
|
treaties: TreatySnapshot[];
|
||||||
|
incidents: DiplomaticIncidentSnapshot[];
|
||||||
|
borderTensions: BorderTensionSnapshot[];
|
||||||
|
wars: WarStateSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryClaimSnapshot {
|
||||||
|
id: string;
|
||||||
|
sourceClaimId?: string | null;
|
||||||
|
factionId: string;
|
||||||
|
systemId: string;
|
||||||
|
celestialId?: string | null;
|
||||||
|
status: string;
|
||||||
|
claimKind: string;
|
||||||
|
claimStrength: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryInfluenceSnapshot {
|
||||||
|
id: string;
|
||||||
|
systemId: string;
|
||||||
|
factionId: string;
|
||||||
|
claimStrength: number;
|
||||||
|
assetStrength: number;
|
||||||
|
logisticsStrength: number;
|
||||||
|
totalInfluence: number;
|
||||||
|
isContesting: boolean;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryControlStateSnapshot {
|
||||||
|
systemId: string;
|
||||||
|
controllerFactionId?: string | null;
|
||||||
|
primaryClaimantFactionId?: string | null;
|
||||||
|
controlKind: string;
|
||||||
|
isContested: boolean;
|
||||||
|
controlScore: number;
|
||||||
|
strategicValue: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
claimantFactionIds: string[];
|
||||||
|
influencingFactionIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectorStrategicProfileSnapshot {
|
||||||
|
systemId: string;
|
||||||
|
controllerFactionId?: string | null;
|
||||||
|
zoneKind: string;
|
||||||
|
isContested: boolean;
|
||||||
|
strategicValue: number;
|
||||||
|
securityRating: number;
|
||||||
|
territorialPressure: number;
|
||||||
|
logisticsValue: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
economicRegionId?: string | null;
|
||||||
|
frontLineId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BorderEdgeSnapshot {
|
||||||
|
id: string;
|
||||||
|
sourceSystemId: string;
|
||||||
|
destinationSystemId: string;
|
||||||
|
sourceFactionId?: string | null;
|
||||||
|
destinationFactionId?: string | null;
|
||||||
|
isContested: boolean;
|
||||||
|
relationId?: string | null;
|
||||||
|
tensionScore: number;
|
||||||
|
corridorImportance: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrontLineSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
anchorSystemId?: string | null;
|
||||||
|
pressureScore: number;
|
||||||
|
supplyRisk: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
factionIds: string[];
|
||||||
|
systemIds: string[];
|
||||||
|
borderEdgeIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryZoneSnapshot {
|
||||||
|
id: string;
|
||||||
|
systemId: string;
|
||||||
|
factionId?: string | null;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
reason?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryPressureSnapshot {
|
||||||
|
id: string;
|
||||||
|
systemId: string;
|
||||||
|
factionId?: string | null;
|
||||||
|
kind: string;
|
||||||
|
pressureScore: number;
|
||||||
|
securityScore: number;
|
||||||
|
hostileInfluence: number;
|
||||||
|
corridorRisk: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TerritoryStateSnapshot {
|
||||||
|
claims: TerritoryClaimSnapshot[];
|
||||||
|
influences: TerritoryInfluenceSnapshot[];
|
||||||
|
controlStates: TerritoryControlStateSnapshot[];
|
||||||
|
strategicProfiles: SectorStrategicProfileSnapshot[];
|
||||||
|
borderEdges: BorderEdgeSnapshot[];
|
||||||
|
frontLines: FrontLineSnapshot[];
|
||||||
|
zones: TerritoryZoneSnapshot[];
|
||||||
|
pressures: TerritoryPressureSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EconomicRegionSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionId?: string | null;
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
coreSystemId: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
systemIds: string[];
|
||||||
|
stationIds: string[];
|
||||||
|
frontLineIds: string[];
|
||||||
|
corridorIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupplyNetworkSnapshot {
|
||||||
|
id: string;
|
||||||
|
regionId: string;
|
||||||
|
throughputScore: number;
|
||||||
|
riskScore: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
stationIds: string[];
|
||||||
|
producerItemIds: string[];
|
||||||
|
consumerItemIds: string[];
|
||||||
|
constructionItemIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogisticsCorridorSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionId?: string | null;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
riskScore: number;
|
||||||
|
throughputScore: number;
|
||||||
|
accessState: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
systemPathIds: string[];
|
||||||
|
regionIds: string[];
|
||||||
|
borderEdgeIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionalProductionProfileSnapshot {
|
||||||
|
regionId: string;
|
||||||
|
primaryIndustry: string;
|
||||||
|
shipyardCount: number;
|
||||||
|
stationCount: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
producedItemIds: string[];
|
||||||
|
scarceItemIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionalTradeBalanceSnapshot {
|
||||||
|
regionId: string;
|
||||||
|
importsRequiredCount: number;
|
||||||
|
exportsSurplusCount: number;
|
||||||
|
criticalShortageCount: number;
|
||||||
|
netTradeScore: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionalBottleneckSnapshot {
|
||||||
|
id: string;
|
||||||
|
regionId: string;
|
||||||
|
itemId: string;
|
||||||
|
cause: string;
|
||||||
|
status: string;
|
||||||
|
severity: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionalSecurityAssessmentSnapshot {
|
||||||
|
regionId: string;
|
||||||
|
supplyRisk: number;
|
||||||
|
borderPressure: number;
|
||||||
|
activeWarCount: number;
|
||||||
|
hostileRelationCount: number;
|
||||||
|
accessFriction: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionalEconomicAssessmentSnapshot {
|
||||||
|
regionId: string;
|
||||||
|
sustainmentScore: number;
|
||||||
|
productionDepth: number;
|
||||||
|
constructionPressure: number;
|
||||||
|
corridorDependency: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EconomyRegionStateSnapshot {
|
||||||
|
regions: EconomicRegionSnapshot[];
|
||||||
|
supplyNetworks: SupplyNetworkSnapshot[];
|
||||||
|
corridors: LogisticsCorridorSnapshot[];
|
||||||
|
productionProfiles: RegionalProductionProfileSnapshot[];
|
||||||
|
tradeBalances: RegionalTradeBalanceSnapshot[];
|
||||||
|
bottlenecks: RegionalBottleneckSnapshot[];
|
||||||
|
securityAssessments: RegionalSecurityAssessmentSnapshot[];
|
||||||
|
economicAssessments: RegionalEconomicAssessmentSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeopoliticalStateSnapshot {
|
||||||
|
cycle: number;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
routes: SystemRouteLinkSnapshot[];
|
||||||
|
diplomacy: DiplomaticStateSnapshot;
|
||||||
|
territory: TerritoryStateSnapshot;
|
||||||
|
economyRegions: EconomyRegionStateSnapshot;
|
||||||
|
}
|
||||||
289
apps/viewer/src/contractsPlayerFaction.ts
Normal file
289
apps/viewer/src/contractsPlayerFaction.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import type { Vector3Dto } from "./contractsCommon";
|
||||||
|
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||||
|
|
||||||
|
export interface PlayerAssetRegistrySnapshot {
|
||||||
|
shipIds: string[];
|
||||||
|
stationIds: string[];
|
||||||
|
commanderIds: string[];
|
||||||
|
claimIds: string[];
|
||||||
|
constructionSiteIds: string[];
|
||||||
|
policySetIds: string[];
|
||||||
|
marketOrderIds: string[];
|
||||||
|
fleetIds: string[];
|
||||||
|
taskForceIds: string[];
|
||||||
|
stationGroupIds: string[];
|
||||||
|
economicRegionIds: string[];
|
||||||
|
frontIds: string[];
|
||||||
|
reserveIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStrategicIntentSnapshot {
|
||||||
|
strategicPosture: string;
|
||||||
|
economicPosture: string;
|
||||||
|
militaryPosture: string;
|
||||||
|
logisticsPosture: string;
|
||||||
|
desiredReserveRatio: number;
|
||||||
|
allowDelegatedCombatAutomation: boolean;
|
||||||
|
allowDelegatedEconomicAutomation: boolean;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerFleetSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
role: string;
|
||||||
|
commanderId?: string | null;
|
||||||
|
frontId?: string | null;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
reinforcementPolicyId?: string | null;
|
||||||
|
assetIds: string[];
|
||||||
|
taskForceIds: string[];
|
||||||
|
directiveIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerTaskForceSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
role: string;
|
||||||
|
fleetId?: string | null;
|
||||||
|
commanderId?: string | null;
|
||||||
|
frontId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
assetIds: string[];
|
||||||
|
directiveIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStationGroupSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
role: string;
|
||||||
|
economicRegionId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
stationIds: string[];
|
||||||
|
directiveIds: string[];
|
||||||
|
focusItemIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerEconomicRegionSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
role: string;
|
||||||
|
sharedEconomicRegionId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
systemIds: string[];
|
||||||
|
stationGroupIds: string[];
|
||||||
|
directiveIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerFrontSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
priority: number;
|
||||||
|
posture: string;
|
||||||
|
sharedFrontLineId?: string | null;
|
||||||
|
targetFactionId?: string | null;
|
||||||
|
systemIds: string[];
|
||||||
|
fleetIds: string[];
|
||||||
|
reserveIds: string[];
|
||||||
|
directiveIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerReserveGroupSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
reserveKind: string;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
assetIds: string[];
|
||||||
|
frontIds: string[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerFactionPolicySnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId?: string | null;
|
||||||
|
policySetId?: string | null;
|
||||||
|
allowDelegatedCombat: boolean;
|
||||||
|
allowDelegatedTrade: boolean;
|
||||||
|
reserveCreditsRatio: number;
|
||||||
|
reserveMilitaryRatio: number;
|
||||||
|
tradeAccessPolicy: string;
|
||||||
|
dockingAccessPolicy: string;
|
||||||
|
constructionAccessPolicy: string;
|
||||||
|
operationalRangePolicy: string;
|
||||||
|
combatEngagementPolicy: string;
|
||||||
|
avoidHostileSystems: boolean;
|
||||||
|
fleeHullRatio: number;
|
||||||
|
blacklistedSystemIds: string[];
|
||||||
|
notes?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAutomationPolicySnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId?: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
behaviorKind: string;
|
||||||
|
useOrders: boolean;
|
||||||
|
stagingOrderKind?: string | null;
|
||||||
|
maxSystemRange: number;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
radius: number;
|
||||||
|
waitSeconds: number;
|
||||||
|
preferredItemId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerReinforcementPolicySnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId?: string | null;
|
||||||
|
shipKind: string;
|
||||||
|
desiredAssetCount: number;
|
||||||
|
minimumReserveCount: number;
|
||||||
|
autoTransferReserves: boolean;
|
||||||
|
autoQueueProduction: boolean;
|
||||||
|
sourceReserveId?: string | null;
|
||||||
|
targetFrontId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerProductionProgramSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
kind: string;
|
||||||
|
targetShipKind?: string | null;
|
||||||
|
targetModuleId?: string | null;
|
||||||
|
targetItemId?: string | null;
|
||||||
|
targetCount: number;
|
||||||
|
currentCount: number;
|
||||||
|
stationGroupId?: string | null;
|
||||||
|
reinforcementPolicyId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerDirectiveSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
kind: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId: string;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
sourceStationId?: string | null;
|
||||||
|
destinationStationId?: string | null;
|
||||||
|
behaviorKind: string;
|
||||||
|
useOrders: boolean;
|
||||||
|
stagingOrderKind?: string | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
preferredNodeId?: string | null;
|
||||||
|
preferredConstructionSiteId?: string | null;
|
||||||
|
preferredModuleId?: string | null;
|
||||||
|
priority: number;
|
||||||
|
radius: number;
|
||||||
|
waitSeconds: number;
|
||||||
|
maxSystemRange: number;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
patrolPoints: Vector3Dto[];
|
||||||
|
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAssignmentSnapshot {
|
||||||
|
id: string;
|
||||||
|
assetKind: string;
|
||||||
|
assetId: string;
|
||||||
|
fleetId?: string | null;
|
||||||
|
taskForceId?: string | null;
|
||||||
|
stationGroupId?: string | null;
|
||||||
|
economicRegionId?: string | null;
|
||||||
|
frontId?: string | null;
|
||||||
|
reserveId?: string | null;
|
||||||
|
directiveId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerDecisionLogEntrySnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
summary: string;
|
||||||
|
relatedEntityKind?: string | null;
|
||||||
|
relatedEntityId?: string | null;
|
||||||
|
occurredAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAlertSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
severity: string;
|
||||||
|
summary: string;
|
||||||
|
assetKind?: string | null;
|
||||||
|
assetId?: string | null;
|
||||||
|
relatedDirectiveId?: string | null;
|
||||||
|
status: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerFactionSnapshot {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
sovereignFactionId: string;
|
||||||
|
status: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
assetRegistry: PlayerAssetRegistrySnapshot;
|
||||||
|
strategicIntent: PlayerStrategicIntentSnapshot;
|
||||||
|
fleets: PlayerFleetSnapshot[];
|
||||||
|
taskForces: PlayerTaskForceSnapshot[];
|
||||||
|
stationGroups: PlayerStationGroupSnapshot[];
|
||||||
|
economicRegions: PlayerEconomicRegionSnapshot[];
|
||||||
|
fronts: PlayerFrontSnapshot[];
|
||||||
|
reserves: PlayerReserveGroupSnapshot[];
|
||||||
|
policies: PlayerFactionPolicySnapshot[];
|
||||||
|
automationPolicies: PlayerAutomationPolicySnapshot[];
|
||||||
|
reinforcementPolicies: PlayerReinforcementPolicySnapshot[];
|
||||||
|
productionPrograms: PlayerProductionProgramSnapshot[];
|
||||||
|
directives: PlayerDirectiveSnapshot[];
|
||||||
|
assignments: PlayerAssignmentSnapshot[];
|
||||||
|
decisionLog: PlayerDecisionLogEntrySnapshot[];
|
||||||
|
alerts: PlayerAlertSnapshot[];
|
||||||
|
}
|
||||||
@@ -1,5 +1,140 @@
|
|||||||
import type { InventoryEntry, Vector3Dto } from "./contractsCommon";
|
import type { InventoryEntry, Vector3Dto } from "./contractsCommon";
|
||||||
|
|
||||||
|
export interface ShipSkillProfileSnapshot {
|
||||||
|
navigation: number;
|
||||||
|
trade: number;
|
||||||
|
mining: number;
|
||||||
|
combat: number;
|
||||||
|
construction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipOrderSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
priority: number;
|
||||||
|
interruptCurrentPlan: boolean;
|
||||||
|
createdAtUtc: string;
|
||||||
|
label?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
sourceStationId?: string | null;
|
||||||
|
destinationStationId?: string | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
nodeId?: string | null;
|
||||||
|
constructionSiteId?: string | null;
|
||||||
|
moduleId?: string | null;
|
||||||
|
waitSeconds: number;
|
||||||
|
radius: number;
|
||||||
|
maxSystemRange?: number | null;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
failureReason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipOrderTemplateSnapshot {
|
||||||
|
kind: string;
|
||||||
|
label?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
sourceStationId?: string | null;
|
||||||
|
destinationStationId?: string | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
nodeId?: string | null;
|
||||||
|
constructionSiteId?: string | null;
|
||||||
|
moduleId?: string | null;
|
||||||
|
waitSeconds: number;
|
||||||
|
radius: number;
|
||||||
|
maxSystemRange?: number | null;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultBehaviorSnapshot {
|
||||||
|
kind: string;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
areaSystemId?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
preferredItemId?: string | null;
|
||||||
|
preferredNodeId?: string | null;
|
||||||
|
preferredConstructionSiteId?: string | null;
|
||||||
|
preferredModuleId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
waitSeconds: number;
|
||||||
|
radius: number;
|
||||||
|
maxSystemRange: number;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
patrolPoints: Vector3Dto[];
|
||||||
|
patrolIndex: number;
|
||||||
|
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||||
|
repeatIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipAssignmentSnapshot {
|
||||||
|
commanderId: string;
|
||||||
|
parentCommanderId?: string | null;
|
||||||
|
kind: string;
|
||||||
|
behaviorKind: string;
|
||||||
|
status: string;
|
||||||
|
objectiveId?: string | null;
|
||||||
|
campaignId?: string | null;
|
||||||
|
theaterId?: string | null;
|
||||||
|
priority: number;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
updatedAtUtc?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipSubTaskSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
summary: string;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetNodeId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
moduleId?: string | null;
|
||||||
|
threshold: number;
|
||||||
|
amount: number;
|
||||||
|
progress: number;
|
||||||
|
elapsedSeconds: number;
|
||||||
|
totalSeconds: number;
|
||||||
|
blockingReason?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipPlanStepSnapshot {
|
||||||
|
id: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
summary: string;
|
||||||
|
blockingReason?: string | null;
|
||||||
|
currentSubTaskIndex: number;
|
||||||
|
subTasks: ShipSubTaskSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipPlanSnapshot {
|
||||||
|
id: string;
|
||||||
|
sourceKind: string;
|
||||||
|
sourceId: string;
|
||||||
|
kind: string;
|
||||||
|
status: string;
|
||||||
|
summary: string;
|
||||||
|
currentStepIndex: number;
|
||||||
|
createdAtUtc: string;
|
||||||
|
updatedAtUtc: string;
|
||||||
|
interruptReason?: string | null;
|
||||||
|
failureReason?: string | null;
|
||||||
|
steps: ShipPlanStepSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShipSnapshot {
|
export interface ShipSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,34 +145,34 @@ export interface ShipSnapshot {
|
|||||||
localVelocity: Vector3Dto;
|
localVelocity: Vector3Dto;
|
||||||
targetLocalPosition: Vector3Dto;
|
targetLocalPosition: Vector3Dto;
|
||||||
state: string;
|
state: string;
|
||||||
orderKind: string | null;
|
orderQueue: ShipOrderSnapshot[];
|
||||||
defaultBehaviorKind: string;
|
defaultBehavior: DefaultBehaviorSnapshot;
|
||||||
behaviorPhase: string | null;
|
assignment?: ShipAssignmentSnapshot | null;
|
||||||
controllerTaskKind: string;
|
skills: ShipSkillProfileSnapshot;
|
||||||
commanderObjective: string | null;
|
activePlan?: ShipPlanSnapshot | null;
|
||||||
|
currentStepId?: string | null;
|
||||||
|
activeSubTasks: ShipSubTaskSnapshot[];
|
||||||
|
controlSourceKind: string;
|
||||||
|
controlSourceId?: string | null;
|
||||||
|
controlReason?: string | null;
|
||||||
|
lastReplanReason?: string | null;
|
||||||
|
lastAccessFailureReason?: string | null;
|
||||||
celestialId?: string | null;
|
celestialId?: string | null;
|
||||||
dockedStationId?: string | null;
|
dockedStationId?: string | null;
|
||||||
commanderId?: string | null;
|
commanderId?: string | null;
|
||||||
policySetId?: string | null;
|
policySetId?: string | null;
|
||||||
cargoCapacity: number;
|
cargoCapacity: number;
|
||||||
|
|
||||||
travelSpeed: number;
|
travelSpeed: number;
|
||||||
travelSpeedUnit: string;
|
travelSpeedUnit: string;
|
||||||
inventory: InventoryEntry[];
|
inventory: InventoryEntry[];
|
||||||
factionId: string;
|
factionId: string;
|
||||||
health: number;
|
health: number;
|
||||||
history: string[];
|
history: string[];
|
||||||
currentAction?: ShipActionProgressSnapshot | null;
|
|
||||||
spatialState: ShipSpatialStateSnapshot;
|
spatialState: ShipSpatialStateSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShipDelta extends ShipSnapshot {}
|
export interface ShipDelta extends ShipSnapshot {}
|
||||||
|
|
||||||
export interface ShipActionProgressSnapshot {
|
|
||||||
label: string;
|
|
||||||
progress: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShipSpatialStateSnapshot {
|
export interface ShipSpatialStateSnapshot {
|
||||||
spaceLayer: string;
|
spaceLayer: string;
|
||||||
currentSystemId: string;
|
currentSystemId: string;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import type {
|
|||||||
PolicySetSnapshot,
|
PolicySetSnapshot,
|
||||||
MarketOrderSnapshot,
|
MarketOrderSnapshot,
|
||||||
} from "./contractsEconomy";
|
} from "./contractsEconomy";
|
||||||
|
import type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||||
|
import type { GeopoliticalStateSnapshot } from "./contractsGeopolitics";
|
||||||
import type {
|
import type {
|
||||||
ShipDelta,
|
ShipDelta,
|
||||||
ShipSnapshot,
|
ShipSnapshot,
|
||||||
@@ -44,6 +46,8 @@ export interface WorldSnapshot {
|
|||||||
policies: PolicySetSnapshot[];
|
policies: PolicySetSnapshot[];
|
||||||
ships: ShipSnapshot[];
|
ships: ShipSnapshot[];
|
||||||
factions: FactionSnapshot[];
|
factions: FactionSnapshot[];
|
||||||
|
playerFaction?: PlayerFactionSnapshot | null;
|
||||||
|
geopolitics?: GeopoliticalStateSnapshot | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorldDelta {
|
export interface WorldDelta {
|
||||||
@@ -63,6 +67,8 @@ export interface WorldDelta {
|
|||||||
policies: PolicySetDelta[];
|
policies: PolicySetDelta[];
|
||||||
ships: ShipDelta[];
|
ships: ShipDelta[];
|
||||||
factions: FactionDelta[];
|
factions: FactionDelta[];
|
||||||
|
playerFaction?: PlayerFactionSnapshot | null;
|
||||||
|
geopolitics?: GeopoliticalStateSnapshot | null;
|
||||||
scope?: ObserverScope | null;
|
scope?: ObserverScope | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
124
apps/viewer/src/playerFactionCommands.ts
Normal file
124
apps/viewer/src/playerFactionCommands.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import type { Vector3Dto } from "./contractsCommon";
|
||||||
|
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||||
|
|
||||||
|
export interface PlayerOrganizationCommandRequest {
|
||||||
|
kind: string;
|
||||||
|
label: string;
|
||||||
|
parentOrganizationId?: string | null;
|
||||||
|
frontId?: string | null;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
reinforcementPolicyId?: string | null;
|
||||||
|
targetFactionId?: string | null;
|
||||||
|
priority?: number | null;
|
||||||
|
role?: string | null;
|
||||||
|
reserveKind?: string | null;
|
||||||
|
systemIds?: string[] | null;
|
||||||
|
focusItemIds?: string[] | null;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerOrganizationMembershipCommandRequest {
|
||||||
|
assetIds?: string[] | null;
|
||||||
|
childOrganizationIds?: string[] | null;
|
||||||
|
systemIds?: string[] | null;
|
||||||
|
frontIds?: string[] | null;
|
||||||
|
replace?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerDirectiveCommandRequest {
|
||||||
|
label: string;
|
||||||
|
kind: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId: string;
|
||||||
|
behaviorKind: string;
|
||||||
|
useOrders: boolean;
|
||||||
|
stagingOrderKind?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
sourceStationId?: string | null;
|
||||||
|
destinationStationId?: string | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
preferredNodeId?: string | null;
|
||||||
|
preferredConstructionSiteId?: string | null;
|
||||||
|
preferredModuleId?: string | null;
|
||||||
|
priority: number;
|
||||||
|
radius?: number | null;
|
||||||
|
waitSeconds?: number | null;
|
||||||
|
maxSystemRange?: number | null;
|
||||||
|
knownStationsOnly?: boolean | null;
|
||||||
|
patrolPoints?: Vector3Dto[] | null;
|
||||||
|
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerPolicyCommandRequest {
|
||||||
|
label: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId?: string | null;
|
||||||
|
policySetId?: string | null;
|
||||||
|
allowDelegatedCombat: boolean;
|
||||||
|
allowDelegatedTrade: boolean;
|
||||||
|
reserveCreditsRatio: number;
|
||||||
|
reserveMilitaryRatio: number;
|
||||||
|
notes?: string | null;
|
||||||
|
tradeAccessPolicy?: string | null;
|
||||||
|
dockingAccessPolicy?: string | null;
|
||||||
|
constructionAccessPolicy?: string | null;
|
||||||
|
operationalRangePolicy?: string | null;
|
||||||
|
combatEngagementPolicy?: string | null;
|
||||||
|
avoidHostileSystems?: boolean | null;
|
||||||
|
fleeHullRatio?: number | null;
|
||||||
|
blacklistedSystemIds?: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAutomationPolicyCommandRequest {
|
||||||
|
label: string;
|
||||||
|
scopeKind: string;
|
||||||
|
scopeId?: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
behaviorKind: string;
|
||||||
|
useOrders: boolean;
|
||||||
|
stagingOrderKind?: string | null;
|
||||||
|
maxSystemRange: number;
|
||||||
|
knownStationsOnly: boolean;
|
||||||
|
radius: number;
|
||||||
|
waitSeconds: number;
|
||||||
|
preferredItemId?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerAssetAssignmentCommandRequest {
|
||||||
|
assetKind: string;
|
||||||
|
assetId: string;
|
||||||
|
fleetId?: string | null;
|
||||||
|
taskForceId?: string | null;
|
||||||
|
stationGroupId?: string | null;
|
||||||
|
economicRegionId?: string | null;
|
||||||
|
frontId?: string | null;
|
||||||
|
reserveId?: string | null;
|
||||||
|
directiveId?: string | null;
|
||||||
|
policyId?: string | null;
|
||||||
|
automationPolicyId?: string | null;
|
||||||
|
role: string;
|
||||||
|
clearConflicts?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerStrategicIntentCommandRequest {
|
||||||
|
strategicPosture: string;
|
||||||
|
economicPosture: string;
|
||||||
|
militaryPosture: string;
|
||||||
|
logisticsPosture: string;
|
||||||
|
desiredReserveRatio: number;
|
||||||
|
allowDelegatedCombatAutomation: boolean;
|
||||||
|
allowDelegatedEconomicAutomation: boolean;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
41
apps/viewer/src/shipCommands.ts
Normal file
41
apps/viewer/src/shipCommands.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Vector3Dto } from "./contractsCommon";
|
||||||
|
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||||
|
|
||||||
|
export interface ShipOrderCommandRequest {
|
||||||
|
kind: string;
|
||||||
|
priority: number;
|
||||||
|
interruptCurrentPlan: boolean;
|
||||||
|
label?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
targetSystemId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
sourceStationId?: string | null;
|
||||||
|
destinationStationId?: string | null;
|
||||||
|
itemId?: string | null;
|
||||||
|
nodeId?: string | null;
|
||||||
|
constructionSiteId?: string | null;
|
||||||
|
moduleId?: string | null;
|
||||||
|
waitSeconds?: number | null;
|
||||||
|
radius?: number | null;
|
||||||
|
maxSystemRange?: number | null;
|
||||||
|
knownStationsOnly?: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipDefaultBehaviorCommandRequest {
|
||||||
|
kind: string;
|
||||||
|
homeSystemId?: string | null;
|
||||||
|
homeStationId?: string | null;
|
||||||
|
areaSystemId?: string | null;
|
||||||
|
targetEntityId?: string | null;
|
||||||
|
preferredItemId?: string | null;
|
||||||
|
preferredNodeId?: string | null;
|
||||||
|
preferredConstructionSiteId?: string | null;
|
||||||
|
preferredModuleId?: string | null;
|
||||||
|
targetPosition?: Vector3Dto | null;
|
||||||
|
waitSeconds?: number | null;
|
||||||
|
radius?: number | null;
|
||||||
|
maxSystemRange?: number | null;
|
||||||
|
knownStationsOnly?: boolean | null;
|
||||||
|
patrolPoints?: Vector3Dto[] | null;
|
||||||
|
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { ShipSnapshot } from "../../contractsShips";
|
|||||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
import type { FactionSnapshot } from "../../contractsFactions";
|
import type { FactionSnapshot } from "../../contractsFactions";
|
||||||
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
||||||
|
import type { GeopoliticalStateSnapshot } from "../../contractsGeopolitics";
|
||||||
|
|
||||||
export const useGmStore = defineStore("gm", {
|
export const useGmStore = defineStore("gm", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -10,6 +11,7 @@ export const useGmStore = defineStore("gm", {
|
|||||||
stations: [] as StationSnapshot[],
|
stations: [] as StationSnapshot[],
|
||||||
factions: [] as FactionSnapshot[],
|
factions: [] as FactionSnapshot[],
|
||||||
marketOrders: [] as MarketOrderSnapshot[],
|
marketOrders: [] as MarketOrderSnapshot[],
|
||||||
|
geopolitics: null as GeopoliticalStateSnapshot | null,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
updateWorld(
|
updateWorld(
|
||||||
@@ -17,11 +19,21 @@ export const useGmStore = defineStore("gm", {
|
|||||||
stations: StationSnapshot[],
|
stations: StationSnapshot[],
|
||||||
factions: FactionSnapshot[],
|
factions: FactionSnapshot[],
|
||||||
marketOrders: MarketOrderSnapshot[],
|
marketOrders: MarketOrderSnapshot[],
|
||||||
|
geopolitics: GeopoliticalStateSnapshot | null,
|
||||||
) {
|
) {
|
||||||
this.ships = ships;
|
this.ships = ships;
|
||||||
this.stations = stations;
|
this.stations = stations;
|
||||||
this.factions = factions;
|
this.factions = factions;
|
||||||
this.marketOrders = marketOrders;
|
this.marketOrders = marketOrders;
|
||||||
|
this.geopolitics = geopolitics;
|
||||||
|
},
|
||||||
|
upsertShip(ship: ShipSnapshot) {
|
||||||
|
const index = this.ships.findIndex((candidate) => candidate.id === ship.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
this.ships.splice(index, 1, ship);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.ships.push(ship);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
43
apps/viewer/src/ui/stores/playerFactionStore.ts
Normal file
43
apps/viewer/src/ui/stores/playerFactionStore.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import type { PlayerFactionSnapshot } from "../../contractsPlayerFaction";
|
||||||
|
|
||||||
|
export const usePlayerFactionStore = defineStore("playerFaction", {
|
||||||
|
state: () => ({
|
||||||
|
playerFaction: null as PlayerFactionSnapshot | null,
|
||||||
|
selectedOrganizationId: null as string | null,
|
||||||
|
selectedDirectiveId: null as string | null,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setPlayerFaction(snapshot: PlayerFactionSnapshot | null) {
|
||||||
|
this.playerFaction = snapshot;
|
||||||
|
if (snapshot == null) {
|
||||||
|
this.selectedOrganizationId = null;
|
||||||
|
this.selectedDirectiveId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownOrganizationIds = new Set<string>([
|
||||||
|
...snapshot.fleets.map((entry) => entry.id),
|
||||||
|
...snapshot.taskForces.map((entry) => entry.id),
|
||||||
|
...snapshot.stationGroups.map((entry) => entry.id),
|
||||||
|
...snapshot.economicRegions.map((entry) => entry.id),
|
||||||
|
...snapshot.fronts.map((entry) => entry.id),
|
||||||
|
...snapshot.reserves.map((entry) => entry.id),
|
||||||
|
]);
|
||||||
|
if (this.selectedOrganizationId && !knownOrganizationIds.has(this.selectedOrganizationId)) {
|
||||||
|
this.selectedOrganizationId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownDirectiveIds = new Set(snapshot.directives.map((entry) => entry.id));
|
||||||
|
if (this.selectedDirectiveId && !knownDirectiveIds.has(this.selectedDirectiveId)) {
|
||||||
|
this.selectedDirectiveId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectOrganization(organizationId: string | null) {
|
||||||
|
this.selectedOrganizationId = organizationId;
|
||||||
|
},
|
||||||
|
selectDirective(directiveId: string | null) {
|
||||||
|
this.selectedDirectiveId = directiveId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,29 +7,59 @@ import type {
|
|||||||
OpsStationCardState,
|
OpsStationCardState,
|
||||||
OpsStripState,
|
OpsStripState,
|
||||||
} from "./viewerHudState";
|
} from "./viewerHudState";
|
||||||
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
import { describeShipCurrentAction, describeShipLocation, describeShipState } from "./viewerSelection";
|
||||||
import type { CameraMode, Selectable, WorldState, PovLevel } from "./viewerTypes";
|
import type { CameraMode, Selectable, WorldState, PovLevel } from "./viewerTypes";
|
||||||
|
|
||||||
function buildFactionCard(faction: FactionSnapshot): OpsFactionCardState {
|
function buildFactionCard(world: WorldState, faction: FactionSnapshot): OpsFactionCardState {
|
||||||
const state = faction.strategicAssessment;
|
const playerFaction = world.playerFaction;
|
||||||
const blackboard = faction.blackboard;
|
if (playerFaction && playerFaction.sovereignFactionId === faction.id) {
|
||||||
const leadTask = [...(faction.issuedTasks ?? [])]
|
const selectedDirective = playerFaction.directives[0];
|
||||||
|
return {
|
||||||
|
kind: "faction",
|
||||||
|
id: faction.id,
|
||||||
|
label: `${faction.label} Command`,
|
||||||
|
stateLines: [
|
||||||
|
`Player ${playerFaction.assetRegistry.shipIds.length} ships · ${playerFaction.assetRegistry.stationIds.length} stations`,
|
||||||
|
`Groups ${playerFaction.fleets.length + playerFaction.taskForces.length + playerFaction.stationGroups.length + playerFaction.economicRegions.length + playerFaction.fronts.length + playerFaction.reserves.length}`,
|
||||||
|
`Intent ${playerFaction.strategicIntent.strategicPosture} · ${playerFaction.strategicIntent.economicPosture}`,
|
||||||
|
`Alerts ${playerFaction.alerts.length} · Decisions ${playerFaction.decisionLog.length}`,
|
||||||
|
`Lead ${selectedDirective ? `${selectedDirective.behaviorKind} · ${selectedDirective.scopeKind}` : "no active directives"}`,
|
||||||
|
],
|
||||||
|
priorities: [
|
||||||
|
{ label: "Reserve", value: `${Math.round(playerFaction.strategicIntent.desiredReserveRatio * 100)}%` },
|
||||||
|
{ label: "Auto", value: `${Number(playerFaction.strategicIntent.allowDelegatedEconomicAutomation)}/${Number(playerFaction.strategicIntent.allowDelegatedCombatAutomation)}` },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategicState = faction.strategicState;
|
||||||
|
const economic = strategicState.economicAssessment;
|
||||||
|
const activeCampaigns = strategicState.campaigns.filter((campaign) => campaign.status === "active");
|
||||||
|
const activeTheaters = strategicState.theaters.filter((theater) => theater.status === "active");
|
||||||
|
const activeWars = world.geopolitics?.diplomacy.wars.filter((war) => war.factionAId === faction.id || war.factionBId === faction.id).length ?? 0;
|
||||||
|
const contestedSystems = world.geopolitics?.territory.controlStates.filter((state) =>
|
||||||
|
state.isContested && (state.controllerFactionId === faction.id || state.primaryClaimantFactionId === faction.id || state.claimantFactionIds.includes(faction.id))).length ?? 0;
|
||||||
|
const leadCampaign = [...strategicState.campaigns]
|
||||||
.sort((left, right) => right.priority - left.priority)[0];
|
.sort((left, right) => right.priority - left.priority)[0];
|
||||||
|
const leadTheater = [...strategicState.theaters]
|
||||||
|
.sort((left, right) => right.priority - left.priority)[0];
|
||||||
|
const latestDecision = [...faction.decisionLog]
|
||||||
|
.sort((left, right) => right.occurredAtUtc.localeCompare(left.occurredAtUtc))[0];
|
||||||
return {
|
return {
|
||||||
kind: "faction",
|
kind: "faction",
|
||||||
id: faction.id,
|
id: faction.id,
|
||||||
label: faction.label,
|
label: faction.label,
|
||||||
stateLines: state ? [
|
stateLines: [
|
||||||
`Military ${state.militaryShipCount} · Miners ${state.minerShipCount}`,
|
`Posture ${faction.doctrine.strategicPosture} · ${faction.doctrine.militaryPosture}`,
|
||||||
`Transport ${state.transportShipCount} · Constructors ${state.constructorShipCount}`,
|
`Campaigns ${activeCampaigns.length} · Fronts ${activeTheaters.length} · Wars ${activeWars}`,
|
||||||
`Systems ${state.controlledSystemCount} / ${state.targetSystemCount}`,
|
`Commit ${economic.militaryShipCount}/${economic.targetMilitaryShipCount} mil · ${economic.minerShipCount}/${economic.targetMinerShipCount} min`,
|
||||||
`Shipyard ${blackboard?.hasShipyard ? "yes" : "no"} · War industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"}`,
|
`Reserve ${strategicState.budget.reservedMilitaryAssets} mil · ${strategicState.budget.reservedLogisticsAssets} log`,
|
||||||
leadTask ? `Task ${leadTask.kind}${leadTask.shipRole ? ` · ${leadTask.shipRole}` : ""}` : `Ore ${state.oreStockpile.toFixed(0)}`,
|
`Bottleneck ${economic.industrialBottleneckItemId ?? "none"} · Contested ${contestedSystems}${latestDecision ? ` · ${latestDecision.kind}` : ""}`,
|
||||||
] : [],
|
],
|
||||||
priorities: (faction.strategicPriorities ?? []).map((entry) => ({
|
priorities: [
|
||||||
label: entry.goalName,
|
...(leadCampaign ? [{ label: leadCampaign.kind, value: leadCampaign.priority.toFixed(0) }] : []),
|
||||||
value: entry.priority.toFixed(0),
|
...(leadTheater ? [{ label: leadTheater.kind, value: leadTheater.priority.toFixed(0) }] : []),
|
||||||
})),
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +99,9 @@ function buildShipCard(
|
|||||||
const shipLocation = describeShipLocation(world, ship);
|
const shipLocation = describeShipLocation(world, ship);
|
||||||
const shipState = describeShipState(world, ship);
|
const shipState = describeShipState(world, ship);
|
||||||
const shipAction = describeShipCurrentAction(ship);
|
const shipAction = describeShipCurrentAction(ship);
|
||||||
|
const currentStep = ship.activePlan?.steps[ship.activePlan.currentStepIndex];
|
||||||
|
const topOrder = [...ship.orderQueue]
|
||||||
|
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "ship",
|
kind: "ship",
|
||||||
@@ -84,9 +117,10 @@ function buildShipCard(
|
|||||||
],
|
],
|
||||||
action: shipAction ? buildProgressBar(shipAction.label, shipAction.progress) : undefined,
|
action: shipAction ? buildProgressBar(shipAction.label, shipAction.progress) : undefined,
|
||||||
aiLines: [
|
aiLines: [
|
||||||
...(ship.commanderObjective ? [`Objective ${describeShipObjective(ship.commanderObjective)}`] : []),
|
`Assignment ${ship.assignment?.kind ?? "unassigned"}`,
|
||||||
`Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}`,
|
`Behavior ${ship.defaultBehavior.kind}`,
|
||||||
`Task ${ship.controllerTaskKind}`,
|
`Plan ${ship.activePlan ? `${ship.activePlan.kind}${currentStep ? ` · ${currentStep.kind}` : ""}` : "none"}`,
|
||||||
|
`Orders ${topOrder ? `${topOrder.kind} +${Math.max(0, ship.orderQueue.length - 1)}` : "none"}`,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -111,7 +145,7 @@ export function buildOpsStripState(
|
|||||||
|
|
||||||
const factions = [...world.factions.values()]
|
const factions = [...world.factions.values()]
|
||||||
.sort((left, right) => left.label.localeCompare(right.label))
|
.sort((left, right) => left.label.localeCompare(right.label))
|
||||||
.map(buildFactionCard);
|
.map((faction) => buildFactionCard(world, faction));
|
||||||
|
|
||||||
const stations = [...world.stations.values()]
|
const stations = [...world.stations.values()]
|
||||||
.filter((station) => !isSystemFiltered || station.systemId === activeSystemId)
|
.filter((station) => !isSystemFiltered || station.systemId === activeSystemId)
|
||||||
|
|||||||
@@ -212,26 +212,37 @@ function formatStorageWithInventory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSystemOwnership(world: WorldState, systemId: string): string {
|
function renderSystemOwnership(world: WorldState, systemId: string): string {
|
||||||
|
const control = world.geopolitics?.territory.controlStates.find((state) => state.systemId === systemId);
|
||||||
|
const zone = world.geopolitics?.territory.zones.find((entry) => entry.systemId === systemId);
|
||||||
|
const profile = world.geopolitics?.territory.strategicProfiles.find((entry) => entry.systemId === systemId);
|
||||||
|
const pressure = world.geopolitics?.territory.pressures.find((entry) => entry.systemId === systemId);
|
||||||
|
const region = world.geopolitics?.economyRegions.regions.find((entry) => entry.systemIds.includes(systemId));
|
||||||
|
const front = world.geopolitics?.territory.frontLines.find((entry) => entry.systemIds.includes(systemId));
|
||||||
|
|
||||||
|
if (!control) {
|
||||||
const claims = [...world.claims.values()].filter((claim) =>
|
const claims = [...world.claims.values()].filter((claim) =>
|
||||||
claim.systemId === systemId && claim.state !== "destroyed");
|
claim.systemId === systemId && claim.state !== "destroyed");
|
||||||
if (claims.length === 0) {
|
return claims.length === 0 ? "Territory unknown" : `Claims ${claims.length}`;
|
||||||
return "Ownership none";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownershipByFaction = new Map<string, number>();
|
const controllerLabel = control.controllerFactionId
|
||||||
for (const claim of claims) {
|
? (world.factions.get(control.controllerFactionId)?.label ?? control.controllerFactionId)
|
||||||
ownershipByFaction.set(claim.factionId, (ownershipByFaction.get(claim.factionId) ?? 0) + 1);
|
: "none";
|
||||||
|
const claimantLabel = control.primaryClaimantFactionId
|
||||||
|
? (world.factions.get(control.primaryClaimantFactionId)?.label ?? control.primaryClaimantFactionId)
|
||||||
|
: "none";
|
||||||
|
const lines = [
|
||||||
|
`Control ${control.controlKind} · Controller ${controllerLabel}`,
|
||||||
|
`Claimant ${claimantLabel} · Zone ${zone?.kind ?? profile?.zoneKind ?? "unknown"}`,
|
||||||
|
`Pressure ${Math.round((pressure?.pressureScore ?? profile?.territorialPressure ?? 0) * 100)}% · Security ${Math.round((pressure?.securityScore ?? profile?.securityRating ?? 0) * 100)}%`,
|
||||||
|
];
|
||||||
|
if (front) {
|
||||||
|
lines.push(`Front ${front.id} · ${front.factionIds.join(" vs ")}`);
|
||||||
}
|
}
|
||||||
|
if (region) {
|
||||||
return [...ownershipByFaction.entries()]
|
lines.push(`Region ${region.label} · ${region.kind}`);
|
||||||
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
}
|
||||||
.map(([factionId, count]) => {
|
return lines.join("<br>");
|
||||||
const faction = world.factions.get(factionId);
|
|
||||||
const label = faction?.label ?? factionId;
|
|
||||||
const share = Math.round((count / claims.length) * 100);
|
|
||||||
return `${label} ${count}/${claims.length} (${share}%)`;
|
|
||||||
})
|
|
||||||
.join("<br>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDetailPanelState(params: DetailPanelParams) {
|
export function buildDetailPanelState(params: DetailPanelParams) {
|
||||||
@@ -284,14 +295,32 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
|||||||
const shipBehavior = describeShipBehavior(ship);
|
const shipBehavior = describeShipBehavior(ship);
|
||||||
const shipOrder = describeShipOrder(ship);
|
const shipOrder = describeShipOrder(ship);
|
||||||
const shipAction = describeShipCurrentAction(ship);
|
const shipAction = describeShipCurrentAction(ship);
|
||||||
|
const currentStep = ship.activePlan?.steps[ship.activePlan.currentStepIndex];
|
||||||
|
const orderQueue = ship.orderQueue.length > 0
|
||||||
|
? ship.orderQueue.slice(0, 4).map((order) => `${order.kind} [${order.status}]`).join("<br>")
|
||||||
|
: "none";
|
||||||
|
const subTaskList = ship.activeSubTasks.length > 0
|
||||||
|
? ship.activeSubTasks.slice(0, 4).map((subTask) => `${subTask.summary || subTask.kind} · ${subTask.status}`).join("<br>")
|
||||||
|
: "none";
|
||||||
|
const playerAssignment = world.playerFaction?.assignments.find((assignment) => assignment.assetKind === "ship" && assignment.assetId === ship.id);
|
||||||
|
const playerDirective = playerAssignment?.directiveId
|
||||||
|
? world.playerFaction?.directives.find((directive) => directive.id === playerAssignment.directiveId)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
title: ship.label,
|
title: ship.label,
|
||||||
bodyHtml: `
|
bodyHtml: `
|
||||||
<p>Parent ${parent}</p>
|
<p>Parent ${parent}</p>
|
||||||
<p>Behavior ${shipBehavior}</p>
|
<p>Behavior ${shipBehavior}</p>
|
||||||
|
<p>Command ${ship.controlSourceKind}${ship.controlSourceId ? `<br>ID ${ship.controlSourceId}` : ""}${ship.controlReason ? `<br>${ship.controlReason}` : ""}</p>
|
||||||
|
<p>Assignment ${ship.assignment?.kind ?? "unassigned"}${ship.assignment?.campaignId ? `<br>Campaign ${ship.assignment.campaignId}` : ""}</p>
|
||||||
|
${playerAssignment ? `<p>Player ${playerAssignment.role}${playerDirective ? `<br>Directive ${playerDirective.label}` : ""}${playerAssignment.fleetId ? `<br>Fleet ${playerAssignment.fleetId}` : ""}${playerAssignment.taskForceId ? `<br>Task Force ${playerAssignment.taskForceId}` : ""}${playerAssignment.frontId ? `<br>Front ${playerAssignment.frontId}` : ""}</p>` : ""}
|
||||||
<p>State ${shipState}</p>
|
<p>State ${shipState}</p>
|
||||||
<p>Order ${shipOrder}</p>
|
<p>Order ${shipOrder}</p>
|
||||||
<p>Task ${ship.controllerTaskKind}</p>
|
<p>Queue ${orderQueue}</p>
|
||||||
|
<p>Plan ${ship.activePlan ? `${ship.activePlan.kind} · ${ship.activePlan.status}` : "none"}${currentStep ? `<br>Step ${currentStep.kind} · ${currentStep.status}` : ""}</p>
|
||||||
|
<p>Subtasks ${subTaskList}</p>
|
||||||
|
${ship.lastReplanReason ? `<p>Last replan ${ship.lastReplanReason}</p>` : ""}
|
||||||
|
${ship.lastAccessFailureReason ? `<p>Access ${ship.lastAccessFailureReason}</p>` : ""}
|
||||||
${shipAction ? `
|
${shipAction ? `
|
||||||
<div class="detail-progress">
|
<div class="detail-progress">
|
||||||
<div class="detail-progress-label">
|
<div class="detail-progress-label">
|
||||||
@@ -322,11 +351,16 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
|||||||
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
|
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
|
||||||
: "none";
|
: "none";
|
||||||
const stationStorage = formatStorageWithInventory(station.storageUsage, station.inventory);
|
const stationStorage = formatStorageWithInventory(station.storageUsage, station.inventory);
|
||||||
|
const playerAssignment = world.playerFaction?.assignments.find((assignment) => assignment.assetKind === "station" && assignment.assetId === station.id);
|
||||||
|
const playerDirective = playerAssignment?.directiveId
|
||||||
|
? world.playerFaction?.directives.find((directive) => directive.id === playerAssignment.directiveId)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
title: station.label,
|
title: station.label,
|
||||||
bodyHtml: `
|
bodyHtml: `
|
||||||
<p>${station.category} · ${station.systemId}</p>
|
<p>${station.category} · ${station.systemId}</p>
|
||||||
<p>Parent ${parent}</p>
|
<p>Parent ${parent}</p>
|
||||||
|
${playerAssignment ? `<p>Player ${playerAssignment.role}${playerDirective ? `<br>Directive ${playerDirective.label}` : ""}${playerAssignment.stationGroupId ? `<br>Group ${playerAssignment.stationGroupId}` : ""}${playerAssignment.economicRegionId ? `<br>Region ${playerAssignment.economicRegionId}` : ""}</p>` : ""}
|
||||||
<p>Docked ${station.dockedShips} / ${station.dockingPads}
|
<p>Docked ${station.dockedShips} / ${station.dockingPads}
|
||||||
<br>
|
<br>
|
||||||
${dockedShipLabels}</p>
|
${dockedShipLabels}</p>
|
||||||
|
|||||||
@@ -320,8 +320,9 @@ export function renderSystemDetails(
|
|||||||
|
|
||||||
export function describeShipState(world: WorldState | undefined, ship: ShipSnapshot): string {
|
export function describeShipState(world: WorldState | undefined, ship: ShipSnapshot): string {
|
||||||
const baseState = ship.state;
|
const baseState = ship.state;
|
||||||
|
const currentSubTask = ship.activeSubTasks[0];
|
||||||
if (baseState === "capacitor-starved") {
|
if (baseState === "capacitor-starved") {
|
||||||
return `${baseState} while ${describeControllerTask(ship.controllerTaskKind)}`;
|
return currentSubTask ? `${baseState} while ${titleTask(currentSubTask.kind)}` : baseState;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!world || (baseState !== "ftl" && baseState !== "spooling-ftl" && baseState !== "warping" && baseState !== "spooling-warp")) {
|
if (!world || (baseState !== "ftl" && baseState !== "spooling-ftl" && baseState !== "warping" && baseState !== "spooling-warp")) {
|
||||||
@@ -347,75 +348,52 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
|
|||||||
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function describeControllerTask(taskKind: string): string {
|
|
||||||
switch (taskKind) {
|
|
||||||
case "travel":
|
|
||||||
return "travel";
|
|
||||||
case "extract":
|
|
||||||
return "mining";
|
|
||||||
case "dock":
|
|
||||||
return "docking";
|
|
||||||
case "unload":
|
|
||||||
return "transfer";
|
|
||||||
case "deliver-construction":
|
|
||||||
return "material delivery";
|
|
||||||
case "build-construction-site":
|
|
||||||
return "site construction";
|
|
||||||
case "construct-module":
|
|
||||||
return "module construction";
|
|
||||||
case "undock":
|
|
||||||
return "undocking";
|
|
||||||
case "load-workers":
|
|
||||||
return "worker loading";
|
|
||||||
case "unload-workers":
|
|
||||||
return "worker unloading";
|
|
||||||
default:
|
|
||||||
return taskKind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function describeShipObjective(objective: string): string {
|
export function describeShipObjective(objective: string): string {
|
||||||
switch (objective) {
|
return objective.replace(/[-_]+/g, " ");
|
||||||
case "set-mining-objective": return "mine resources";
|
|
||||||
case "set-patrol-objective": return "patrol";
|
|
||||||
case "set-construction-objective": return "build station";
|
|
||||||
case "set-idle-objective": return "idle";
|
|
||||||
default: return objective;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeShipBehavior(ship: ShipSnapshot): string {
|
export function describeShipBehavior(ship: ShipSnapshot): string {
|
||||||
return ship.behaviorPhase
|
const parts = [ship.defaultBehavior.kind];
|
||||||
? `${ship.defaultBehaviorKind} · ${ship.behaviorPhase}`
|
if (ship.assignment?.kind) {
|
||||||
: ship.defaultBehaviorKind;
|
parts.push(ship.assignment.kind);
|
||||||
|
}
|
||||||
|
return parts.join(" · ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeShipOrder(ship: ShipSnapshot): string {
|
export function describeShipOrder(ship: ShipSnapshot): string {
|
||||||
const orderParts: string[] = [];
|
const activeOrder = [...ship.orderQueue]
|
||||||
if (ship.orderKind) {
|
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||||
orderParts.push(ship.orderKind);
|
if (activeOrder) {
|
||||||
}
|
return activeOrder.label ?? activeOrder.kind;
|
||||||
if (ship.commanderObjective) {
|
|
||||||
orderParts.push(describeShipObjective(ship.commanderObjective));
|
|
||||||
}
|
|
||||||
if (orderParts.length > 0) {
|
|
||||||
return orderParts.join(" · ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return describeControllerTask(ship.controllerTaskKind);
|
if (ship.assignment?.kind) {
|
||||||
|
return describeShipObjective(ship.assignment.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ship.activePlan) {
|
||||||
|
return ship.activePlan.summary || ship.activePlan.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ship.defaultBehavior.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined {
|
export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined {
|
||||||
if (!ship.currentAction) {
|
const subTask = ship.activeSubTasks[0];
|
||||||
|
if (!subTask) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: ship.currentAction.label,
|
label: subTask.summary || titleTask(subTask.kind),
|
||||||
progress: Math.max(0, Math.min(ship.currentAction.progress, 1)),
|
progress: Math.max(0, Math.min(subTask.progress, 1)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function titleTask(value: string): string {
|
||||||
|
return value.replace(/[-_]+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
export function describeShipLocation(world: WorldState | undefined, ship: ShipSnapshot): { system: string; local?: string } {
|
export function describeShipLocation(world: WorldState | undefined, ship: ShipSnapshot): { system: string; local?: string } {
|
||||||
const systemId = ship.spatialState.currentSystemId || ship.systemId;
|
const systemId = ship.spatialState.currentSystemId || ship.systemId;
|
||||||
const system = world?.systems.get(systemId);
|
const system = world?.systems.get(systemId);
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
|
|||||||
policies: new Map(snapshot.policies.map((policy) => [policy.id, policy])),
|
policies: new Map(snapshot.policies.map((policy) => [policy.id, policy])),
|
||||||
ships: new Map(snapshot.ships.map((ship) => [ship.id, ship])),
|
ships: new Map(snapshot.ships.map((ship) => [ship.id, ship])),
|
||||||
factions: new Map(snapshot.factions.map((faction) => [faction.id, faction])),
|
factions: new Map(snapshot.factions.map((faction) => [faction.id, faction])),
|
||||||
|
playerFaction: snapshot.playerFaction ?? null,
|
||||||
|
geopolitics: snapshot.geopolitics ?? null,
|
||||||
recentEvents: [],
|
recentEvents: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -88,8 +90,14 @@ export function applyDeltaToWorld(world: WorldState, delta: WorldDelta): boolean
|
|||||||
for (const faction of delta.factions) {
|
for (const faction of delta.factions) {
|
||||||
world.factions.set(faction.id, faction);
|
world.factions.set(faction.id, faction);
|
||||||
}
|
}
|
||||||
|
if (delta.playerFaction !== undefined) {
|
||||||
|
world.playerFaction = delta.playerFaction ?? null;
|
||||||
|
}
|
||||||
|
if (delta.geopolitics !== undefined) {
|
||||||
|
world.geopolitics = delta.geopolitics ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
return delta.factions.length > 0;
|
return delta.factions.length > 0 || delta.playerFaction !== undefined || delta.geopolitics !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta, rawBytes: number): void {
|
export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta, rawBytes: number): void {
|
||||||
@@ -102,10 +110,11 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
|
|||||||
+ delta.marketOrders.length
|
+ delta.marketOrders.length
|
||||||
+ delta.policies.length
|
+ delta.policies.length
|
||||||
+ delta.factions.length;
|
+ delta.factions.length;
|
||||||
|
const changedEntitiesWithPlayer = changedEntities + (delta.playerFaction ? 1 : 0) + (delta.geopolitics ? 1 : 0);
|
||||||
networkStats.deltasReceived += 1;
|
networkStats.deltasReceived += 1;
|
||||||
networkStats.deltaBytes += rawBytes;
|
networkStats.deltaBytes += rawBytes;
|
||||||
networkStats.lastDeltaBytes = rawBytes;
|
networkStats.lastDeltaBytes = rawBytes;
|
||||||
networkStats.lastEntityChanges = changedEntities;
|
networkStats.lastEntityChanges = changedEntitiesWithPlayer;
|
||||||
networkStats.eventsReceived += delta.events.length;
|
networkStats.eventsReceived += delta.events.length;
|
||||||
networkStats.lastDeltaAtMs = performance.now();
|
networkStats.lastDeltaAtMs = performance.now();
|
||||||
networkStats.throughputSamples.push({ atMs: performance.now(), bytes: rawBytes });
|
networkStats.throughputSamples.push({ atMs: performance.now(), bytes: rawBytes });
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
StationSnapshot,
|
StationSnapshot,
|
||||||
SystemSnapshot,
|
SystemSnapshot,
|
||||||
OrbitalSimulationSnapshot,
|
OrbitalSimulationSnapshot,
|
||||||
|
PlayerFactionSnapshot,
|
||||||
|
GeopoliticalStateSnapshot,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
|
|
||||||
export type PovLevel = "local" | "system" | "galaxy";
|
export type PovLevel = "local" | "system" | "galaxy";
|
||||||
@@ -152,6 +154,8 @@ export interface WorldState {
|
|||||||
policies: Map<string, PolicySetSnapshot>;
|
policies: Map<string, PolicySetSnapshot>;
|
||||||
ships: Map<string, ShipSnapshot>;
|
ships: Map<string, ShipSnapshot>;
|
||||||
factions: Map<string, FactionSnapshot>;
|
factions: Map<string, FactionSnapshot>;
|
||||||
|
playerFaction: PlayerFactionSnapshot | null;
|
||||||
|
geopolitics: GeopoliticalStateSnapshot | null;
|
||||||
recentEvents: SimulationEventRecord[];
|
recentEvents: SimulationEventRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 { useGmStore } from "./ui/stores/gmStore";
|
||||||
|
import { usePlayerFactionStore } from "./ui/stores/playerFactionStore";
|
||||||
import { viewerPinia } from "./ui/stores/pinia";
|
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";
|
||||||
@@ -205,7 +206,9 @@ export class ViewerWorldLifecycle {
|
|||||||
[...world.stations.values()],
|
[...world.stations.values()],
|
||||||
[...world.factions.values()],
|
[...world.factions.values()],
|
||||||
[...world.marketOrders.values()],
|
[...world.marketOrders.values()],
|
||||||
|
world.geopolitics,
|
||||||
);
|
);
|
||||||
|
usePlayerFactionStore(viewerPinia).setPlayerFaction(world.playerFaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user