feat: improved ops-strip with faction and stations
This commit is contained in:
@@ -1,5 +1,18 @@
|
|||||||
namespace SpaceGame.Simulation.Api.Contracts;
|
namespace SpaceGame.Simulation.Api.Contracts;
|
||||||
|
|
||||||
|
public sealed record FactionGoapStateSnapshot(
|
||||||
|
int MilitaryShipCount,
|
||||||
|
int MinerShipCount,
|
||||||
|
int TransportShipCount,
|
||||||
|
int ConstructorShipCount,
|
||||||
|
int ControlledSystemCount,
|
||||||
|
int TargetSystemCount,
|
||||||
|
bool HasShipFactory,
|
||||||
|
float OreStockpile,
|
||||||
|
float RefinedMetalsStockpile);
|
||||||
|
|
||||||
|
public sealed record FactionGoapPrioritySnapshot(string GoalName, float Priority);
|
||||||
|
|
||||||
public sealed record FactionSnapshot(
|
public sealed record FactionSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
string Label,
|
string Label,
|
||||||
@@ -10,7 +23,9 @@ public sealed record FactionSnapshot(
|
|||||||
float GoodsProduced,
|
float GoodsProduced,
|
||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId);
|
string? DefaultPolicySetId,
|
||||||
|
FactionGoapStateSnapshot? GoapState,
|
||||||
|
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
|
||||||
|
|
||||||
public sealed record FactionDelta(
|
public sealed record FactionDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -22,4 +37,6 @@ public sealed record FactionDelta(
|
|||||||
float GoodsProduced,
|
float GoodsProduced,
|
||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost,
|
int ShipsLost,
|
||||||
string? DefaultPolicySetId);
|
string? DefaultPolicySetId,
|
||||||
|
FactionGoapStateSnapshot? GoapState,
|
||||||
|
IReadOnlyList<FactionGoapPrioritySnapshot>? GoapPriorities);
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public sealed class CommanderRuntime
|
|||||||
public CommanderTaskRuntime? ActiveTask { get; set; }
|
public CommanderTaskRuntime? ActiveTask { get; set; }
|
||||||
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
public bool IsAlive { get; set; } = true;
|
public bool IsAlive { get; set; } = true;
|
||||||
|
public FactionPlanningState? LastPlanningState { get; set; }
|
||||||
|
public IReadOnlyList<(string Name, float Priority)>? LastGoalPriorities { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CommanderBehaviorRuntime
|
public sealed class CommanderBehaviorRuntime
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ public sealed partial class ScenarioLoader
|
|||||||
factionIds.Add(DefaultFactionId);
|
factionIds.Add(DefaultFactionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
factionIds.Add(UnclaimedFactionId);
|
|
||||||
|
|
||||||
return factionIds
|
return factionIds
|
||||||
.Distinct(StringComparer.Ordinal)
|
|
||||||
.Select(CreateFaction)
|
.Select(CreateFaction)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
@@ -40,13 +37,6 @@ public sealed partial class ScenarioLoader
|
|||||||
Color = "#7ed4ff",
|
Color = "#7ed4ff",
|
||||||
Credits = MinimumFactionCredits,
|
Credits = MinimumFactionCredits,
|
||||||
},
|
},
|
||||||
UnclaimedFactionId => new FactionRuntime
|
|
||||||
{
|
|
||||||
Id = factionId,
|
|
||||||
Label = "Unclaimed",
|
|
||||||
Color = "#7f8794",
|
|
||||||
Credits = 0f,
|
|
||||||
},
|
|
||||||
_ => new FactionRuntime
|
_ => new FactionRuntime
|
||||||
{
|
{
|
||||||
Id = factionId,
|
Id = factionId,
|
||||||
@@ -107,26 +97,21 @@ public sealed partial class ScenarioLoader
|
|||||||
var claims = new List<ClaimRuntime>();
|
var claims = new List<ClaimRuntime>();
|
||||||
foreach (var node in nodes.Where((candidate) => candidate.Kind == SpatialNodeKind.LagrangePoint))
|
foreach (var node in nodes.Where((candidate) => candidate.Kind == SpatialNodeKind.LagrangePoint))
|
||||||
{
|
{
|
||||||
var owningFactionId = stationsByAnchorNodeId.TryGetValue(node.Id, out var station)
|
if (!stationsByAnchorNodeId.TryGetValue(node.Id, out var station))
|
||||||
? station.FactionId
|
{
|
||||||
: UnclaimedFactionId;
|
continue;
|
||||||
var activatesAtUtc = owningFactionId == UnclaimedFactionId
|
}
|
||||||
? nowUtc
|
|
||||||
: nowUtc.AddSeconds(8);
|
|
||||||
var state = owningFactionId == UnclaimedFactionId
|
|
||||||
? ClaimStateKinds.Active
|
|
||||||
: ClaimStateKinds.Activating;
|
|
||||||
|
|
||||||
claims.Add(new ClaimRuntime
|
claims.Add(new ClaimRuntime
|
||||||
{
|
{
|
||||||
Id = $"claim-{node.Id}",
|
Id = $"claim-{node.Id}",
|
||||||
FactionId = owningFactionId,
|
FactionId = station.FactionId,
|
||||||
SystemId = node.SystemId,
|
SystemId = node.SystemId,
|
||||||
NodeId = node.Id,
|
NodeId = node.Id,
|
||||||
BubbleId = node.BubbleId,
|
BubbleId = node.BubbleId,
|
||||||
PlacedAtUtc = nowUtc,
|
PlacedAtUtc = nowUtc,
|
||||||
ActivatesAtUtc = activatesAtUtc,
|
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
||||||
State = state,
|
State = ClaimStateKinds.Activating,
|
||||||
Health = 100f,
|
Health = 100f,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -248,11 +233,6 @@ public sealed partial class ScenarioLoader
|
|||||||
var policies = new List<PolicySetRuntime>(factions.Count);
|
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||||
foreach (var faction in factions)
|
foreach (var faction in factions)
|
||||||
{
|
{
|
||||||
if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var policyId = $"policy-{faction.Id}";
|
var policyId = $"policy-{faction.Id}";
|
||||||
faction.DefaultPolicySetId = policyId;
|
faction.DefaultPolicySetId = policyId;
|
||||||
policies.Add(new PolicySetRuntime
|
policies.Add(new PolicySetRuntime
|
||||||
@@ -277,11 +257,6 @@ public sealed partial class ScenarioLoader
|
|||||||
|
|
||||||
foreach (var faction in factions)
|
foreach (var faction in factions)
|
||||||
{
|
{
|
||||||
if (string.Equals(faction.Id, UnclaimedFactionId, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var commander = new CommanderRuntime
|
var commander = new CommanderRuntime
|
||||||
{
|
{
|
||||||
Id = $"commander-faction-{faction.Id}",
|
Id = $"commander-faction-{faction.Id}",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
|||||||
public sealed partial class ScenarioLoader
|
public sealed partial class ScenarioLoader
|
||||||
{
|
{
|
||||||
private const string DefaultFactionId = "sol-dominion";
|
private const string DefaultFactionId = "sol-dominion";
|
||||||
private const string UnclaimedFactionId = "unclaimed";
|
|
||||||
private const int WorldSeed = 1;
|
private const int WorldSeed = 1;
|
||||||
private const float MinimumFactionCredits = 0f;
|
private const float MinimumFactionCredits = 0f;
|
||||||
private const float MinimumRefineryOre = 0f;
|
private const float MinimumRefineryOre = 0f;
|
||||||
|
|||||||
@@ -81,7 +81,11 @@ public sealed partial class SimulationEngine
|
|||||||
var rankedGoals = _factionGoals
|
var rankedGoals = _factionGoals
|
||||||
.Select(g => (goal: g, priority: g.ComputePriority(state, world, commander)))
|
.Select(g => (goal: g, priority: g.ComputePriority(state, world, commander)))
|
||||||
.Where(x => x.priority > 0f)
|
.Where(x => x.priority > 0f)
|
||||||
.OrderByDescending(x => x.priority);
|
.OrderByDescending(x => x.priority)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
commander.LastPlanningState = state;
|
||||||
|
commander.LastGoalPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList();
|
||||||
|
|
||||||
// Execute the first action of each active goal's plan (top 3 to avoid conflicts).
|
// Execute the first action of each active goal's plan (top 3 to avoid conflicts).
|
||||||
foreach (var (goal, _) in rankedGoals.Take(3))
|
foreach (var (goal, _) in rankedGoals.Take(3))
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ public sealed partial class SimulationEngine
|
|||||||
ship.History,
|
ship.History,
|
||||||
ship.CurrentAction,
|
ship.CurrentAction,
|
||||||
ship.SpatialState)).ToList(),
|
ship.SpatialState)).ToList(),
|
||||||
world.Factions.Select(ToFactionDelta).Select(faction => new FactionSnapshot(
|
world.Factions.Select(faction => ToFactionDelta(faction, FindFactionCommander(world, faction.Id))).Select(faction => new FactionSnapshot(
|
||||||
faction.Id,
|
faction.Id,
|
||||||
faction.Label,
|
faction.Label,
|
||||||
faction.Color,
|
faction.Color,
|
||||||
@@ -179,7 +179,9 @@ public sealed partial class SimulationEngine
|
|||||||
faction.GoodsProduced,
|
faction.GoodsProduced,
|
||||||
faction.ShipsBuilt,
|
faction.ShipsBuilt,
|
||||||
faction.ShipsLost,
|
faction.ShipsLost,
|
||||||
faction.DefaultPolicySetId)).ToList());
|
faction.DefaultPolicySetId,
|
||||||
|
faction.GoapState,
|
||||||
|
faction.GoapPriorities)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PrimeDeltaBaseline(SimulationWorld world)
|
public void PrimeDeltaBaseline(SimulationWorld world)
|
||||||
@@ -231,7 +233,7 @@ public sealed partial class SimulationEngine
|
|||||||
|
|
||||||
foreach (var faction in world.Factions)
|
foreach (var faction in world.Factions)
|
||||||
{
|
{
|
||||||
faction.LastDeltaSignature = BuildFactionSignature(faction);
|
faction.LastDeltaSignature = BuildFactionSignature(faction, FindFactionCommander(world, faction.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,19 +404,25 @@ public sealed partial class SimulationEngine
|
|||||||
var deltas = new List<FactionDelta>();
|
var deltas = new List<FactionDelta>();
|
||||||
foreach (var faction in world.Factions)
|
foreach (var faction in world.Factions)
|
||||||
{
|
{
|
||||||
var signature = BuildFactionSignature(faction);
|
var commander = FindFactionCommander(world, faction.Id);
|
||||||
|
var signature = BuildFactionSignature(faction, commander);
|
||||||
if (signature == faction.LastDeltaSignature)
|
if (signature == faction.LastDeltaSignature)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
faction.LastDeltaSignature = signature;
|
faction.LastDeltaSignature = signature;
|
||||||
deltas.Add(ToFactionDelta(faction));
|
deltas.Add(ToFactionDelta(faction, commander));
|
||||||
}
|
}
|
||||||
|
|
||||||
return deltas;
|
return deltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) =>
|
||||||
|
world.Commanders.FirstOrDefault(c =>
|
||||||
|
c.FactionId == factionId &&
|
||||||
|
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
|
||||||
|
|
||||||
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
|
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
|
||||||
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}";
|
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}";
|
||||||
|
|
||||||
@@ -508,8 +516,13 @@ public sealed partial class SimulationEngine
|
|||||||
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
||||||
.Select(entry => $"{entry.Key}:{entry.Value:0.###}"));
|
.Select(entry => $"{entry.Key}:{entry.Value:0.###}"));
|
||||||
|
|
||||||
private static string BuildFactionSignature(FactionRuntime faction) =>
|
private static string BuildFactionSignature(FactionRuntime faction, CommanderRuntime? commander)
|
||||||
$"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}";
|
{
|
||||||
|
var goapSig = commander?.LastGoalPriorities is { } prios
|
||||||
|
? string.Join(",", prios.Select(p => $"{p.Name}:{p.Priority:0.##}"))
|
||||||
|
: string.Empty;
|
||||||
|
return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{goapSig}";
|
||||||
|
}
|
||||||
|
|
||||||
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
||||||
node.Id,
|
node.Id,
|
||||||
@@ -755,17 +768,44 @@ public sealed partial class SimulationEngine
|
|||||||
.Select(entry => new InventoryEntry(entry.Key, entry.Value))
|
.Select(entry => new InventoryEntry(entry.Key, entry.Value))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
private static FactionDelta ToFactionDelta(FactionRuntime faction) => new(
|
private static FactionDelta ToFactionDelta(FactionRuntime faction, CommanderRuntime? commander)
|
||||||
faction.Id,
|
{
|
||||||
faction.Label,
|
FactionGoapStateSnapshot? goapState = null;
|
||||||
faction.Color,
|
IReadOnlyList<FactionGoapPrioritySnapshot>? goapPriorities = null;
|
||||||
faction.Credits,
|
|
||||||
faction.PopulationTotal,
|
if (commander?.LastPlanningState is { } ps)
|
||||||
faction.OreMined,
|
{
|
||||||
faction.GoodsProduced,
|
goapState = new FactionGoapStateSnapshot(
|
||||||
faction.ShipsBuilt,
|
ps.MilitaryShipCount,
|
||||||
faction.ShipsLost,
|
ps.MinerShipCount,
|
||||||
faction.DefaultPolicySetId);
|
ps.TransportShipCount,
|
||||||
|
ps.ConstructorShipCount,
|
||||||
|
ps.ControlledSystemCount,
|
||||||
|
ps.TargetSystemCount,
|
||||||
|
ps.HasShipFactory,
|
||||||
|
ps.OreStockpile,
|
||||||
|
ps.RefinedMetalsStockpile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commander?.LastGoalPriorities is { } prios)
|
||||||
|
{
|
||||||
|
goapPriorities = prios.Select(p => new FactionGoapPrioritySnapshot(p.Name, p.Priority)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FactionDelta(
|
||||||
|
faction.Id,
|
||||||
|
faction.Label,
|
||||||
|
faction.Color,
|
||||||
|
faction.Credits,
|
||||||
|
faction.PopulationTotal,
|
||||||
|
faction.OreMined,
|
||||||
|
faction.GoodsProduced,
|
||||||
|
faction.ShipsBuilt,
|
||||||
|
faction.ShipsLost,
|
||||||
|
faction.DefaultPolicySetId,
|
||||||
|
goapState,
|
||||||
|
goapPriorities);
|
||||||
|
}
|
||||||
|
|
||||||
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
||||||
state.SpaceLayer,
|
state.SpaceLayer,
|
||||||
|
|||||||
@@ -300,27 +300,24 @@ public sealed partial class SimulationEngine
|
|||||||
|
|
||||||
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
private static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
||||||
{
|
{
|
||||||
return world.Claims
|
return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id));
|
||||||
.Where(claim => claim.State != ClaimStateKinds.Destroyed)
|
|
||||||
.Select(claim => claim.SystemId)
|
|
||||||
.Distinct(StringComparer.Ordinal)
|
|
||||||
.Count(systemId => FactionControlsSystem(world, factionId, systemId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||||
{
|
{
|
||||||
var buildableLocations = world.Claims
|
var totalLagrangePoints = world.SpatialNodes.Count(node =>
|
||||||
.Where(claim =>
|
node.SystemId == systemId &&
|
||||||
claim.SystemId == systemId &&
|
node.Kind == SpatialNodeKind.LagrangePoint);
|
||||||
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active)
|
if (totalLagrangePoints == 0)
|
||||||
.ToList();
|
|
||||||
if (buildableLocations.Count == 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownedLocations = buildableLocations.Count(claim => claim.FactionId == factionId);
|
var ownedLocations = world.Claims.Count(claim =>
|
||||||
return ownedLocations > (buildableLocations.Count / 2f);
|
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);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export class ViewerAppController {
|
|||||||
private readonly systemBodyEl: HTMLDivElement;
|
private readonly systemBodyEl: HTMLDivElement;
|
||||||
private readonly detailTitleEl: HTMLHeadingElement;
|
private readonly detailTitleEl: HTMLHeadingElement;
|
||||||
private readonly detailBodyEl: HTMLDivElement;
|
private readonly detailBodyEl: HTMLDivElement;
|
||||||
private readonly factionStripEl: HTMLDivElement;
|
private readonly opsStripEl: HTMLDivElement;
|
||||||
private readonly networkSectionEl: HTMLDivElement;
|
private readonly networkSectionEl: HTMLDivElement;
|
||||||
private readonly networkSummaryEl: HTMLSpanElement;
|
private readonly networkSummaryEl: HTMLSpanElement;
|
||||||
private readonly networkPanelEl: HTMLDivElement;
|
private readonly networkPanelEl: HTMLDivElement;
|
||||||
@@ -208,7 +208,7 @@ export class ViewerAppController {
|
|||||||
this.systemBodyEl = hud.systemBodyEl;
|
this.systemBodyEl = hud.systemBodyEl;
|
||||||
this.detailTitleEl = hud.detailTitleEl;
|
this.detailTitleEl = hud.detailTitleEl;
|
||||||
this.detailBodyEl = hud.detailBodyEl;
|
this.detailBodyEl = hud.detailBodyEl;
|
||||||
this.factionStripEl = hud.factionStripEl;
|
this.opsStripEl = hud.opsStripEl;
|
||||||
this.networkSummaryEl = hud.networkSummaryEl;
|
this.networkSummaryEl = hud.networkSummaryEl;
|
||||||
this.networkPanelEl = hud.networkPanelEl;
|
this.networkPanelEl = hud.networkPanelEl;
|
||||||
this.performanceSectionEl = hud.performanceSectionEl;
|
this.performanceSectionEl = hud.performanceSectionEl;
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
export interface FactionGoapState {
|
||||||
|
militaryShipCount: number;
|
||||||
|
minerShipCount: number;
|
||||||
|
transportShipCount: number;
|
||||||
|
constructorShipCount: number;
|
||||||
|
controlledSystemCount: number;
|
||||||
|
targetSystemCount: number;
|
||||||
|
hasShipFactory: boolean;
|
||||||
|
oreStockpile: number;
|
||||||
|
refinedMetalsStockpile: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionGoapPriority {
|
||||||
|
goalName: string;
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FactionSnapshot {
|
export interface FactionSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -9,6 +26,8 @@ export interface FactionSnapshot {
|
|||||||
shipsBuilt: number;
|
shipsBuilt: number;
|
||||||
shipsLost: number;
|
shipsLost: number;
|
||||||
defaultPolicySetId?: string | null;
|
defaultPolicySetId?: string | null;
|
||||||
|
goapState?: FactionGoapState | null;
|
||||||
|
goapPriorities?: FactionGoapPriority[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionDelta extends FactionSnapshot {}
|
export interface FactionDelta extends FactionSnapshot {}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ canvas {
|
|||||||
.info-panel,
|
.info-panel,
|
||||||
.network-panel,
|
.network-panel,
|
||||||
.performance-panel,
|
.performance-panel,
|
||||||
.ship-strip {
|
.ops-strip {
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
@@ -413,7 +413,7 @@ canvas {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ship-strip {
|
.ops-strip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -536,6 +536,24 @@ canvas {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.faction-card {
|
||||||
|
border-top-color: rgba(180, 130, 255, 0.3);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-card:hover {
|
||||||
|
transform: none;
|
||||||
|
border-color: rgba(180, 130, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-card {
|
||||||
|
border-top-color: rgba(127, 255, 180, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.station-card:hover {
|
||||||
|
border-color: rgba(127, 255, 180, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -544,7 +562,7 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.ship-strip {
|
.ops-strip {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,7 +602,7 @@ canvas {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ship-strip {
|
.ops-strip {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function createViewerControllers(host: any) {
|
|||||||
getNetworkStats: () => host.networkStats,
|
getNetworkStats: () => host.networkStats,
|
||||||
getSystemSummaryVisuals: () => host.systemSummaryVisuals,
|
getSystemSummaryVisuals: () => host.systemSummaryVisuals,
|
||||||
errorEl: host.errorEl,
|
errorEl: host.errorEl,
|
||||||
factionStripEl: host.factionStripEl,
|
opsStripEl: host.opsStripEl,
|
||||||
detailTitleEl: host.detailTitleEl,
|
detailTitleEl: host.detailTitleEl,
|
||||||
detailBodyEl: host.detailBodyEl,
|
detailBodyEl: host.detailBodyEl,
|
||||||
worldLabel: () => host.world?.label ?? "",
|
worldLabel: () => host.world?.label ?? "",
|
||||||
@@ -267,8 +267,8 @@ export function wireViewerEvents(host: any) {
|
|||||||
host.renderer.domElement.addEventListener("click", host.interactionController.onClick);
|
host.renderer.domElement.addEventListener("click", host.interactionController.onClick);
|
||||||
host.renderer.domElement.addEventListener("dblclick", host.interactionController.onDoubleClick);
|
host.renderer.domElement.addEventListener("dblclick", host.interactionController.onDoubleClick);
|
||||||
host.renderer.domElement.addEventListener("wheel", host.interactionController.onWheel, { passive: false });
|
host.renderer.domElement.addEventListener("wheel", host.interactionController.onWheel, { passive: false });
|
||||||
host.factionStripEl.addEventListener("click", host.interactionController.onShipStripClick);
|
host.opsStripEl.addEventListener("click", host.interactionController.onOpsStripClick);
|
||||||
host.factionStripEl.addEventListener("dblclick", host.interactionController.onShipStripDoubleClick);
|
host.opsStripEl.addEventListener("dblclick", host.interactionController.onOpsStripDoubleClick);
|
||||||
host.historyLayerEl.addEventListener("click", host.interactionController.onHistoryLayerClick);
|
host.historyLayerEl.addEventListener("click", host.interactionController.onHistoryLayerClick);
|
||||||
host.historyLayerEl.addEventListener("pointerdown", host.interactionController.onHistoryLayerPointerDown);
|
host.historyLayerEl.addEventListener("pointerdown", host.interactionController.onHistoryLayerPointerDown);
|
||||||
window.addEventListener("pointermove", host.interactionController.onHistoryWindowPointerMove);
|
window.addEventListener("pointermove", host.interactionController.onHistoryWindowPointerMove);
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { inventoryAmount } from "./viewerMath";
|
|
||||||
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
|
||||||
import type { CameraMode, Selectable, WorldState, ZoomLevel } from "./viewerTypes";
|
|
||||||
|
|
||||||
export function renderFactionStrip(
|
|
||||||
world: WorldState | undefined,
|
|
||||||
selectedItems: Selectable[],
|
|
||||||
cameraMode: CameraMode,
|
|
||||||
cameraTargetShipId?: string,
|
|
||||||
zoomLevel?: ZoomLevel,
|
|
||||||
activeSystemId?: string,
|
|
||||||
) {
|
|
||||||
if (!world) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const ships = [...world.ships.values()]
|
|
||||||
.filter((ship) => {
|
|
||||||
if (zoomLevel === "universe" || !activeSystemId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ship.systemId === activeSystemId;
|
|
||||||
})
|
|
||||||
.sort((left, right) => left.label.localeCompare(right.label));
|
|
||||||
|
|
||||||
return ships
|
|
||||||
.map((ship) => {
|
|
||||||
const cargo = ship.inventory.reduce((sum, e) => sum + e.amount, 0);
|
|
||||||
const shipLocation = describeShipLocation(world, ship);
|
|
||||||
const shipState = describeShipState(world, ship);
|
|
||||||
const shipAction = describeShipCurrentAction(ship);
|
|
||||||
const isSelected = selectedItems.length === 1
|
|
||||||
&& selectedItems[0].kind === "ship"
|
|
||||||
&& selectedItems[0].id === ship.id;
|
|
||||||
const isFollowed = cameraMode === "follow" && cameraTargetShipId === ship.id;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<article class="ship-card${isSelected ? " is-selected" : ""}${isFollowed ? " is-followed" : ""}" data-ship-id="${ship.id}">
|
|
||||||
<div class="ship-card-header">
|
|
||||||
<h3>${ship.label}</h3>
|
|
||||||
<div class="ship-card-meta">
|
|
||||||
<span class="ship-card-badge">${ship.class}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ship-card-history-button"
|
|
||||||
data-history-ship-id="${ship.id}"
|
|
||||||
aria-label="Open history for ${ship.label}"
|
|
||||||
title="Open history"
|
|
||||||
>🕔</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>${shipLocation.system}${shipLocation.local ? `<br>${shipLocation.local}` : ""}</p>
|
|
||||||
<p>Cargo ${cargo.toFixed(0)}</p>
|
|
||||||
<p>State ${shipState}</p>
|
|
||||||
${shipAction ? `
|
|
||||||
<div class="ship-action-progress">
|
|
||||||
<div class="ship-action-progress-label">
|
|
||||||
<span>${shipAction.label}</span>
|
|
||||||
<span>${Math.round(shipAction.progress * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="ship-action-progress-track">
|
|
||||||
<div class="ship-action-progress-fill" style="width: ${(shipAction.progress * 100).toFixed(1)}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ""}
|
|
||||||
<div class="ship-card-ai">
|
|
||||||
${ship.commanderObjective ? `<p>Objective ${describeShipObjective(ship.commanderObjective)}</p>` : ""}
|
|
||||||
<p>Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}</p>
|
|
||||||
<p>Task ${ship.controllerTaskKind}</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ export interface ViewerHudElements {
|
|||||||
systemBodyEl: HTMLDivElement;
|
systemBodyEl: HTMLDivElement;
|
||||||
detailTitleEl: HTMLHeadingElement;
|
detailTitleEl: HTMLHeadingElement;
|
||||||
detailBodyEl: HTMLDivElement;
|
detailBodyEl: HTMLDivElement;
|
||||||
factionStripEl: HTMLDivElement;
|
opsStripEl: HTMLDivElement;
|
||||||
networkSummaryEl: HTMLSpanElement;
|
networkSummaryEl: HTMLSpanElement;
|
||||||
networkPanelEl: HTMLDivElement;
|
networkPanelEl: HTMLDivElement;
|
||||||
performanceSectionEl: HTMLDivElement;
|
performanceSectionEl: HTMLDivElement;
|
||||||
@@ -71,7 +71,7 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
|
|||||||
<div class="error-strip" hidden></div>
|
<div class="error-strip" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-layer"></div>
|
<div class="history-layer"></div>
|
||||||
<section class="ship-strip"></section>
|
<section class="ops-strip"></section>
|
||||||
<div class="marquee-box"></div>
|
<div class="marquee-box"></div>
|
||||||
<div class="hover-label" hidden></div>
|
<div class="hover-label" hidden></div>
|
||||||
`;
|
`;
|
||||||
@@ -87,7 +87,7 @@ export function createViewerHud(documentRef: Document): ViewerHudElements {
|
|||||||
systemBodyEl: root.querySelector(".system-body") as HTMLDivElement,
|
systemBodyEl: root.querySelector(".system-body") as HTMLDivElement,
|
||||||
detailTitleEl: root.querySelector(".detail-title") as HTMLHeadingElement,
|
detailTitleEl: root.querySelector(".detail-title") as HTMLHeadingElement,
|
||||||
detailBodyEl: root.querySelector(".detail-body") as HTMLDivElement,
|
detailBodyEl: root.querySelector(".detail-body") as HTMLDivElement,
|
||||||
factionStripEl: root.querySelector(".ship-strip") as HTMLDivElement,
|
opsStripEl: root.querySelector(".ops-strip") as HTMLDivElement,
|
||||||
networkSummaryEl: root.querySelector(".network-summary") as HTMLSpanElement,
|
networkSummaryEl: root.querySelector(".network-summary") as HTMLSpanElement,
|
||||||
networkPanelEl: root.querySelector(".network-body") as HTMLDivElement,
|
networkPanelEl: root.querySelector(".network-body") as HTMLDivElement,
|
||||||
performanceSectionEl: root.querySelector(".performance-panel") as HTMLDivElement,
|
performanceSectionEl: root.querySelector(".performance-panel") as HTMLDivElement,
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ViewerInteractionController {
|
|||||||
this.context.updatePanels();
|
this.context.updatePanels();
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly onShipStripClick = (event: MouseEvent) => {
|
readonly onOpsStripClick = (event: MouseEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -163,18 +163,25 @@ export class ViewerInteractionController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const card = target.closest<HTMLElement>("[data-ship-id]");
|
const shipCard = target.closest<HTMLElement>("[data-ship-id]");
|
||||||
const shipId = card?.dataset.shipId;
|
const shipId = shipCard?.dataset.shipId;
|
||||||
if (!shipId) {
|
if (shipId) {
|
||||||
|
this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
|
||||||
|
this.context.syncFollowStateFromSelection();
|
||||||
|
this.context.updatePanels();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
|
const stationCard = target.closest<HTMLElement>("[data-station-id]");
|
||||||
this.context.syncFollowStateFromSelection();
|
const stationId = stationCard?.dataset.stationId;
|
||||||
this.context.updatePanels();
|
if (stationId) {
|
||||||
|
this.context.setSelectedItems([{ kind: "station", id: stationId }]);
|
||||||
|
this.context.syncFollowStateFromSelection();
|
||||||
|
this.context.updatePanels();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly onShipStripDoubleClick = (event: MouseEvent) => {
|
readonly onOpsStripDoubleClick = (event: MouseEvent) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -184,18 +191,28 @@ export class ViewerInteractionController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const card = target.closest<HTMLElement>("[data-ship-id]");
|
const shipCard = target.closest<HTMLElement>("[data-ship-id]");
|
||||||
const shipId = card?.dataset.shipId;
|
const shipId = shipCard?.dataset.shipId;
|
||||||
if (!shipId) {
|
if (shipId) {
|
||||||
|
this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
|
||||||
|
this.context.syncFollowStateFromSelection();
|
||||||
|
this.context.focusOnSelection({ kind: "ship", id: shipId });
|
||||||
|
this.toggleCameraMode("follow");
|
||||||
|
this.context.updatePanels();
|
||||||
|
this.context.updateGamePanel("Live");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context.setSelectedItems([{ kind: "ship", id: shipId }]);
|
const stationCard = target.closest<HTMLElement>("[data-station-id]");
|
||||||
this.context.syncFollowStateFromSelection();
|
const stationId = stationCard?.dataset.stationId;
|
||||||
this.context.focusOnSelection({ kind: "ship", id: shipId });
|
if (stationId) {
|
||||||
this.toggleCameraMode("follow");
|
this.context.setSelectedItems([{ kind: "station", id: stationId }]);
|
||||||
this.context.updatePanels();
|
this.context.syncFollowStateFromSelection();
|
||||||
this.context.updateGamePanel("Live");
|
this.toggleCameraMode("tactical");
|
||||||
|
this.context.focusOnSelection({ kind: "station", id: stationId });
|
||||||
|
this.context.updatePanels();
|
||||||
|
this.context.updateGamePanel("Live");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly onHistoryLayerClick = (event: MouseEvent) => this.context.historyController.onHistoryLayerClick(event);
|
readonly onHistoryLayerClick = (event: MouseEvent) => this.context.historyController.onHistoryLayerClick(event);
|
||||||
|
|||||||
154
apps/viewer/src/viewerOpsStrip.ts
Normal file
154
apps/viewer/src/viewerOpsStrip.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { StationSnapshot } from "./contractsInfrastructure";
|
||||||
|
import type { FactionSnapshot } from "./contractsFactions";
|
||||||
|
import { inventoryAmount } from "./viewerMath";
|
||||||
|
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
||||||
|
import type { CameraMode, Selectable, WorldState, ZoomLevel } from "./viewerTypes";
|
||||||
|
|
||||||
|
function renderFactionCard(faction: FactionSnapshot): string {
|
||||||
|
const state = faction.goapState;
|
||||||
|
const priorities = faction.goapPriorities;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="ship-card faction-card" data-faction-id="${faction.id}">
|
||||||
|
<div class="ship-card-header">
|
||||||
|
<h3>${faction.label}</h3>
|
||||||
|
<span class="ship-card-badge">faction</span>
|
||||||
|
</div>
|
||||||
|
${state ? `
|
||||||
|
<div class="ship-card-ai">
|
||||||
|
<p class="ship-card-section-title">GOAP State</p>
|
||||||
|
<p>Military ${state.militaryShipCount} · Miners ${state.minerShipCount}</p>
|
||||||
|
<p>Transport ${state.transportShipCount} · Constructors ${state.constructorShipCount}</p>
|
||||||
|
<p>Systems ${state.controlledSystemCount} / ${state.targetSystemCount}</p>
|
||||||
|
<p>Factory ${state.hasShipFactory ? "yes" : "no"} · Ore ${state.oreStockpile.toFixed(0)}</p>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
${priorities && priorities.length > 0 ? `
|
||||||
|
<div class="ship-card-ai">
|
||||||
|
<p class="ship-card-section-title">Priorities</p>
|
||||||
|
${priorities.map(p => `<p>${p.goalName} <span style="float:right">${p.priority.toFixed(0)}</span></p>`).join("")}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStationCard(station: StationSnapshot, isSelected: boolean): string {
|
||||||
|
const cargo = station.inventory.reduce((sum, e) => sum + e.amount, 0);
|
||||||
|
const processes = station.currentProcesses;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="ship-card station-card${isSelected ? " is-selected" : ""}" data-station-id="${station.id}">
|
||||||
|
<div class="ship-card-header">
|
||||||
|
<h3>${station.label}</h3>
|
||||||
|
<span class="ship-card-badge">${station.category}</span>
|
||||||
|
</div>
|
||||||
|
<p>${station.systemId}</p>
|
||||||
|
<p>Docked ${station.dockedShips} / ${station.dockingPads}</p>
|
||||||
|
<p>Cargo ${cargo.toFixed(0)} · Pop ${station.population.toFixed(0)}</p>
|
||||||
|
<p>Modules ${station.installedModules.length}</p>
|
||||||
|
${processes.length > 0 ? `
|
||||||
|
<div class="ship-card-ai">
|
||||||
|
${processes.map(p => `
|
||||||
|
<div class="ship-action-progress">
|
||||||
|
<div class="ship-action-progress-label">
|
||||||
|
<span>${p.label}</span>
|
||||||
|
<span>${Math.round(p.progress * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="ship-action-progress-track">
|
||||||
|
<div class="ship-action-progress-fill" style="width: ${(p.progress * 100).toFixed(1)}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderOpsStrip(
|
||||||
|
world: WorldState | undefined,
|
||||||
|
selectedItems: Selectable[],
|
||||||
|
cameraMode: CameraMode,
|
||||||
|
cameraTargetShipId?: string,
|
||||||
|
zoomLevel?: ZoomLevel,
|
||||||
|
activeSystemId?: string,
|
||||||
|
) {
|
||||||
|
if (!world) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSystemFiltered = zoomLevel !== "universe" && activeSystemId != null;
|
||||||
|
|
||||||
|
const factionCards = [...world.factions.values()]
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
.map(renderFactionCard)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const stationCards = [...world.stations.values()]
|
||||||
|
.filter((station) => !isSystemFiltered || station.systemId === activeSystemId)
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label))
|
||||||
|
.map((station) => {
|
||||||
|
const isSelected = selectedItems.length === 1
|
||||||
|
&& selectedItems[0].kind === "station"
|
||||||
|
&& selectedItems[0].id === station.id;
|
||||||
|
return renderStationCard(station, isSelected);
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const ships = [...world.ships.values()]
|
||||||
|
.filter((ship) => !isSystemFiltered || ship.systemId === activeSystemId)
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
const shipCards = ships
|
||||||
|
.map((ship) => {
|
||||||
|
const cargo = ship.inventory.reduce((sum, e) => sum + e.amount, 0);
|
||||||
|
const shipLocation = describeShipLocation(world, ship);
|
||||||
|
const shipState = describeShipState(world, ship);
|
||||||
|
const shipAction = describeShipCurrentAction(ship);
|
||||||
|
const isSelected = selectedItems.length === 1
|
||||||
|
&& selectedItems[0].kind === "ship"
|
||||||
|
&& selectedItems[0].id === ship.id;
|
||||||
|
const isFollowed = cameraMode === "follow" && cameraTargetShipId === ship.id;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="ship-card${isSelected ? " is-selected" : ""}${isFollowed ? " is-followed" : ""}" data-ship-id="${ship.id}">
|
||||||
|
<div class="ship-card-header">
|
||||||
|
<h3>${ship.label}</h3>
|
||||||
|
<div class="ship-card-meta">
|
||||||
|
<span class="ship-card-badge">${ship.class}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ship-card-history-button"
|
||||||
|
data-history-ship-id="${ship.id}"
|
||||||
|
aria-label="Open history for ${ship.label}"
|
||||||
|
title="Open history"
|
||||||
|
>🕔</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>${shipLocation.system}${shipLocation.local ? `<br>${shipLocation.local}` : ""}</p>
|
||||||
|
<p>Cargo ${cargo.toFixed(0)}</p>
|
||||||
|
<p>State ${shipState}</p>
|
||||||
|
${shipAction ? `
|
||||||
|
<div class="ship-action-progress">
|
||||||
|
<div class="ship-action-progress-label">
|
||||||
|
<span>${shipAction.label}</span>
|
||||||
|
<span>${Math.round(shipAction.progress * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="ship-action-progress-track">
|
||||||
|
<div class="ship-action-progress-fill" style="width: ${(shipAction.progress * 100).toFixed(1)}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
<div class="ship-card-ai">
|
||||||
|
${ship.commanderObjective ? `<p>Objective ${describeShipObjective(ship.commanderObjective)}</p>` : ""}
|
||||||
|
<p>Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}</p>
|
||||||
|
<p>Task ${ship.controllerTaskKind}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return factionCards + stationCards + shipCards;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fetchWorldSnapshot, openWorldStream } from "./api";
|
import { fetchWorldSnapshot, openWorldStream } from "./api";
|
||||||
import { renderFactionStrip } from "./viewerFactionStrip";
|
import { renderOpsStrip } from "./viewerOpsStrip";
|
||||||
import { updateDetailPanel } from "./viewerPanels";
|
import { updateDetailPanel } from "./viewerPanels";
|
||||||
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
||||||
import type {
|
import type {
|
||||||
@@ -49,7 +49,7 @@ export interface ViewerWorldLifecycleContext {
|
|||||||
getNetworkStats: () => NetworkStats;
|
getNetworkStats: () => NetworkStats;
|
||||||
getSystemSummaryVisuals: () => Map<string, unknown>;
|
getSystemSummaryVisuals: () => Map<string, unknown>;
|
||||||
errorEl: HTMLDivElement;
|
errorEl: HTMLDivElement;
|
||||||
factionStripEl: HTMLDivElement;
|
opsStripEl: HTMLDivElement;
|
||||||
detailTitleEl: HTMLHeadingElement;
|
detailTitleEl: HTMLHeadingElement;
|
||||||
detailBodyEl: HTMLDivElement;
|
detailBodyEl: HTMLDivElement;
|
||||||
worldLabel: () => string;
|
worldLabel: () => string;
|
||||||
@@ -194,7 +194,7 @@ export class ViewerWorldLifecycle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rebuildFactions(_factions: FactionSnapshot[]) {
|
rebuildFactions(_factions: FactionSnapshot[]) {
|
||||||
this.context.factionStripEl.innerHTML = renderFactionStrip(
|
this.context.opsStripEl.innerHTML = renderOpsStrip(
|
||||||
this.context.getWorld(),
|
this.context.getWorld(),
|
||||||
this.context.getSelectedItems(),
|
this.context.getSelectedItems(),
|
||||||
this.context.getCameraMode(),
|
this.context.getCameraMode(),
|
||||||
|
|||||||
Reference in New Issue
Block a user