Add player onboarding and tactical viewer updates
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import type { StationSnapshot } from "../contractsInfrastructure";
|
||||
import type { PlayerFleetSnapshot } from "../contractsPlayerFaction";
|
||||
import type { ShipSnapshot } from "../contractsShips";
|
||||
import { getShipBehaviorLabel } from "../shipAutomationPresentation";
|
||||
import { useGmStore } from "../ui/stores/gmStore";
|
||||
import { usePlayerFactionStore } from "../ui/stores/playerFactionStore";
|
||||
@@ -9,22 +12,27 @@ import { useViewerSelectionStore, type ViewerSelectionSummary } from "../ui/stor
|
||||
import type { Selectable } from "../viewerTypes";
|
||||
|
||||
type BrowserTab = "visible" | "owned";
|
||||
type BrowserSortKey = "entity" | "location" | "ai" | "hp";
|
||||
type BrowserRowKind = "system" | "station" | "fleet" | "ship";
|
||||
|
||||
interface BrowserItem {
|
||||
interface BrowserRow {
|
||||
key: string;
|
||||
label: string;
|
||||
subtitle: string;
|
||||
meta?: string;
|
||||
kind: BrowserRowKind;
|
||||
kindLabel: string;
|
||||
name: string;
|
||||
ident: string;
|
||||
location: string;
|
||||
aiStates: string[];
|
||||
hpLabel: string;
|
||||
hpValue: number;
|
||||
selection?: ViewerSelectionSummary;
|
||||
focusSelection?: Selectable;
|
||||
focusMode?: "follow" | "tactical";
|
||||
children: BrowserRow[];
|
||||
}
|
||||
|
||||
interface BrowserSection {
|
||||
key: string;
|
||||
label: string;
|
||||
count: number;
|
||||
items: BrowserItem[];
|
||||
interface BrowserDisplayRow extends BrowserRow {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,22 +48,28 @@ const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||
const { playerFaction } = storeToRefs(playerStore);
|
||||
const { activeSystemId, povLevel } = storeToRefs(sceneStore);
|
||||
|
||||
const activeTab = ref<BrowserTab>("visible");
|
||||
const activeTab = ref<BrowserTab>("owned");
|
||||
const sortKey = ref<BrowserSortKey>("entity");
|
||||
const sortDirection = ref<"asc" | "desc">("asc");
|
||||
const searchText = ref("");
|
||||
const expandedRowKeys = ref<Record<string, boolean>>({});
|
||||
|
||||
const systemById = computed(() => new Map(gmStore.systems.map((system) => [system.id, system])));
|
||||
const stationById = computed(() => new Map(gmStore.stations.map((station) => [station.id, station])));
|
||||
const playerFleetByShipId = computed(() => {
|
||||
const mapping = new Map<string, PlayerFleetSnapshot>();
|
||||
for (const fleet of playerFaction.value?.fleets ?? []) {
|
||||
for (const assetId of fleet.assetIds) {
|
||||
mapping.set(assetId, fleet);
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
});
|
||||
|
||||
function normalize(text: string) {
|
||||
return text.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function matchesSearch(item: BrowserItem, search: string) {
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const haystack = `${item.label} ${item.subtitle} ${item.meta ?? ""}`.toLowerCase();
|
||||
return haystack.includes(search);
|
||||
}
|
||||
|
||||
function titleCase(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
return "Unknown";
|
||||
@@ -69,168 +83,387 @@ function titleCase(value: string | null | undefined) {
|
||||
.replace(/\b\w/g, (part) => part.toUpperCase());
|
||||
}
|
||||
|
||||
function buildVisibleSections(): BrowserSection[] {
|
||||
const sections: BrowserSection[] = [];
|
||||
function compactLabel(value: string | null | undefined, fallback: string) {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const words = titleCase(value).split(" ");
|
||||
if (words.length === 1) {
|
||||
return words[0].slice(0, 4).toUpperCase();
|
||||
}
|
||||
return words
|
||||
.slice(0, 2)
|
||||
.map((word) => word.slice(0, 3).toUpperCase())
|
||||
.join("-");
|
||||
}
|
||||
|
||||
function shortId(value: string) {
|
||||
if (value.length <= 8) {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
return `${value.slice(0, 4).toUpperCase()}-${value.slice(-4).toUpperCase()}`;
|
||||
}
|
||||
|
||||
function uniqueTokens(tokens: string[]) {
|
||||
return tokens.filter((token, index) => token.length > 0 && tokens.indexOf(token) === index);
|
||||
}
|
||||
|
||||
function formatShipLocation(ship: ShipSnapshot) {
|
||||
const dockedStation = ship.dockedStationId ? stationById.value.get(ship.dockedStationId) : undefined;
|
||||
if (dockedStation) {
|
||||
return `Docked ${dockedStation.label}`;
|
||||
}
|
||||
|
||||
if (ship.spatialState.transit?.destinationNodeId) {
|
||||
return `Transit ${ship.systemId}`;
|
||||
}
|
||||
|
||||
if (ship.celestialId) {
|
||||
return `Orbit ${titleCase(ship.celestialId)}`;
|
||||
}
|
||||
|
||||
const system = systemById.value.get(ship.systemId);
|
||||
return system?.label ?? ship.systemId;
|
||||
}
|
||||
|
||||
function formatStationLocation(station: StationSnapshot) {
|
||||
const system = systemById.value.get(station.systemId);
|
||||
if (station.celestialId) {
|
||||
return `${system?.label ?? station.systemId} · ${titleCase(station.celestialId)}`;
|
||||
}
|
||||
|
||||
return system?.label ?? station.systemId;
|
||||
}
|
||||
|
||||
function shipAiStates(ship: ShipSnapshot) {
|
||||
const travelToken = ship.spatialState.transit ? "TRV" : "";
|
||||
const dockToken = ship.dockedStationId ? "DCK" : "";
|
||||
const behaviorToken = compactLabel(getShipBehaviorLabel(ship.defaultBehavior.kind), "AUTO");
|
||||
const planToken = ship.activePlan?.steps.length ? "PLAN" : "";
|
||||
const orderToken = ship.orderQueue.length > 0 ? "ORD" : "";
|
||||
const commandToken = ship.commanderId ? "CMD" : "";
|
||||
|
||||
return uniqueTokens([behaviorToken, orderToken, planToken, travelToken, dockToken, commandToken]).slice(0, 5);
|
||||
}
|
||||
|
||||
function stationAiStates(station: StationSnapshot) {
|
||||
return uniqueTokens([
|
||||
station.currentProcesses.length > 0 ? "PROC" : "",
|
||||
station.dockedShips > 0 ? "DCK" : "",
|
||||
station.commanderId ? "CMD" : "",
|
||||
]);
|
||||
}
|
||||
|
||||
function fleetAiStates(fleet: PlayerFleetSnapshot) {
|
||||
return uniqueTokens([
|
||||
compactLabel(fleet.status, "STAT"),
|
||||
compactLabel(fleet.role, "ROLE"),
|
||||
fleet.commanderId ? "CMD" : "",
|
||||
]);
|
||||
}
|
||||
|
||||
function systemAiStates(systemId: string) {
|
||||
const stations = gmStore.stations.filter((station) => station.systemId === systemId).length;
|
||||
const ships = gmStore.ships.filter((ship) => ship.systemId === systemId).length;
|
||||
return uniqueTokens([stations > 0 ? `ST${stations}` : "", ships > 0 ? `SH${ships}` : ""]);
|
||||
}
|
||||
|
||||
function buildShipRow(ship: ShipSnapshot): BrowserRow {
|
||||
return {
|
||||
key: `ship-${ship.id}`,
|
||||
kind: "ship",
|
||||
kindLabel: "SH",
|
||||
name: ship.name,
|
||||
ident: `${titleCase(ship.type)} · ${shortId(ship.id)}`,
|
||||
location: formatShipLocation(ship),
|
||||
aiStates: shipAiStates(ship),
|
||||
hpLabel: Math.round(ship.health).toString(),
|
||||
hpValue: ship.health,
|
||||
selection: { id: ship.id, kind: "ship", label: ship.name },
|
||||
focusSelection: { kind: "ship", id: ship.id },
|
||||
focusMode: "tactical",
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildStationRow(station: StationSnapshot, children: BrowserRow[]): BrowserRow {
|
||||
return {
|
||||
key: `station-${station.id}`,
|
||||
kind: "station",
|
||||
kindLabel: "ST",
|
||||
name: station.label,
|
||||
ident: `${titleCase(station.category)} · ${titleCase(station.objective)}`,
|
||||
location: formatStationLocation(station),
|
||||
aiStates: stationAiStates(station),
|
||||
hpLabel: "--",
|
||||
hpValue: -1,
|
||||
selection: { id: station.id, kind: "station", label: station.label },
|
||||
focusSelection: { kind: "station", id: station.id },
|
||||
focusMode: "tactical",
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function buildFleetRow(fleet: PlayerFleetSnapshot, children: BrowserRow[]): BrowserRow {
|
||||
const homeStation = fleet.homeStationId ? stationById.value.get(fleet.homeStationId) : undefined;
|
||||
const homeSystem = fleet.homeSystemId ? systemById.value.get(fleet.homeSystemId) : undefined;
|
||||
return {
|
||||
key: `fleet-${fleet.id}`,
|
||||
kind: "fleet",
|
||||
kindLabel: "FL",
|
||||
name: fleet.label,
|
||||
ident: `${titleCase(fleet.role)} · ${shortId(fleet.id)}`,
|
||||
location: homeStation ? `Home ${homeStation.label}` : (homeSystem?.label ?? "No home"),
|
||||
aiStates: fleetAiStates(fleet),
|
||||
hpLabel: `${children.length}`,
|
||||
hpValue: children.length,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSystemRow(systemId: string): BrowserRow | null {
|
||||
const system = systemById.value.get(systemId);
|
||||
if (!system) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
key: `system-${system.id}`,
|
||||
kind: "system",
|
||||
kindLabel: "SY",
|
||||
name: system.label,
|
||||
ident: shortId(system.id),
|
||||
location: "Galaxy",
|
||||
aiStates: systemAiStates(system.id),
|
||||
hpLabel: "--",
|
||||
hpValue: -1,
|
||||
selection: { id: system.id, kind: "system", label: system.label },
|
||||
focusSelection: { kind: "system", id: system.id },
|
||||
focusMode: "tactical",
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildVisibleRows() {
|
||||
if (povLevel.value === "galaxy" || !activeSystemId.value) {
|
||||
const systems = [...gmStore.systems]
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.map<BrowserItem>((system) => ({
|
||||
key: `system-${system.id}`,
|
||||
label: system.label,
|
||||
subtitle: `${system.planets.length} planets · ${system.stars.length} stars`,
|
||||
meta: system.id,
|
||||
selection: { id: system.id, kind: "system", label: system.label },
|
||||
focusSelection: { kind: "system", id: system.id },
|
||||
focusMode: "tactical",
|
||||
}));
|
||||
sections.push({
|
||||
key: "systems",
|
||||
label: "Systems",
|
||||
count: systems.length,
|
||||
items: systems,
|
||||
});
|
||||
return sections;
|
||||
return gmStore.systems
|
||||
.map((system) => buildSystemRow(system.id))
|
||||
.filter((row): row is BrowserRow => row != null);
|
||||
}
|
||||
|
||||
const systemId = activeSystemId.value;
|
||||
const ships = gmStore.ships
|
||||
.filter((ship) => ship.systemId === systemId)
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map<BrowserItem>((ship) => ({
|
||||
key: `ship-${ship.id}`,
|
||||
label: ship.name,
|
||||
subtitle: `${titleCase(ship.type)} · ${titleCase(ship.state)}`,
|
||||
meta: `${getShipBehaviorLabel(ship.defaultBehavior.kind)}${ship.defaultBehavior.itemId ? ` · ${ship.defaultBehavior.itemId}` : ""}`,
|
||||
selection: { id: ship.id, kind: "ship", label: ship.name },
|
||||
focusSelection: { kind: "ship", id: ship.id },
|
||||
focusMode: "follow",
|
||||
}));
|
||||
const stations = gmStore.stations
|
||||
.filter((station) => station.systemId === systemId)
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.map<BrowserItem>((station) => ({
|
||||
key: `station-${station.id}`,
|
||||
label: station.label,
|
||||
subtitle: `${titleCase(station.category)} · Docked ${station.dockedShips}/${station.dockingPads}`,
|
||||
meta: station.factionId,
|
||||
selection: { id: station.id, kind: "station", label: station.label },
|
||||
focusSelection: { kind: "station", id: station.id },
|
||||
focusMode: "tactical",
|
||||
}));
|
||||
const stations = gmStore.stations.filter((station) => station.systemId === systemId);
|
||||
const ships = gmStore.ships.filter((ship) => ship.systemId === systemId);
|
||||
const stationIds = new Set(stations.map((station) => station.id));
|
||||
const stationChildren = new Map<string, BrowserRow[]>();
|
||||
const fleetChildren = new Map<string, BrowserRow[]>();
|
||||
const independentShips: BrowserRow[] = [];
|
||||
|
||||
sections.push({
|
||||
key: "ships",
|
||||
label: "Ships",
|
||||
count: ships.length,
|
||||
items: ships,
|
||||
});
|
||||
sections.push({
|
||||
key: "stations",
|
||||
label: "Stations",
|
||||
count: stations.length,
|
||||
items: stations,
|
||||
});
|
||||
for (const ship of ships) {
|
||||
const row = buildShipRow(ship);
|
||||
|
||||
return sections;
|
||||
if (ship.dockedStationId && stationIds.has(ship.dockedStationId)) {
|
||||
const children = stationChildren.get(ship.dockedStationId) ?? [];
|
||||
children.push(row);
|
||||
stationChildren.set(ship.dockedStationId, children);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fleet = playerFleetByShipId.value.get(ship.id);
|
||||
if (fleet) {
|
||||
const children = fleetChildren.get(fleet.id) ?? [];
|
||||
children.push(row);
|
||||
fleetChildren.set(fleet.id, children);
|
||||
continue;
|
||||
}
|
||||
|
||||
independentShips.push(row);
|
||||
}
|
||||
|
||||
const stationRows = stations.map((station) => buildStationRow(station, stationChildren.get(station.id) ?? []));
|
||||
const fleetRows = (playerFaction.value?.fleets ?? [])
|
||||
.filter((fleet) => (fleetChildren.get(fleet.id)?.length ?? 0) > 0)
|
||||
.map((fleet) => buildFleetRow(fleet, fleetChildren.get(fleet.id) ?? []));
|
||||
|
||||
return [...stationRows, ...fleetRows, ...independentShips];
|
||||
}
|
||||
|
||||
function buildOwnedSections(): BrowserSection[] {
|
||||
function buildOwnedRows() {
|
||||
const player = playerFaction.value;
|
||||
if (!player) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ships = player.assetRegistry.shipIds
|
||||
const ownedShips = player.assetRegistry.shipIds
|
||||
.map((shipId) => gmStore.ships.find((ship) => ship.id === shipId))
|
||||
.filter((ship): ship is NonNullable<typeof ship> => ship != null)
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map<BrowserItem>((ship) => ({
|
||||
key: `owned-ship-${ship.id}`,
|
||||
label: ship.name,
|
||||
subtitle: `${ship.systemId} · ${titleCase(ship.state)}`,
|
||||
meta: getShipBehaviorLabel(ship.defaultBehavior.kind),
|
||||
selection: { id: ship.id, kind: "ship", label: ship.name },
|
||||
focusSelection: { kind: "ship", id: ship.id },
|
||||
focusMode: "follow",
|
||||
}));
|
||||
const stations = player.assetRegistry.stationIds
|
||||
.filter((ship): ship is ShipSnapshot => ship != null);
|
||||
const ownedStations = player.assetRegistry.stationIds
|
||||
.map((stationId) => gmStore.stations.find((station) => station.id === stationId))
|
||||
.filter((station): station is NonNullable<typeof station> => station != null)
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.map<BrowserItem>((station) => ({
|
||||
key: `owned-station-${station.id}`,
|
||||
label: station.label,
|
||||
subtitle: `${station.systemId} · ${titleCase(station.category)}`,
|
||||
meta: `${station.installedModules.length} modules`,
|
||||
selection: { id: station.id, kind: "station", label: station.label },
|
||||
focusSelection: { kind: "station", id: station.id },
|
||||
focusMode: "tactical",
|
||||
}));
|
||||
const fleets = player.fleets
|
||||
.slice()
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.map<BrowserItem>((fleet) => ({
|
||||
key: `fleet-${fleet.id}`,
|
||||
label: fleet.label,
|
||||
subtitle: `${titleCase(fleet.role)} · ${titleCase(fleet.status)}`,
|
||||
meta: `${fleet.assetIds.length} assets`,
|
||||
}));
|
||||
.filter((station): station is StationSnapshot => station != null);
|
||||
const ownedFleetShipIds = new Set(player.fleets.flatMap((fleet) => fleet.assetIds));
|
||||
const ownedStationIds = new Set(ownedStations.map((station) => station.id));
|
||||
|
||||
return [
|
||||
{
|
||||
key: "owned-fleets",
|
||||
label: "Fleets",
|
||||
count: fleets.length,
|
||||
items: fleets,
|
||||
},
|
||||
{
|
||||
key: "owned-stations",
|
||||
label: "Stations",
|
||||
count: stations.length,
|
||||
items: stations,
|
||||
},
|
||||
{
|
||||
key: "owned-ships",
|
||||
label: "Ships",
|
||||
count: ships.length,
|
||||
items: ships,
|
||||
},
|
||||
];
|
||||
const stationChildren = new Map<string, BrowserRow[]>();
|
||||
for (const ship of ownedShips) {
|
||||
if (!ship.dockedStationId || !ownedStationIds.has(ship.dockedStationId) || ownedFleetShipIds.has(ship.id)) {
|
||||
continue;
|
||||
}
|
||||
const children = stationChildren.get(ship.dockedStationId) ?? [];
|
||||
children.push(buildShipRow(ship));
|
||||
stationChildren.set(ship.dockedStationId, children);
|
||||
}
|
||||
|
||||
const stationRows = ownedStations.map((station) => buildStationRow(station, stationChildren.get(station.id) ?? []));
|
||||
const fleetRows = player.fleets.map((fleet) => buildFleetRow(
|
||||
fleet,
|
||||
fleet.assetIds
|
||||
.map((shipId) => ownedShips.find((ship) => ship.id === shipId))
|
||||
.filter((ship): ship is ShipSnapshot => ship != null)
|
||||
.map((ship) => buildShipRow(ship)),
|
||||
));
|
||||
const independentShips = ownedShips
|
||||
.filter((ship) => !ownedFleetShipIds.has(ship.id) && (!ship.dockedStationId || !ownedStationIds.has(ship.dockedStationId)))
|
||||
.map((ship) => buildShipRow(ship));
|
||||
|
||||
return [...stationRows, ...fleetRows, ...independentShips];
|
||||
}
|
||||
|
||||
const filteredSections = computed(() => {
|
||||
const search = normalize(searchText.value);
|
||||
const sections = activeTab.value === "visible" ? buildVisibleSections() : buildOwnedSections();
|
||||
return sections
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) => matchesSearch(item, search)),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
function getRowSortValue(row: BrowserRow, key: BrowserSortKey) {
|
||||
if (key === "hp") {
|
||||
return row.hpValue;
|
||||
}
|
||||
|
||||
if (key === "location") {
|
||||
return row.location;
|
||||
}
|
||||
|
||||
if (key === "ai") {
|
||||
return row.aiStates.join(" ");
|
||||
}
|
||||
|
||||
return `${row.name} ${row.ident}`;
|
||||
}
|
||||
|
||||
function sortRows(rows: BrowserRow[]): BrowserRow[] {
|
||||
const direction = sortDirection.value === "asc" ? 1 : -1;
|
||||
return [...rows]
|
||||
.sort((left, right) => {
|
||||
const leftValue = getRowSortValue(left, sortKey.value);
|
||||
const rightValue = getRowSortValue(right, sortKey.value);
|
||||
|
||||
if (typeof leftValue === "number" && typeof rightValue === "number") {
|
||||
return (leftValue - rightValue) * direction;
|
||||
}
|
||||
|
||||
return String(leftValue).localeCompare(String(rightValue)) * direction;
|
||||
})
|
||||
.map((row) => ({
|
||||
...row,
|
||||
children: sortRows(row.children),
|
||||
}));
|
||||
}
|
||||
|
||||
function rowMatches(row: BrowserRow, search: string) {
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const haystack = `${row.name} ${row.ident} ${row.location} ${row.aiStates.join(" ")} ${row.hpLabel}`.toLowerCase();
|
||||
return haystack.includes(search);
|
||||
}
|
||||
|
||||
function isExpanded(row: BrowserRow) {
|
||||
if (row.children.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expandedRowKeys.value[row.key] ?? true;
|
||||
}
|
||||
|
||||
function flattenRows(rows: BrowserRow[], search: string, depth = 0, forceExpand = false): BrowserDisplayRow[] {
|
||||
const flattened: BrowserDisplayRow[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const descendantMatches = flattenRows(row.children, search, depth + 1, forceExpand);
|
||||
const matches = rowMatches(row, search);
|
||||
if (!matches && descendantMatches.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flattened.push({
|
||||
...row,
|
||||
depth,
|
||||
});
|
||||
|
||||
if (row.children.length > 0 && (forceExpand || isExpanded(row))) {
|
||||
flattened.push(...descendantMatches);
|
||||
}
|
||||
}
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
const rawRows = computed(() => {
|
||||
if (activeTab.value === "owned") {
|
||||
return buildOwnedRows();
|
||||
}
|
||||
return buildVisibleRows();
|
||||
});
|
||||
|
||||
function selectItem(item: BrowserItem) {
|
||||
if (!item.selection) {
|
||||
const displayRows = computed(() => {
|
||||
const search = normalize(searchText.value);
|
||||
const sortedRows = sortRows(rawRows.value);
|
||||
return flattenRows(sortedRows, search, 0, search.length > 0);
|
||||
});
|
||||
|
||||
function toggleSort(nextKey: BrowserSortKey) {
|
||||
if (sortKey.value === nextKey) {
|
||||
sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
|
||||
return;
|
||||
}
|
||||
|
||||
selectionStore.selectSelection(item.selection, "ui");
|
||||
sortKey.value = nextKey;
|
||||
sortDirection.value = "asc";
|
||||
}
|
||||
|
||||
function focusItem(item: BrowserItem) {
|
||||
if (item.selection) {
|
||||
selectionStore.selectSelection(item.selection, "ui");
|
||||
function toggleRow(row: BrowserDisplayRow) {
|
||||
if (row.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (item.focusSelection) {
|
||||
emit("focus", item.focusSelection, item.focusMode);
|
||||
|
||||
expandedRowKeys.value[row.key] = !isExpanded(row);
|
||||
}
|
||||
|
||||
function selectItem(row: BrowserDisplayRow) {
|
||||
if (!row.selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionStore.selectSelection(row.selection, "ui");
|
||||
}
|
||||
|
||||
function focusItem(row: BrowserDisplayRow) {
|
||||
if (row.selection) {
|
||||
selectionStore.selectSelection(row.selection, "ui");
|
||||
}
|
||||
if (row.focusSelection) {
|
||||
emit("focus", row.focusSelection, row.focusMode);
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(item: BrowserItem) {
|
||||
return !!item.selection
|
||||
&& item.selection.id === selectedEntityId.value
|
||||
&& item.selection.kind === selectedEntityKind.value;
|
||||
function isSelected(row: BrowserDisplayRow) {
|
||||
return !!row.selection
|
||||
&& row.selection.id === selectedEntityId.value
|
||||
&& row.selection.kind === selectedEntityKind.value;
|
||||
}
|
||||
|
||||
function sortMarker(key: BrowserSortKey) {
|
||||
if (sortKey.value !== key) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return sortDirection.value === "asc" ? " ▲" : " ▼";
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -276,47 +509,91 @@ function isSelected(item: BrowserItem) {
|
||||
<div v-if="activeTab === 'owned' && !playerFaction" class="entity-browser-panel__empty">
|
||||
No player-owned assets yet.
|
||||
</div>
|
||||
<div v-else-if="filteredSections.length === 0" class="entity-browser-panel__empty">
|
||||
<div v-else-if="displayRows.length === 0" class="entity-browser-panel__empty">
|
||||
Nothing matches the current view.
|
||||
</div>
|
||||
<div v-else class="entity-browser-panel__sections">
|
||||
<section
|
||||
v-for="section in filteredSections"
|
||||
:key="section.key"
|
||||
class="entity-browser-section"
|
||||
>
|
||||
<header class="entity-browser-section__header">
|
||||
<span>{{ section.label }}</span>
|
||||
<span>{{ section.items.length }}</span>
|
||||
</header>
|
||||
<div class="entity-browser-section__items">
|
||||
<div
|
||||
v-for="item in section.items"
|
||||
:key="item.key"
|
||||
class="entity-browser-item"
|
||||
:class="isSelected(item) ? 'entity-browser-item--selected' : ''"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="entity-browser-item__body"
|
||||
:disabled="!item.selection"
|
||||
@click="selectItem(item)"
|
||||
<div class="entity-browser-table-wrap">
|
||||
<table class="entity-browser-table entity-browser-table--tree">
|
||||
<colgroup>
|
||||
<col class="entity-browser-table__col entity-browser-table__col--entity">
|
||||
<col class="entity-browser-table__col entity-browser-table__col--ident">
|
||||
<col class="entity-browser-table__col entity-browser-table__col--location">
|
||||
<col class="entity-browser-table__col entity-browser-table__col--ai">
|
||||
<col class="entity-browser-table__col entity-browser-table__col--hp">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<button type="button" class="entity-browser-table__sort" @click="toggleSort('entity')">
|
||||
Entity{{ sortMarker("entity") }}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col">Ident</th>
|
||||
<th scope="col">
|
||||
<button type="button" class="entity-browser-table__sort" @click="toggleSort('location')">
|
||||
Location{{ sortMarker("location") }}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<button type="button" class="entity-browser-table__sort" @click="toggleSort('ai')">
|
||||
AI{{ sortMarker("ai") }}
|
||||
</button>
|
||||
</th>
|
||||
<th scope="col" class="entity-browser-table__numeric">
|
||||
<button type="button" class="entity-browser-table__sort" @click="toggleSort('hp')">
|
||||
HP{{ sortMarker("hp") }}
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in displayRows"
|
||||
:key="row.key"
|
||||
class="entity-browser-table__row"
|
||||
:class="isSelected(row) ? 'entity-browser-table__row--selected' : ''"
|
||||
@click="selectItem(row)"
|
||||
@dblclick="focusItem(row)"
|
||||
>
|
||||
<div class="entity-browser-item__label">{{ item.label }}</div>
|
||||
<div class="entity-browser-item__subtitle">{{ item.subtitle }}</div>
|
||||
<div v-if="item.meta" class="entity-browser-item__meta">{{ item.meta }}</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.focusSelection"
|
||||
type="button"
|
||||
class="entity-browser-item__focus"
|
||||
@click.stop="focusItem(item)"
|
||||
>
|
||||
Focus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<td class="entity-browser-table__name">
|
||||
<div class="entity-browser-row" :style="{ paddingLeft: `${row.depth * 0.9}rem` }">
|
||||
<button
|
||||
v-if="row.children.length > 0"
|
||||
type="button"
|
||||
class="entity-browser-row__toggle"
|
||||
@click.stop="toggleRow(row)"
|
||||
>
|
||||
{{ isExpanded(row) ? "-" : "+" }}
|
||||
</button>
|
||||
<span v-else class="entity-browser-row__toggle entity-browser-row__toggle--spacer" />
|
||||
<span class="entity-browser-row__kind" :class="`entity-browser-row__kind--${row.kind}`">
|
||||
{{ row.kindLabel }}
|
||||
</span>
|
||||
<span class="entity-browser-row__label">{{ row.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="entity-browser-table__detail entity-browser-table__cell--truncate">{{ row.ident }}</td>
|
||||
<td class="entity-browser-table__cell--truncate">{{ row.location }}</td>
|
||||
<td class="entity-browser-table__cell--ai">
|
||||
<div class="entity-browser-ai">
|
||||
<span
|
||||
v-for="token in row.aiStates"
|
||||
:key="`${row.key}-${token}`"
|
||||
class="entity-browser-ai__token"
|
||||
>
|
||||
{{ token }}
|
||||
</span>
|
||||
<span v-if="row.aiStates.length === 0" class="entity-browser-table__muted">--</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="entity-browser-table__numeric">
|
||||
{{ row.hpLabel }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user