improvement on gm windows, ai

This commit is contained in:
2026-03-20 12:40:26 -04:00
parent ff078fe939
commit 3b56785f9a
39 changed files with 2594 additions and 358 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, h, ref } from "vue";
import {
useVueTable,
getCoreRowModel,
@@ -18,6 +18,7 @@ import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
import type { ShipSnapshot } from "../../contractsShips";
import type { StationSnapshot } from "../../contractsInfrastructure";
import type { FactionSnapshot } from "../../contractsFactions";
import type { MarketOrderSnapshot } from "../../contractsEconomy";
// ── Column ordering composable ─────────────────────────────────────────────
@@ -85,6 +86,20 @@ const factionMap = computed(() =>
new Map(gmStore.factions.map((f) => [f.id, f.label])),
);
const factionColorMap = computed(() =>
new Map(gmStore.factions.map((f) => [f.id, f.color])),
);
function renderColorCell(color: string | null | undefined) {
const resolved = color && color !== "—" ? color : "#6b7280";
return h("div", { class: "flex items-center justify-center" }, [
h("span", {
class: "inline-block h-3 w-3 rounded-full border border-white/20",
style: { backgroundColor: resolved },
}),
]);
}
function titleCaseToken(value: string | null | undefined) {
if (!value) return "—";
return value
@@ -106,6 +121,13 @@ function compactRate(value: number | null | undefined) {
return `${sign}${value.toFixed(2)}/s`;
}
function formatCargoAmount(value: number | null | undefined) {
if (value == null || Number.isNaN(value)) return "—";
const rounded = Math.round(value);
if (Math.abs(value - rounded) < 0.005) return String(rounded);
return value.toFixed(2).replace(/\.?0+$/, "");
}
function getLeadObjective(faction: FactionSnapshot) {
return [...(faction.objectives ?? [])]
.sort((left, right) => right.priority - left.priority)
@@ -184,6 +206,7 @@ type ShipRow = {
id: string;
label: string;
class: string;
factionColor: string;
faction: string;
system: string;
state: string;
@@ -201,6 +224,7 @@ const shipRows = computed<ShipRow[]>(() =>
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),
@@ -218,6 +242,11 @@ const shipColumnHelper = createColumnHelper<ShipRow>();
const shipColumns = [
shipColumnHelper.accessor("label", { header: "Name" }),
shipColumnHelper.accessor("class", { header: "Class" }),
shipColumnHelper.accessor("factionColor", {
id: "factionColor",
header: "Color",
cell: (info) => renderColorCell(info.getValue()),
}),
shipColumnHelper.accessor("faction", { header: "Faction" }),
shipColumnHelper.accessor("system", { header: "System" }),
shipColumnHelper.accessor("state", { header: "Ship State" }),
@@ -226,13 +255,16 @@ const shipColumns = [
shipColumnHelper.accessor("phase", { header: "Phase" }),
shipColumnHelper.accessor("action", { header: "Current Action" }),
shipColumnHelper.accessor("task", { header: "Task" }),
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
shipColumnHelper.accessor("cargo", {
header: "Cargo",
cell: (info) => formatCargoAmount(info.getValue()),
}),
shipColumnHelper.accessor("health", { header: "HP" }),
];
const shipFilter = ref("");
const shipSorting = ref<SortingState>([]);
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
const shipTable = useVueTable({
get data() { return shipRows.value; },
@@ -259,22 +291,29 @@ type StationRow = {
label: string;
category: string;
objective: string;
factionColor: string;
faction: string;
system: string;
process: string;
workforce: string;
docked: string;
orders: number;
orderDetails: MarketOrderSnapshot[];
cargo: number;
modules: number;
};
const marketOrderMap = computed(() =>
new Map(gmStore.marketOrders.map((o) => [o.id, o])),
);
const stationRows = computed<StationRow[]>(() =>
gmStore.stations.map((s) => ({
id: s.id,
label: s.label,
category: s.category,
objective: titleCaseToken(s.objective),
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
faction: factionMap.value.get(s.factionId) ?? s.factionId,
system: s.systemId,
process: s.currentProcesses.length > 0
@@ -283,6 +322,10 @@ const stationRows = computed<StationRow[]>(() =>
workforce: `${Math.round(s.population)} / ${Math.round(s.populationCapacity)} · ${Math.round(s.workforceEffectiveRatio * 100)}%`,
docked: `${s.dockedShips} / ${s.dockingPads}`,
orders: s.marketOrderIds.length,
orderDetails: s.marketOrderIds.flatMap((id) => {
const order = marketOrderMap.value.get(id);
return order ? [order] : [];
}),
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
modules: s.installedModules.length,
})),
@@ -293,19 +336,27 @@ const stationColumns = [
stationColumnHelper.accessor("label", { header: "Name" }),
stationColumnHelper.accessor("category", { header: "Category" }),
stationColumnHelper.accessor("objective", { header: "Objective" }),
stationColumnHelper.accessor("factionColor", {
id: "factionColor",
header: "Color",
cell: (info) => renderColorCell(info.getValue()),
}),
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("cargo", {
header: "Cargo",
cell: (info) => formatCargoAmount(info.getValue()),
}),
stationColumnHelper.accessor("modules", { header: "Modules" }),
];
const stationFilter = ref("");
const stationSorting = ref<SortingState>([]);
const stationOrder = useColumnOrder(["label", "category", "objective", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
const stationTable = useVueTable({
get data() { return stationRows.value; },
@@ -330,6 +381,7 @@ const stationTable = useVueTable({
type FactionRow = {
id: string;
label: string;
color: string;
planCycle: number;
priority: string;
strategicState: string;
@@ -353,6 +405,7 @@ const factionRows = computed<FactionRow[]>(() =>
return {
id: f.id,
label: f.label,
color: f.color,
planCycle: blackboard?.planCycle ?? 0,
priority: describeFactionPriority(f),
strategicState: describeFactionStrategicState(f),
@@ -374,6 +427,10 @@ const factionRows = computed<FactionRow[]>(() =>
const factionColumnHelper = createColumnHelper<FactionRow>();
const factionColumns = [
factionColumnHelper.accessor("label", { header: "Faction" }),
factionColumnHelper.accessor("color", {
header: "Color",
cell: (info) => renderColorCell(info.getValue()),
}),
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
@@ -392,7 +449,7 @@ const factionColumns = [
const factionFilter = ref("");
const factionSorting = ref<SortingState>([]);
const factionOrder = useColumnOrder(["label", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
const factionOrder = useColumnOrder(["label", "color", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
const factionTable = useVueTable({
get data() { return factionRows.value; },
@@ -472,6 +529,31 @@ function isShipSelected(id: string) {
function isStationSelected(id: string) {
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
}
// ── Orders tooltip ─────────────────────────────────────────────────────────
const ordersTooltip = ref<{
visible: boolean;
x: number;
y: number;
orders: MarketOrderSnapshot[];
}>({ visible: false, x: 0, y: 0, orders: [] });
function showOrdersTooltip(e: MouseEvent, orders: MarketOrderSnapshot[]) {
if (orders.length === 0) return;
ordersTooltip.value = { visible: true, x: e.clientX, y: e.clientY, orders };
}
function moveOrdersTooltip(e: MouseEvent) {
if (ordersTooltip.value.visible) {
ordersTooltip.value.x = e.clientX;
ordersTooltip.value.y = e.clientY;
}
}
function hideOrdersTooltip() {
ordersTooltip.value.visible = false;
}
</script>
<template>
@@ -660,6 +742,15 @@ function isStationSelected(id: string) {
>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</span>
<span
v-else-if="cell.column.id === 'orders'"
:class="row.original.orderDetails.length > 0 ? 'gm-orders-trigger' : ''"
@mouseenter="showOrdersTooltip($event, row.original.orderDetails)"
@mousemove="moveOrdersTooltip"
@mouseleave="hideOrdersTooltip"
>
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</span>
<FlexRender
v-else
:render="cell.column.columnDef.cell"
@@ -740,4 +831,35 @@ function isStationSelected(id: string) {
</div>
</div>
</GmWindow>
<Teleport to="body">
<div
v-if="ordersTooltip.visible && ordersTooltip.orders.length > 0"
class="gm-orders-tooltip"
:style="{ left: `${ordersTooltip.x + 14}px`, top: `${ordersTooltip.y + 14}px` }"
>
<table class="gm-orders-tooltip-table">
<thead>
<tr>
<th>Item</th>
<th>Kind</th>
<th>Amount</th>
<th>Remaining</th>
<th>Valuation</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr v-for="order in ordersTooltip.orders" :key="order.id">
<td>{{ order.itemId }}</td>
<td>{{ titleCaseToken(order.kind) }}</td>
<td class="tabular-nums">{{ order.amount }}</td>
<td class="tabular-nums">{{ order.remainingAmount }}</td>
<td class="tabular-nums">{{ order.valuation }}</td>
<td>{{ titleCaseToken(order.state) }}</td>
</tr>
</tbody>
</table>
</div>
</Teleport>
</template>