321 lines
9.4 KiB
Vue
321 lines
9.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
import { storeToRefs } from "pinia";
|
|
import { enqueueShipOrder } from "../api";
|
|
import { getShipOrderLabel, getShipOrderNotes, getShipOrderSupportStatusLabel } from "../shipAutomationPresentation";
|
|
import { useAuthStore } from "../ui/stores/authStore";
|
|
import { useGmStore } from "../ui/stores/gmStore";
|
|
import { usePlayerFactionStore } from "../ui/stores/playerFactionStore";
|
|
import { useShipAutomationCatalogStore } from "../ui/stores/shipAutomationCatalogStore";
|
|
import { useViewerOrderContextMenuStore } from "../ui/stores/viewerOrderContextMenu";
|
|
import { useViewerSelectionStore } from "../ui/stores/viewerSelection";
|
|
|
|
type MenuAction =
|
|
| "mine-resource"
|
|
| "fly-to-and-wait"
|
|
| "follow"
|
|
| "attack";
|
|
|
|
interface OrderMenuActionEntry {
|
|
key: MenuAction;
|
|
orderKind: string;
|
|
label: string;
|
|
detail?: string;
|
|
supportStatus?: string | null;
|
|
notes?: string | null;
|
|
}
|
|
|
|
const rootEl = ref<HTMLElement | null>(null);
|
|
const authStore = useAuthStore();
|
|
const gmStore = useGmStore();
|
|
const playerStore = usePlayerFactionStore();
|
|
const automationStore = useShipAutomationCatalogStore();
|
|
const selectionStore = useViewerSelectionStore();
|
|
const orderMenuStore = useViewerOrderContextMenuStore();
|
|
|
|
const { playerFaction } = storeToRefs(playerStore);
|
|
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
|
const { isOpen, x, y, target } = storeToRefs(orderMenuStore);
|
|
|
|
const selectedShip = computed(() => {
|
|
if (selectedEntityKind.value !== "ship" || !selectedEntityId.value) {
|
|
return null;
|
|
}
|
|
|
|
return gmStore.ships.find((ship) => ship.id === selectedEntityId.value) ?? null;
|
|
});
|
|
|
|
const canControlSelectedShip = computed(() => {
|
|
if (!selectedShip.value) {
|
|
return false;
|
|
}
|
|
|
|
if (authStore.canAccessGm && !authStore.isActingAsAlternateIdentity) {
|
|
return true;
|
|
}
|
|
|
|
if (!playerFaction.value) {
|
|
return false;
|
|
}
|
|
|
|
return playerFaction.value.assetRegistry.shipIds.includes(selectedShip.value.id);
|
|
});
|
|
|
|
function emptyActions(): OrderMenuActionEntry[] {
|
|
return [];
|
|
}
|
|
|
|
const actions = computed<OrderMenuActionEntry[]>(() => {
|
|
if (!target.value || !selectedShip.value || !canControlSelectedShip.value) {
|
|
return emptyActions();
|
|
}
|
|
|
|
switch (target.value.selection.kind) {
|
|
case "node":
|
|
return [{
|
|
key: "mine-resource",
|
|
orderKind: "mine-and-deliver",
|
|
label: getShipOrderLabel("mine-and-deliver"),
|
|
detail: `${target.value.itemId ?? "resource"} · ${target.value.systemId ?? selectedShip.value.systemId}`,
|
|
supportStatus: getShipOrderSupportStatusLabel("mine-and-deliver"),
|
|
notes: getShipOrderNotes("mine-and-deliver"),
|
|
}];
|
|
case "ship":
|
|
if (target.value.selection.id === selectedShip.value.id) {
|
|
return emptyActions();
|
|
}
|
|
return [
|
|
{
|
|
key: "follow",
|
|
orderKind: "follow-ship",
|
|
label: getShipOrderLabel("follow-ship"),
|
|
detail: target.value.label,
|
|
supportStatus: getShipOrderSupportStatusLabel("follow-ship"),
|
|
notes: getShipOrderNotes("follow-ship"),
|
|
},
|
|
{
|
|
key: "attack",
|
|
orderKind: "attack-target",
|
|
label: getShipOrderLabel("attack-target"),
|
|
detail: target.value.label,
|
|
supportStatus: getShipOrderSupportStatusLabel("attack-target"),
|
|
notes: getShipOrderNotes("attack-target"),
|
|
},
|
|
];
|
|
case "station":
|
|
case "celestial":
|
|
case "construction-site":
|
|
return [{
|
|
key: "fly-to-and-wait",
|
|
orderKind: "fly-and-wait",
|
|
label: getShipOrderLabel("fly-and-wait"),
|
|
detail: target.value.label,
|
|
supportStatus: getShipOrderSupportStatusLabel("fly-and-wait"),
|
|
notes: getShipOrderNotes("fly-and-wait"),
|
|
}];
|
|
case "system":
|
|
return emptyActions();
|
|
default:
|
|
return emptyActions();
|
|
}
|
|
});
|
|
|
|
const menuStyle = computed(() => ({
|
|
left: `${Math.min(x.value, window.innerWidth - 280)}px`,
|
|
top: `${Math.min(y.value, window.innerHeight - 240)}px`,
|
|
}));
|
|
|
|
watch([selectedEntityKind, selectedEntityId], () => {
|
|
if (isOpen.value && selectedEntityKind.value !== "ship") {
|
|
orderMenuStore.close();
|
|
}
|
|
});
|
|
|
|
function closeMenu() {
|
|
orderMenuStore.close();
|
|
}
|
|
|
|
async function runAction(action: MenuAction) {
|
|
if (!selectedShip.value || !target.value || !canControlSelectedShip.value) {
|
|
return;
|
|
}
|
|
|
|
if (action === "mine-resource") {
|
|
const itemId = target.value.itemId;
|
|
if (!itemId) {
|
|
return;
|
|
}
|
|
|
|
const ship = await enqueueShipOrder(selectedShip.value.id, {
|
|
kind: "mine-and-deliver",
|
|
priority: 100,
|
|
interruptCurrentPlan: true,
|
|
label: `Mine ${itemId} in ${target.value.systemId ?? selectedShip.value.systemId}`,
|
|
targetEntityId: null,
|
|
targetSystemId: target.value.systemId ?? selectedShip.value.systemId,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId,
|
|
anchorId: target.value.anchorId ?? null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 0,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
closeMenu();
|
|
return;
|
|
}
|
|
|
|
if (action === "fly-to-and-wait") {
|
|
const ship = await enqueueShipOrder(selectedShip.value.id, {
|
|
kind: "fly-and-wait",
|
|
priority: 100,
|
|
interruptCurrentPlan: true,
|
|
label: `Fly to ${target.value.label}`,
|
|
targetEntityId: null,
|
|
targetSystemId: target.value.systemId ?? selectedShip.value.systemId,
|
|
targetPosition: target.value.targetPosition ?? null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId: null,
|
|
anchorId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 8,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
closeMenu();
|
|
return;
|
|
}
|
|
|
|
if (action === "follow") {
|
|
const ship = await enqueueShipOrder(selectedShip.value.id, {
|
|
kind: "follow-ship",
|
|
priority: 100,
|
|
interruptCurrentPlan: true,
|
|
label: `Follow ${target.value.label}`,
|
|
targetEntityId: target.value.selection.kind === "ship" ? target.value.selection.id : null,
|
|
targetSystemId: target.value.systemId ?? null,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId: null,
|
|
anchorId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 6,
|
|
radius: 22,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
closeMenu();
|
|
return;
|
|
}
|
|
|
|
if (action === "attack") {
|
|
const ship = await enqueueShipOrder(selectedShip.value.id, {
|
|
kind: "attack-target",
|
|
priority: 100,
|
|
interruptCurrentPlan: true,
|
|
label: `Attack ${target.value.label}`,
|
|
targetEntityId: target.value.selection.kind === "ship" ? target.value.selection.id : null,
|
|
targetSystemId: target.value.systemId ?? null,
|
|
targetPosition: null,
|
|
sourceStationId: null,
|
|
destinationStationId: null,
|
|
itemId: null,
|
|
anchorId: null,
|
|
constructionSiteId: null,
|
|
moduleId: null,
|
|
waitSeconds: 0,
|
|
radius: 0,
|
|
maxSystemRange: 0,
|
|
knownStationsOnly: false,
|
|
});
|
|
gmStore.upsertShip(ship);
|
|
closeMenu();
|
|
}
|
|
}
|
|
|
|
function onActionClick(action: { key: MenuAction }) {
|
|
void runAction(action.key);
|
|
}
|
|
|
|
function onWindowPointerDown(event: PointerEvent) {
|
|
if (!isOpen.value) {
|
|
return;
|
|
}
|
|
|
|
if (rootEl.value?.contains(event.target as Node)) {
|
|
return;
|
|
}
|
|
|
|
closeMenu();
|
|
}
|
|
|
|
function onWindowKeyDown(event: KeyboardEvent) {
|
|
if (event.key === "Escape") {
|
|
closeMenu();
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void automationStore.load();
|
|
window.addEventListener("pointerdown", onWindowPointerDown, true);
|
|
window.addEventListener("keydown", onWindowKeyDown);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener("pointerdown", onWindowPointerDown, true);
|
|
window.removeEventListener("keydown", onWindowKeyDown);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
v-if="isOpen && target"
|
|
ref="rootEl"
|
|
class="viewer-order-context-menu pointer-events-auto"
|
|
:style="menuStyle"
|
|
@pointerdown.stop
|
|
@contextmenu.prevent.stop
|
|
>
|
|
<header class="viewer-order-context-menu__header">
|
|
<div class="viewer-order-context-menu__kicker">Direct Order</div>
|
|
<h4>{{ target.label }}</h4>
|
|
<p v-if="selectedShip">Selected ship: {{ selectedShip.name }}</p>
|
|
</header>
|
|
|
|
<div v-if="!selectedShip" class="viewer-order-context-menu__empty">
|
|
Select a ship first.
|
|
</div>
|
|
<div v-else-if="!canControlSelectedShip" class="viewer-order-context-menu__empty">
|
|
Direct orders are only available for player-owned ships or GM users.
|
|
</div>
|
|
<div v-else-if="actions.length === 0" class="viewer-order-context-menu__empty">
|
|
No direct actions for this target yet.
|
|
</div>
|
|
<div v-else class="viewer-order-context-menu__actions">
|
|
<button
|
|
v-for="action in actions"
|
|
:key="action.key"
|
|
type="button"
|
|
class="viewer-order-context-menu__action"
|
|
@click="onActionClick(action)"
|
|
>
|
|
<span>{{ action.label }}</span>
|
|
<strong v-if="action.detail">{{ action.detail }}</strong>
|
|
<small v-if="action.supportStatus || action.notes">{{ [action.supportStatus, action.notes].filter(Boolean).join(" · ") }}</small>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|