Files
space-game/apps/viewer/src/components/gm/GmPlayerFactionPanel.vue

1156 lines
48 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { storeToRefs } from "pinia";
import {
createPlayerOrganization,
deletePlayerDirective,
deletePlayerOrganization,
enqueueShipOrder,
fetchPlayerFaction,
updatePlayerStrategicIntent,
updateShipDefaultBehavior,
upsertPlayerAssignment,
upsertPlayerAutomationPolicy,
upsertPlayerDirective,
upsertPlayerPolicy,
} from "../../api";
import { getShipBehaviorLabel, getShipOrderLabel } from "../../shipAutomationPresentation";
import type { PlayerFactionSnapshot } from "../../contractsPlayerFaction";
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";
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 { session } = storeToRefs(authStore);
const statusMessage = ref("");
const errorMessage = ref("");
const busy = ref(false);
const player = computed(() => playerFaction.value);
const selectedShip = computed(() => {
if (selectedEntityKind.value !== "ship" || !selectedEntityId.value || !player.value?.assetRegistry.shipIds.includes(selectedEntityId.value)) {
return null;
}
return gmStore.ships.find((ship) => ship.id === selectedEntityId.value) ?? null;
});
const selectedStation = computed(() => {
if (selectedEntityKind.value !== "station" || !selectedEntityId.value || !player.value?.assetRegistry.stationIds.includes(selectedEntityId.value)) {
return null;
}
return gmStore.stations.find((station) => station.id === selectedEntityId.value) ?? null;
});
const selectedAssignment = computed(() => {
const assetKind = selectedShip.value ? "ship" : selectedStation.value ? "station" : null;
const assetId = selectedShip.value?.id ?? selectedStation.value?.id;
if (!assetKind || !assetId || !player.value) {
return null;
}
return player.value.assignments.find((assignment) => assignment.assetKind === assetKind && assignment.assetId === assetId) ?? null;
});
const organizations = computed(() => {
if (!player.value) {
return [];
}
return [
...player.value.fleets.map((entry) => ({ kind: "fleet", id: entry.id, label: entry.label, summary: `${entry.role} · ${entry.assetIds.length} ships`, updatedAtUtc: entry.updatedAtUtc })),
...player.value.taskForces.map((entry) => ({ kind: "task-force", id: entry.id, label: entry.label, summary: `${entry.role} · ${entry.assetIds.length} ships`, updatedAtUtc: entry.updatedAtUtc })),
...player.value.stationGroups.map((entry) => ({ kind: "station-group", id: entry.id, label: entry.label, summary: `${entry.role} · ${entry.stationIds.length} stations`, updatedAtUtc: entry.updatedAtUtc })),
...player.value.economicRegions.map((entry) => ({ kind: "economic-region", id: entry.id, label: entry.label, summary: `${entry.role} · ${entry.systemIds.length} systems`, updatedAtUtc: entry.updatedAtUtc })),
...player.value.fronts.map((entry) => ({ kind: "front", id: entry.id, label: entry.label, summary: `${entry.posture} · P${Math.round(entry.priority)}`, updatedAtUtc: entry.updatedAtUtc })),
...player.value.reserves.map((entry) => ({ kind: "reserve", id: entry.id, label: entry.label, summary: `${entry.reserveKind} · ${entry.assetIds.length} ships`, updatedAtUtc: entry.updatedAtUtc })),
].sort((left, right) => left.kind.localeCompare(right.kind) || left.label.localeCompare(right.label));
});
const selectedOrganizationDetails = computed(() => {
if (!player.value || !playerStore.selectedOrganizationId) {
return null;
}
const id = playerStore.selectedOrganizationId;
return player.value.fleets.find((entry) => entry.id === id)
?? player.value.taskForces.find((entry) => entry.id === id)
?? player.value.stationGroups.find((entry) => entry.id === id)
?? player.value.economicRegions.find((entry) => entry.id === id)
?? player.value.fronts.find((entry) => entry.id === id)
?? player.value.reserves.find((entry) => entry.id === id)
?? null;
});
const selectedOrganizationSummary = computed(() => {
const entry = selectedOrganizationDetails.value;
if (!entry) {
return [];
}
const lines: string[] = [];
if ("role" in entry) {
lines.push(`Role ${entry.role}`);
}
if ("posture" in entry) {
lines.push(`Posture ${entry.posture}`);
}
if ("commanderId" in entry && entry.commanderId) {
lines.push(`Commander ${entry.commanderId}`);
}
if ("frontId" in entry && entry.frontId) {
lines.push(`Front ${entry.frontId}`);
}
if ("fleetId" in entry && entry.fleetId) {
lines.push(`Fleet ${entry.fleetId}`);
}
if ("economicRegionId" in entry && entry.economicRegionId) {
lines.push(`Region ${entry.economicRegionId}`);
}
if ("sharedEconomicRegionId" in entry && entry.sharedEconomicRegionId) {
lines.push(`Shared Region ${entry.sharedEconomicRegionId}`);
}
if ("sharedFrontLineId" in entry && entry.sharedFrontLineId) {
lines.push(`Shared Front ${entry.sharedFrontLineId}`);
}
if ("homeSystemId" in entry && entry.homeSystemId) {
lines.push(`Home ${entry.homeSystemId}`);
}
if ("assetIds" in entry) {
lines.push(`Assets ${entry.assetIds.length}`);
}
if ("stationIds" in entry) {
lines.push(`Stations ${entry.stationIds.length}`);
}
if ("systemIds" in entry) {
lines.push(`Systems ${entry.systemIds.length}`);
}
if ("directiveIds" in entry) {
lines.push(`Directives ${entry.directiveIds.length}`);
}
return lines;
});
const behaviorOptions = computed(() =>
(automationStore.catalog?.behaviors ?? [])
.filter((entry) => entry.supportStatus !== "InternalOnly")
.map((entry) => entry.id),
);
onMounted(async () => {
await automationStore.load();
await loadPlayerSnapshotIfNeeded();
});
const orderOptions = computed(() =>
(automationStore.catalog?.orders ?? [])
.filter((entry) => entry.supportStatus !== "InternalOnly")
.map((entry) => entry.id),
);
const orgForm = reactive({
kind: "fleet",
label: "",
parentOrganizationId: "",
frontId: "",
homeSystemId: "",
homeStationId: "",
policyId: "",
automationPolicyId: "",
reinforcementPolicyId: "",
targetFactionId: "",
priority: 50,
role: "",
reserveKind: "military",
notes: "",
});
const directiveForm = reactive({
label: "",
kind: "delegated-control",
scopeKind: "asset",
scopeId: "",
behaviorKind: "hold-position",
useOrders: false,
stagingOrderKind: "",
targetEntityId: "",
targetSystemId: "",
itemId: "",
priority: 60,
radius: 24,
waitSeconds: 3,
maxSystemRange: 0,
knownStationsOnly: false,
policyId: "",
automationPolicyId: "",
notes: "",
});
const assignmentForm = reactive({
fleetId: "",
taskForceId: "",
stationGroupId: "",
economicRegionId: "",
frontId: "",
reserveId: "",
directiveId: "",
policyId: "",
automationPolicyId: "",
role: "line",
});
const strategyForm = reactive({
strategicPosture: "balanced",
economicPosture: "delegated",
militaryPosture: "layered-defense",
logisticsPosture: "stable",
desiredReserveRatio: 0.2,
allowDelegatedCombatAutomation: true,
allowDelegatedEconomicAutomation: true,
notes: "",
});
const policyForm = reactive({
label: "Core Empire Policy",
allowDelegatedCombat: true,
allowDelegatedTrade: true,
reserveCreditsRatio: 0.2,
reserveMilitaryRatio: 0.2,
tradeAccessPolicy: "owner-and-allies",
dockingAccessPolicy: "owner-and-allies",
constructionAccessPolicy: "owner-only",
operationalRangePolicy: "unrestricted",
combatEngagementPolicy: "defensive",
avoidHostileSystems: true,
fleeHullRatio: 0.35,
blacklistedSystemIds: "",
notes: "",
});
const automationForm = reactive({
label: "Core Automation",
enabled: true,
behaviorKind: "idle",
useOrders: false,
stagingOrderKind: "",
maxSystemRange: 0,
knownStationsOnly: false,
radius: 24,
waitSeconds: 3,
preferredItemId: "",
notes: "",
});
const behaviorForm = reactive({
kind: "hold-position",
homeSystemId: "",
areaSystemId: "",
targetEntityId: "",
itemId: "",
preferredAnchorId: "",
preferredConstructionSiteId: "",
preferredModuleId: "",
waitSeconds: 3,
radius: 18,
maxSystemRange: 0,
knownStationsOnly: false,
});
const orderForm = reactive({
kind: "hold-position",
priority: 100,
interruptCurrentPlan: true,
label: "",
targetEntityId: "",
targetSystemId: "",
itemId: "",
anchorId: "",
constructionSiteId: "",
moduleId: "",
waitSeconds: 3,
radius: 18,
maxSystemRange: 0,
knownStationsOnly: false,
});
watch(player, (value) => {
if (!value) {
return;
}
strategyForm.strategicPosture = value.strategicIntent.strategicPosture;
strategyForm.economicPosture = value.strategicIntent.economicPosture;
strategyForm.militaryPosture = value.strategicIntent.militaryPosture;
strategyForm.logisticsPosture = value.strategicIntent.logisticsPosture;
strategyForm.desiredReserveRatio = value.strategicIntent.desiredReserveRatio;
strategyForm.allowDelegatedCombatAutomation = value.strategicIntent.allowDelegatedCombatAutomation;
strategyForm.allowDelegatedEconomicAutomation = value.strategicIntent.allowDelegatedEconomicAutomation;
strategyForm.notes = value.strategicIntent.notes ?? "";
const corePolicy = value.policies.find((entry) => entry.id === "player-core-policy") ?? value.policies[0];
if (corePolicy) {
policyForm.label = corePolicy.label;
policyForm.allowDelegatedCombat = corePolicy.allowDelegatedCombat;
policyForm.allowDelegatedTrade = corePolicy.allowDelegatedTrade;
policyForm.reserveCreditsRatio = corePolicy.reserveCreditsRatio;
policyForm.reserveMilitaryRatio = corePolicy.reserveMilitaryRatio;
policyForm.tradeAccessPolicy = corePolicy.tradeAccessPolicy;
policyForm.dockingAccessPolicy = corePolicy.dockingAccessPolicy;
policyForm.constructionAccessPolicy = corePolicy.constructionAccessPolicy;
policyForm.operationalRangePolicy = corePolicy.operationalRangePolicy;
policyForm.combatEngagementPolicy = corePolicy.combatEngagementPolicy;
policyForm.avoidHostileSystems = corePolicy.avoidHostileSystems;
policyForm.fleeHullRatio = corePolicy.fleeHullRatio;
policyForm.blacklistedSystemIds = corePolicy.blacklistedSystemIds.join(", ");
policyForm.notes = corePolicy.notes ?? "";
}
const coreAutomation = value.automationPolicies.find((entry) => entry.id === "player-core-automation") ?? value.automationPolicies[0];
if (coreAutomation) {
automationForm.label = coreAutomation.label;
automationForm.enabled = coreAutomation.enabled;
automationForm.behaviorKind = coreAutomation.behaviorKind;
automationForm.useOrders = coreAutomation.useOrders;
automationForm.stagingOrderKind = coreAutomation.stagingOrderKind ?? "";
automationForm.maxSystemRange = coreAutomation.maxSystemRange;
automationForm.knownStationsOnly = coreAutomation.knownStationsOnly;
automationForm.radius = coreAutomation.radius;
automationForm.waitSeconds = coreAutomation.waitSeconds;
automationForm.preferredItemId = coreAutomation.preferredItemId ?? "";
automationForm.notes = coreAutomation.notes ?? "";
}
}, { immediate: true });
watch(session, async (value) => {
if (!value) {
playerStore.setPlayerFaction(null);
return;
}
await loadPlayerSnapshotIfNeeded();
});
watch(selectedShip, (ship) => {
if (!ship) {
return;
}
behaviorForm.kind = ship.defaultBehavior.kind;
behaviorForm.homeSystemId = ship.defaultBehavior.homeSystemId ?? ship.systemId;
behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId;
behaviorForm.targetEntityId = ship.defaultBehavior.targetEntityId ?? "";
behaviorForm.itemId = ship.defaultBehavior.itemId ?? "";
behaviorForm.preferredAnchorId = ship.defaultBehavior.preferredAnchorId ?? "";
behaviorForm.preferredConstructionSiteId = ship.defaultBehavior.preferredConstructionSiteId ?? "";
behaviorForm.preferredModuleId = ship.defaultBehavior.preferredModuleId ?? "";
behaviorForm.waitSeconds = ship.defaultBehavior.waitSeconds;
behaviorForm.radius = ship.defaultBehavior.radius;
behaviorForm.maxSystemRange = ship.defaultBehavior.maxSystemRange;
behaviorForm.knownStationsOnly = ship.defaultBehavior.knownStationsOnly;
if (!directiveForm.scopeId) {
directiveForm.scopeId = ship.id;
}
if (directiveForm.scopeKind === "asset") {
directiveForm.scopeKind = "ship";
}
}, { immediate: true });
watch(selectedStation, (station) => {
if (!station) {
return;
}
if (!directiveForm.scopeId) {
directiveForm.scopeId = station.id;
}
if (directiveForm.scopeKind === "asset") {
directiveForm.scopeKind = "station";
}
}, { immediate: true });
watch(selectedAssignment, (assignment) => {
assignmentForm.fleetId = assignment?.fleetId ?? "";
assignmentForm.taskForceId = assignment?.taskForceId ?? "";
assignmentForm.stationGroupId = assignment?.stationGroupId ?? "";
assignmentForm.economicRegionId = assignment?.economicRegionId ?? "";
assignmentForm.frontId = assignment?.frontId ?? "";
assignmentForm.reserveId = assignment?.reserveId ?? "";
assignmentForm.directiveId = assignment?.directiveId ?? "";
assignmentForm.policyId = assignment?.policyId ?? "";
assignmentForm.automationPolicyId = assignment?.automationPolicyId ?? "";
assignmentForm.role = assignment?.role ?? "line";
}, { immediate: true });
function titleCase(value: string | null | undefined) {
if (!value) {
return "—";
}
return value
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (character) => character.toUpperCase());
}
function formatDate(value: string | null | undefined) {
if (!value) {
return "—";
}
return new Date(value).toLocaleString();
}
function setPlayerSnapshot(snapshot: PlayerFactionSnapshot) {
playerStore.setPlayerFaction(snapshot);
}
async function loadPlayerSnapshotIfNeeded() {
if (!authStore.isAuthenticated || playerStore.playerFaction) {
return;
}
try {
setPlayerSnapshot(await fetchPlayerFaction());
} catch {
// Player domain may not exist yet for the current world; leave the panel empty.
}
}
async function runAction(action: () => Promise<void>, successMessage: string) {
busy.value = true;
errorMessage.value = "";
statusMessage.value = "";
try {
await action();
statusMessage.value = successMessage;
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : "Request failed.";
} finally {
busy.value = false;
}
}
async function submitOrganization() {
await runAction(async () => {
const snapshot = await createPlayerOrganization({
kind: orgForm.kind,
label: orgForm.label,
parentOrganizationId: orgForm.parentOrganizationId || null,
frontId: orgForm.frontId || null,
homeSystemId: orgForm.homeSystemId || null,
homeStationId: orgForm.homeStationId || null,
policyId: orgForm.policyId || null,
automationPolicyId: orgForm.automationPolicyId || null,
reinforcementPolicyId: orgForm.reinforcementPolicyId || null,
targetFactionId: orgForm.targetFactionId || null,
priority: Number.isFinite(orgForm.priority) ? orgForm.priority : null,
role: orgForm.role || null,
reserveKind: orgForm.reserveKind || null,
systemIds: [],
focusItemIds: [],
notes: orgForm.notes || null,
});
setPlayerSnapshot(snapshot);
orgForm.label = "";
orgForm.role = "";
orgForm.notes = "";
}, "Organization created.");
}
async function removeOrganization(organizationId: string) {
await runAction(async () => {
setPlayerSnapshot(await deletePlayerOrganization(organizationId));
}, "Organization removed.");
}
async function submitDirective() {
await runAction(async () => {
const snapshot = await upsertPlayerDirective({
label: directiveForm.label,
kind: directiveForm.kind,
scopeKind: directiveForm.scopeKind,
scopeId: directiveForm.scopeId,
behaviorKind: directiveForm.behaviorKind,
useOrders: directiveForm.useOrders,
stagingOrderKind: directiveForm.stagingOrderKind || null,
targetEntityId: directiveForm.targetEntityId || null,
targetSystemId: directiveForm.targetSystemId || null,
targetPosition: null,
homeSystemId: null,
homeStationId: null,
sourceStationId: null,
destinationStationId: null,
itemId: directiveForm.itemId || null,
preferredAnchorId: null,
preferredConstructionSiteId: null,
preferredModuleId: null,
priority: directiveForm.priority,
radius: directiveForm.radius,
waitSeconds: directiveForm.waitSeconds,
maxSystemRange: directiveForm.maxSystemRange,
knownStationsOnly: directiveForm.knownStationsOnly,
patrolPoints: [],
repeatOrders: [],
policyId: directiveForm.policyId || null,
automationPolicyId: directiveForm.automationPolicyId || null,
notes: directiveForm.notes || null,
});
setPlayerSnapshot(snapshot);
directiveForm.label = "";
directiveForm.notes = "";
}, "Directive saved.");
}
async function removeDirective(directiveId: string) {
await runAction(async () => {
setPlayerSnapshot(await deletePlayerDirective(directiveId));
}, "Directive removed.");
}
async function submitAssignment() {
const asset = selectedShip.value ?? selectedStation.value;
if (!asset) {
errorMessage.value = "Select a player-owned ship or station first.";
return;
}
await runAction(async () => {
const snapshot = await upsertPlayerAssignment(asset.id, {
assetKind: selectedShip.value ? "ship" : "station",
assetId: asset.id,
fleetId: assignmentForm.fleetId || null,
taskForceId: assignmentForm.taskForceId || null,
stationGroupId: assignmentForm.stationGroupId || null,
economicRegionId: assignmentForm.economicRegionId || null,
frontId: assignmentForm.frontId || null,
reserveId: assignmentForm.reserveId || null,
directiveId: assignmentForm.directiveId || null,
policyId: assignmentForm.policyId || null,
automationPolicyId: assignmentForm.automationPolicyId || null,
role: assignmentForm.role,
clearConflicts: true,
});
setPlayerSnapshot(snapshot);
}, "Assignment saved.");
}
async function submitStrategicIntent() {
await runAction(async () => {
setPlayerSnapshot(await updatePlayerStrategicIntent({
strategicPosture: strategyForm.strategicPosture,
economicPosture: strategyForm.economicPosture,
militaryPosture: strategyForm.militaryPosture,
logisticsPosture: strategyForm.logisticsPosture,
desiredReserveRatio: strategyForm.desiredReserveRatio,
allowDelegatedCombatAutomation: strategyForm.allowDelegatedCombatAutomation,
allowDelegatedEconomicAutomation: strategyForm.allowDelegatedEconomicAutomation,
notes: strategyForm.notes || null,
}));
}, "Strategic intent updated.");
}
async function submitCorePolicy() {
await runAction(async () => {
setPlayerSnapshot(await upsertPlayerPolicy({
label: policyForm.label,
scopeKind: "player-faction",
scopeId: player.value?.id ?? null,
policySetId: player.value?.policies.find((entry) => entry.id === "player-core-policy")?.policySetId ?? null,
allowDelegatedCombat: policyForm.allowDelegatedCombat,
allowDelegatedTrade: policyForm.allowDelegatedTrade,
reserveCreditsRatio: policyForm.reserveCreditsRatio,
reserveMilitaryRatio: policyForm.reserveMilitaryRatio,
notes: policyForm.notes || null,
tradeAccessPolicy: policyForm.tradeAccessPolicy,
dockingAccessPolicy: policyForm.dockingAccessPolicy,
constructionAccessPolicy: policyForm.constructionAccessPolicy,
operationalRangePolicy: policyForm.operationalRangePolicy,
combatEngagementPolicy: policyForm.combatEngagementPolicy,
avoidHostileSystems: policyForm.avoidHostileSystems,
fleeHullRatio: policyForm.fleeHullRatio,
blacklistedSystemIds: policyForm.blacklistedSystemIds.split(",").map((entry) => entry.trim()).filter(Boolean),
}, "player-core-policy"));
}, "Core policy updated.");
}
async function submitCoreAutomation() {
await runAction(async () => {
setPlayerSnapshot(await upsertPlayerAutomationPolicy({
label: automationForm.label,
scopeKind: "player-faction",
scopeId: player.value?.id ?? null,
enabled: automationForm.enabled,
behaviorKind: automationForm.behaviorKind,
useOrders: automationForm.useOrders,
stagingOrderKind: automationForm.stagingOrderKind || null,
maxSystemRange: automationForm.maxSystemRange,
knownStationsOnly: automationForm.knownStationsOnly,
radius: automationForm.radius,
waitSeconds: automationForm.waitSeconds,
preferredItemId: automationForm.preferredItemId || null,
notes: automationForm.notes || null,
repeatOrders: [],
}, "player-core-automation"));
}, "Core automation updated.");
}
async function submitDirectBehavior() {
if (!selectedShip.value) {
errorMessage.value = "Select a player-owned ship first.";
return;
}
const selected = selectedShip.value;
await runAction(async () => {
const ship = await updateShipDefaultBehavior(selected.id, {
kind: behaviorForm.kind,
homeSystemId: behaviorForm.homeSystemId || selected.systemId || null,
homeStationId: null,
areaSystemId: behaviorForm.areaSystemId || null,
targetEntityId: behaviorForm.targetEntityId || null,
itemId: behaviorForm.itemId || null,
preferredAnchorId: behaviorForm.preferredAnchorId || null,
preferredConstructionSiteId: behaviorForm.preferredConstructionSiteId || null,
preferredModuleId: behaviorForm.preferredModuleId || null,
targetPosition: null,
waitSeconds: behaviorForm.waitSeconds,
radius: behaviorForm.radius,
maxSystemRange: behaviorForm.maxSystemRange,
knownStationsOnly: behaviorForm.knownStationsOnly,
patrolPoints: [],
repeatOrders: [],
});
gmStore.upsertShip(ship);
}, "Ship behavior updated.");
}
async function submitDirectOrder() {
if (!selectedShip.value) {
errorMessage.value = "Select a player-owned ship first.";
return;
}
const selected = selectedShip.value;
await runAction(async () => {
const ship = await enqueueShipOrder(selected.id, {
kind: orderForm.kind,
priority: orderForm.priority,
interruptCurrentPlan: orderForm.interruptCurrentPlan,
label: orderForm.label || null,
targetEntityId: orderForm.targetEntityId || null,
targetSystemId: orderForm.targetSystemId || null,
targetPosition: null,
sourceStationId: null,
destinationStationId: null,
itemId: orderForm.itemId || null,
anchorId: orderForm.anchorId || null,
constructionSiteId: orderForm.constructionSiteId || null,
moduleId: orderForm.moduleId || null,
waitSeconds: orderForm.waitSeconds,
radius: orderForm.radius,
maxSystemRange: orderForm.maxSystemRange,
knownStationsOnly: orderForm.knownStationsOnly,
});
gmStore.upsertShip(ship);
}, "Ship order queued.");
}
</script>
<template>
<div class="player-panel" v-if="player">
<div class="player-banner">
<div>
<div class="player-kicker">Player Faction</div>
<h3>{{ player.label }}</h3>
<p>{{ player.sovereignFactionId }} · {{ titleCase(player.strategicIntent.strategicPosture) }} / {{ titleCase(player.strategicIntent.economicPosture) }} / {{ titleCase(player.strategicIntent.militaryPosture) }}</p>
</div>
<div class="player-banner-metrics">
<div><strong>{{ player.assetRegistry.shipIds.length }}</strong><span>Ships</span></div>
<div><strong>{{ player.assetRegistry.stationIds.length }}</strong><span>Stations</span></div>
<div><strong>{{ organizations.length }}</strong><span>Groups</span></div>
<div><strong>{{ player.alerts.length }}</strong><span>Alerts</span></div>
</div>
</div>
<div v-if="statusMessage" class="player-message player-message--ok">{{ statusMessage }}</div>
<div v-if="errorMessage" class="player-message player-message--error">{{ errorMessage }}</div>
<div class="player-grid">
<section class="player-section">
<h4>Selected Asset Control</h4>
<div v-if="selectedShip || selectedStation" class="player-card-list">
<div class="player-card">
<strong>{{ selectedShip?.name ?? selectedStation?.label }}</strong>
<span>{{ selectedShip ? "Ship" : "Station" }} · {{ selectedShip?.systemId ?? selectedStation?.systemId }}</span>
<span v-if="selectedAssignment">Assignment {{ selectedAssignment.role }} · {{ selectedAssignment.directiveId ?? "no directive" }}</span>
<span v-else>No player assignment</span>
</div>
<div v-if="selectedShip" class="player-card">
<strong>Behavior</strong>
<span>{{ getShipBehaviorLabel(selectedShip.defaultBehavior.kind) }}</span>
<span>Orders {{ selectedShip.orderQueue.length }} · Tasks {{ selectedShip.activeSubTasks.length }}</span>
<span>Command {{ titleCase(selectedShip.controlSourceKind) }}<template v-if="selectedShip.controlReason"> · {{ selectedShip.controlReason }}</template></span>
<span v-if="selectedShip.lastReplanReason">Replan {{ selectedShip.lastReplanReason }}</span>
<span v-if="selectedShip.lastAccessFailureReason">Access {{ selectedShip.lastAccessFailureReason }}</span>
</div>
</div>
<p v-else class="player-empty">Select a player-owned ship or station to configure it.</p>
<form v-if="selectedShip" class="player-form" @submit.prevent="submitDirectBehavior">
<h5>Direct Ship Behavior</h5>
<label><span>Behavior</span><select v-model="behaviorForm.kind"><option v-for="option in behaviorOptions" :key="option" :value="option">{{ getShipBehaviorLabel(option) }}</option></select></label>
<label><span>Home System</span><input v-model="behaviorForm.homeSystemId" type="text"></label>
<label><span>Area System</span><input v-model="behaviorForm.areaSystemId" type="text"></label>
<label><span>Target Entity</span><input v-model="behaviorForm.targetEntityId" type="text"></label>
<label><span>Item</span><input v-model="behaviorForm.itemId" type="text"></label>
<label><span>Preferred Anchor</span><input v-model="behaviorForm.preferredAnchorId" type="text"></label>
<label><span>Construction Site</span><input v-model="behaviorForm.preferredConstructionSiteId" type="text"></label>
<label><span>Module</span><input v-model="behaviorForm.preferredModuleId" type="text"></label>
<label><span>Radius</span><input v-model.number="behaviorForm.radius" type="number" min="0" step="1"></label>
<label><span>Wait Seconds</span><input v-model.number="behaviorForm.waitSeconds" type="number" min="0" step="1"></label>
<label><span>System Range</span><input v-model.number="behaviorForm.maxSystemRange" type="number" min="0" step="1"></label>
<label class="player-check"><input v-model="behaviorForm.knownStationsOnly" type="checkbox"><span>Known stations only</span></label>
<button type="submit" :disabled="busy">Apply Behavior</button>
</form>
<form v-if="selectedShip" class="player-form" @submit.prevent="submitDirectOrder">
<h5>Direct Ship Order</h5>
<label><span>Order</span><select v-model="orderForm.kind"><option v-for="option in orderOptions" :key="option" :value="option">{{ getShipOrderLabel(option) }}</option></select></label>
<label><span>Label</span><input v-model="orderForm.label" type="text"></label>
<label><span>Target System</span><input v-model="orderForm.targetSystemId" type="text"></label>
<label><span>Target Entity</span><input v-model="orderForm.targetEntityId" type="text"></label>
<label><span>Item</span><input v-model="orderForm.itemId" type="text"></label>
<label><span>Anchor</span><input v-model="orderForm.anchorId" type="text"></label>
<label><span>Construction Site</span><input v-model="orderForm.constructionSiteId" type="text"></label>
<label><span>Module</span><input v-model="orderForm.moduleId" type="text"></label>
<label><span>Priority</span><input v-model.number="orderForm.priority" type="number" min="0" step="1"></label>
<label><span>Radius</span><input v-model.number="orderForm.radius" type="number" min="0" step="1"></label>
<label><span>Wait Seconds</span><input v-model.number="orderForm.waitSeconds" type="number" min="0" step="1"></label>
<label><span>System Range</span><input v-model.number="orderForm.maxSystemRange" type="number" min="0" step="1"></label>
<label class="player-check"><input v-model="orderForm.interruptCurrentPlan" type="checkbox"><span>Interrupt current plan</span></label>
<button type="submit" :disabled="busy">Queue Order</button>
</form>
<form v-if="selectedShip || selectedStation" class="player-form" @submit.prevent="submitAssignment">
<h5>Assignment</h5>
<label><span>Fleet</span><select v-model="assignmentForm.fleetId"><option value="">—</option><option v-for="entry in player.fleets" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Task Force</span><select v-model="assignmentForm.taskForceId"><option value="">—</option><option v-for="entry in player.taskForces" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Station Group</span><select v-model="assignmentForm.stationGroupId"><option value="">—</option><option v-for="entry in player.stationGroups" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Region</span><select v-model="assignmentForm.economicRegionId"><option value="">—</option><option v-for="entry in player.economicRegions" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Front</span><select v-model="assignmentForm.frontId"><option value="">—</option><option v-for="entry in player.fronts" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Reserve</span><select v-model="assignmentForm.reserveId"><option value="">—</option><option v-for="entry in player.reserves" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Directive</span><select v-model="assignmentForm.directiveId"><option value="">—</option><option v-for="entry in player.directives" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Policy</span><select v-model="assignmentForm.policyId"><option value="">—</option><option v-for="entry in player.policies" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Automation</span><select v-model="assignmentForm.automationPolicyId"><option value="">—</option><option v-for="entry in player.automationPolicies" :key="entry.id" :value="entry.id">{{ entry.label }}</option></select></label>
<label><span>Role</span><input v-model="assignmentForm.role" type="text"></label>
<button type="submit" :disabled="busy">Save Assignment</button>
</form>
</section>
<section class="player-section">
<h4>Strategic Control</h4>
<form class="player-form" @submit.prevent="submitStrategicIntent">
<label><span>Strategic</span><input v-model="strategyForm.strategicPosture" type="text"></label>
<label><span>Economic</span><input v-model="strategyForm.economicPosture" type="text"></label>
<label><span>Military</span><input v-model="strategyForm.militaryPosture" type="text"></label>
<label><span>Logistics</span><input v-model="strategyForm.logisticsPosture" type="text"></label>
<label><span>Reserve Ratio</span><input v-model.number="strategyForm.desiredReserveRatio" type="number" min="0" max="1" step="0.05"></label>
<label class="player-check"><input v-model="strategyForm.allowDelegatedCombatAutomation" type="checkbox"><span>Allow delegated combat automation</span></label>
<label class="player-check"><input v-model="strategyForm.allowDelegatedEconomicAutomation" type="checkbox"><span>Allow delegated economic automation</span></label>
<label class="player-full"><span>Notes</span><textarea v-model="strategyForm.notes" rows="2"></textarea></label>
<button type="submit" :disabled="busy">Update Strategic Intent</button>
</form>
<form class="player-form" @submit.prevent="submitCorePolicy">
<h5>Core Policy</h5>
<label><span>Label</span><input v-model="policyForm.label" type="text"></label>
<label><span>Trade Access</span><input v-model="policyForm.tradeAccessPolicy" type="text"></label>
<label><span>Docking Access</span><input v-model="policyForm.dockingAccessPolicy" type="text"></label>
<label><span>Construction Access</span><input v-model="policyForm.constructionAccessPolicy" type="text"></label>
<label><span>Operational Range</span><input v-model="policyForm.operationalRangePolicy" type="text"></label>
<label><span>Combat Policy</span><input v-model="policyForm.combatEngagementPolicy" type="text"></label>
<label><span>Reserve Credits</span><input v-model.number="policyForm.reserveCreditsRatio" type="number" min="0" max="1" step="0.05"></label>
<label><span>Reserve Military</span><input v-model.number="policyForm.reserveMilitaryRatio" type="number" min="0" max="1" step="0.05"></label>
<label><span>Flee Hull Ratio</span><input v-model.number="policyForm.fleeHullRatio" type="number" min="0" max="1" step="0.05"></label>
<label><span>Blacklisted Systems</span><input v-model="policyForm.blacklistedSystemIds" type="text" placeholder="sys-a, sys-b"></label>
<label class="player-check"><input v-model="policyForm.allowDelegatedCombat" type="checkbox"><span>Allow delegated combat</span></label>
<label class="player-check"><input v-model="policyForm.allowDelegatedTrade" type="checkbox"><span>Allow delegated trade</span></label>
<label class="player-check"><input v-model="policyForm.avoidHostileSystems" type="checkbox"><span>Avoid hostile systems</span></label>
<label class="player-full"><span>Notes</span><textarea v-model="policyForm.notes" rows="2"></textarea></label>
<button type="submit" :disabled="busy">Update Core Policy</button>
</form>
<form class="player-form" @submit.prevent="submitCoreAutomation">
<h5>Core Automation</h5>
<label><span>Label</span><input v-model="automationForm.label" type="text"></label>
<label><span>Behavior</span><select v-model="automationForm.behaviorKind"><option v-for="option in behaviorOptions" :key="option" :value="option">{{ getShipBehaviorLabel(option) }}</option></select></label>
<label><span>Order Staging</span><input v-model="automationForm.stagingOrderKind" type="text"></label>
<label><span>Preferred Item</span><input v-model="automationForm.preferredItemId" type="text"></label>
<label><span>System Range</span><input v-model.number="automationForm.maxSystemRange" type="number" min="0" step="1"></label>
<label><span>Radius</span><input v-model.number="automationForm.radius" type="number" min="0" step="1"></label>
<label><span>Wait Seconds</span><input v-model.number="automationForm.waitSeconds" type="number" min="0" step="1"></label>
<label class="player-check"><input v-model="automationForm.enabled" type="checkbox"><span>Enabled</span></label>
<label class="player-check"><input v-model="automationForm.useOrders" type="checkbox"><span>Use staging orders</span></label>
<label class="player-check"><input v-model="automationForm.knownStationsOnly" type="checkbox"><span>Known stations only</span></label>
<label class="player-full"><span>Notes</span><textarea v-model="automationForm.notes" rows="2"></textarea></label>
<button type="submit" :disabled="busy">Update Core Automation</button>
</form>
</section>
<section class="player-section">
<h4>Organization and Directives</h4>
<form class="player-form" @submit.prevent="submitOrganization">
<h5>Create Organization</h5>
<label><span>Kind</span><select v-model="orgForm.kind"><option value="fleet">Fleet</option><option value="task-force">Task Force</option><option value="station-group">Station Group</option><option value="economic-region">Economic Region</option><option value="front">Front</option><option value="reserve">Reserve</option></select></label>
<label><span>Label</span><input v-model="orgForm.label" type="text"></label>
<label><span>Role / Posture</span><input v-model="orgForm.role" type="text"></label>
<label><span>Parent Org</span><input v-model="orgForm.parentOrganizationId" type="text"></label>
<label><span>Front</span><input v-model="orgForm.frontId" type="text"></label>
<label><span>Home System</span><input v-model="orgForm.homeSystemId" type="text"></label>
<label><span>Home Station</span><input v-model="orgForm.homeStationId" type="text"></label>
<label><span>Target Faction</span><input v-model="orgForm.targetFactionId" type="text"></label>
<label><span>Priority</span><input v-model.number="orgForm.priority" type="number" min="0" step="1"></label>
<label><span>Reserve Kind</span><input v-model="orgForm.reserveKind" type="text"></label>
<label class="player-full"><span>Notes</span><textarea v-model="orgForm.notes" rows="2"></textarea></label>
<button type="submit" :disabled="busy || !orgForm.label">Create Organization</button>
</form>
<div class="player-list">
<div class="player-list-header"><span>Organizations</span><span>{{ organizations.length }}</span></div>
<button
v-for="entry in organizations"
:key="entry.id"
type="button"
class="player-list-item"
:class="playerStore.selectedOrganizationId === entry.id ? 'player-list-item--active' : ''"
@click="playerStore.selectOrganization(entry.id)"
>
<span><strong>{{ entry.label }}</strong><em>{{ titleCase(entry.kind) }}</em></span>
<span>{{ entry.summary }}</span>
<span class="player-actions"><small>{{ formatDate(entry.updatedAtUtc) }}</small><span class="player-link" @click.stop="removeOrganization(entry.id)">Delete</span></span>
</button>
</div>
<div v-if="selectedOrganizationDetails" class="player-card">
<strong>{{ selectedOrganizationDetails.label ?? playerStore.selectedOrganizationId }}</strong>
<span v-for="line in selectedOrganizationSummary" :key="line">{{ line }}</span>
</div>
<form class="player-form" @submit.prevent="submitDirective">
<h5>Create Directive</h5>
<label><span>Label</span><input v-model="directiveForm.label" type="text"></label>
<label><span>Kind</span><input v-model="directiveForm.kind" type="text"></label>
<label><span>Scope Kind</span><input v-model="directiveForm.scopeKind" type="text"></label>
<label><span>Scope Id</span><input v-model="directiveForm.scopeId" type="text" :placeholder="selectedEntityId ?? ''"></label>
<label><span>Behavior</span><select v-model="directiveForm.behaviorKind"><option v-for="option in behaviorOptions" :key="option" :value="option">{{ getShipBehaviorLabel(option) }}</option></select></label>
<label><span>Staging Order</span><input v-model="directiveForm.stagingOrderKind" type="text"></label>
<label><span>Target System</span><input v-model="directiveForm.targetSystemId" type="text"></label>
<label><span>Target Entity</span><input v-model="directiveForm.targetEntityId" type="text"></label>
<label><span>Item</span><input v-model="directiveForm.itemId" type="text"></label>
<label><span>Priority</span><input v-model.number="directiveForm.priority" type="number" min="0" step="1"></label>
<label><span>Radius</span><input v-model.number="directiveForm.radius" type="number" min="0" step="1"></label>
<label><span>Wait Seconds</span><input v-model.number="directiveForm.waitSeconds" type="number" min="0" step="1"></label>
<label><span>System Range</span><input v-model.number="directiveForm.maxSystemRange" type="number" min="0" step="1"></label>
<label class="player-check"><input v-model="directiveForm.useOrders" type="checkbox"><span>Use staging order</span></label>
<label class="player-check"><input v-model="directiveForm.knownStationsOnly" type="checkbox"><span>Known stations only</span></label>
<label class="player-full"><span>Notes</span><textarea v-model="directiveForm.notes" rows="2"></textarea></label>
<button type="submit" :disabled="busy || !directiveForm.label || !directiveForm.scopeId">Save Directive</button>
</form>
<div class="player-list">
<div class="player-list-header"><span>Directives</span><span>{{ player.directives.length }}</span></div>
<button
v-for="directive in player.directives"
:key="directive.id"
type="button"
class="player-list-item"
:class="playerStore.selectedDirectiveId === directive.id ? 'player-list-item--active' : ''"
@click="playerStore.selectDirective(directive.id)"
>
<span><strong>{{ directive.label }}</strong><em>{{ titleCase(directive.scopeKind) }} / {{ directive.scopeId }}</em></span>
<span>{{ titleCase(directive.behaviorKind) }} · {{ titleCase(directive.status) }}</span>
<span class="player-actions"><small>{{ formatDate(directive.updatedAtUtc) }}</small><span class="player-link" @click.stop="removeDirective(directive.id)">Delete</span></span>
</button>
</div>
</section>
<section class="player-section">
<h4>Observability</h4>
<div class="player-list">
<div class="player-list-header"><span>Alerts</span><span>{{ player.alerts.length }}</span></div>
<div v-for="alert in player.alerts" :key="alert.id" class="player-list-item player-list-item--static">
<span><strong>{{ titleCase(alert.kind) }}</strong><em>{{ titleCase(alert.severity) }}</em></span>
<span>{{ alert.summary }}</span>
<span><small>{{ formatDate(alert.createdAtUtc) }}</small></span>
</div>
</div>
<div class="player-list">
<div class="player-list-header"><span>Decision Log</span><span>{{ player.decisionLog.length }}</span></div>
<div v-for="decision in player.decisionLog.slice(0, 12)" :key="decision.id" class="player-list-item player-list-item--static">
<span><strong>{{ titleCase(decision.kind) }}</strong><em>{{ decision.relatedEntityKind ?? "—" }}</em></span>
<span>{{ decision.summary }}</span>
<span><small>{{ formatDate(decision.occurredAtUtc) }}</small></span>
</div>
</div>
</section>
</div>
</div>
<div v-else class="player-empty-state">
Player faction state is not available.
</div>
</template>
<style scoped>
.player-panel {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.75rem;
color: rgba(226, 232, 240, 0.92);
}
.player-banner {
display: flex;
justify-content: space-between;
gap: 1rem;
border: 1px solid rgba(148, 163, 184, 0.2);
background: linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(12, 18, 31, 0.9));
padding: 0.85rem 1rem;
}
.player-banner h3 {
margin: 0;
font-size: 1rem;
}
.player-banner p,
.player-kicker {
margin: 0.2rem 0 0;
font-size: 0.75rem;
opacity: 0.75;
}
.player-kicker {
text-transform: uppercase;
letter-spacing: 0.14em;
color: rgba(125, 211, 252, 0.88);
}
.player-banner-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.75rem;
min-width: 20rem;
}
.player-banner-metrics div {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 0.75rem;
}
.player-banner-metrics strong {
font-size: 1.1rem;
}
.player-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.player-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(15, 23, 42, 0.78);
padding: 0.85rem;
}
.player-section h4,
.player-section h5 {
margin: 0;
}
.player-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem 0.75rem;
}
.player-form label {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.72rem;
}
.player-form label span {
opacity: 0.74;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.player-form input,
.player-form select,
.player-form textarea,
.player-form button {
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(15, 23, 42, 0.94);
color: inherit;
padding: 0.45rem 0.55rem;
font: inherit;
}
.player-form button {
cursor: pointer;
background: rgba(14, 116, 144, 0.24);
font-weight: 600;
}
.player-form button:disabled {
cursor: wait;
opacity: 0.55;
}
.player-full,
.player-form button {
grid-column: 1 / -1;
}
.player-check {
flex-direction: row !important;
align-items: center;
gap: 0.45rem !important;
}
.player-check span {
text-transform: none !important;
letter-spacing: normal !important;
}
.player-list {
display: flex;
flex-direction: column;
border: 1px solid rgba(148, 163, 184, 0.16);
}
.player-list-header,
.player-list-item {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 0.7fr);
gap: 0.75rem;
padding: 0.5rem 0.65rem;
font-size: 0.74rem;
}
.player-list-header {
background: rgba(30, 41, 59, 0.72);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.65rem;
}
.player-list-item {
border-top: 1px solid rgba(148, 163, 184, 0.08);
background: transparent;
color: inherit;
text-align: left;
}
.player-list-item--active {
background: rgba(8, 47, 73, 0.46);
}
.player-list-item--static {
display: grid;
}
.player-list-item strong,
.player-card strong {
display: block;
}
.player-list-item em,
.player-card span,
.player-actions small {
display: block;
font-style: normal;
opacity: 0.72;
}
.player-actions {
text-align: right;
}
.player-link {
color: rgba(248, 113, 113, 0.92);
cursor: pointer;
}
.player-card-list {
display: grid;
gap: 0.65rem;
}
.player-card {
border: 1px solid rgba(148, 163, 184, 0.16);
padding: 0.55rem 0.65rem;
background: rgba(15, 23, 42, 0.52);
font-size: 0.74rem;
}
.player-message {
padding: 0.55rem 0.7rem;
font-size: 0.76rem;
}
.player-message--ok {
background: rgba(22, 163, 74, 0.16);
border: 1px solid rgba(22, 163, 74, 0.3);
}
.player-message--error {
background: rgba(220, 38, 38, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
}
.player-empty,
.player-empty-state {
font-size: 0.74rem;
opacity: 0.72;
}
@media (max-width: 1100px) {
.player-grid {
grid-template-columns: 1fr;
}
.player-banner {
flex-direction: column;
}
.player-banner-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
min-width: 0;
}
.player-form,
.player-list-item,
.player-list-header {
grid-template-columns: 1fr;
}
.player-actions {
text-align: left;
}
}
</style>