Update viewer AI state panels

This commit is contained in:
2026-03-20 02:44:25 -04:00
parent a2f66b0dca
commit ff078fe939
4 changed files with 303 additions and 38 deletions

View File

@@ -85,6 +85,99 @@ const factionMap = computed(() =>
new Map(gmStore.factions.map((f) => [f.id, f.label])),
);
function titleCaseToken(value: string | null | undefined) {
if (!value) return "—";
return value
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (c) => c.toUpperCase());
}
function compactNumber(value: number | null | undefined, digits = 0) {
if (value == null || Number.isNaN(value)) return "—";
return value.toFixed(digits);
}
function compactRate(value: number | null | undefined) {
if (value == null || Number.isNaN(value)) return "—";
const sign = value > 0.001 ? "+" : "";
return `${sign}${value.toFixed(2)}/s`;
}
function getLeadObjective(faction: FactionSnapshot) {
return [...(faction.objectives ?? [])]
.sort((left, right) => right.priority - left.priority)
.find((objective) => objective.state !== "Complete" && objective.state !== "Cancelled")
?? faction.objectives?.[0];
}
function getLeadStep(faction: FactionSnapshot) {
const objective = getLeadObjective(faction);
return [...(objective?.steps ?? [])]
.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) {
const signal = faction.blackboard?.commoditySignals.find((entry) => entry.itemId === itemId);
if (!signal) return `${shortLabel}`;
return `${shortLabel} ${titleCaseToken(signal.level)} ${compactRate(signal.projectedNetRatePerSecond)}`;
}
function describeFactionStrategicState(faction: FactionSnapshot) {
const objective = getLeadObjective(faction);
if (!objective) return "No objectives";
return `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.state)}`;
}
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) {
const task = getLeadTask(faction);
if (!task) return "No tasks";
const target = task.shipRole ?? task.commodityId ?? task.moduleId ?? task.targetFactionId ?? task.targetSiteId;
return target
? `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)} · ${target}`
: `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)}`;
}
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) {
return [
describeCommodityState(faction, "refinedmetals", "RM"),
describeCommodityState(faction, "hullparts", "HP"),
describeCommodityState(faction, "claytronics", "CL"),
].join(" | ");
}
function describeFactionThreat(faction: FactionSnapshot) {
const blackboard = faction.blackboard;
if (!blackboard) return "—";
return `Enemy ships ${blackboard.enemyShipCount} · stations ${blackboard.enemyStationCount}`;
}
// ── Ships table ────────────────────────────────────────────────────────────
type ShipRow = {
@@ -94,7 +187,10 @@ type ShipRow = {
faction: string;
system: string;
state: string;
objective: string;
behavior: string;
phase: string;
action: string;
task: string;
cargo: number;
health: number;
@@ -107,9 +203,12 @@ const shipRows = computed<ShipRow[]>(() =>
class: s.class,
faction: factionMap.value.get(s.factionId) ?? s.factionId,
system: s.systemId,
state: s.state,
behavior: s.defaultBehaviorKind + (s.behaviorPhase ? ` · ${s.behaviorPhase}` : ""),
task: s.controllerTaskKind,
state: titleCaseToken(s.state),
objective: s.commanderObjective ? titleCaseToken(s.commanderObjective) : "",
behavior: titleCaseToken(s.defaultBehaviorKind),
phase: s.behaviorPhase ? titleCaseToken(s.behaviorPhase) : "—",
action: s.currentAction ? `${s.currentAction.label} ${Math.round(s.currentAction.progress * 100)}%` : "—",
task: titleCaseToken(s.controllerTaskKind),
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
health: Math.round(s.health),
})),
@@ -121,8 +220,11 @@ const shipColumns = [
shipColumnHelper.accessor("class", { header: "Class" }),
shipColumnHelper.accessor("faction", { header: "Faction" }),
shipColumnHelper.accessor("system", { header: "System" }),
shipColumnHelper.accessor("state", { header: "State" }),
shipColumnHelper.accessor("state", { header: "Ship State" }),
shipColumnHelper.accessor("objective", { header: "Commander Objective" }),
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
shipColumnHelper.accessor("phase", { header: "Phase" }),
shipColumnHelper.accessor("action", { header: "Current Action" }),
shipColumnHelper.accessor("task", { header: "Task" }),
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
shipColumnHelper.accessor("health", { header: "HP" }),
@@ -130,7 +232,7 @@ const shipColumns = [
const shipFilter = ref("");
const shipSorting = ref<SortingState>([]);
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "behavior", "task", "cargo", "health"]);
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
const shipTable = useVueTable({
get data() { return shipRows.value; },
@@ -156,11 +258,14 @@ type StationRow = {
id: string;
label: string;
category: string;
objective: string;
faction: string;
system: string;
process: string;
workforce: string;
docked: string;
orders: number;
cargo: number;
population: number;
modules: number;
};
@@ -169,11 +274,16 @@ const stationRows = computed<StationRow[]>(() =>
id: s.id,
label: s.label,
category: s.category,
objective: titleCaseToken(s.objective),
faction: factionMap.value.get(s.factionId) ?? s.factionId,
system: s.systemId,
process: s.currentProcesses.length > 0
? s.currentProcesses.map((process) => `${process.label} ${Math.round(process.progress * 100)}%`).join(" | ")
: "Idle",
workforce: `${Math.round(s.population)} / ${Math.round(s.populationCapacity)} · ${Math.round(s.workforceEffectiveRatio * 100)}%`,
docked: `${s.dockedShips} / ${s.dockingPads}`,
orders: s.marketOrderIds.length,
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
population: Math.round(s.population),
modules: s.installedModules.length,
})),
);
@@ -182,17 +292,20 @@ const stationColumnHelper = createColumnHelper<StationRow>();
const stationColumns = [
stationColumnHelper.accessor("label", { header: "Name" }),
stationColumnHelper.accessor("category", { header: "Category" }),
stationColumnHelper.accessor("objective", { header: "Objective" }),
stationColumnHelper.accessor("faction", { header: "Faction" }),
stationColumnHelper.accessor("system", { header: "System" }),
stationColumnHelper.accessor("process", { header: "Production" }),
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
stationColumnHelper.accessor("docked", { header: "Docked" }),
stationColumnHelper.accessor("orders", { header: "Orders" }),
stationColumnHelper.accessor("cargo", { header: "Cargo" }),
stationColumnHelper.accessor("population", { header: "Pop" }),
stationColumnHelper.accessor("modules", { header: "Modules" }),
];
const stationFilter = ref("");
const stationSorting = ref<SortingState>([]);
const stationOrder = useColumnOrder(["label", "category", "faction", "system", "docked", "cargo", "population", "modules"]);
const stationOrder = useColumnOrder(["label", "category", "objective", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
const stationTable = useVueTable({
get data() { return stationRows.value; },
@@ -217,32 +330,41 @@ const stationTable = useVueTable({
type FactionRow = {
id: string;
label: string;
planCycle: number;
priority: string;
strategicState: string;
leadStep: string;
leadTask: string;
warReadiness: string;
economy: string;
threat: string;
fleets: string;
systems: string;
credits: number;
population: number;
military: number;
miners: number;
transport: number;
constructors: number;
systems: string;
ore: number;
shipsBuilt: number;
shipsLost: number;
};
const factionRows = computed<FactionRow[]>(() =>
gmStore.factions.map((f) => {
const gs = f.goapState;
const assessment = f.strategicAssessment;
const blackboard = f.blackboard;
return {
id: f.id,
label: f.label,
planCycle: blackboard?.planCycle ?? 0,
priority: describeFactionPriority(f),
strategicState: describeFactionStrategicState(f),
leadStep: describeFactionLeadStep(f),
leadTask: describeFactionLeadTask(f),
warReadiness: `Industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"} · Shipyard ${blackboard?.hasShipyard ? "yes" : "no"}${blackboard?.hasActiveExpansionProject ? ` · Expanding ${blackboard.activeExpansionCommodityId ?? blackboard.activeExpansionModuleId ?? "site"}` : ""}`,
economy: describeFactionEconomy(f),
threat: describeFactionThreat(f),
fleets: assessment ? `M ${assessment.militaryShipCount}/${blackboard?.targetWarshipCount ?? 0} · Mn ${assessment.minerShipCount} · Tr ${assessment.transportShipCount} · Cn ${assessment.constructorShipCount}` : "—",
systems: assessment ? `${assessment.controlledSystemCount} / ${assessment.targetSystemCount}` : "—",
credits: Math.round(f.credits),
population: Math.round(f.populationTotal),
military: gs?.militaryShipCount ?? 0,
miners: gs?.minerShipCount ?? 0,
transport: gs?.transportShipCount ?? 0,
constructors: gs?.constructorShipCount ?? 0,
systems: gs ? `${gs.controlledSystemCount} / ${gs.targetSystemCount}` : "—",
ore: gs ? Math.round(gs.oreStockpile) : 0,
shipsBuilt: f.shipsBuilt,
shipsLost: f.shipsLost,
};
@@ -252,21 +374,25 @@ const factionRows = computed<FactionRow[]>(() =>
const factionColumnHelper = createColumnHelper<FactionRow>();
const factionColumns = [
factionColumnHelper.accessor("label", { header: "Faction" }),
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
factionColumnHelper.accessor("leadStep", { header: "Lead Step" }),
factionColumnHelper.accessor("leadTask", { header: "Issued Task" }),
factionColumnHelper.accessor("warReadiness", { header: "Campaign State" }),
factionColumnHelper.accessor("economy", { header: "Economy" }),
factionColumnHelper.accessor("threat", { header: "Threat" }),
factionColumnHelper.accessor("fleets", { header: "Fleets" }),
factionColumnHelper.accessor("systems", { header: "Systems" }),
factionColumnHelper.accessor("credits", { header: "Credits" }),
factionColumnHelper.accessor("population", { header: "Pop" }),
factionColumnHelper.accessor("military", { header: "Military" }),
factionColumnHelper.accessor("miners", { header: "Miners" }),
factionColumnHelper.accessor("transport", { header: "Transport" }),
factionColumnHelper.accessor("constructors", { header: "Constructors" }),
factionColumnHelper.accessor("systems", { header: "Systems" }),
factionColumnHelper.accessor("ore", { header: "Ore" }),
factionColumnHelper.accessor("shipsBuilt", { header: "Built" }),
factionColumnHelper.accessor("shipsLost", { header: "Lost" }),
];
const factionFilter = ref("");
const factionSorting = ref<SortingState>([]);
const factionOrder = useColumnOrder(["label", "credits", "population", "military", "miners", "transport", "constructors", "systems", "ore", "shipsBuilt", "shipsLost"]);
const factionOrder = useColumnOrder(["label", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
const factionTable = useVueTable({
get data() { return factionRows.value; },
@@ -350,7 +476,7 @@ function isStationSelected(id: string) {
<template>
<GmWindow
title="Ships"
title="AI States"
:initial-width="980"
:initial-height="560"
:initial-x="80"

View File

@@ -1,4 +1,4 @@
export interface FactionGoapState {
export interface FactionPlanningStateSnapshot {
militaryShipCount: number;
minerShipCount: number;
transportShipCount: number;
@@ -7,14 +7,145 @@ export interface FactionGoapState {
targetSystemCount: number;
hasShipFactory: boolean;
oreStockpile: number;
refinedMetalsStockpile: number;
refinedMetalsAvailableStock: number;
refinedMetalsUsageRate: number;
refinedMetalsProjectedProductionRate: number;
refinedMetalsProjectedNetRate: number;
refinedMetalsLevelSeconds: number;
refinedMetalsLevel: string;
hullpartsAvailableStock: number;
hullpartsUsageRate: number;
hullpartsProjectedProductionRate: number;
hullpartsProjectedNetRate: number;
hullpartsLevelSeconds: 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 FactionGoapPriority {
export interface FactionStrategicPrioritySnapshot {
goalName: string;
priority: number;
}
export interface FactionCommoditySignalSnapshot {
itemId: string;
availableStock: number;
onHand: number;
productionRatePerSecond: number;
committedProductionRatePerSecond: number;
usageRatePerSecond: number;
netRatePerSecond: number;
projectedNetRatePerSecond: number;
levelSeconds: number;
level: string;
projectedProductionRatePerSecond: number;
buyBacklog: number;
reservedForConstruction: number;
}
export interface FactionThreatSignalSnapshot {
scopeId: string;
scopeKind: string;
enemyShipCount: number;
enemyStationCount: number;
}
export interface FactionBlackboardSnapshot {
planCycle: number;
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;
minerShipCount: number;
transportShipCount: number;
constructorShipCount: number;
controlledSystemCount: number;
commoditySignals: FactionCommoditySignalSnapshot[];
threatSignals: FactionThreatSignalSnapshot[];
}
export interface FactionPlanStepSnapshot {
id: string;
kind: string;
status: string;
priority: number;
commodityId?: string | null;
moduleId?: string | null;
targetFactionId?: string | null;
targetSiteId?: string | null;
blockingReason?: string | null;
notes?: string | null;
lastEvaluatedCycle: number;
dependencyStepIds: string[];
requiredFacts: string[];
producedFacts: string[];
assignedAssets: string[];
issuedTaskIds: string[];
}
export interface FactionIssuedTaskSnapshot {
id: string;
kind: string;
state: string;
objectiveId: string;
stepId: string;
priority: number;
shipRole?: string | null;
commodityId?: string | null;
moduleId?: string | null;
targetFactionId?: string | null;
targetSystemId?: string | null;
targetSiteId?: string | null;
createdAtCycle: number;
updatedAtCycle: number;
blockingReason?: string | null;
notes?: string | null;
assignedAssets: string[];
}
export interface FactionObjectiveSnapshot {
id: string;
kind: string;
state: string;
priority: number;
parentObjectiveId?: string | null;
targetFactionId?: string | null;
targetSystemId?: string | null;
targetSiteId?: string | null;
targetRegionId?: string | null;
commodityId?: string | null;
moduleId?: string | null;
budgetWeight: number;
slotCost: number;
createdAtCycle: number;
updatedAtCycle: number;
invalidationReason?: string | null;
blockingReason?: string | null;
prerequisiteObjectiveIds: string[];
assignedAssets: string[];
steps: FactionPlanStepSnapshot[];
}
export interface FactionSnapshot {
id: string;
label: string;
@@ -26,8 +157,11 @@ export interface FactionSnapshot {
shipsBuilt: number;
shipsLost: number;
defaultPolicySetId?: string | null;
goapState?: FactionGoapState | null;
goapPriorities?: FactionGoapPriority[] | null;
strategicAssessment?: FactionPlanningStateSnapshot | null;
strategicPriorities?: FactionStrategicPrioritySnapshot[] | null;
blackboard?: FactionBlackboardSnapshot | null;
objectives?: FactionObjectiveSnapshot[] | null;
issuedTasks?: FactionIssuedTaskSnapshot[] | null;
}
export interface FactionDelta extends FactionSnapshot {}

View File

@@ -25,6 +25,7 @@ export interface StationSnapshot {
id: string;
label: string;
category: string;
objective: string;
systemId: string;
localPosition: Vector3Dto;
celestialId?: string | null;

View File

@@ -11,7 +11,10 @@ import { describeShipCurrentAction, describeShipLocation, describeShipObjective,
import type { CameraMode, Selectable, WorldState, PovLevel } from "./viewerTypes";
function buildFactionCard(faction: FactionSnapshot): OpsFactionCardState {
const state = faction.goapState;
const state = faction.strategicAssessment;
const blackboard = faction.blackboard;
const leadTask = [...(faction.issuedTasks ?? [])]
.sort((left, right) => right.priority - left.priority)[0];
return {
kind: "faction",
id: faction.id,
@@ -20,9 +23,10 @@ function buildFactionCard(faction: FactionSnapshot): OpsFactionCardState {
`Military ${state.militaryShipCount} · Miners ${state.minerShipCount}`,
`Transport ${state.transportShipCount} · Constructors ${state.constructorShipCount}`,
`Systems ${state.controlledSystemCount} / ${state.targetSystemCount}`,
`Factory ${state.hasShipFactory ? "yes" : "no"} · Ore ${state.oreStockpile.toFixed(0)}`,
`Shipyard ${blackboard?.hasShipyard ? "yes" : "no"} · War industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"}`,
leadTask ? `Task ${leadTask.kind}${leadTask.shipRole ? ` · ${leadTask.shipRole}` : ""}` : `Ore ${state.oreStockpile.toFixed(0)}`,
] : [],
priorities: (faction.goapPriorities ?? []).map((entry) => ({
priorities: (faction.strategicPriorities ?? []).map((entry) => ({
label: entry.goalName,
value: entry.priority.toFixed(0),
})),