feat: massive AI generation
This commit is contained in:
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";
|
||||
import { storeToRefs } from "pinia";
|
||||
import GmWindow from "./GmWindow.vue";
|
||||
import GmPlayerFactionPanel from "./GmPlayerFactionPanel.vue";
|
||||
import GmGeopoliticsPanel from "./GmGeopoliticsPanel.vue";
|
||||
import { useGmStore } from "../../ui/stores/gmStore";
|
||||
import { usePlayerFactionStore } from "../../ui/stores/playerFactionStore";
|
||||
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||
import type { ShipSnapshot } from "../../contractsShips";
|
||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||
@@ -74,10 +77,11 @@ const emit = defineEmits<{
|
||||
focus: [id: string, kind: "ship" | "station"];
|
||||
}>();
|
||||
|
||||
type TabId = "ships" | "stations" | "factions";
|
||||
type TabId = "ships" | "stations" | "factions" | "player" | "geopolitics";
|
||||
const activeTab = ref<TabId>("ships");
|
||||
|
||||
const gmStore = useGmStore();
|
||||
const playerFactionStore = usePlayerFactionStore();
|
||||
const selectionStore = useViewerSelectionStore();
|
||||
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||
|
||||
@@ -128,62 +132,51 @@ function formatCargoAmount(value: number | null | undefined) {
|
||||
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) {
|
||||
return [...(faction.objectives ?? [])]
|
||||
return [...faction.strategicState.objectives]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
.find((objective) => objective.state !== "Complete" && objective.state !== "Cancelled")
|
||||
?? faction.objectives?.[0];
|
||||
.find((objective) => objective.status !== "completed" && objective.status !== "cancelled")
|
||||
?? faction.strategicState.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 getLatestDecision(faction: FactionSnapshot) {
|
||||
return [...faction.decisionLog]
|
||||
.sort((left, right) => right.occurredAtUtc.localeCompare(left.occurredAtUtc))[0];
|
||||
}
|
||||
|
||||
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} —`;
|
||||
return `${shortLabel} ${titleCaseToken(signal.level)} ${compactRate(signal.projectedNetRatePerSecond)}`;
|
||||
}
|
||||
|
||||
function describeFactionStrategicState(faction: FactionSnapshot) {
|
||||
const campaign = getLeadCampaign(faction);
|
||||
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)}`;
|
||||
if (!campaign && !objective) return "No campaigns";
|
||||
if (!campaign) return `${titleCaseToken(objective?.kind)} · ${titleCaseToken(objective?.status)}`;
|
||||
return `${titleCaseToken(campaign.kind)} · ${titleCaseToken(campaign.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;
|
||||
const objective = getLeadObjective(faction);
|
||||
if (!objective) return "No objectives";
|
||||
const target = objective.itemId ?? objective.targetEntityId ?? objective.targetSystemId ?? objective.homeStationId;
|
||||
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)}` : "—";
|
||||
? `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)} · ${target}`
|
||||
: `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)}`;
|
||||
}
|
||||
|
||||
function describeFactionEconomy(faction: FactionSnapshot) {
|
||||
@@ -195,9 +188,57 @@ function describeFactionEconomy(faction: FactionSnapshot) {
|
||||
}
|
||||
|
||||
function describeFactionThreat(faction: FactionSnapshot) {
|
||||
const blackboard = faction.blackboard;
|
||||
if (!blackboard) return "—";
|
||||
return `Enemy ships ${blackboard.enemyShipCount} · stations ${blackboard.enemyStationCount}`;
|
||||
const threat = faction.strategicState.threatAssessment;
|
||||
return `Enemy ships ${threat.enemyShipCount} · stations ${threat.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 ────────────────────────────────────────────────────────────
|
||||
@@ -210,32 +251,40 @@ type ShipRow = {
|
||||
faction: string;
|
||||
system: string;
|
||||
state: string;
|
||||
objective: string;
|
||||
assignment: string;
|
||||
behavior: string;
|
||||
phase: string;
|
||||
action: string;
|
||||
task: string;
|
||||
orders: string;
|
||||
plan: string;
|
||||
step: string;
|
||||
subtask: string;
|
||||
cargo: number;
|
||||
health: number;
|
||||
};
|
||||
|
||||
const shipRows = computed<ShipRow[]>(() =>
|
||||
gmStore.ships.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
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),
|
||||
})),
|
||||
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,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
state: titleCaseToken(s.state),
|
||||
assignment: s.assignment ? titleCaseToken(s.assignment.kind) : "—",
|
||||
behavior: titleCaseToken(s.defaultBehavior.kind),
|
||||
orders: topOrder ? `${titleCaseToken(topOrder.kind)} · ${s.orderQueue.length}` : "—",
|
||||
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
|
||||
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),
|
||||
health: Math.round(s.health),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||
@@ -250,11 +299,12 @@ const shipColumns = [
|
||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
shipColumnHelper.accessor("system", { header: "System" }),
|
||||
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
||||
shipColumnHelper.accessor("objective", { header: "Commander Objective" }),
|
||||
shipColumnHelper.accessor("assignment", { header: "Assignment" }),
|
||||
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
||||
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
||||
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||
shipColumnHelper.accessor("orders", { header: "Orders" }),
|
||||
shipColumnHelper.accessor("plan", { header: "Plan" }),
|
||||
shipColumnHelper.accessor("step", { header: "Current Step" }),
|
||||
shipColumnHelper.accessor("subtask", { header: "SubTask" }),
|
||||
shipColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
@@ -264,7 +314,7 @@ const shipColumns = [
|
||||
|
||||
const shipFilter = ref("");
|
||||
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({
|
||||
get data() { return shipRows.value; },
|
||||
@@ -383,14 +433,18 @@ type FactionRow = {
|
||||
label: string;
|
||||
color: string;
|
||||
planCycle: number;
|
||||
priority: string;
|
||||
strategicState: string;
|
||||
leadStep: string;
|
||||
leadTask: string;
|
||||
warReadiness: string;
|
||||
posture: string;
|
||||
fronts: string;
|
||||
leadCampaign: string;
|
||||
leadObjective: string;
|
||||
commitments: string;
|
||||
reserves: string;
|
||||
bottleneck: string;
|
||||
intent: string;
|
||||
decision: string;
|
||||
memory: string;
|
||||
economy: string;
|
||||
threat: string;
|
||||
fleets: string;
|
||||
systems: string;
|
||||
credits: number;
|
||||
population: number;
|
||||
@@ -399,29 +453,29 @@ type FactionRow = {
|
||||
};
|
||||
|
||||
const factionRows = computed<FactionRow[]>(() =>
|
||||
gmStore.factions.map((f) => {
|
||||
const assessment = f.strategicAssessment;
|
||||
const blackboard = f.blackboard;
|
||||
return {
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
color: f.color,
|
||||
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),
|
||||
shipsBuilt: f.shipsBuilt,
|
||||
shipsLost: f.shipsLost,
|
||||
};
|
||||
}),
|
||||
gmStore.factions.map((f) => ({
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
color: f.color,
|
||||
planCycle: f.strategicState.planCycle,
|
||||
posture: `${titleCaseToken(f.doctrine.strategicPosture)} · ${titleCaseToken(f.doctrine.militaryPosture)} · ${titleCaseToken(f.doctrine.economicPosture)}`,
|
||||
fronts: describeFactionFronts(f),
|
||||
leadCampaign: describeFactionStrategicState(f),
|
||||
leadObjective: describeFactionLeadTask(f),
|
||||
commitments: describeFactionCommitments(f),
|
||||
reserves: describeFactionReserves(f),
|
||||
bottleneck: describeFactionBottleneck(f),
|
||||
intent: describeFactionIntent(f),
|
||||
decision: describeFactionDecision(f),
|
||||
memory: describeFactionMemory(f),
|
||||
economy: describeFactionEconomy(f),
|
||||
threat: describeFactionThreat(f),
|
||||
systems: `${f.strategicState.economicAssessment.controlledSystemCount} / ${f.doctrine.desiredControlledSystems}`,
|
||||
credits: Math.round(f.credits),
|
||||
population: Math.round(f.populationTotal),
|
||||
shipsBuilt: f.shipsBuilt,
|
||||
shipsLost: f.shipsLost,
|
||||
})),
|
||||
);
|
||||
|
||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||
@@ -432,14 +486,18 @@ const factionColumns = [
|
||||
cell: (info) => renderColorCell(info.getValue()),
|
||||
}),
|
||||
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("posture", { header: "Posture" }),
|
||||
factionColumnHelper.accessor("fronts", { header: "Fronts" }),
|
||||
factionColumnHelper.accessor("leadCampaign", { header: "Lead Campaign" }),
|
||||
factionColumnHelper.accessor("leadObjective", { header: "Lead Objective" }),
|
||||
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("threat", { header: "Threat" }),
|
||||
factionColumnHelper.accessor("fleets", { header: "Fleets" }),
|
||||
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
||||
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
||||
factionColumnHelper.accessor("population", { header: "Pop" }),
|
||||
@@ -449,7 +507,7 @@ const factionColumns = [
|
||||
|
||||
const factionFilter = ref("");
|
||||
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({
|
||||
get data() { return factionRows.value; },
|
||||
@@ -472,6 +530,8 @@ const factionTable = useVueTable({
|
||||
// ── Row counts ─────────────────────────────────────────────────────────────
|
||||
|
||||
const tabs: { id: TabId; label: string }[] = [
|
||||
{ id: "player", label: "Player" },
|
||||
{ id: "geopolitics", label: "Geopolitics" },
|
||||
{ id: "ships", label: "Ships" },
|
||||
{ id: "stations", label: "Stations" },
|
||||
{ id: "factions", label: "Factions" },
|
||||
@@ -479,11 +539,15 @@ const tabs: { id: TabId; label: string }[] = [
|
||||
|
||||
const activeFilter = computed({
|
||||
get: () => {
|
||||
if (activeTab.value === "player") return "";
|
||||
if (activeTab.value === "geopolitics") return "";
|
||||
if (activeTab.value === "ships") return shipFilter.value;
|
||||
if (activeTab.value === "stations") return stationFilter.value;
|
||||
return factionFilter.value;
|
||||
},
|
||||
set: (v: string) => {
|
||||
if (activeTab.value === "player") return;
|
||||
if (activeTab.value === "geopolitics") return;
|
||||
if (activeTab.value === "ships") shipFilter.value = v;
|
||||
else if (activeTab.value === "stations") stationFilter.value = v;
|
||||
else factionFilter.value = v;
|
||||
@@ -491,12 +555,24 @@ const activeFilter = 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 === "stations") return stationTable.getFilteredRowModel().rows.length;
|
||||
return factionTable.getFilteredRowModel().rows.length;
|
||||
});
|
||||
|
||||
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 === "stations") return gmStore.stations.length;
|
||||
return gmStore.factions.length;
|
||||
@@ -558,7 +634,7 @@ function hideOrdersTooltip() {
|
||||
|
||||
<template>
|
||||
<GmWindow
|
||||
title="AI States"
|
||||
title="Empire / AI States"
|
||||
:initial-width="980"
|
||||
:initial-height="560"
|
||||
:initial-x="80"
|
||||
@@ -581,7 +657,7 @@ function hideOrdersTooltip() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1">
|
||||
<div v-if="activeTab !== 'player' && activeTab !== 'geopolitics'" class="relative flex-1">
|
||||
<input
|
||||
v-model="activeFilter"
|
||||
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
||||
@@ -600,12 +676,30 @@ function hideOrdersTooltip() {
|
||||
✕
|
||||
</button>
|
||||
</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">
|
||||
{{ activeRowCount }} / {{ activeTotalCount }}
|
||||
</span>
|
||||
</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 -->
|
||||
<div
|
||||
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
Reference in New Issue
Block a user