935 lines
33 KiB
Vue
935 lines
33 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive, ref, watch } from "vue";
|
|
import { storeToRefs } from "pinia";
|
|
import modulesData from "../../../../shared/data/modules.json";
|
|
import { enqueueShipOrder, removeShipOrder, updateShipDefaultBehavior } from "../api";
|
|
import {
|
|
formatShipAutomationSupportStatus,
|
|
getShipBehaviorLabel,
|
|
getShipBehaviorNotes,
|
|
getShipBehaviorSupportStatusLabel,
|
|
getShipOrderLabel,
|
|
getShipOrderNotes,
|
|
getShipOrderSupportStatusLabel,
|
|
} from "../shipAutomationPresentation";
|
|
import { useGmStore } from "../ui/stores/gmStore";
|
|
import { useAuthStore } from "../ui/stores/authStore";
|
|
import { usePlayerFactionStore } from "../ui/stores/playerFactionStore";
|
|
import { useShipAutomationCatalogStore } from "../ui/stores/shipAutomationCatalogStore";
|
|
import { useViewerSelectionStore } from "../ui/stores/viewerSelection";
|
|
import type { Selectable } from "../viewerTypes";
|
|
|
|
const props = defineProps<{
|
|
fallbackTitle: string;
|
|
fallbackHtml: string;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
focus: [selection: Selectable, cameraMode?: "follow" | "tactical"];
|
|
}>();
|
|
|
|
const gmStore = useGmStore();
|
|
const authStore = useAuthStore();
|
|
const playerStore = usePlayerFactionStore();
|
|
const automationStore = useShipAutomationCatalogStore();
|
|
const selectionStore = useViewerSelectionStore();
|
|
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
|
const { playerFaction } = storeToRefs(playerStore);
|
|
|
|
const commonMiningItems = ["ore", "silicon", "ice", "methane", "hydrogen", "helium"];
|
|
|
|
const behaviorForm = reactive({
|
|
kind: "hold-position",
|
|
areaSystemId: "",
|
|
itemId: "ore",
|
|
});
|
|
|
|
const mineOrderForm = reactive({
|
|
systemId: "",
|
|
itemId: "ore",
|
|
});
|
|
|
|
const moveOrderSystemId = ref("");
|
|
const actionBusy = ref(false);
|
|
const actionStatus = ref("");
|
|
const actionError = ref("");
|
|
|
|
const moduleNameById = new Map<string, string>(
|
|
(modulesData as { id: string; name: string }[]).map((module) => [module.id, module.name]),
|
|
);
|
|
|
|
function titleCase(value: string | null | undefined) {
|
|
if (!value) {
|
|
return "Unknown";
|
|
}
|
|
|
|
return value
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/[-_]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/\b\w/g, (part) => part.toUpperCase());
|
|
}
|
|
|
|
function formatAmount(value: number) {
|
|
const rounded = Math.round(value);
|
|
return Math.abs(value - rounded) < 0.005 ? String(rounded) : value.toFixed(1);
|
|
}
|
|
|
|
function formatPercent(value: number) {
|
|
return `${Math.round(value * 100)}%`;
|
|
}
|
|
|
|
function joinDetail(parts: Array<string | null | undefined>) {
|
|
return parts.filter((part): part is string => !!part && part.trim().length > 0).join(" · ");
|
|
}
|
|
|
|
function describeOrderTarget(order: {
|
|
itemId?: string | null;
|
|
targetEntityId?: string | null;
|
|
targetSystemId?: string | null;
|
|
nodeId?: string | null;
|
|
constructionSiteId?: string | null;
|
|
sourceStationId?: string | null;
|
|
destinationStationId?: string | null;
|
|
moduleId?: string | null;
|
|
}) {
|
|
return order.itemId
|
|
?? order.targetEntityId
|
|
?? order.targetSystemId
|
|
?? order.nodeId
|
|
?? order.constructionSiteId
|
|
?? order.destinationStationId
|
|
?? order.sourceStationId
|
|
?? order.moduleId
|
|
?? "—";
|
|
}
|
|
|
|
function describeSubTaskTarget(subTask: {
|
|
itemId?: string | null;
|
|
targetEntityId?: string | null;
|
|
targetSystemId?: string | null;
|
|
targetNodeId?: string | null;
|
|
moduleId?: string | null;
|
|
}) {
|
|
return subTask.itemId
|
|
?? subTask.targetEntityId
|
|
?? subTask.targetSystemId
|
|
?? subTask.targetNodeId
|
|
?? subTask.moduleId
|
|
?? "—";
|
|
}
|
|
|
|
const selectedShip = computed(() => {
|
|
if (selectedEntityKind.value !== "ship" || !selectedEntityId.value) {
|
|
return null;
|
|
}
|
|
|
|
return gmStore.ships.find((ship) => ship.id === selectedEntityId.value) ?? null;
|
|
});
|
|
|
|
const selectedStation = computed(() => {
|
|
if (selectedEntityKind.value !== "station" || !selectedEntityId.value) {
|
|
return null;
|
|
}
|
|
|
|
return gmStore.stations.find((station) => station.id === selectedEntityId.value) ?? null;
|
|
});
|
|
|
|
const factionLabelById = computed(() =>
|
|
new Map(gmStore.factions.map((faction) => [faction.id, faction.label])),
|
|
);
|
|
|
|
const playerShipIds = computed(() =>
|
|
new Set(playerFaction.value?.assetRegistry.shipIds ?? []),
|
|
);
|
|
|
|
const canAccessGmDirectly = computed(() => authStore.canAccessGm && !authStore.isActingAsAlternateIdentity);
|
|
|
|
const canDirectControlSelectedShip = computed(() =>
|
|
!!selectedShip.value && (canAccessGmDirectly.value || playerShipIds.value.has(selectedShip.value.id)),
|
|
);
|
|
|
|
const directOrders = computed(() =>
|
|
selectedShip.value?.orderQueue.filter((order) => order.sourceKind !== "behavior") ?? [],
|
|
);
|
|
|
|
const behaviorOrders = computed(() =>
|
|
selectedShip.value?.orderQueue.filter((order) => order.sourceKind === "behavior") ?? [],
|
|
);
|
|
|
|
const editableBehaviorDefinitions = computed(() =>
|
|
(automationStore.catalog?.behaviors ?? [])
|
|
.filter((entry) => entry.supportStatus !== "InternalOnly"),
|
|
);
|
|
|
|
const selectedBehaviorStatus = computed(() =>
|
|
selectedShip.value ? getShipBehaviorSupportStatusLabel(selectedShip.value.defaultBehavior.kind) : null,
|
|
);
|
|
|
|
const selectedBehaviorNotes = computed(() =>
|
|
selectedShip.value ? getShipBehaviorNotes(selectedShip.value.defaultBehavior.kind) : null,
|
|
);
|
|
|
|
const formBehaviorStatus = computed(() =>
|
|
getShipBehaviorSupportStatusLabel(behaviorForm.kind),
|
|
);
|
|
|
|
const formBehaviorNotes = computed(() =>
|
|
getShipBehaviorNotes(behaviorForm.kind),
|
|
);
|
|
|
|
const shipStatusRows = computed(() => {
|
|
if (!selectedShip.value) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{ label: "State", value: titleCase(selectedShip.value.state) },
|
|
{ label: "Behavior", value: getShipBehaviorLabel(selectedShip.value.defaultBehavior.kind) },
|
|
{ label: "Control", value: titleCase(selectedShip.value.controlSourceKind) },
|
|
{ label: "Assignment", value: selectedShip.value.assignment?.kind ?? "unassigned" },
|
|
{
|
|
label: "Plan",
|
|
value: selectedShip.value.activePlan
|
|
? `${selectedShip.value.activePlan.kind} · ${titleCase(selectedShip.value.activePlan.status)}`
|
|
: "none",
|
|
},
|
|
{ label: "Failure", value: selectedShip.value.lastAccessFailureReason ?? "none" },
|
|
{ label: "Commander", value: selectedShip.value.commanderId ?? "none" },
|
|
{ label: "Docked", value: selectedShip.value.dockedStationId ?? "no" },
|
|
];
|
|
});
|
|
|
|
const shipCargoSummaryRows = computed(() => {
|
|
if (!selectedShip.value) {
|
|
return [];
|
|
}
|
|
|
|
const usedCargo = selectedShip.value.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
|
return [
|
|
{ label: "Used", value: formatAmount(usedCargo) },
|
|
{ label: "Capacity", value: formatAmount(selectedShip.value.cargoCapacity) },
|
|
{ label: "Free", value: formatAmount(Math.max(selectedShip.value.cargoCapacity - usedCargo, 0)) },
|
|
{ label: "Travel", value: `${formatAmount(selectedShip.value.travelSpeed)} ${selectedShip.value.travelSpeedUnit}` },
|
|
{ label: "Hull", value: formatAmount(selectedShip.value.health) },
|
|
{ label: "Regime", value: titleCase(selectedShip.value.spatialState.movementRegime) },
|
|
];
|
|
});
|
|
|
|
const shipCargoRows = computed(() =>
|
|
selectedShip.value?.inventory.map((entry) => ({
|
|
key: entry.itemId,
|
|
ware: entry.itemId,
|
|
amount: formatAmount(entry.amount),
|
|
})) ?? [],
|
|
);
|
|
|
|
const shipBehaviorRows = computed(() => {
|
|
if (!selectedShip.value) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{ label: "Area", value: selectedShip.value.defaultBehavior.areaSystemId ?? "none" },
|
|
{ label: "Item", value: selectedShip.value.defaultBehavior.itemId ?? "none" },
|
|
{ label: "Home Station", value: selectedShip.value.defaultBehavior.homeStationId ?? "none" },
|
|
{ label: "Target", value: selectedShip.value.defaultBehavior.targetEntityId ?? "none" },
|
|
{ label: "Range", value: String(selectedShip.value.defaultBehavior.maxSystemRange) },
|
|
{ label: "Known Only", value: selectedShip.value.defaultBehavior.knownStationsOnly ? "yes" : "no" },
|
|
];
|
|
});
|
|
|
|
const directOrderRows = computed(() =>
|
|
directOrders.value.map((order) => ({
|
|
id: order.id,
|
|
label: getShipOrderLabel(order.kind),
|
|
status: titleCase(order.status),
|
|
target: describeOrderTarget(order),
|
|
detail: joinDetail([
|
|
`P${order.priority}`,
|
|
titleCase(order.sourceKind),
|
|
order.failureReason ?? undefined,
|
|
]),
|
|
})),
|
|
);
|
|
|
|
const behaviorOrderRows = computed(() =>
|
|
behaviorOrders.value.map((order) => ({
|
|
id: order.id,
|
|
label: getShipOrderLabel(order.kind),
|
|
status: titleCase(order.status),
|
|
target: describeOrderTarget(order),
|
|
detail: joinDetail([
|
|
`P${order.priority}`,
|
|
getShipOrderSupportStatusLabel(order.kind) ?? undefined,
|
|
getShipOrderNotes(order.kind) ?? undefined,
|
|
order.failureReason ?? undefined,
|
|
]),
|
|
})),
|
|
);
|
|
|
|
const shipPlanRows = computed(() => {
|
|
if (!selectedShip.value?.activePlan) {
|
|
return [];
|
|
}
|
|
|
|
return selectedShip.value.activePlan.steps.flatMap((step) => {
|
|
const stepRow = {
|
|
id: step.id,
|
|
scope: "Step",
|
|
activity: step.summary || titleCase(step.kind),
|
|
status: titleCase(step.status),
|
|
detail: joinDetail([
|
|
step.blockingReason ?? undefined,
|
|
`${step.subTasks.length} subtasks`,
|
|
]),
|
|
isSubTask: false,
|
|
};
|
|
|
|
const subTaskRows = step.subTasks.map((subTask) => ({
|
|
id: subTask.id,
|
|
scope: "Subtask",
|
|
activity: subTask.summary || titleCase(subTask.kind),
|
|
status: titleCase(subTask.status),
|
|
detail: joinDetail([
|
|
describeSubTaskTarget(subTask),
|
|
subTask.blockingReason ?? undefined,
|
|
`${Math.round(subTask.progress * 100)}%`,
|
|
]),
|
|
isSubTask: true,
|
|
}));
|
|
|
|
return [stepRow, ...subTaskRows];
|
|
});
|
|
});
|
|
|
|
const stationStatusRows = computed(() => {
|
|
if (!selectedStation.value) {
|
|
return [];
|
|
}
|
|
|
|
return [
|
|
{ label: "Category", value: titleCase(selectedStation.value.category) },
|
|
{ label: "Objective", value: titleCase(selectedStation.value.objective) },
|
|
{ label: "Docked", value: `${selectedStation.value.dockedShips} / ${selectedStation.value.dockingPads}` },
|
|
{
|
|
label: "Population",
|
|
value: `${formatAmount(selectedStation.value.population)} / ${formatAmount(selectedStation.value.populationCapacity)}`,
|
|
},
|
|
{ label: "Workforce", value: formatAmount(selectedStation.value.workforceRequired) },
|
|
{ label: "Efficiency", value: formatPercent(selectedStation.value.workforceEffectiveRatio) },
|
|
{ label: "Commander", value: selectedStation.value.commanderId ?? "none" },
|
|
{ label: "Policy", value: selectedStation.value.policySetId ?? "none" },
|
|
];
|
|
});
|
|
|
|
const stationModuleRows = computed(() =>
|
|
selectedStation.value?.installedModules.map((moduleId) => ({
|
|
key: moduleId,
|
|
module: moduleNameById.get(moduleId) ?? moduleId,
|
|
moduleId,
|
|
})) ?? [],
|
|
);
|
|
|
|
const stationStorageRows = computed(() =>
|
|
selectedStation.value?.storageUsage.map((entry) => ({
|
|
key: entry.storageClass,
|
|
storageClass: titleCase(entry.storageClass),
|
|
used: formatAmount(entry.used),
|
|
capacity: formatAmount(entry.capacity),
|
|
fill: entry.capacity > 0 ? formatPercent(entry.used / entry.capacity) : "0%",
|
|
})) ?? [],
|
|
);
|
|
|
|
const stationInventoryRows = computed(() =>
|
|
selectedStation.value?.inventory.map((entry) => ({
|
|
key: entry.itemId,
|
|
ware: entry.itemId,
|
|
amount: formatAmount(entry.amount),
|
|
})) ?? [],
|
|
);
|
|
|
|
const stationProcessRows = computed(() =>
|
|
selectedStation.value?.currentProcesses.map((process) => ({
|
|
key: `${process.lane}-${process.label}`,
|
|
lane: process.lane,
|
|
label: process.label,
|
|
progress: formatPercent(process.progress),
|
|
timing: `${Math.ceil(process.timeRemainingSeconds)}s / ${Math.ceil(process.cycleSeconds)}s`,
|
|
})) ?? [],
|
|
);
|
|
|
|
watch(
|
|
() => `${selectedEntityKind.value ?? "none"}:${selectedEntityId.value ?? "none"}`,
|
|
() => {
|
|
const ship = selectedShip.value;
|
|
if (!ship) {
|
|
actionStatus.value = "";
|
|
actionError.value = "";
|
|
return;
|
|
}
|
|
|
|
behaviorForm.kind = ship.defaultBehavior.kind;
|
|
behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId ?? "";
|
|
behaviorForm.itemId = ship.defaultBehavior.itemId ?? "ore";
|
|
mineOrderForm.systemId = ship.systemId ?? "";
|
|
mineOrderForm.itemId = "ore";
|
|
moveOrderSystemId.value = ship.systemId ?? "";
|
|
actionStatus.value = "";
|
|
actionError.value = "";
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
function focusShip(cameraMode?: "follow" | "tactical") {
|
|
if (!selectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
emit("focus", { kind: "ship", id: selectedShip.value.id }, cameraMode);
|
|
}
|
|
|
|
function focusStation() {
|
|
if (!selectedStation.value) {
|
|
return;
|
|
}
|
|
|
|
emit("focus", { kind: "station", id: selectedStation.value.id }, "tactical");
|
|
}
|
|
|
|
async function runShipAction(action: () => Promise<void>, successMessage: string) {
|
|
actionBusy.value = true;
|
|
actionError.value = "";
|
|
actionStatus.value = "";
|
|
try {
|
|
await action();
|
|
actionStatus.value = successMessage;
|
|
} catch (error) {
|
|
actionError.value = error instanceof Error ? error.message : "Ship action failed.";
|
|
} finally {
|
|
actionBusy.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveBehavior() {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
const ship = await updateShipDefaultBehavior(selectedShip.value!.id, {
|
|
kind: behaviorForm.kind,
|
|
homeSystemId: selectedShip.value!.systemId,
|
|
homeStationId: null,
|
|
areaSystemId: behaviorForm.kind === "local-auto-mine"
|
|
? (behaviorForm.areaSystemId || selectedShip.value!.systemId || null)
|
|
: null,
|
|
targetEntityId: null,
|
|
itemId: behaviorForm.kind === "local-auto-mine"
|
|
? (behaviorForm.itemId.trim() || null)
|
|
: null,
|
|
preferredNodeId: null,
|
|
preferredConstructionSiteId: null,
|
|
preferredModuleId: null,
|
|
targetPosition: null,
|
|
waitSeconds: selectedShip.value!.defaultBehavior.waitSeconds,
|
|
radius: selectedShip.value!.defaultBehavior.radius,
|
|
maxSystemRange: selectedShip.value!.defaultBehavior.maxSystemRange,
|
|
knownStationsOnly: selectedShip.value!.defaultBehavior.knownStationsOnly,
|
|
patrolPoints: [],
|
|
repeatOrders: [],
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
}, "Default behavior updated.");
|
|
}
|
|
|
|
async function queueHoldPositionOrder() {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
const ship = await enqueueShipOrder(selectedShip.value!.id, {
|
|
kind: "hold-position",
|
|
priority: 100,
|
|
interruptCurrentPlan: true,
|
|
label: "Hold position",
|
|
targetEntityId: null,
|
|
targetSystemId: null,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId: null,
|
|
nodeId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 0,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
}, "Hold position order queued.");
|
|
}
|
|
|
|
async function queueMoveOrder() {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
const targetSystemId = moveOrderSystemId.value.trim();
|
|
if (!targetSystemId) {
|
|
actionError.value = "Select a target system.";
|
|
actionStatus.value = "";
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
const ship = await enqueueShipOrder(selectedShip.value!.id, {
|
|
kind: "move",
|
|
priority: 90,
|
|
interruptCurrentPlan: true,
|
|
label: `Move to ${targetSystemId}`,
|
|
targetEntityId: null,
|
|
targetSystemId,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId: null,
|
|
nodeId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 0,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
}, "Move order queued.");
|
|
}
|
|
|
|
async function queueMineResourceOrder() {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
const targetSystemId = mineOrderForm.systemId.trim() || selectedShip.value.systemId;
|
|
const itemId = mineOrderForm.itemId.trim();
|
|
if (!targetSystemId) {
|
|
actionError.value = "Select a mining system.";
|
|
actionStatus.value = "";
|
|
return;
|
|
}
|
|
|
|
if (!itemId) {
|
|
actionError.value = "Select a ware to mine.";
|
|
actionStatus.value = "";
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
const ship = await enqueueShipOrder(selectedShip.value!.id, {
|
|
kind: "mine-and-deliver",
|
|
priority: 95,
|
|
interruptCurrentPlan: true,
|
|
label: `Mine ${itemId} in ${targetSystemId}`,
|
|
targetEntityId: null,
|
|
targetSystemId,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId,
|
|
nodeId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 0,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
}, "Mine Resource order queued.");
|
|
}
|
|
|
|
async function removeOrder(orderId: string) {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
const ship = await removeShipOrder(selectedShip.value!.id, orderId);
|
|
gmStore.upsertShip(ship);
|
|
}, "Order removed.");
|
|
}
|
|
|
|
async function clearOrders() {
|
|
if (!selectedShip.value || !canDirectControlSelectedShip.value || directOrders.value.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await runShipAction(async () => {
|
|
let latestShip = selectedShip.value!;
|
|
for (const order of [...latestShip.orderQueue.filter((entry) => entry.sourceKind !== "behavior")]) {
|
|
latestShip = await removeShipOrder(latestShip.id, order.id);
|
|
}
|
|
gmStore.upsertShip(latestShip);
|
|
}, "Orders cleared.");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="entity-inspector-panel pointer-events-auto">
|
|
<template v-if="selectedShip">
|
|
<header class="entity-inspector-panel__header">
|
|
<div>
|
|
<div class="entity-inspector-panel__kicker">Ship Inspector</div>
|
|
<h3>{{ selectedShip.name }}</h3>
|
|
<p>{{ factionLabelById.get(selectedShip.factionId) ?? selectedShip.factionId }} · {{ selectedShip.systemId }}</p>
|
|
</div>
|
|
<div class="entity-inspector-panel__actions">
|
|
<button type="button" class="entity-inspector-panel__action" @click="focusShip('tactical')">Focus</button>
|
|
<button type="button" class="entity-inspector-panel__action" @click="focusShip('follow')">Track</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Status</h4>
|
|
<div class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table entity-inspector-table--kv">
|
|
<tbody>
|
|
<tr v-for="row in shipStatusRows" :key="row.label">
|
|
<th scope="row">{{ row.label }}</th>
|
|
<td>{{ row.value }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Cargo</h4>
|
|
<div class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table entity-inspector-table--kv">
|
|
<tbody>
|
|
<tr v-for="row in shipCargoSummaryRows" :key="row.label">
|
|
<th scope="row">{{ row.label }}</th>
|
|
<td>{{ row.value }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="shipCargoRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Ware</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in shipCargoRows" :key="row.key">
|
|
<td>{{ row.ware }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.amount }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No cargo.</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Behavior</h4>
|
|
<div v-if="selectedBehaviorStatus || selectedBehaviorNotes" class="entity-inspector-note">
|
|
{{ [selectedBehaviorStatus, selectedBehaviorNotes].filter(Boolean).join(" · ") }}
|
|
</div>
|
|
<div class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table entity-inspector-table--kv">
|
|
<tbody>
|
|
<tr v-for="row in shipBehaviorRows" :key="row.label">
|
|
<th scope="row">{{ row.label }}</th>
|
|
<td>{{ row.value }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="canDirectControlSelectedShip" class="entity-inspector-form">
|
|
<label class="entity-inspector-field">
|
|
<span>Default Behavior</span>
|
|
<select v-model="behaviorForm.kind">
|
|
<option v-for="entry in editableBehaviorDefinitions" :key="entry.id" :value="entry.id">
|
|
{{ entry.label }} ({{ formatShipAutomationSupportStatus(entry.supportStatus) }})
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<div v-if="formBehaviorStatus || formBehaviorNotes" class="entity-inspector-note">
|
|
{{ [formBehaviorStatus, formBehaviorNotes].filter(Boolean).join(" · ") }}
|
|
</div>
|
|
<label v-if="behaviorForm.kind === 'local-auto-mine'" class="entity-inspector-field">
|
|
<span>System</span>
|
|
<select v-model="behaviorForm.areaSystemId">
|
|
<option value="">Current system</option>
|
|
<option v-for="system in gmStore.systems" :key="system.id" :value="system.id">{{ system.label }} ({{ system.id }})</option>
|
|
</select>
|
|
</label>
|
|
<label v-if="behaviorForm.kind === 'local-auto-mine'" class="entity-inspector-field">
|
|
<span>Item</span>
|
|
<select v-model="behaviorForm.itemId">
|
|
<option v-for="itemId in commonMiningItems" :key="itemId" :value="itemId">{{ itemId }}</option>
|
|
</select>
|
|
</label>
|
|
<div class="entity-inspector-actions-row">
|
|
<button type="button" class="entity-inspector-panel__action" :disabled="actionBusy" @click="saveBehavior">Save Behavior</button>
|
|
</div>
|
|
</div>
|
|
<div v-else class="entity-inspector-note">
|
|
Direct behavior editing is only available for player-owned ships or GM users.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Orders</h4>
|
|
<div v-if="canDirectControlSelectedShip" class="entity-inspector-form">
|
|
<div class="entity-inspector-actions-row">
|
|
<button type="button" class="entity-inspector-panel__action" :disabled="actionBusy" @click="queueHoldPositionOrder">Hold Position</button>
|
|
<button type="button" class="entity-inspector-panel__action" :disabled="actionBusy || directOrders.length === 0" @click="clearOrders">Clear Orders</button>
|
|
</div>
|
|
<div class="entity-inspector-inline-form">
|
|
<label class="entity-inspector-field entity-inspector-field--grow">
|
|
<span>Move To System</span>
|
|
<select v-model="moveOrderSystemId">
|
|
<option value="">Select system</option>
|
|
<option v-for="system in gmStore.systems" :key="system.id" :value="system.id">{{ system.label }} ({{ system.id }})</option>
|
|
</select>
|
|
</label>
|
|
<button type="button" class="entity-inspector-panel__action" :disabled="actionBusy" @click="queueMoveOrder">Queue Move</button>
|
|
</div>
|
|
<div class="entity-inspector-inline-form">
|
|
<label class="entity-inspector-field entity-inspector-field--grow">
|
|
<span>Mine Resource System</span>
|
|
<select v-model="mineOrderForm.systemId">
|
|
<option value="">Current system</option>
|
|
<option v-for="system in gmStore.systems" :key="system.id" :value="system.id">{{ system.label }} ({{ system.id }})</option>
|
|
</select>
|
|
</label>
|
|
<label class="entity-inspector-field entity-inspector-field--grow">
|
|
<span>Ware</span>
|
|
<select v-model="mineOrderForm.itemId">
|
|
<option v-for="itemId in commonMiningItems" :key="itemId" :value="itemId">{{ itemId }}</option>
|
|
</select>
|
|
</label>
|
|
<button type="button" class="entity-inspector-panel__action" :disabled="actionBusy" @click="queueMineResourceOrder">Queue Mine</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="actionStatus" class="entity-inspector-message entity-inspector-message--ok">{{ actionStatus }}</div>
|
|
<div v-if="actionError" class="entity-inspector-message entity-inspector-message--error">{{ actionError }}</div>
|
|
<div v-if="directOrderRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Order</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Target</th>
|
|
<th scope="col">Detail</th>
|
|
<th v-if="canDirectControlSelectedShip" scope="col" class="entity-inspector-table__action-col">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="order in directOrderRows" :key="order.id">
|
|
<td>{{ order.label }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td>{{ order.target }}</td>
|
|
<td class="entity-inspector-table__detail">{{ order.detail }}</td>
|
|
<td v-if="canDirectControlSelectedShip" class="entity-inspector-table__action-col">
|
|
<button
|
|
type="button"
|
|
class="entity-inspector-order-remove"
|
|
:disabled="actionBusy"
|
|
@click="removeOrder(order.id)"
|
|
>
|
|
Remove
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No direct orders queued.</div>
|
|
<div class="entity-inspector-divider">
|
|
<span>Behavior: {{ getShipBehaviorLabel(selectedShip.defaultBehavior.kind) }}</span>
|
|
</div>
|
|
<div v-if="behaviorOrderRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Order</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Target</th>
|
|
<th scope="col">Detail</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="order in behaviorOrderRows" :key="order.id">
|
|
<td>{{ order.label }}</td>
|
|
<td>{{ order.status }}</td>
|
|
<td>{{ order.target }}</td>
|
|
<td class="entity-inspector-table__detail">{{ order.detail }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No behavior orders queued.</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Plan Steps</h4>
|
|
<div v-if="shipPlanRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Scope</th>
|
|
<th scope="col">Activity</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Detail</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in shipPlanRows" :key="row.id" :class="row.isSubTask ? 'entity-inspector-table__row--subtask' : ''">
|
|
<td>{{ row.scope }}</td>
|
|
<td :class="row.isSubTask ? 'entity-inspector-table__subtask' : ''">{{ row.activity }}</td>
|
|
<td>{{ row.status }}</td>
|
|
<td class="entity-inspector-table__detail">{{ row.detail }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No active plan.</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else-if="selectedStation">
|
|
<header class="entity-inspector-panel__header">
|
|
<div>
|
|
<div class="entity-inspector-panel__kicker">Station Inspector</div>
|
|
<h3>{{ selectedStation.label }}</h3>
|
|
<p>{{ factionLabelById.get(selectedStation.factionId) ?? selectedStation.factionId }} · {{ selectedStation.systemId }}</p>
|
|
</div>
|
|
<div class="entity-inspector-panel__actions">
|
|
<button type="button" class="entity-inspector-panel__action" @click="focusStation">Focus</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Status</h4>
|
|
<div class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table entity-inspector-table--kv">
|
|
<tbody>
|
|
<tr v-for="row in stationStatusRows" :key="row.label">
|
|
<th scope="row">{{ row.label }}</th>
|
|
<td>{{ row.value }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Modules</h4>
|
|
<div v-if="stationModuleRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Module</th>
|
|
<th scope="col">Id</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in stationModuleRows" :key="row.key">
|
|
<td>{{ row.module }}</td>
|
|
<td>{{ row.moduleId }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No modules installed.</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Storage</h4>
|
|
<div v-if="stationStorageRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Class</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Used</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Capacity</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Fill</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in stationStorageRows" :key="row.key">
|
|
<td>{{ row.storageClass }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.used }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.capacity }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.fill }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="stationInventoryRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Ware</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in stationInventoryRows" :key="row.key">
|
|
<td>{{ row.ware }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.amount }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else-if="stationStorageRows.length === 0" class="entity-inspector-empty">No inventory.</div>
|
|
</div>
|
|
|
|
<div class="entity-inspector-section">
|
|
<h4>Production</h4>
|
|
<div v-if="stationProcessRows.length > 0" class="entity-inspector-table-wrap">
|
|
<table class="entity-inspector-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Lane</th>
|
|
<th scope="col">Process</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Progress</th>
|
|
<th scope="col" class="entity-inspector-table__numeric">Timing</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in stationProcessRows" :key="row.key">
|
|
<td>{{ row.lane }}</td>
|
|
<td>{{ row.label }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.progress }}</td>
|
|
<td class="entity-inspector-table__numeric">{{ row.timing }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-else class="entity-inspector-empty">No active processes.</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<header class="entity-inspector-panel__header">
|
|
<div>
|
|
<div class="entity-inspector-panel__kicker">Inspector</div>
|
|
<h3>{{ props.fallbackTitle }}</h3>
|
|
</div>
|
|
</header>
|
|
<div class="entity-inspector-panel__fallback" v-html="props.fallbackHtml" />
|
|
</template>
|
|
</section>
|
|
</template>
|