Refactor runtime bootstrap and ship control flows
This commit is contained in:
594
apps/viewer/src/components/ViewerEntityInspectorPanel.vue
Normal file
594
apps/viewer/src/components/ViewerEntityInspectorPanel.vue
Normal file
@@ -0,0 +1,594 @@
|
||||
<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);
|
||||
}
|
||||
|
||||
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 canAccessGm = computed(() => authStore.canAccessGm);
|
||||
|
||||
const canDirectControlSelectedShip = computed(() =>
|
||||
!!selectedShip.value && (canAccessGm.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),
|
||||
);
|
||||
|
||||
watch(selectedShip, (ship) => {
|
||||
if (!ship) {
|
||||
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')">Follow</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Status</h4>
|
||||
<div class="entity-inspector-grid">
|
||||
<div><span>State</span><strong>{{ titleCase(selectedShip.state) }}</strong></div>
|
||||
<div><span>Behavior</span><strong>{{ getShipBehaviorLabel(selectedShip.defaultBehavior.kind) }}</strong></div>
|
||||
<div><span>Control</span><strong>{{ selectedShip.controlSourceKind }}</strong></div>
|
||||
<div><span>Assignment</span><strong>{{ selectedShip.assignment?.kind ?? "unassigned" }}</strong></div>
|
||||
<div><span>Plan</span><strong>{{ selectedShip.activePlan ? `${selectedShip.activePlan.kind} · ${selectedShip.activePlan.status}` : "none" }}</strong></div>
|
||||
<div><span>Failure</span><strong>{{ selectedShip.lastAccessFailureReason ?? "none" }}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Cargo</h4>
|
||||
<div class="entity-inspector-grid">
|
||||
<div><span>Used</span><strong>{{ formatAmount(selectedShip.inventory.reduce((sum, entry) => sum + entry.amount, 0)) }}</strong></div>
|
||||
<div><span>Capacity</span><strong>{{ formatAmount(selectedShip.cargoCapacity) }}</strong></div>
|
||||
<div><span>Travel</span><strong>{{ formatAmount(selectedShip.travelSpeed) }} {{ selectedShip.travelSpeedUnit }}</strong></div>
|
||||
<div><span>Hull</span><strong>{{ formatAmount(selectedShip.health) }}</strong></div>
|
||||
</div>
|
||||
<ul v-if="selectedShip.inventory.length > 0" class="entity-inspector-list">
|
||||
<li v-for="entry in selectedShip.inventory" :key="entry.itemId">
|
||||
<span>{{ entry.itemId }}</span>
|
||||
<strong>{{ formatAmount(entry.amount) }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<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-grid">
|
||||
<div><span>Area</span><strong>{{ selectedShip.defaultBehavior.areaSystemId ?? "none" }}</strong></div>
|
||||
<div><span>Item</span><strong>{{ selectedShip.defaultBehavior.itemId ?? "none" }}</strong></div>
|
||||
<div><span>Home Station</span><strong>{{ selectedShip.defaultBehavior.homeStationId ?? "none" }}</strong></div>
|
||||
<div><span>Target</span><strong>{{ selectedShip.defaultBehavior.targetEntityId ?? "none" }}</strong></div>
|
||||
<div><span>Range</span><strong>{{ selectedShip.defaultBehavior.maxSystemRange }}</strong></div>
|
||||
<div><span>Known Only</span><strong>{{ selectedShip.defaultBehavior.knownStationsOnly ? "yes" : "no" }}</strong></div>
|
||||
</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>
|
||||
<ul v-if="directOrders.length > 0" class="entity-inspector-list">
|
||||
<li v-for="order in directOrders" :key="order.id">
|
||||
<span>{{ getShipOrderLabel(order.kind) }} · {{ order.status }}</span>
|
||||
<div class="entity-inspector-order-actions">
|
||||
<strong>{{ order.itemId ?? order.targetEntityId ?? order.targetSystemId ?? "—" }}</strong>
|
||||
<button
|
||||
v-if="canDirectControlSelectedShip"
|
||||
type="button"
|
||||
class="entity-inspector-order-remove"
|
||||
:disabled="actionBusy"
|
||||
@click="removeOrder(order.id)"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
<ul v-if="behaviorOrders.length > 0" class="entity-inspector-list">
|
||||
<li v-for="order in behaviorOrders" :key="order.id">
|
||||
<span>{{ getShipOrderLabel(order.kind) }} · {{ order.status }}</span>
|
||||
<strong>{{ [order.itemId ?? order.targetEntityId ?? order.targetSystemId ?? "—", getShipOrderSupportStatusLabel(order.kind), getShipOrderNotes(order.kind)].filter(Boolean).join(" · ") }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="entity-inspector-empty">No behavior orders queued.</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Plan Steps</h4>
|
||||
<ul v-if="selectedShip.activePlan" class="entity-inspector-plan">
|
||||
<li v-for="step in selectedShip.activePlan.steps" :key="step.id">
|
||||
<div class="entity-inspector-plan__step">
|
||||
<span>{{ step.kind }} · {{ step.status }}</span>
|
||||
<strong>{{ step.blockingReason ?? "ok" }}</strong>
|
||||
</div>
|
||||
<ul class="entity-inspector-subtasks">
|
||||
<li v-for="subTask in step.subTasks" :key="subTask.id">
|
||||
<span>{{ subTask.kind }} · {{ subTask.status }}</span>
|
||||
<strong>{{ subTask.blockingReason ?? `${Math.round(subTask.progress * 100)}%` }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<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-grid">
|
||||
<div><span>Category</span><strong>{{ titleCase(selectedStation.category) }}</strong></div>
|
||||
<div><span>Objective</span><strong>{{ titleCase(selectedStation.objective) }}</strong></div>
|
||||
<div><span>Docked</span><strong>{{ selectedStation.dockedShips }} / {{ selectedStation.dockingPads }}</strong></div>
|
||||
<div><span>Population</span><strong>{{ formatAmount(selectedStation.population) }} / {{ formatAmount(selectedStation.populationCapacity) }}</strong></div>
|
||||
<div><span>Workforce</span><strong>{{ formatAmount(selectedStation.workforceRequired) }}</strong></div>
|
||||
<div><span>Efficiency</span><strong>{{ Math.round(selectedStation.workforceEffectiveRatio * 100) }}%</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Modules</h4>
|
||||
<ul v-if="selectedStation.installedModules.length > 0" class="entity-inspector-list">
|
||||
<li v-for="moduleId in selectedStation.installedModules" :key="moduleId">
|
||||
<span>{{ moduleNameById.get(moduleId) ?? moduleId }}</span>
|
||||
<strong>{{ moduleId }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="entity-inspector-empty">No modules installed.</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Storage</h4>
|
||||
<ul v-if="selectedStation.inventory.length > 0" class="entity-inspector-list">
|
||||
<li v-for="entry in selectedStation.inventory" :key="entry.itemId">
|
||||
<span>{{ entry.itemId }}</span>
|
||||
<strong>{{ formatAmount(entry.amount) }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="entity-inspector-empty">No inventory.</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Production</h4>
|
||||
<ul v-if="selectedStation.currentProcesses.length > 0" class="entity-inspector-list">
|
||||
<li v-for="process in selectedStation.currentProcesses" :key="`${process.lane}-${process.label}`">
|
||||
<span>{{ process.label }}</span>
|
||||
<strong>{{ Math.round(process.progress * 100) }}% · {{ Math.ceil(process.timeRemainingSeconds) }}s</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
Reference in New Issue
Block a user