1156 lines
48 KiB
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>
|