From ff078fe939f41fd5f421a46ce7e9560d56b50f2f Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Fri, 20 Mar 2026 02:44:25 -0400 Subject: [PATCH] Update viewer AI state panels --- apps/viewer/src/components/gm/GmOpsWindow.vue | 186 +++++++++++++++--- apps/viewer/src/contractsFactions.ts | 144 +++++++++++++- apps/viewer/src/contractsInfrastructure.ts | 1 + apps/viewer/src/viewerOpsStrip.ts | 10 +- 4 files changed, 303 insertions(+), 38 deletions(-) diff --git a/apps/viewer/src/components/gm/GmOpsWindow.vue b/apps/viewer/src/components/gm/GmOpsWindow.vue index 94a627f..923462d 100644 --- a/apps/viewer/src/components/gm/GmOpsWindow.vue +++ b/apps/viewer/src/components/gm/GmOpsWindow.vue @@ -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(() => 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([]); -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(() => 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(); 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([]); -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(() => 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(() => const factionColumnHelper = createColumnHelper(); 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([]); -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) {