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"