Complete universe model migration
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { GameViewer } from "./GameViewer";
|
||||
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
||||
import ViewerEntityBrowserPanel from "./components/ViewerEntityBrowserPanel.vue";
|
||||
@@ -31,8 +31,8 @@ const authStore = useAuthStore();
|
||||
const playerFactionStore = usePlayerFactionStore();
|
||||
const automationCatalogStore = useShipAutomationCatalogStore();
|
||||
const selectionStore = useViewerSelectionStore();
|
||||
const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore);
|
||||
const { canAccessGm, effectivePlayerId } = storeToRefs(authStore);
|
||||
const { selectedEntityId } = storeToRefs(selectionStore);
|
||||
const { canAccessGm, effectivePlayerId, isActingAsAlternateIdentity } = storeToRefs(authStore);
|
||||
const { playerFaction } = storeToRefs(playerFactionStore);
|
||||
let viewer: GameViewer | undefined;
|
||||
|
||||
@@ -42,14 +42,27 @@ const gmSettingsOpen = ref(false);
|
||||
const gmMenuOpen = ref(false);
|
||||
const leftSidebarTab = ref<"player" | "entities">("player");
|
||||
const playerContextReady = ref(false);
|
||||
const rightSidebarWidth = ref(380);
|
||||
const rightSidebarResizing = ref(false);
|
||||
const shouldShowOnboarding = computed(() =>
|
||||
!!playerContextReady.value
|
||||
&& !!playerFaction.value?.requiresOnboarding
|
||||
&& (!canAccessGm.value || isActingAsAlternateIdentity.value),
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("pointermove", onWindowPointerMove);
|
||||
window.addEventListener("pointerup", stopRightSidebarResize);
|
||||
window.addEventListener("pointercancel", stopRightSidebarResize);
|
||||
void automationCatalogStore.load();
|
||||
await refreshPlayerContext();
|
||||
await startViewerIfAuthenticated();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("pointermove", onWindowPointerMove);
|
||||
window.removeEventListener("pointerup", stopRightSidebarResize);
|
||||
window.removeEventListener("pointercancel", stopRightSidebarResize);
|
||||
viewer?.dispose();
|
||||
});
|
||||
|
||||
@@ -71,7 +84,7 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => playerFaction.value?.requiresOnboarding ?? false,
|
||||
() => shouldShowOnboarding.value,
|
||||
async (requiresOnboarding) => {
|
||||
if (requiresOnboarding) {
|
||||
viewer?.dispose();
|
||||
@@ -101,8 +114,31 @@ function onFocusSelection(selection: Selectable, cameraMode?: "follow" | "tactic
|
||||
viewer?.focusSelection(selection, cameraMode);
|
||||
}
|
||||
|
||||
function startRightSidebarResize(event: PointerEvent) {
|
||||
if (window.innerWidth <= 760 || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
rightSidebarResizing.value = true;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onWindowPointerMove(event: PointerEvent) {
|
||||
if (!rightSidebarResizing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minWidth = 280;
|
||||
const maxWidth = Math.min(720, Math.max(window.innerWidth - 240, minWidth));
|
||||
rightSidebarWidth.value = Math.min(maxWidth, Math.max(minWidth, window.innerWidth - event.clientX));
|
||||
}
|
||||
|
||||
function stopRightSidebarResize() {
|
||||
rightSidebarResizing.value = false;
|
||||
}
|
||||
|
||||
async function startViewerIfAuthenticated() {
|
||||
if (!authStore.isAuthenticated || viewer || !playerContextReady.value || playerFaction.value?.requiresOnboarding) {
|
||||
if (!authStore.isAuthenticated || viewer || !playerContextReady.value || shouldShowOnboarding.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +191,7 @@ async function refreshPlayerContext() {
|
||||
<p>Loading your in-universe identity and ownership state.</p>
|
||||
</div>
|
||||
</div>
|
||||
<PlayerOnboardingPanel v-else-if="playerContextReady && playerFaction?.requiresOnboarding" />
|
||||
<PlayerOnboardingPanel v-else-if="shouldShowOnboarding" />
|
||||
<div v-else class="viewer-app">
|
||||
<div
|
||||
ref="canvasHostEl"
|
||||
@@ -232,27 +268,28 @@ async function refreshPlayerContext() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute right-5 top-5 flex max-h-[calc(100vh-40px)] w-[min(380px,calc(100vw-40px))] flex-col gap-4 overflow-hidden max-[760px]:bottom-[148px] max-[760px]:left-5 max-[760px]:right-5 max-[760px]:top-auto max-[760px]:max-h-[38vh] max-[760px]:w-auto">
|
||||
<ViewerEntityInspectorPanel
|
||||
class="min-h-0 flex-1"
|
||||
:fallback-title="hudState.detailPanel.title"
|
||||
:fallback-html="hudState.detailPanel.bodyHtml"
|
||||
@focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-auto rounded-xl bg-[rgba(255,116,88,0.14)] px-3.5 py-3 text-[#ffd8cf]"
|
||||
:hidden="hudState.error.hidden"
|
||||
>
|
||||
{{ hudState.error.message }}
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedEntityId"
|
||||
type="button"
|
||||
class="selection-action-button pointer-events-auto self-end rounded-full border border-white/10 bg-white/5 px-3.5 py-2.5 text-sm text-[color:var(--viewer-text)] transition hover:bg-white/10"
|
||||
@click="selectionStore.clearSelection('ui')"
|
||||
>
|
||||
Clear {{ selectedEntityLabel ?? "Selection" }}
|
||||
</button>
|
||||
<div class="viewer-right-sidebar-dock" :style="{ width: `${rightSidebarWidth}px` }">
|
||||
<section class="viewer-right-sidebar pointer-events-auto">
|
||||
<div
|
||||
class="viewer-right-sidebar__resize-handle"
|
||||
:class="rightSidebarResizing ? 'viewer-right-sidebar__resize-handle--active' : ''"
|
||||
@pointerdown="startRightSidebarResize"
|
||||
/>
|
||||
<div class="viewer-right-sidebar__body">
|
||||
<ViewerEntityInspectorPanel
|
||||
class="viewer-right-sidebar__panel"
|
||||
:fallback-title="hudState.detailPanel.title"
|
||||
:fallback-html="hudState.detailPanel.bodyHtml"
|
||||
@focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="viewer-right-sidebar__error"
|
||||
:hidden="hudState.error.hidden"
|
||||
>
|
||||
{{ hudState.error.message }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div ref="historyLayerHostEl">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updatePanFromKeyboard } from "./viewerCamera";
|
||||
import { setShellReticleOpacity } from "./viewerControls";
|
||||
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
|
||||
import { updateSystemStarPresentation } from "./viewerPresentation";
|
||||
import { resolveFocusedCelestialId } from "./viewerSelection";
|
||||
import { resolveFocusedAnchorId } from "./viewerSelection";
|
||||
import { describeSelectionParent } from "./viewerPanels";
|
||||
import {
|
||||
createInitialNetworkStats,
|
||||
@@ -195,6 +195,7 @@ export class ViewerAppController {
|
||||
return this.sceneDataController.createWorldPresentationContext({
|
||||
world: this.world,
|
||||
activeSystemId: this.activeSystemId,
|
||||
focusedAnchorId: this.resolveFocusedAnchorId(),
|
||||
cameraMode: this.cameraMode,
|
||||
povLevel: this.povLevel,
|
||||
orbitYaw: this.orbitYaw,
|
||||
@@ -284,6 +285,7 @@ export class ViewerAppController {
|
||||
this.sceneStore.setViewContext(this.activeSystemId ?? null, this.povLevel);
|
||||
}
|
||||
this.navigationController.updateActiveSystem();
|
||||
this.navigationController.syncGalaxyAnchorToActiveSystem();
|
||||
|
||||
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
|
||||
// Follow camera directly controls systemLayer.camera in updateFollowCamera.
|
||||
@@ -350,8 +352,8 @@ export class ViewerAppController {
|
||||
this.interactionController.refreshHistoryWindows();
|
||||
}
|
||||
|
||||
private resolveFocusedCelestialId() {
|
||||
return resolveFocusedCelestialId(this.world, this.selectedItems);
|
||||
private resolveFocusedAnchorId() {
|
||||
return resolveFocusedAnchorId(this.world, this.selectedItems);
|
||||
}
|
||||
|
||||
private onResize(width: number, height: number) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
export interface WorldStreamScope {
|
||||
scopeKind?: string;
|
||||
systemId?: string | null;
|
||||
bubbleId?: string | null;
|
||||
anchorId?: string | null;
|
||||
}
|
||||
|
||||
async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit, options?: { skipAuth?: boolean; skipRefresh?: boolean }): Promise<T> {
|
||||
@@ -105,8 +105,8 @@ export function openWorldStream(
|
||||
if (scope?.systemId) {
|
||||
query.set("systemId", scope.systemId);
|
||||
}
|
||||
if (scope?.bubbleId) {
|
||||
query.set("bubbleId", scope.bubbleId);
|
||||
if (scope?.anchorId) {
|
||||
query.set("anchorId", scope.anchorId);
|
||||
}
|
||||
|
||||
const stream = new EventSource(`/api/world/stream?${query.toString()}`);
|
||||
|
||||
@@ -115,12 +115,13 @@ function formatShipLocation(ship: ShipSnapshot) {
|
||||
return `Docked ${dockedStation.label}`;
|
||||
}
|
||||
|
||||
if (ship.spatialState.transit?.destinationNodeId) {
|
||||
return `Transit ${ship.systemId}`;
|
||||
const transitAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
|
||||
if (transitAnchorId) {
|
||||
return `Transit ${titleCase(transitAnchorId)}`;
|
||||
}
|
||||
|
||||
if (ship.celestialId) {
|
||||
return `Orbit ${titleCase(ship.celestialId)}`;
|
||||
if (ship.spatialState.currentAnchorId) {
|
||||
return `Anchor ${compactAnchorId(ship.spatialState.currentAnchorId)}`;
|
||||
}
|
||||
|
||||
const system = systemById.value.get(ship.systemId);
|
||||
@@ -129,13 +130,32 @@ function formatShipLocation(ship: ShipSnapshot) {
|
||||
|
||||
function formatStationLocation(station: StationSnapshot) {
|
||||
const system = systemById.value.get(station.systemId);
|
||||
if (station.celestialId) {
|
||||
return `${system?.label ?? station.systemId} · ${titleCase(station.celestialId)}`;
|
||||
if (station.anchorId) {
|
||||
return `${system?.label ?? station.systemId} · ${compactAnchorId(station.anchorId)}`;
|
||||
}
|
||||
|
||||
return system?.label ?? station.systemId;
|
||||
}
|
||||
|
||||
function compactAnchorId(value: string) {
|
||||
const lagrangeMatch = value.match(/(l[1-5])$/i);
|
||||
if (lagrangeMatch) {
|
||||
return lagrangeMatch[1].toUpperCase();
|
||||
}
|
||||
|
||||
const moonMatch = value.match(/moon-(\d+)$/i);
|
||||
if (moonMatch) {
|
||||
return `Moon ${moonMatch[1]}`;
|
||||
}
|
||||
|
||||
const planetMatch = value.match(/planet-(\d+)$/i);
|
||||
if (planetMatch) {
|
||||
return `Planet ${planetMatch[1]}`;
|
||||
}
|
||||
|
||||
return titleCase(value);
|
||||
}
|
||||
|
||||
function shipAiStates(ship: ShipSnapshot) {
|
||||
const travelToken = ship.spatialState.transit ? "TRV" : "";
|
||||
const dockToken = ship.dockedStationId ? "DCK" : "";
|
||||
|
||||
@@ -80,6 +80,14 @@ function formatPercent(value: number) {
|
||||
return `${Math.round(value * 100)}%`;
|
||||
}
|
||||
|
||||
function formatCargoTypeLabel(types: string[] | null | undefined) {
|
||||
if (!types || types.length === 0) {
|
||||
return "general";
|
||||
}
|
||||
|
||||
return types.join(" / ");
|
||||
}
|
||||
|
||||
function joinDetail(parts: Array<string | null | undefined>) {
|
||||
return parts.filter((part): part is string => !!part && part.trim().length > 0).join(" · ");
|
||||
}
|
||||
@@ -88,7 +96,7 @@ function describeOrderTarget(order: {
|
||||
itemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
@@ -97,7 +105,7 @@ function describeOrderTarget(order: {
|
||||
return order.itemId
|
||||
?? order.targetEntityId
|
||||
?? order.targetSystemId
|
||||
?? order.nodeId
|
||||
?? order.anchorId
|
||||
?? order.constructionSiteId
|
||||
?? order.destinationStationId
|
||||
?? order.sourceStationId
|
||||
@@ -109,13 +117,15 @@ function describeSubTaskTarget(subTask: {
|
||||
itemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetNodeId?: string | null;
|
||||
targetAnchorId?: string | null;
|
||||
targetResourceNodeId?: string | null;
|
||||
moduleId?: string | null;
|
||||
}) {
|
||||
return subTask.itemId
|
||||
?? subTask.targetEntityId
|
||||
?? subTask.targetSystemId
|
||||
?? subTask.targetNodeId
|
||||
?? subTask.targetAnchorId
|
||||
?? subTask.targetResourceNodeId
|
||||
?? subTask.moduleId
|
||||
?? "—";
|
||||
}
|
||||
@@ -184,8 +194,14 @@ const shipStatusRows = computed(() => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shipLocation = selectedShip.value.spatialState.currentAnchorId
|
||||
?? selectedShip.value.anchorId
|
||||
?? selectedShip.value.systemId
|
||||
?? "unknown";
|
||||
|
||||
return [
|
||||
{ label: "State", value: titleCase(selectedShip.value.state) },
|
||||
{ label: "Location", value: shipLocation },
|
||||
{ label: "Behavior", value: getShipBehaviorLabel(selectedShip.value.defaultBehavior.kind) },
|
||||
{ label: "Control", value: titleCase(selectedShip.value.controlSourceKind) },
|
||||
{ label: "Assignment", value: selectedShip.value.assignment?.kind ?? "unassigned" },
|
||||
@@ -201,20 +217,21 @@ const shipStatusRows = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const shipCargoSummaryRows = computed(() => {
|
||||
const shipCargoBarRows = computed(() => {
|
||||
if (!selectedShip.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const usedCargo = selectedShip.value.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
||||
return [
|
||||
{ label: "Used", value: formatAmount(usedCargo) },
|
||||
{ label: "Capacity", value: formatAmount(selectedShip.value.cargoCapacity) },
|
||||
{ label: "Free", value: formatAmount(Math.max(selectedShip.value.cargoCapacity - usedCargo, 0)) },
|
||||
{ label: "Travel", value: `${formatAmount(selectedShip.value.travelSpeed)} ${selectedShip.value.travelSpeedUnit}` },
|
||||
{ label: "Hull", value: formatAmount(selectedShip.value.health) },
|
||||
{ label: "Regime", value: titleCase(selectedShip.value.spatialState.movementRegime) },
|
||||
];
|
||||
return [{
|
||||
key: "cargo",
|
||||
label: `${formatCargoTypeLabel(selectedShip.value.cargoTypes)}`,
|
||||
value: usedCargo,
|
||||
valueLabel: formatAmount(usedCargo),
|
||||
max: selectedShip.value.cargoCapacity,
|
||||
maxLabel: formatAmount(selectedShip.value.cargoCapacity),
|
||||
fillRatio: selectedShip.value.cargoCapacity > 0 ? usedCargo / selectedShip.value.cargoCapacity : 0,
|
||||
}];
|
||||
});
|
||||
|
||||
const shipCargoRows = computed(() =>
|
||||
@@ -309,8 +326,13 @@ const stationStatusRows = computed(() => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stationLocation = selectedStation.value.anchorId
|
||||
?? selectedStation.value.systemId
|
||||
?? "unknown";
|
||||
|
||||
return [
|
||||
{ label: "Category", value: titleCase(selectedStation.value.category) },
|
||||
{ label: "Location", value: stationLocation },
|
||||
{ label: "Objective", value: titleCase(selectedStation.value.objective) },
|
||||
{ label: "Docked", value: `${selectedStation.value.dockedShips} / ${selectedStation.value.dockingPads}` },
|
||||
{
|
||||
@@ -335,10 +357,12 @@ const stationModuleRows = computed(() =>
|
||||
const stationStorageRows = computed(() =>
|
||||
selectedStation.value?.storageUsage.map((entry) => ({
|
||||
key: entry.storageClass,
|
||||
storageClass: titleCase(entry.storageClass),
|
||||
used: formatAmount(entry.used),
|
||||
capacity: formatAmount(entry.capacity),
|
||||
fill: entry.capacity > 0 ? formatPercent(entry.used / entry.capacity) : "0%",
|
||||
label: titleCase(entry.storageClass),
|
||||
value: entry.used,
|
||||
valueLabel: formatAmount(entry.used),
|
||||
max: entry.capacity,
|
||||
maxLabel: formatAmount(entry.capacity),
|
||||
fillRatio: entry.capacity > 0 ? entry.used / entry.capacity : 0,
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
@@ -429,7 +453,7 @@ async function saveBehavior() {
|
||||
itemId: behaviorForm.kind === "local-auto-mine"
|
||||
? (behaviorForm.itemId.trim() || null)
|
||||
: null,
|
||||
preferredNodeId: null,
|
||||
preferredAnchorId: null,
|
||||
preferredConstructionSiteId: null,
|
||||
preferredModuleId: null,
|
||||
targetPosition: null,
|
||||
@@ -461,7 +485,7 @@ async function queueHoldPositionOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -497,7 +521,7 @@ async function queueMoveOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -540,7 +564,7 @@ async function queueMineResourceOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -609,15 +633,20 @@ async function clearOrders() {
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Cargo</h4>
|
||||
<div class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table entity-inspector-table--kv">
|
||||
<tbody>
|
||||
<tr v-for="row in shipCargoSummaryRows" :key="row.label">
|
||||
<th scope="row">{{ row.label }}</th>
|
||||
<td>{{ row.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="entity-inspector-capacity-list">
|
||||
<div v-for="row in shipCargoBarRows" :key="row.key" class="entity-inspector-capacity">
|
||||
<div class="entity-inspector-capacity__header">
|
||||
<span class="entity-inspector-capacity__label">{{ row.label }}</span>
|
||||
<span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
|
||||
</div>
|
||||
<div class="entity-inspector-capacity__scale">
|
||||
<span>0</span>
|
||||
<div class="entity-inspector-capacity__track">
|
||||
<div class="entity-inspector-capacity__fill" :style="{ width: `${Math.max(0, Math.min(100, row.fillRatio * 100))}%` }"></div>
|
||||
</div>
|
||||
<span>{{ row.maxLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="shipCargoRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
@@ -635,7 +664,7 @@ async function clearOrders() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="entity-inspector-empty">No cargo.</div>
|
||||
<div v-else class="entity-inspector-empty">No wares loaded.</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
@@ -856,25 +885,20 @@ async function clearOrders() {
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Storage</h4>
|
||||
<div v-if="stationStorageRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Class</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Used</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Capacity</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Fill</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in stationStorageRows" :key="row.key">
|
||||
<td>{{ row.storageClass }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.used }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.capacity }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.fill }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="stationStorageRows.length > 0" class="entity-inspector-capacity-list">
|
||||
<div v-for="row in stationStorageRows" :key="row.key" class="entity-inspector-capacity">
|
||||
<div class="entity-inspector-capacity__header">
|
||||
<span class="entity-inspector-capacity__label">{{ row.label }}</span>
|
||||
<span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
|
||||
</div>
|
||||
<div class="entity-inspector-capacity__scale">
|
||||
<span>0</span>
|
||||
<div class="entity-inspector-capacity__track">
|
||||
<div class="entity-inspector-capacity__fill" :style="{ width: `${Math.max(0, Math.min(100, row.fillRatio * 100))}%` }"></div>
|
||||
</div>
|
||||
<span>{{ row.maxLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="stationInventoryRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
|
||||
@@ -157,7 +157,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId,
|
||||
nodeId: target.value.selection.kind === "node" ? target.value.selection.id : null,
|
||||
anchorId: target.value.anchorId ?? null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -182,7 +182,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 8,
|
||||
@@ -207,7 +207,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 6,
|
||||
@@ -232,7 +232,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
|
||||
@@ -149,13 +149,6 @@ function compactRate(value: number | null | undefined) {
|
||||
return `${sign}${value.toFixed(2)}/s`;
|
||||
}
|
||||
|
||||
function formatCargoAmount(value: number | null | undefined) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
const rounded = Math.round(value);
|
||||
if (Math.abs(value - rounded) < 0.005) return String(rounded);
|
||||
return value.toFixed(2).replace(/\.?0+$/, "");
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
return `${Math.round(value * 100)}%`;
|
||||
@@ -281,8 +274,6 @@ type ShipRow = {
|
||||
plan: string;
|
||||
step: string;
|
||||
subtask: string;
|
||||
cargo: number;
|
||||
health: number;
|
||||
};
|
||||
|
||||
const shipRows = computed<ShipRow[]>(() =>
|
||||
@@ -305,8 +296,6 @@ const shipRows = computed<ShipRow[]>(() =>
|
||||
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
|
||||
step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—",
|
||||
subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—",
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
health: Math.round(s.health),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -329,16 +318,11 @@ const shipColumns = [
|
||||
shipColumnHelper.accessor("plan", { header: "Plan" }),
|
||||
shipColumnHelper.accessor("step", { header: "Current Step" }),
|
||||
shipColumnHelper.accessor("subtask", { header: "SubTask" }),
|
||||
shipColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||
];
|
||||
|
||||
const shipFilter = ref("");
|
||||
const shipSorting = ref<SortingState>([]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask", "cargo", "health"]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask"]);
|
||||
|
||||
const shipTable = useVueTable({
|
||||
get data() { return shipRows.value; },
|
||||
@@ -373,7 +357,6 @@ type StationRow = {
|
||||
docked: string;
|
||||
orders: number;
|
||||
orderDetails: MarketOrderSnapshot[];
|
||||
cargo: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
@@ -400,7 +383,6 @@ const stationRows = computed<StationRow[]>(() =>
|
||||
const order = marketOrderMap.value.get(id);
|
||||
return order ? [order] : [];
|
||||
}),
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
modules: s.installedModules.length,
|
||||
})),
|
||||
);
|
||||
@@ -421,16 +403,12 @@ const stationColumns = [
|
||||
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
|
||||
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||
stationColumnHelper.accessor("orders", { header: "Orders" }),
|
||||
stationColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||
];
|
||||
|
||||
const stationFilter = ref("");
|
||||
const stationSorting = ref<SortingState>([]);
|
||||
const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
|
||||
const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "modules"]);
|
||||
|
||||
const stationTable = useVueTable({
|
||||
get data() { return stationRows.value; },
|
||||
|
||||
@@ -251,7 +251,7 @@ const behaviorForm = reactive({
|
||||
areaSystemId: "",
|
||||
targetEntityId: "",
|
||||
itemId: "",
|
||||
preferredNodeId: "",
|
||||
preferredAnchorId: "",
|
||||
preferredConstructionSiteId: "",
|
||||
preferredModuleId: "",
|
||||
waitSeconds: 3,
|
||||
@@ -268,7 +268,7 @@ const orderForm = reactive({
|
||||
targetEntityId: "",
|
||||
targetSystemId: "",
|
||||
itemId: "",
|
||||
nodeId: "",
|
||||
anchorId: "",
|
||||
constructionSiteId: "",
|
||||
moduleId: "",
|
||||
waitSeconds: 3,
|
||||
@@ -344,7 +344,7 @@ watch(selectedShip, (ship) => {
|
||||
behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId;
|
||||
behaviorForm.targetEntityId = ship.defaultBehavior.targetEntityId ?? "";
|
||||
behaviorForm.itemId = ship.defaultBehavior.itemId ?? "";
|
||||
behaviorForm.preferredNodeId = ship.defaultBehavior.preferredNodeId ?? "";
|
||||
behaviorForm.preferredAnchorId = ship.defaultBehavior.preferredAnchorId ?? "";
|
||||
behaviorForm.preferredConstructionSiteId = ship.defaultBehavior.preferredConstructionSiteId ?? "";
|
||||
behaviorForm.preferredModuleId = ship.defaultBehavior.preferredModuleId ?? "";
|
||||
behaviorForm.waitSeconds = ship.defaultBehavior.waitSeconds;
|
||||
@@ -484,7 +484,7 @@ async function submitDirective() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: directiveForm.itemId || null,
|
||||
preferredNodeId: null,
|
||||
preferredAnchorId: null,
|
||||
preferredConstructionSiteId: null,
|
||||
preferredModuleId: null,
|
||||
priority: directiveForm.priority,
|
||||
@@ -612,7 +612,7 @@ async function submitDirectBehavior() {
|
||||
areaSystemId: behaviorForm.areaSystemId || null,
|
||||
targetEntityId: behaviorForm.targetEntityId || null,
|
||||
itemId: behaviorForm.itemId || null,
|
||||
preferredNodeId: behaviorForm.preferredNodeId || null,
|
||||
preferredAnchorId: behaviorForm.preferredAnchorId || null,
|
||||
preferredConstructionSiteId: behaviorForm.preferredConstructionSiteId || null,
|
||||
preferredModuleId: behaviorForm.preferredModuleId || null,
|
||||
targetPosition: null,
|
||||
@@ -646,7 +646,7 @@ async function submitDirectOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: orderForm.itemId || null,
|
||||
nodeId: orderForm.nodeId || null,
|
||||
anchorId: orderForm.anchorId || null,
|
||||
constructionSiteId: orderForm.constructionSiteId || null,
|
||||
moduleId: orderForm.moduleId || null,
|
||||
waitSeconds: orderForm.waitSeconds,
|
||||
@@ -706,7 +706,7 @@ async function submitDirectOrder() {
|
||||
<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 Node</span><input v-model="behaviorForm.preferredNodeId" 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>
|
||||
@@ -723,7 +723,7 @@ async function submitDirectOrder() {
|
||||
<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>Node</span><input v-model="orderForm.nodeId" 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>
|
||||
|
||||
@@ -7,10 +7,13 @@ export type {
|
||||
OrbitalSimulationSnapshot,
|
||||
} from "./contractsWorld";
|
||||
export type {
|
||||
AnchorSnapshot,
|
||||
AnchorDelta,
|
||||
StarSnapshot,
|
||||
MoonSnapshot,
|
||||
SystemSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceDepositSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
ResourceNodeDelta,
|
||||
CelestialSnapshot,
|
||||
|
||||
@@ -46,26 +46,50 @@ export interface PlanetSnapshot {
|
||||
hasRing: boolean;
|
||||
}
|
||||
|
||||
export interface ResourceDepositSnapshot {
|
||||
id: string;
|
||||
nodeId: string;
|
||||
anchorId: string;
|
||||
localPosition: Vector3Dto;
|
||||
oreRemaining: number;
|
||||
maxOre: number;
|
||||
}
|
||||
|
||||
export interface ResourceNodeSnapshot {
|
||||
id: string;
|
||||
anchorId: string;
|
||||
systemId: string;
|
||||
localPosition: Vector3Dto;
|
||||
celestialId?: string | null;
|
||||
localSpaceRadius: number;
|
||||
sourceKind: string;
|
||||
oreRemaining: number;
|
||||
maxOre: number;
|
||||
itemId: string;
|
||||
deposits: ResourceDepositSnapshot[];
|
||||
}
|
||||
|
||||
export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
|
||||
|
||||
export interface AnchorSnapshot {
|
||||
id: string;
|
||||
systemId: string;
|
||||
kind: string;
|
||||
systemPosition: Vector3Dto;
|
||||
localSpaceRadius: number;
|
||||
parentAnchorId?: string | null;
|
||||
occupyingStructureId?: string | null;
|
||||
orbitReferenceId?: string | null;
|
||||
}
|
||||
|
||||
export interface AnchorDelta extends AnchorSnapshot {}
|
||||
|
||||
export interface CelestialSnapshot {
|
||||
id: string;
|
||||
systemId: string;
|
||||
kind: string;
|
||||
orbitalAnchor: Vector3Dto;
|
||||
localSpaceRadius: number;
|
||||
parentNodeId?: string | null;
|
||||
parentAnchorId?: string | null;
|
||||
occupyingStructureId?: string | null;
|
||||
orbitReferenceId?: string | null;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface TerritoryClaimSnapshot {
|
||||
sourceClaimId?: string | null;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId?: string | null;
|
||||
anchorId: string;
|
||||
status: string;
|
||||
claimKind: string;
|
||||
claimStrength: number;
|
||||
|
||||
@@ -27,8 +27,8 @@ export interface StationSnapshot {
|
||||
category: string;
|
||||
objective: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
localPosition: Vector3Dto;
|
||||
celestialId?: string | null;
|
||||
color: string;
|
||||
dockedShips: number;
|
||||
dockedShipIds: string[];
|
||||
@@ -53,7 +53,7 @@ export interface ClaimSnapshot {
|
||||
id: string;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
state: string;
|
||||
health: number;
|
||||
placedAtUtc: string;
|
||||
@@ -66,7 +66,7 @@ export interface ConstructionSiteSnapshot {
|
||||
id: string;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
targetKind: string;
|
||||
targetDefinitionId: string;
|
||||
blueprintId?: string | null;
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface PlayerDirectiveSnapshot {
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface ShipOrderSnapshot {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
@@ -43,7 +43,7 @@ export interface ShipOrderTemplateSnapshot {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
@@ -59,7 +59,7 @@ export interface DefaultBehaviorSnapshot {
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
@@ -100,7 +100,9 @@ export interface ShipSubTaskSnapshot {
|
||||
summary: string;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetNodeId?: string | null;
|
||||
targetAnchorId?: string | null;
|
||||
targetResourceNodeId?: string | null;
|
||||
targetResourceDepositId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
moduleId?: string | null;
|
||||
@@ -143,6 +145,7 @@ export interface ShipSnapshot {
|
||||
purpose: string;
|
||||
type: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
localPosition: Vector3Dto;
|
||||
localVelocity: Vector3Dto;
|
||||
targetLocalPosition: Vector3Dto;
|
||||
@@ -159,11 +162,11 @@ export interface ShipSnapshot {
|
||||
controlReason?: string | null;
|
||||
lastReplanReason?: string | null;
|
||||
lastAccessFailureReason?: string | null;
|
||||
celestialId?: string | null;
|
||||
dockedStationId?: string | null;
|
||||
commanderId?: string | null;
|
||||
policySetId?: string | null;
|
||||
cargoCapacity: number;
|
||||
cargoTypes: string[];
|
||||
travelSpeed: number;
|
||||
travelSpeedUnit: string;
|
||||
inventory: InventoryEntry[];
|
||||
@@ -178,18 +181,18 @@ export interface ShipDelta extends ShipSnapshot {}
|
||||
export interface ShipSpatialStateSnapshot {
|
||||
spaceLayer: string;
|
||||
currentSystemId: string;
|
||||
currentCelestialId?: string | null;
|
||||
currentAnchorId?: string | null;
|
||||
localPosition?: Vector3Dto | null;
|
||||
systemPosition?: Vector3Dto | null;
|
||||
movementRegime: string;
|
||||
destinationNodeId?: string | null;
|
||||
destinationAnchorId?: string | null;
|
||||
transit?: ShipTransitSnapshot | null;
|
||||
}
|
||||
|
||||
export interface ShipTransitSnapshot {
|
||||
regime: string;
|
||||
originNodeId?: string | null;
|
||||
destinationNodeId?: string | null;
|
||||
originAnchorId?: string | null;
|
||||
destinationAnchorId?: string | null;
|
||||
startedAtUtc?: string | null;
|
||||
arrivalDueAtUtc?: string | null;
|
||||
progress: number;
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
FactionSnapshot,
|
||||
} from "./contractsFactions";
|
||||
import type {
|
||||
AnchorDelta,
|
||||
AnchorSnapshot,
|
||||
CelestialDelta,
|
||||
CelestialSnapshot,
|
||||
ResourceNodeDelta,
|
||||
@@ -37,6 +39,7 @@ export interface WorldSnapshot {
|
||||
generatedAtUtc: string;
|
||||
systems: SystemSnapshot[];
|
||||
celestials: CelestialSnapshot[];
|
||||
anchors: AnchorSnapshot[];
|
||||
nodes: ResourceNodeSnapshot[];
|
||||
stations: import("./contractsInfrastructure").StationSnapshot[];
|
||||
claims: ClaimSnapshot[];
|
||||
@@ -57,6 +60,7 @@ export interface WorldDelta {
|
||||
requiresSnapshotRefresh: boolean;
|
||||
events: SimulationEventRecord[];
|
||||
celestials: CelestialDelta[];
|
||||
anchors: AnchorDelta[];
|
||||
nodes: ResourceNodeDelta[];
|
||||
stations: import("./contractsInfrastructure").StationDelta[];
|
||||
claims: ClaimDelta[];
|
||||
@@ -84,7 +88,7 @@ export interface SimulationEventRecord {
|
||||
export interface ObserverScope {
|
||||
scopeKind: string;
|
||||
systemId?: string | null;
|
||||
celestialId?: string | null;
|
||||
anchorId?: string | null;
|
||||
}
|
||||
|
||||
export interface OrbitalSimulationSnapshot {
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface PlayerDirectiveCommandRequest {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ShipOrderCommandRequest {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds?: number | null;
|
||||
@@ -28,7 +28,7 @@ export interface ShipDefaultBehaviorCommandRequest {
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
|
||||
@@ -366,6 +366,74 @@ canvas {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar-dock {
|
||||
position: absolute;
|
||||
inset: 0 0 0 auto;
|
||||
width: min(380px, 100vw);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(7, 14, 27, 0.9), rgba(7, 14, 27, 0.78)),
|
||||
radial-gradient(circle at top right, rgba(127, 214, 255, 0.08), transparent 34%);
|
||||
border-left: 1px solid rgba(132, 196, 255, 0.14);
|
||||
backdrop-filter: blur(18px);
|
||||
box-shadow: -18px 0 42px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__resize-handle {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 -8px;
|
||||
width: 16px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__resize-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 6px;
|
||||
background: rgba(132, 196, 255, 0.06);
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__resize-handle:hover::before,
|
||||
.viewer-right-sidebar__resize-handle--active::before {
|
||||
background: rgba(132, 196, 255, 0.18);
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__panel.entity-inspector-panel {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__error {
|
||||
margin-top: 0.9rem;
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 116, 88, 0.14);
|
||||
padding: 0.85rem 0.95rem;
|
||||
color: #ffd8cf;
|
||||
}
|
||||
|
||||
|
||||
.viewer-stats-overlay {
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
font-size: 0.8rem;
|
||||
@@ -1705,6 +1773,62 @@ canvas {
|
||||
color: rgba(173, 220, 255, 0.58);
|
||||
}
|
||||
|
||||
.entity-inspector-capacity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.entity-inspector-capacity {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 0.8rem 0.9rem;
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__header,
|
||||
.entity-inspector-capacity__scale {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__header {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__label {
|
||||
font-size: 0.74rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(173, 220, 255, 0.72);
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__value,
|
||||
.entity-inspector-capacity__scale span {
|
||||
font-family: var(--viewer-mono-font);
|
||||
font-size: 0.76rem;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__track {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entity-inspector-capacity__fill {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, rgba(116, 196, 255, 0.5), rgba(116, 196, 255, 0.9));
|
||||
}
|
||||
|
||||
.entity-inspector-panel__fallback {
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.83rem;
|
||||
@@ -1910,6 +2034,23 @@ canvas {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar-dock {
|
||||
inset: auto 20px 148px 20px;
|
||||
width: auto;
|
||||
max-height: 38vh;
|
||||
}
|
||||
|
||||
.viewer-right-sidebar {
|
||||
padding: 14px;
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(132, 196, 255, 0.14);
|
||||
box-shadow: 0 -18px 42px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.viewer-right-sidebar__resize-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.viewer-stats-overlay-dock {
|
||||
top: 96px;
|
||||
left: 20px;
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ViewerOrderContextMenuTarget {
|
||||
selection: Selectable;
|
||||
label: string;
|
||||
systemId?: string | null;
|
||||
anchorId?: string | null;
|
||||
itemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ResolveSelectionPositionParams {
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
planetVisuals: PlanetVisual[];
|
||||
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
|
||||
}
|
||||
|
||||
interface FocusOnSelectionParams extends ResolveSelectionPositionParams {
|
||||
@@ -47,7 +47,7 @@ interface SeedSystemFocusParams {
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
planetVisuals: PlanetVisual[];
|
||||
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
|
||||
}
|
||||
|
||||
interface CameraFocusParams {
|
||||
@@ -107,6 +107,7 @@ export function applyPanFromScreenDelta(
|
||||
delta: THREE.Vector2,
|
||||
orbitYaw: number,
|
||||
currentDistance: number,
|
||||
cameraFovDegrees: number,
|
||||
povLevel: PovLevel,
|
||||
activeSystemId: string | undefined,
|
||||
systemAnchor: THREE.Vector3,
|
||||
@@ -125,18 +126,19 @@ export function applyPanFromScreenDelta(
|
||||
|
||||
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
|
||||
const right = new THREE.Vector3(-forward.z, 0, forward.x);
|
||||
const pan = right.multiplyScalar(-normalized.x).add(forward.multiplyScalar(-normalized.y));
|
||||
const visibleHeight = 2 * Math.tan(THREE.MathUtils.degToRad(cameraFovDegrees) * 0.5) * currentDistance;
|
||||
const visibleWidth = visibleHeight * (safeWidth / safeHeight);
|
||||
const horizontalDistance = normalized.x * visibleWidth;
|
||||
const verticalDistance = -normalized.y * visibleHeight;
|
||||
const pan = right.multiplyScalar(horizontalDistance).add(forward.multiplyScalar(verticalDistance));
|
||||
|
||||
if (activeSystemId) {
|
||||
const scale = povLevel === "system"
|
||||
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.35, KILOMETERS_PER_AU * 6.5)
|
||||
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 1200, 180000);
|
||||
systemAnchor.addScaledVector(pan, scale);
|
||||
const systemDisplayToKilometers = 1 / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||
systemAnchor.addScaledVector(pan, systemDisplayToKilometers);
|
||||
return;
|
||||
}
|
||||
|
||||
const galaxyScale = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 1800, 22000);
|
||||
galaxyAnchor.addScaledVector(pan, galaxyScale);
|
||||
galaxyAnchor.add(pan);
|
||||
}
|
||||
|
||||
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
|
||||
@@ -235,11 +237,11 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
||||
}
|
||||
if (selection.kind === "claim") {
|
||||
const claim = world.claims.get(selection.id);
|
||||
return claim ? resolvePointPosition(claim.systemId, claim.celestialId) : undefined;
|
||||
return claim ? resolvePointPosition(claim.systemId, null, claim.anchorId) : undefined;
|
||||
}
|
||||
if (selection.kind === "construction-site") {
|
||||
const site = world.constructionSites.get(selection.id);
|
||||
return site ? resolvePointPosition(site.systemId, site.celestialId) : undefined;
|
||||
return site ? resolvePointPosition(site.systemId, null, site.anchorId) : undefined;
|
||||
}
|
||||
if (selection.kind === "planet") {
|
||||
const system = world.systems.get(selection.systemId);
|
||||
|
||||
@@ -30,8 +30,14 @@ export function createViewerControllers(host: any) {
|
||||
claimGroup: host.systemLayer.claimGroup,
|
||||
constructionSiteGroup: host.systemLayer.constructionSiteGroup,
|
||||
shipGroup: host.systemLayer.shipGroup,
|
||||
localNodeGroup: host.localLayer.nodeGroup,
|
||||
localStationGroup: host.localLayer.stationGroup,
|
||||
localClaimGroup: host.localLayer.claimGroup,
|
||||
localConstructionSiteGroup: host.localLayer.constructionSiteGroup,
|
||||
localShipGroup: host.localLayer.shipGroup,
|
||||
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
localSelectableTargets: host.localLayer.selectableTargets,
|
||||
systemVisuals: host.galaxyLayer.systemVisuals,
|
||||
planetVisuals: host.systemLayer.planetVisuals,
|
||||
celestialVisuals: host.systemLayer.celestialVisuals,
|
||||
@@ -40,6 +46,11 @@ export function createViewerControllers(host: any) {
|
||||
claimVisuals: host.systemLayer.claimVisuals,
|
||||
constructionSiteVisuals: host.systemLayer.constructionSiteVisuals,
|
||||
shipVisuals: host.systemLayer.shipVisuals,
|
||||
localNodeVisuals: host.localLayer.nodeVisuals,
|
||||
localStationVisuals: host.localLayer.stationVisuals,
|
||||
localClaimVisuals: host.localLayer.claimVisuals,
|
||||
localConstructionSiteVisuals: host.localLayer.constructionSiteVisuals,
|
||||
localShipVisuals: host.localLayer.shipVisuals,
|
||||
});
|
||||
|
||||
const navigationController = new ViewerNavigationController({
|
||||
@@ -152,8 +163,9 @@ export function createViewerControllers(host: any) {
|
||||
applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims),
|
||||
applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites),
|
||||
applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs),
|
||||
refreshLocalLayer: () => sceneDataController.refreshLocalLayer(host.world, host.resolveFocusedAnchorId()),
|
||||
refreshHistoryWindows: () => host.refreshHistoryWindows(),
|
||||
resolveFocusedCelestialId: () => host.resolveFocusedCelestialId(),
|
||||
resolveFocusedAnchorId: () => host.resolveFocusedAnchorId(),
|
||||
updateSystemSummaries: () => host.updateSystemSummaries(),
|
||||
applyZoomPresentation: () => presentationController.applyZoomPresentation(),
|
||||
updateNetworkPanel: () => presentationController.updateNetworkPanel(),
|
||||
@@ -191,8 +203,10 @@ export function createViewerControllers(host: any) {
|
||||
mouse: host.mouse,
|
||||
galaxyCamera: host.galaxyLayer.camera,
|
||||
systemCamera: host.systemLayer.camera,
|
||||
localCamera: host.localLayer.camera,
|
||||
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
localSelectableTargets: host.localLayer.selectableTargets,
|
||||
hoverLabelEl: host.hoverLabelEl,
|
||||
hoverConnectorLineEl: host.hoverConnectorLineEl,
|
||||
marqueeEl: host.marqueeEl,
|
||||
@@ -244,6 +258,9 @@ export function createViewerControllers(host: any) {
|
||||
delta,
|
||||
host.orbitYaw,
|
||||
host.currentDistance,
|
||||
host.activeSystemId
|
||||
? (host.povLevel === "local" ? host.localLayer.camera.fov : host.systemLayer.camera.fov)
|
||||
: host.galaxyLayer.camera.fov,
|
||||
host.povLevel,
|
||||
host.activeSystemId,
|
||||
host.systemAnchor,
|
||||
|
||||
@@ -148,9 +148,11 @@ export function updateFollowCamera(params: {
|
||||
|
||||
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||
systemAnchor.set(0, 0, 0);
|
||||
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||
const destinationCelestial = destinationNodeId ? world.celestials.get(destinationNodeId) : undefined;
|
||||
const destinationSystem = destinationCelestial ? world.systems.get(destinationCelestial.systemId) : undefined;
|
||||
const destinationAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
|
||||
const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
|
||||
const destinationSystem = destinationAnchor
|
||||
? world.systems.get(destinationAnchor.systemId)
|
||||
: undefined;
|
||||
const originSystem = world.systems.get(ship.systemId);
|
||||
if (originSystem && destinationSystem) {
|
||||
followCameraDesiredDirection
|
||||
|
||||
@@ -36,14 +36,17 @@ export function pickSelectableAtClientPosition(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
raycaster: THREE.Raycaster,
|
||||
mouse: THREE.Vector2,
|
||||
povLevel: PovLevel,
|
||||
galaxyCamera: THREE.Camera,
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
systemCamera: THREE.Camera,
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
localCamera: THREE.Camera,
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
) {
|
||||
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, clientX, clientY);
|
||||
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, povLevel, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, localCamera, localSelectableTargets, clientX, clientY);
|
||||
return hit?.selection;
|
||||
}
|
||||
|
||||
@@ -51,13 +54,23 @@ export function pickSelectableHitAtClientPosition(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
raycaster: THREE.Raycaster,
|
||||
mouse: THREE.Vector2,
|
||||
povLevel: PovLevel,
|
||||
galaxyCamera: THREE.Camera,
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
systemCamera: THREE.Camera,
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
localCamera: THREE.Camera,
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): HoverPickResult | undefined {
|
||||
if (povLevel === "local") {
|
||||
const localHit = pickOneCamera(renderer, raycaster, mouse, localCamera, localSelectableTargets, clientX, clientY);
|
||||
if (localHit) {
|
||||
return localHit;
|
||||
}
|
||||
}
|
||||
|
||||
// Try system camera first (higher priority when in a system)
|
||||
const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY);
|
||||
if (systemHit) {
|
||||
|
||||
@@ -28,8 +28,10 @@ export interface ViewerInteractionContext {
|
||||
mouse: THREE.Vector2;
|
||||
galaxyCamera: THREE.PerspectiveCamera;
|
||||
systemCamera: THREE.PerspectiveCamera;
|
||||
localCamera: THREE.PerspectiveCamera;
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
hoverLabelEl: HTMLDivElement;
|
||||
hoverConnectorLineEl: SVGLineElement;
|
||||
marqueeEl: HTMLDivElement;
|
||||
@@ -391,10 +393,13 @@ export class ViewerInteractionController {
|
||||
this.context.renderer,
|
||||
this.context.raycaster,
|
||||
this.context.mouse,
|
||||
this.context.getPovLevel(),
|
||||
this.context.galaxyCamera,
|
||||
this.context.galaxySelectableTargets,
|
||||
this.context.systemCamera,
|
||||
this.context.systemSelectableTargets,
|
||||
this.context.localCamera,
|
||||
this.context.localSelectableTargets,
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
@@ -405,10 +410,13 @@ export class ViewerInteractionController {
|
||||
this.context.renderer,
|
||||
this.context.raycaster,
|
||||
this.context.mouse,
|
||||
this.context.getPovLevel(),
|
||||
this.context.galaxyCamera,
|
||||
this.context.galaxySelectableTargets,
|
||||
this.context.systemCamera,
|
||||
this.context.systemSelectableTargets,
|
||||
this.context.localCamera,
|
||||
this.context.localSelectableTargets,
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
@@ -466,6 +474,7 @@ export class ViewerInteractionController {
|
||||
selection,
|
||||
label: node.itemId,
|
||||
systemId: node.systemId,
|
||||
anchorId: node.anchorId,
|
||||
itemId: node.itemId,
|
||||
targetPosition: node.localPosition,
|
||||
} : null;
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
import * as THREE from "three";
|
||||
import type {
|
||||
ClaimVisual,
|
||||
ConstructionSiteVisual,
|
||||
NodeVisual,
|
||||
Selectable,
|
||||
ShipVisual,
|
||||
StructureVisual,
|
||||
} from "./viewerTypes";
|
||||
|
||||
/**
|
||||
* Local rendering layer.
|
||||
* Scene coordinate unit: reserved for future close-up detail.
|
||||
* Camera far plane covers immediate surroundings.
|
||||
* Currently empty — populated when local-space objects are introduced.
|
||||
*/
|
||||
export class LocalLayer {
|
||||
readonly scene = new THREE.Scene();
|
||||
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 2000);
|
||||
readonly nodeGroup = new THREE.Group();
|
||||
readonly stationGroup = new THREE.Group();
|
||||
readonly claimGroup = new THREE.Group();
|
||||
readonly constructionSiteGroup = new THREE.Group();
|
||||
readonly shipGroup = new THREE.Group();
|
||||
|
||||
readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
|
||||
readonly shipVisuals = new Map<string, ShipVisual>();
|
||||
readonly nodeVisuals = new Map<string, NodeVisual>();
|
||||
readonly stationVisuals = new Map<string, StructureVisual>();
|
||||
readonly claimVisuals = new Map<string, ClaimVisual>();
|
||||
readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
|
||||
|
||||
private static readonly ORIGIN = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
constructor() {
|
||||
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.8));
|
||||
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.4);
|
||||
keyLight.position.set(180, 220, 140);
|
||||
this.scene.add(keyLight);
|
||||
this.scene.add(
|
||||
this.nodeGroup,
|
||||
this.stationGroup,
|
||||
this.claimGroup,
|
||||
this.constructionSiteGroup,
|
||||
this.shipGroup,
|
||||
);
|
||||
}
|
||||
|
||||
updateCamera(orbitOffset: THREE.Vector3) {
|
||||
this.camera.position.copy(orbitOffset);
|
||||
this.camera.lookAt(LocalLayer.ORIGIN);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import { scaleGalaxyVector, toThreeVector } from "./viewerMath";
|
||||
import {
|
||||
determineActiveSystemId,
|
||||
focusOnSelection,
|
||||
@@ -62,6 +63,22 @@ export interface ViewerNavigationContext {
|
||||
export class ViewerNavigationController {
|
||||
constructor(private readonly context: ViewerNavigationContext) {}
|
||||
|
||||
syncGalaxyAnchorToActiveSystem() {
|
||||
const world = this.context.getWorld();
|
||||
const activeSystemId = this.context.getActiveSystemId();
|
||||
const povLevel = this.context.getPovLevel();
|
||||
if (!world || !activeSystemId || povLevel === "galaxy") {
|
||||
return;
|
||||
}
|
||||
|
||||
const system = world.systems.get(activeSystemId);
|
||||
if (!system) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.context.galaxyAnchor.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition)));
|
||||
}
|
||||
|
||||
focusOnSelection(selection: Selectable) {
|
||||
focusOnSelection({
|
||||
world: this.context.getWorld(),
|
||||
@@ -70,7 +87,7 @@ export class ViewerNavigationController {
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId),
|
||||
resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId),
|
||||
activeSystemId: this.context.getActiveSystemId(),
|
||||
galaxyAnchor: this.context.galaxyAnchor,
|
||||
systemAnchor: this.context.systemAnchor,
|
||||
@@ -85,7 +102,7 @@ export class ViewerNavigationController {
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId),
|
||||
resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,7 +188,7 @@ export class ViewerNavigationController {
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
|
||||
resolvePointPosition: (systemIdValue, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId),
|
||||
resolvePointPosition: (systemIdValue, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId, anchorId),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const moduleProductionById = new Map<string, {
|
||||
const itemTransportById = new Map<string, string>(
|
||||
(itemsData as { id: string; transport: string }[]).map((item) => [item.id, item.transport]),
|
||||
);
|
||||
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||
import { describeAnchorPathWithinSystem, describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
|
||||
import type {
|
||||
CameraMode,
|
||||
NodeVisual,
|
||||
@@ -461,7 +461,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `${celestial.kind} celestial`,
|
||||
bodyHtml: `
|
||||
<p>${celestial.systemId}</p>
|
||||
<p>Parent ${celestial.parentNodeId ?? "none"}<br>Orbit ref ${celestial.orbitReferenceId ?? "none"}</p>
|
||||
<p>Parent ${celestial.parentAnchorId ?? "none"}<br>Orbit ref ${celestial.orbitReferenceId ?? "none"}</p>
|
||||
<p>Occupying structure ${celestial.occupyingStructureId ?? "none"}</p>
|
||||
<p>Local space radius ${celestial.localSpaceRadius.toFixed(0)} km</p>
|
||||
`,
|
||||
@@ -477,7 +477,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `Claim ${claim.id}`,
|
||||
bodyHtml: `
|
||||
<p>${claim.systemId}</p>
|
||||
<p>Celestial ${claim.celestialId}</p>
|
||||
<p>Anchor ${describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId) ?? claim.anchorId}</p>
|
||||
<p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p>
|
||||
<p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
|
||||
`,
|
||||
@@ -494,7 +494,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `Construction ${site.id}`,
|
||||
bodyHtml: `
|
||||
<p>${site.systemId}</p>
|
||||
<p>Celestial ${site.celestialId}</p>
|
||||
<p>Anchor ${describeAnchorPathWithinSystem(world, site.systemId, site.anchorId) ?? site.anchorId}</p>
|
||||
<p>${site.targetKind} ${site.targetDefinitionId}</p>
|
||||
<p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
|
||||
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
|
||||
@@ -608,24 +608,33 @@ export function describeSelectionParent(
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return station.celestialId
|
||||
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId) ?? `${station.systemId} network`
|
||||
return station.anchorId
|
||||
? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId) ?? `${station.systemId} network`
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "node") {
|
||||
const node = world.nodes.get(selection.id);
|
||||
const visual = node ? nodeVisuals.get(selection.id) : undefined;
|
||||
return describeOrbitalParent(world, node?.systemId, visual?.anchor);
|
||||
return node
|
||||
? describeAnchorPathWithinSystem(world, node.systemId, node.anchorId) ?? node.anchorId
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "celestial") {
|
||||
const celestial = world.celestials.get(selection.id);
|
||||
return celestial?.parentNodeId ?? `${celestial?.systemId ?? "unknown"} network`;
|
||||
return celestial
|
||||
? describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${celestial.systemId} network`
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "claim") {
|
||||
return world.claims.get(selection.id)?.celestialId ?? "unknown";
|
||||
const claim = world.claims.get(selection.id);
|
||||
return claim
|
||||
? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId) ?? claim.anchorId
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "construction-site") {
|
||||
return world.constructionSites.get(selection.id)?.celestialId ?? "unknown";
|
||||
const site = world.constructionSites.get(selection.id);
|
||||
return site
|
||||
? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId) ?? site.anchorId
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
|
||||
@@ -14,6 +14,16 @@ import {
|
||||
syncShips as syncShipScene,
|
||||
syncStations as syncStationScene,
|
||||
} from "./viewerSceneSync";
|
||||
import {
|
||||
createClaimMesh,
|
||||
createConstructionSiteMesh,
|
||||
createNodeMesh,
|
||||
createResourceDepositMesh,
|
||||
createShipMesh,
|
||||
createShipTacticalIcon,
|
||||
createStationMesh,
|
||||
createTacticalIcon,
|
||||
} from "./viewerSceneFactory";
|
||||
import {
|
||||
deriveNodeOrbital,
|
||||
deriveOrbitalFromLocalPosition,
|
||||
@@ -43,7 +53,7 @@ import type {
|
||||
SystemSnapshot,
|
||||
} from "./contracts";
|
||||
import type { OrbitalAnchor, Selectable } from "./viewerTypes";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
import { rawObject, registerSelectableTarget } from "./viewerScenePrimitives";
|
||||
|
||||
export interface ViewerSceneDataContext {
|
||||
documentRef: Document;
|
||||
@@ -61,8 +71,14 @@ export interface ViewerSceneDataContext {
|
||||
claimGroup: THREE.Group;
|
||||
constructionSiteGroup: THREE.Group;
|
||||
shipGroup: THREE.Group;
|
||||
localNodeGroup: THREE.Group;
|
||||
localStationGroup: THREE.Group;
|
||||
localClaimGroup: THREE.Group;
|
||||
localConstructionSiteGroup: THREE.Group;
|
||||
localShipGroup: THREE.Group;
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemVisuals: Map<any, any>;
|
||||
planetVisuals: any[];
|
||||
celestialVisuals: Map<any, any>;
|
||||
@@ -71,6 +87,11 @@ export interface ViewerSceneDataContext {
|
||||
claimVisuals: Map<any, any>;
|
||||
constructionSiteVisuals: Map<any, any>;
|
||||
shipVisuals: Map<any, any>;
|
||||
localNodeVisuals: Map<any, any>;
|
||||
localStationVisuals: Map<any, any>;
|
||||
localClaimVisuals: Map<any, any>;
|
||||
localConstructionSiteVisuals: Map<any, any>;
|
||||
localShipVisuals: Map<any, any>;
|
||||
}
|
||||
|
||||
export class ViewerSceneDataController {
|
||||
@@ -136,6 +157,162 @@ export class ViewerSceneDataController {
|
||||
applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
|
||||
}
|
||||
|
||||
refreshLocalLayer(world: any, focusedAnchorId?: string) {
|
||||
this.context.localNodeGroup.clear();
|
||||
this.context.localStationGroup.clear();
|
||||
this.context.localClaimGroup.clear();
|
||||
this.context.localConstructionSiteGroup.clear();
|
||||
this.context.localShipGroup.clear();
|
||||
this.context.localSelectableTargets.clear();
|
||||
this.context.localNodeVisuals.clear();
|
||||
this.context.localStationVisuals.clear();
|
||||
this.context.localClaimVisuals.clear();
|
||||
this.context.localConstructionSiteVisuals.clear();
|
||||
this.context.localShipVisuals.clear();
|
||||
|
||||
if (!world || !focusedAnchorId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSystemId = this.context.getActiveSystemId();
|
||||
const inFocusedSystem = (systemId: string) => !activeSystemId || systemId === activeSystemId;
|
||||
|
||||
for (const node of world.nodes.values()) {
|
||||
if (node.anchorId !== focusedAnchorId || !inFocusedSystem(node.systemId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mesh = createNodeMesh(node);
|
||||
const icon = createTacticalIcon(this.context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
|
||||
const localPosition = new THREE.Vector3(0, 0, 0);
|
||||
mesh.setPosition(localPosition);
|
||||
icon.setPosition(localPosition);
|
||||
this.context.localNodeVisuals.set(node.id, {
|
||||
systemId: node.systemId,
|
||||
anchorId: node.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
sourceKind: node.sourceKind,
|
||||
anchor: { kind: "star" as const },
|
||||
localPosition,
|
||||
orbitRadius: 0,
|
||||
orbitPhase: 0,
|
||||
orbitInclination: 0,
|
||||
});
|
||||
this.context.localNodeGroup.add(rawObject(mesh), rawObject(icon));
|
||||
for (const deposit of node.deposits) {
|
||||
const depositMesh = createResourceDepositMesh(deposit, node);
|
||||
this.context.localNodeGroup.add(rawObject(depositMesh));
|
||||
}
|
||||
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "node", id: node.id });
|
||||
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "node", id: node.id });
|
||||
}
|
||||
|
||||
for (const station of world.stations.values()) {
|
||||
if (station.anchorId !== focusedAnchorId || !inFocusedSystem(station.systemId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mesh = createStationMesh(station);
|
||||
const icon = createTacticalIcon(this.context.documentRef, station.color, 130);
|
||||
const localPosition = new THREE.Vector3(station.localPosition.x, station.localPosition.y, station.localPosition.z);
|
||||
mesh.setPosition(localPosition);
|
||||
icon.setPosition(localPosition);
|
||||
this.context.localStationVisuals.set(station.id, {
|
||||
id: station.id,
|
||||
systemId: station.systemId,
|
||||
anchorId: station.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
anchor: { kind: "star" as const },
|
||||
orbitRadius: 0,
|
||||
orbitPhase: 0,
|
||||
orbitInclination: 0,
|
||||
localPosition,
|
||||
});
|
||||
this.context.localStationGroup.add(rawObject(mesh), rawObject(icon));
|
||||
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "station", id: station.id });
|
||||
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "station", id: station.id });
|
||||
}
|
||||
|
||||
for (const claim of world.claims.values()) {
|
||||
if (claim.anchorId !== focusedAnchorId || !inFocusedSystem(claim.systemId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mesh = createClaimMesh(claim);
|
||||
const icon = createTacticalIcon(this.context.documentRef, "#ff5b5b", 90);
|
||||
const localPosition = new THREE.Vector3(0, 0, 0);
|
||||
mesh.setPosition(localPosition);
|
||||
icon.setPosition(localPosition);
|
||||
this.context.localClaimVisuals.set(claim.id, {
|
||||
id: claim.id,
|
||||
anchorId: claim.anchorId,
|
||||
systemId: claim.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
localPosition,
|
||||
});
|
||||
this.context.localClaimGroup.add(rawObject(mesh), rawObject(icon));
|
||||
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "claim", id: claim.id });
|
||||
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "claim", id: claim.id });
|
||||
}
|
||||
|
||||
for (const site of world.constructionSites.values()) {
|
||||
if (site.anchorId !== focusedAnchorId || !inFocusedSystem(site.systemId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mesh = createConstructionSiteMesh(site);
|
||||
const icon = createTacticalIcon(this.context.documentRef, "#9df29c", 90);
|
||||
const localPosition = new THREE.Vector3(0, 0, 0);
|
||||
mesh.setPosition(localPosition);
|
||||
icon.setPosition(localPosition);
|
||||
this.context.localConstructionSiteVisuals.set(site.id, {
|
||||
id: site.id,
|
||||
anchorId: site.anchorId,
|
||||
systemId: site.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
localPosition,
|
||||
});
|
||||
this.context.localConstructionSiteGroup.add(rawObject(mesh), rawObject(icon));
|
||||
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "construction-site", id: site.id });
|
||||
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "construction-site", id: site.id });
|
||||
}
|
||||
|
||||
for (const ship of world.ships.values()) {
|
||||
const shipAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
|
||||
if (shipAnchorId !== focusedAnchorId || !inFocusedSystem(ship.systemId) || ship.spatialState.spaceLayer !== "local-space") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const shipColor = shipPresentationColor(ship);
|
||||
const mesh = createShipMesh(ship, shipSize(ship), shipLength(ship), shipColor);
|
||||
const icon = createShipTacticalIcon(this.context.documentRef, shipColor, 78);
|
||||
const localPosition = new THREE.Vector3(ship.localPosition.x, ship.localPosition.y, ship.localPosition.z);
|
||||
mesh.setPosition(localPosition);
|
||||
icon.setPosition(localPosition);
|
||||
icon.setColor(shipColor);
|
||||
this.context.localShipVisuals.set(ship.id, {
|
||||
systemId: ship.systemId,
|
||||
anchorId: shipAnchorId,
|
||||
mesh,
|
||||
icon,
|
||||
iconBaseScale: 78,
|
||||
startPosition: localPosition.clone(),
|
||||
authoritativePosition: localPosition.clone(),
|
||||
targetPosition: new THREE.Vector3(ship.targetLocalPosition.x, ship.targetLocalPosition.y, ship.targetLocalPosition.z),
|
||||
velocity: new THREE.Vector3(ship.localVelocity.x, ship.localVelocity.y, ship.localVelocity.z),
|
||||
receivedAtMs: performance.now(),
|
||||
blendDurationMs: Math.max(world.tickIntervalMs ?? 80, 80),
|
||||
});
|
||||
this.context.localShipGroup.add(rawObject(mesh), rawObject(icon));
|
||||
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "ship", id: ship.id });
|
||||
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "ship", id: ship.id });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the active system changes. Swaps which system's root is in systemScene
|
||||
* and updates visibility of all system-filtered objects.
|
||||
@@ -202,6 +379,7 @@ export class ViewerSceneDataController {
|
||||
createWorldPresentationContext(overrides: {
|
||||
world: any;
|
||||
activeSystemId?: string;
|
||||
focusedAnchorId?: string;
|
||||
cameraMode: any;
|
||||
povLevel: any;
|
||||
orbitYaw: number;
|
||||
@@ -215,17 +393,23 @@ export class ViewerSceneDataController {
|
||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||
worldSeed: this.context.getWorldSeed(),
|
||||
activeSystemId: overrides.activeSystemId,
|
||||
focusedAnchorId: overrides.focusedAnchorId,
|
||||
cameraMode: overrides.cameraMode,
|
||||
povLevel: overrides.povLevel,
|
||||
orbitYaw: overrides.orbitYaw,
|
||||
camera: overrides.systemCamera,
|
||||
systemAnchor: overrides.systemAnchor,
|
||||
shipVisuals: this.context.shipVisuals,
|
||||
localShipVisuals: this.context.localShipVisuals,
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
localNodeVisuals: this.context.localNodeVisuals,
|
||||
celestialVisuals: this.context.celestialVisuals,
|
||||
stationVisuals: this.context.stationVisuals,
|
||||
localStationVisuals: this.context.localStationVisuals,
|
||||
claimVisuals: this.context.claimVisuals,
|
||||
localClaimVisuals: this.context.localClaimVisuals,
|
||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||
localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
|
||||
systemVisuals: this.context.systemVisuals,
|
||||
systemSummaryVisuals: new Map(),
|
||||
toDisplayLocalPosition: overrides.toDisplayLocalPosition,
|
||||
@@ -248,8 +432,14 @@ export class ViewerSceneDataController {
|
||||
claimGroup: this.context.claimGroup,
|
||||
constructionSiteGroup: this.context.constructionSiteGroup,
|
||||
shipGroup: this.context.shipGroup,
|
||||
localNodeGroup: this.context.localNodeGroup,
|
||||
localStationGroup: this.context.localStationGroup,
|
||||
localClaimGroup: this.context.localClaimGroup,
|
||||
localConstructionSiteGroup: this.context.localConstructionSiteGroup,
|
||||
localShipGroup: this.context.localShipGroup,
|
||||
galaxySelectableTargets: this.context.galaxySelectableTargets,
|
||||
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||
localSelectableTargets: this.context.localSelectableTargets,
|
||||
systemVisuals: this.context.systemVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
celestialVisuals: this.context.celestialVisuals,
|
||||
@@ -258,12 +448,17 @@ export class ViewerSceneDataController {
|
||||
claimVisuals: this.context.claimVisuals,
|
||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||
shipVisuals: this.context.shipVisuals,
|
||||
localNodeVisuals: this.context.localNodeVisuals,
|
||||
localStationVisuals: this.context.localStationVisuals,
|
||||
localClaimVisuals: this.context.localClaimVisuals,
|
||||
localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
|
||||
localShipVisuals: this.context.localShipVisuals,
|
||||
shipSize,
|
||||
shipLength,
|
||||
shipPresentationColor,
|
||||
celestialColor,
|
||||
createCirclePoints,
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId),
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId, anchorId),
|
||||
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition),
|
||||
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor),
|
||||
deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor),
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ConstructionSiteSnapshot,
|
||||
MoonSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceDepositSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
ShipSnapshot,
|
||||
StationSnapshot,
|
||||
@@ -46,6 +47,23 @@ export function createNodeMesh(node: ResourceNodeSnapshot): SceneNode {
|
||||
return createSceneNode(mesh);
|
||||
}
|
||||
|
||||
export function createResourceDepositMesh(deposit: ResourceDepositSnapshot, node: ResourceNodeSnapshot): SceneNode {
|
||||
const isGas = node.sourceKind === "gas-cloud" || node.itemId === "gas";
|
||||
const oreRatio = deposit.maxOre <= 0.01 ? 0 : deposit.oreRemaining / deposit.maxOre;
|
||||
const mesh = new THREE.Mesh(
|
||||
isGas ? new THREE.SphereGeometry(10, 12, 12) : new THREE.DodecahedronGeometry(8 + (oreRatio * 5), 0),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: isGas ? 0x7fd6ff : 0xc9a165,
|
||||
flatShading: !isGas,
|
||||
transparent: isGas,
|
||||
opacity: isGas ? 0.55 : 1,
|
||||
emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xc9a165).multiplyScalar(isGas ? 0.18 : 0.05),
|
||||
}),
|
||||
);
|
||||
mesh.position.copy(toThreeVector(deposit.localPosition));
|
||||
return createSceneNode(mesh);
|
||||
}
|
||||
|
||||
export function createCelestialMesh(node: CelestialSnapshot, celestialColor: (kind: string) => string): SceneNode {
|
||||
const color = celestialColor(node.kind);
|
||||
return createSceneNode(new THREE.Mesh(
|
||||
|
||||
@@ -70,6 +70,18 @@ function toSystemPos(localPosition: THREE.Vector3): THREE.Vector3 {
|
||||
return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||
}
|
||||
|
||||
function resolveShipSystemPosition(ship: ShipSnapshot | ShipDelta, context: SceneSyncContext) {
|
||||
if (ship.spatialState.systemPosition) {
|
||||
return toThreeVector(ship.spatialState.systemPosition);
|
||||
}
|
||||
|
||||
if (ship.anchorId) {
|
||||
return context.resolvePointPosition(ship.systemId, null, ship.anchorId);
|
||||
}
|
||||
|
||||
return toThreeVector(ship.localPosition);
|
||||
}
|
||||
|
||||
interface SceneSyncContext {
|
||||
documentRef: Document;
|
||||
worldOrbitalTimeSeconds?: number;
|
||||
@@ -98,7 +110,7 @@ interface SceneSyncContext {
|
||||
shipPresentationColor: (ship: ShipSnapshot) => string;
|
||||
celestialColor: (kind: string) => string;
|
||||
createCirclePoints: (radius: number, segments: number) => THREE.Vector3[];
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3;
|
||||
resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
|
||||
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => NodeVisual["anchor"];
|
||||
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => {
|
||||
radius: number;
|
||||
@@ -250,7 +262,8 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
||||
const mesh = createNodeMesh(node);
|
||||
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
|
||||
const localPosition = toThreeVector(node.localPosition);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const systemPosition = context.resolvePointPosition(node.systemId, null, node.anchorId);
|
||||
const displayPos = toSystemPos(systemPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
const isActive = node.systemId === activeSystemId;
|
||||
@@ -260,6 +273,7 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
||||
const orbital = context.deriveNodeOrbital(node, anchor);
|
||||
context.nodeVisuals.set(node.id, {
|
||||
systemId: node.systemId,
|
||||
anchorId: node.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
sourceKind: node.sourceKind,
|
||||
@@ -283,7 +297,8 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
||||
const mesh = createStationMesh(station);
|
||||
const icon = createTacticalIcon(context.documentRef, station.color, 130);
|
||||
const localPosition = toThreeVector(station.localPosition);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const systemPosition = context.resolvePointPosition(station.systemId, null, station.anchorId);
|
||||
const displayPos = toSystemPos(systemPosition);
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
const isActive = station.systemId === activeSystemId;
|
||||
@@ -294,6 +309,7 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
||||
context.stationVisuals.set(station.id, {
|
||||
id: station.id,
|
||||
systemId: station.systemId,
|
||||
anchorId: station.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
anchor,
|
||||
@@ -313,7 +329,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
|
||||
context.claimVisuals.clear();
|
||||
|
||||
for (const claim of claims) {
|
||||
const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId);
|
||||
const localPosition = context.resolvePointPosition(claim.systemId, null, claim.anchorId);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const mesh = createClaimMesh(claim);
|
||||
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90);
|
||||
@@ -324,7 +340,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
|
||||
icon.setVisible(isActive);
|
||||
context.claimVisuals.set(claim.id, {
|
||||
id: claim.id,
|
||||
celestialId: claim.celestialId,
|
||||
anchorId: claim.anchorId,
|
||||
systemId: claim.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
@@ -341,7 +357,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
|
||||
context.constructionSiteVisuals.clear();
|
||||
|
||||
for (const site of sites) {
|
||||
const localPosition = context.resolvePointPosition(site.systemId, site.celestialId);
|
||||
const localPosition = context.resolvePointPosition(site.systemId, null, site.anchorId);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const mesh = createConstructionSiteMesh(site);
|
||||
const icon = createTacticalIcon(context.documentRef, "#9df29c", 90);
|
||||
@@ -352,7 +368,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
|
||||
icon.setVisible(isActive);
|
||||
context.constructionSiteVisuals.set(site.id, {
|
||||
id: site.id,
|
||||
celestialId: site.celestialId,
|
||||
anchorId: site.anchorId,
|
||||
systemId: site.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
@@ -374,7 +390,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
const iconBaseScale = 78;
|
||||
const icon = createShipTacticalIcon(context.documentRef, shipColor, iconBaseScale);
|
||||
const localPosition = toThreeVector(ship.localPosition);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const displayPos = toSystemPos(resolveShipSystemPosition(ship, context));
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
icon.setColor(shipColor);
|
||||
@@ -386,6 +402,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "ship", id: ship.id });
|
||||
context.shipVisuals.set(ship.id, {
|
||||
systemId: ship.systemId,
|
||||
anchorId: ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined,
|
||||
mesh,
|
||||
icon,
|
||||
iconBaseScale,
|
||||
@@ -430,6 +447,7 @@ export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDe
|
||||
}
|
||||
|
||||
visual.systemId = node.systemId;
|
||||
visual.anchorId = node.anchorId;
|
||||
visual.sourceKind = node.sourceKind;
|
||||
visual.localPosition.copy(toThreeVector(node.localPosition));
|
||||
visual.anchor = context.resolveOrbitalAnchor(node.systemId, visual.localPosition);
|
||||
@@ -452,6 +470,7 @@ export function applyStationDeltas(context: SceneSyncContext, stations: StationD
|
||||
}
|
||||
|
||||
visual.systemId = station.systemId;
|
||||
visual.anchorId = station.anchorId;
|
||||
visual.localPosition.copy(toThreeVector(station.localPosition));
|
||||
visual.anchor = context.resolveOrbitalAnchor(station.systemId, visual.localPosition);
|
||||
const orbital = context.deriveOrbitalFromLocalPosition(visual.localPosition, station.systemId, visual.anchor);
|
||||
@@ -474,7 +493,8 @@ export function applyClaimDeltas(context: SceneSyncContext, claims: ClaimDelta[]
|
||||
}
|
||||
|
||||
visual.systemId = claim.systemId;
|
||||
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, claim.celestialId));
|
||||
visual.anchorId = claim.anchorId;
|
||||
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, null, claim.anchorId));
|
||||
const displayPos = toSystemPos(visual.localPosition);
|
||||
visual.mesh.setPosition(displayPos);
|
||||
visual.icon.setPosition(displayPos);
|
||||
@@ -494,7 +514,8 @@ export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: Co
|
||||
}
|
||||
|
||||
visual.systemId = site.systemId;
|
||||
visual.localPosition.copy(context.resolvePointPosition(site.systemId, site.celestialId));
|
||||
visual.anchorId = site.anchorId;
|
||||
visual.localPosition.copy(context.resolvePointPosition(site.systemId, null, site.anchorId));
|
||||
const displayPos = toSystemPos(visual.localPosition);
|
||||
visual.mesh.setPosition(displayPos);
|
||||
visual.icon.setPosition(displayPos);
|
||||
@@ -514,6 +535,7 @@ export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], t
|
||||
}
|
||||
|
||||
visual.systemId = ship.systemId;
|
||||
visual.anchorId = ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined;
|
||||
visual.startPosition.copy(getAnimatedShipLocalPosition(visual));
|
||||
visual.authoritativePosition.copy(toThreeVector(ship.localPosition));
|
||||
visual.targetPosition.copy(toThreeVector(ship.targetLocalPosition));
|
||||
|
||||
@@ -20,10 +20,17 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
|
||||
return world.stations.get(item.id)?.label ?? item.id;
|
||||
}
|
||||
if (item.kind === "node") {
|
||||
return item.id;
|
||||
const node = world.nodes.get(item.id);
|
||||
return node ? `${node.itemId} source` : item.id;
|
||||
}
|
||||
if (item.kind === "celestial") {
|
||||
return `${world.celestials.get(item.id)?.kind ?? "celestial"} ${item.id}`;
|
||||
const celestial = world.celestials.get(item.id);
|
||||
if (!celestial) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id)
|
||||
?? `${world.systems.get(celestial.systemId)?.label ?? celestial.systemId} / ${celestial.kind}`;
|
||||
}
|
||||
if (item.kind === "claim") {
|
||||
return `claim ${item.id}`;
|
||||
@@ -113,9 +120,7 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
||||
return item.id;
|
||||
}
|
||||
|
||||
const anchorPath = node.celestialId
|
||||
? describeCelestialPathWithinSystem(world, node.systemId, node.celestialId)
|
||||
: undefined;
|
||||
const anchorPath = describeAnchorPathWithinSystem(world, node.systemId, node.anchorId);
|
||||
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
|
||||
}
|
||||
|
||||
@@ -135,16 +140,16 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
||||
|
||||
if (item.kind === "claim") {
|
||||
const claim = world.claims.get(item.id);
|
||||
const anchorPath = claim?.celestialId
|
||||
? describeCelestialPathWithinSystem(world, claim.systemId, claim.celestialId)
|
||||
const anchorPath = claim
|
||||
? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId)
|
||||
: undefined;
|
||||
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
|
||||
}
|
||||
|
||||
if (item.kind === "construction-site") {
|
||||
const site = world.constructionSites.get(item.id);
|
||||
const anchorPath = site?.celestialId
|
||||
? describeCelestialPathWithinSystem(world, site.systemId, site.celestialId)
|
||||
const anchorPath = site
|
||||
? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId)
|
||||
: undefined;
|
||||
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
|
||||
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
|
||||
@@ -210,20 +215,74 @@ export function resolveFocusedCelestialId(world: WorldState | undefined, selecte
|
||||
return selected.id;
|
||||
}
|
||||
if (selected.kind === "ship") {
|
||||
return world.ships.get(selected.id)?.spatialState.currentCelestialId ?? world.ships.get(selected.id)?.celestialId ?? undefined;
|
||||
const ship = world.ships.get(selected.id);
|
||||
return ship?.spatialState.currentAnchorId && world.celestials.has(ship.spatialState.currentAnchorId)
|
||||
? ship.spatialState.currentAnchorId
|
||||
: (ship?.anchorId && world.celestials.has(ship.anchorId) ? ship.anchorId : undefined);
|
||||
}
|
||||
if (selected.kind === "station") {
|
||||
return world.stations.get(selected.id)?.celestialId ?? undefined;
|
||||
const station = world.stations.get(selected.id);
|
||||
return station?.anchorId && world.celestials.has(station.anchorId) ? station.anchorId : undefined;
|
||||
}
|
||||
if (selected.kind === "claim") {
|
||||
return world.claims.get(selected.id)?.celestialId ?? undefined;
|
||||
const claim = world.claims.get(selected.id);
|
||||
return claim && world.celestials.has(claim.anchorId) ? claim.anchorId : undefined;
|
||||
}
|
||||
if (selected.kind === "construction-site") {
|
||||
return world.constructionSites.get(selected.id)?.celestialId ?? undefined;
|
||||
const site = world.constructionSites.get(selected.id);
|
||||
return site && world.celestials.has(site.anchorId) ? site.anchorId : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveFocusedAnchorId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
|
||||
if (!world || selectedItems.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const selected = selectedItems[0];
|
||||
if (selected.kind === "node") {
|
||||
return world.nodes.get(selected.id)?.anchorId;
|
||||
}
|
||||
if (selected.kind === "ship") {
|
||||
const ship = world.ships.get(selected.id);
|
||||
return ship?.spatialState.currentAnchorId
|
||||
?? ship?.anchorId
|
||||
?? resolveFocusedCelestialId(world, selectedItems);
|
||||
}
|
||||
if (selected.kind === "station") {
|
||||
const station = world.stations.get(selected.id);
|
||||
return station?.anchorId
|
||||
?? resolveFocusedCelestialId(world, selectedItems);
|
||||
}
|
||||
if (selected.kind === "claim") {
|
||||
const claim = world.claims.get(selected.id);
|
||||
return claim?.anchorId
|
||||
?? resolveFocusedCelestialId(world, selectedItems);
|
||||
}
|
||||
if (selected.kind === "construction-site") {
|
||||
const site = world.constructionSites.get(selected.id);
|
||||
return site?.anchorId
|
||||
?? resolveFocusedCelestialId(world, selectedItems);
|
||||
}
|
||||
if (selected.kind === "celestial") {
|
||||
if (world.anchors.has(selected.id)) {
|
||||
return selected.id;
|
||||
}
|
||||
|
||||
const orbitBackedAnchor = [...world.anchors.values()].find((anchor) => anchor.orbitReferenceId === selected.id);
|
||||
return orbitBackedAnchor?.id;
|
||||
}
|
||||
if (selected.kind === "planet") {
|
||||
return `${selected.systemId}-planet-${selected.planetIndex + 1}`;
|
||||
}
|
||||
if (selected.kind === "moon") {
|
||||
return `${selected.systemId}-planet-${selected.planetIndex + 1}-moon-${selected.moonIndex + 1}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function describeOrbitalParent(world: WorldState | undefined, systemId?: string, anchor?: OrbitalAnchor): string {
|
||||
if (!world || !systemId) {
|
||||
return "unknown";
|
||||
@@ -330,23 +389,31 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
|
||||
return baseState;
|
||||
}
|
||||
|
||||
const destinationNodeId = ship.spatialState.destinationNodeId ?? ship.spatialState.transit?.destinationNodeId;
|
||||
if (!destinationNodeId) {
|
||||
const destinationAnchorId = ship.spatialState.destinationAnchorId ?? ship.spatialState.transit?.destinationAnchorId;
|
||||
if (!destinationAnchorId) {
|
||||
return baseState;
|
||||
}
|
||||
|
||||
const destinationCelestial = world.celestials.get(destinationNodeId);
|
||||
if (!destinationCelestial) {
|
||||
return `${baseState} -> ${destinationNodeId}`;
|
||||
}
|
||||
|
||||
const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
|
||||
if (baseState === "warping" || baseState === "spooling-warp") {
|
||||
const destinationPath = describeCelestialPathWithinSystem(world, destinationCelestial.systemId, destinationNodeId);
|
||||
return `${baseState} -> ${destinationPath ?? destinationNodeId}`;
|
||||
const destinationSystemId = destinationAnchor?.systemId ?? ship.spatialState.currentSystemId ?? ship.systemId;
|
||||
const destinationPath = describeAnchorPathWithinSystem(
|
||||
world,
|
||||
destinationSystemId,
|
||||
destinationAnchorId,
|
||||
);
|
||||
return `${baseState} -> ${destinationPath ?? destinationAnchorId}`;
|
||||
}
|
||||
|
||||
const destinationSystem = world.systems.get(destinationCelestial.systemId);
|
||||
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
||||
const destinationSystemId = destinationAnchor?.systemId
|
||||
?? ship.spatialState.currentSystemId
|
||||
?? ship.systemId;
|
||||
const destinationSystem = world.systems.get(destinationSystemId);
|
||||
if (!destinationSystem) {
|
||||
return `${baseState} -> ${destinationAnchorId}`;
|
||||
}
|
||||
|
||||
return `${baseState} -> ${destinationSystem.label}`;
|
||||
}
|
||||
|
||||
export function describeShipObjective(objective: string): string {
|
||||
@@ -406,8 +473,8 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
||||
if (ship.dockedStationId) {
|
||||
const station = world.stations.get(ship.dockedStationId);
|
||||
if (station) {
|
||||
const anchorPath = station.celestialId
|
||||
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId)
|
||||
const anchorPath = station.anchorId
|
||||
? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId)
|
||||
: undefined;
|
||||
return {
|
||||
system: systemLabel,
|
||||
@@ -416,11 +483,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
||||
}
|
||||
}
|
||||
|
||||
const currentCelestialId = ship.spatialState.currentCelestialId ?? ship.celestialId;
|
||||
if (currentCelestialId) {
|
||||
const celestialPath = describeCelestialPathWithinSystem(world, systemId, currentCelestialId);
|
||||
if (celestialPath) {
|
||||
return { system: systemLabel, local: celestialPath };
|
||||
const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
|
||||
if (currentAnchorId) {
|
||||
const anchorPath = describeAnchorPathWithinSystem(world, systemId, currentAnchorId);
|
||||
if (anchorPath) {
|
||||
return { system: systemLabel, local: anchorPath };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,9 +513,9 @@ export function describeActiveSpace(
|
||||
return activeSystem.label;
|
||||
}
|
||||
|
||||
const celestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||
if (celestialId) {
|
||||
const localPath = describeCelestialPathWithinSystem(world, activeSystem.id, celestialId);
|
||||
const anchorId = resolveFocusedAnchorId(world, selectedItems);
|
||||
if (anchorId) {
|
||||
const localPath = describeAnchorPathWithinSystem(world, activeSystem.id, anchorId);
|
||||
return localPath
|
||||
? `${activeSystem.label} / ${localPath}`
|
||||
: activeSystem.label;
|
||||
@@ -472,10 +539,9 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (celestial.parentNodeId) {
|
||||
const parentPath = describeCelestialPathWithinSystem(world, systemId, celestial.parentNodeId);
|
||||
const segment = describeCelestialSegment(system, celestial);
|
||||
return parentPath ? `${parentPath}/${segment}` : segment;
|
||||
const anchorId = resolveAnchorIdForCelestial(world, celestialId);
|
||||
if (anchorId) {
|
||||
return describeAnchorPathWithinSystem(world, systemId, anchorId);
|
||||
}
|
||||
|
||||
if (celestial.kind === "star") {
|
||||
@@ -485,6 +551,60 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
|
||||
return describeCelestialSegment(system, celestial);
|
||||
}
|
||||
|
||||
export function describeAnchorPathWithinSystem(world: WorldState, systemId: string, anchorId: string, celestialId?: string | null): string | undefined {
|
||||
const anchor = world.anchors.get(anchorId);
|
||||
if (anchor?.parentAnchorId) {
|
||||
const parentPath = describeAnchorPathWithinSystem(world, systemId, anchor.parentAnchorId);
|
||||
const segment = describeAnchorSegment(anchor);
|
||||
return parentPath ? `${parentPath}/${segment}` : segment;
|
||||
}
|
||||
|
||||
if (celestialId) {
|
||||
return describeCelestialPathWithinSystem(world, systemId, celestialId);
|
||||
}
|
||||
|
||||
if (!anchor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return describeAnchorSegment(anchor);
|
||||
}
|
||||
|
||||
function describeAnchorSegment(anchor: { kind: string; id: string; orbitReferenceId?: string | null }): string {
|
||||
if (anchor.orbitReferenceId) {
|
||||
return describeAnchorOrbitReference(anchor.orbitReferenceId);
|
||||
}
|
||||
|
||||
if (anchor.kind === "resource-node") {
|
||||
return anchor.id;
|
||||
}
|
||||
|
||||
return anchor.kind.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
function resolveAnchorIdForCelestial(world: WorldState, celestialId: string): string | undefined {
|
||||
return world.anchors.has(celestialId) ? celestialId : undefined;
|
||||
}
|
||||
|
||||
function describeAnchorOrbitReference(referenceId: string): string {
|
||||
const lagrangeMatch = referenceId.match(/(l[1-5])$/i);
|
||||
if (lagrangeMatch) {
|
||||
return lagrangeMatch[1].toUpperCase();
|
||||
}
|
||||
|
||||
const moonMatch = referenceId.match(/moon-(\d+)$/i);
|
||||
if (moonMatch) {
|
||||
return `Moon ${moonMatch[1]}`;
|
||||
}
|
||||
|
||||
const planetMatch = referenceId.match(/planet-(\d+)$/i);
|
||||
if (planetMatch) {
|
||||
return `Planet ${planetMatch[1]}`;
|
||||
}
|
||||
|
||||
return referenceId;
|
||||
}
|
||||
|
||||
function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string {
|
||||
const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/);
|
||||
if (moonMatch) {
|
||||
|
||||
@@ -41,6 +41,7 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
|
||||
generatedAtUtc: snapshot.generatedAtUtc,
|
||||
systems: new Map(snapshot.systems.map((system) => [system.id, system])),
|
||||
celestials: new Map(snapshot.celestials.map((celestial) => [celestial.id, celestial])),
|
||||
anchors: new Map(snapshot.anchors.map((anchor) => [anchor.id, anchor])),
|
||||
nodes: new Map(snapshot.nodes.map((node) => [node.id, node])),
|
||||
stations: new Map(snapshot.stations.map((station) => [station.id, station])),
|
||||
claims: new Map(snapshot.claims.map((claim) => [claim.id, claim])),
|
||||
@@ -65,6 +66,9 @@ export function applyDeltaToWorld(world: WorldState, delta: WorldDelta): boolean
|
||||
for (const celestial of delta.celestials) {
|
||||
world.celestials.set(celestial.id, celestial);
|
||||
}
|
||||
for (const anchor of delta.anchors) {
|
||||
world.anchors.set(anchor.id, anchor);
|
||||
}
|
||||
for (const node of delta.nodes) {
|
||||
world.nodes.set(node.id, node);
|
||||
}
|
||||
@@ -101,6 +105,7 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
|
||||
+ delta.stations.length
|
||||
+ delta.nodes.length
|
||||
+ delta.celestials.length
|
||||
+ delta.anchors.length
|
||||
+ delta.claims.length
|
||||
+ delta.constructionSites.length
|
||||
+ delta.marketOrders.length
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as THREE from "three";
|
||||
import type { SceneNode } from "./viewerScenePrimitives";
|
||||
import type {
|
||||
AnchorSnapshot,
|
||||
CelestialSnapshot,
|
||||
ClaimSnapshot,
|
||||
ConstructionSiteSnapshot,
|
||||
@@ -35,6 +36,7 @@ export type Selectable =
|
||||
|
||||
export interface ShipVisual {
|
||||
systemId: string;
|
||||
anchorId?: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
@@ -74,6 +76,7 @@ export type OrbitalAnchor =
|
||||
|
||||
export interface NodeVisual {
|
||||
systemId: string;
|
||||
anchorId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
sourceKind: string;
|
||||
@@ -96,7 +99,8 @@ export interface CelestialVisual {
|
||||
|
||||
export interface ClaimVisual {
|
||||
id: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
celestialId?: string | null;
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
@@ -105,7 +109,8 @@ export interface ClaimVisual {
|
||||
|
||||
export interface ConstructionSiteVisual {
|
||||
id: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
celestialId?: string | null;
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
@@ -115,6 +120,7 @@ export interface ConstructionSiteVisual {
|
||||
export interface StructureVisual {
|
||||
id: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
anchor: OrbitalAnchor;
|
||||
@@ -145,6 +151,7 @@ export interface WorldState {
|
||||
generatedAtUtc: string;
|
||||
systems: Map<string, SystemSnapshot>;
|
||||
celestials: Map<string, CelestialSnapshot>;
|
||||
anchors: Map<string, AnchorSnapshot>;
|
||||
nodes: Map<string, ResourceNodeSnapshot>;
|
||||
stations: Map<string, StationSnapshot>;
|
||||
claims: Map<string, ClaimSnapshot>;
|
||||
|
||||
@@ -65,8 +65,9 @@ export interface ViewerWorldLifecycleContext {
|
||||
applyClaimDeltas: (claims: ClaimDelta[]) => void;
|
||||
applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void;
|
||||
applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void;
|
||||
refreshLocalLayer: () => void;
|
||||
refreshHistoryWindows: () => void;
|
||||
resolveFocusedCelestialId: () => string | undefined;
|
||||
resolveFocusedAnchorId: () => string | undefined;
|
||||
updateSystemSummaries: () => void;
|
||||
applyZoomPresentation: () => void;
|
||||
updateNetworkPanel: () => void;
|
||||
@@ -165,6 +166,7 @@ export class ViewerWorldLifecycle {
|
||||
this.context.syncClaims(snapshot.claims);
|
||||
this.context.syncConstructionSites(snapshot.constructionSites);
|
||||
this.context.syncShips(snapshot.ships, snapshot.tickIntervalMs);
|
||||
this.context.refreshLocalLayer();
|
||||
this.rebuildFactions(snapshot.factions);
|
||||
this.context.updateSystemSummaries();
|
||||
this.context.applyZoomPresentation();
|
||||
@@ -185,6 +187,7 @@ export class ViewerWorldLifecycle {
|
||||
this.context.applyClaimDeltas(delta.claims);
|
||||
this.context.applyConstructionSiteDeltas(delta.constructionSites);
|
||||
this.context.applyShipDeltas(delta.ships, delta.tickIntervalMs);
|
||||
this.context.refreshLocalLayer();
|
||||
this.rebuildFactions(cloneFactions(world));
|
||||
this.context.updateSystemSummaries();
|
||||
}
|
||||
@@ -219,6 +222,7 @@ export class ViewerWorldLifecycle {
|
||||
}
|
||||
|
||||
this.context.refreshHistoryWindows();
|
||||
this.context.refreshLocalLayer();
|
||||
this.context.updateSystemPanel();
|
||||
this.refreshStreamScopeIfNeeded();
|
||||
const detailState = buildDetailPanelState({
|
||||
@@ -241,12 +245,12 @@ export class ViewerWorldLifecycle {
|
||||
return { scopeKind: "universe" as const };
|
||||
}
|
||||
|
||||
const celestialId = this.context.resolveFocusedCelestialId();
|
||||
if (this.context.getPovLevel() === "local" && celestialId) {
|
||||
const anchorId = this.context.resolveFocusedAnchorId();
|
||||
if (this.context.getPovLevel() === "local" && anchorId) {
|
||||
return {
|
||||
scopeKind: "local-celestial" as const,
|
||||
scopeKind: "local-anchor" as const,
|
||||
systemId: activeSystemId,
|
||||
celestialId,
|
||||
anchorId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
toThreeVector,
|
||||
} from "./viewerMath";
|
||||
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection";
|
||||
import { describeActiveSpace, resolveFocusedAnchorId } from "./viewerSelection";
|
||||
import {
|
||||
resolveShipHeading,
|
||||
updateSystemStarPresentation,
|
||||
@@ -58,15 +58,21 @@ export interface WorldOrbitalContext {
|
||||
|
||||
export interface WorldPresentationContext extends WorldOrbitalContext {
|
||||
activeSystemId?: string;
|
||||
focusedAnchorId?: string;
|
||||
cameraMode: CameraMode;
|
||||
povLevel: PovLevel;
|
||||
orbitYaw: number;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
systemAnchor: THREE.Vector3;
|
||||
shipVisuals: Map<string, ShipVisual>;
|
||||
localShipVisuals: Map<string, ShipVisual>;
|
||||
claimVisuals: Map<string, ClaimVisual>;
|
||||
localClaimVisuals: Map<string, ClaimVisual>;
|
||||
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||
localConstructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||
systemVisuals: Map<string, SystemVisual>;
|
||||
localNodeVisuals: Map<string, NodeVisual>;
|
||||
localStationVisuals: Map<string, StructureVisual>;
|
||||
systemSummaryVisuals: Map<string, any>;
|
||||
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
|
||||
updateSystemDetailVisibility: () => void;
|
||||
@@ -95,7 +101,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
||||
const worldPosition = resolveShipRenderPosition(context, ship, visual, now, renderMode);
|
||||
const displayPosition = context.toDisplayLocalPosition(worldPosition);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
@@ -124,8 +130,22 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const [shipId, visual] of context.localShipVisuals.entries()) {
|
||||
const ship = context.world?.ships.get(shipId);
|
||||
if (!ship) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const localPosition = getAnimatedShipLocalPosition(visual, now);
|
||||
const displayPosition = context.toDisplayLocalPosition(localPosition);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(renderMode === "local");
|
||||
visual.icon.setVisible(renderMode === "local");
|
||||
}
|
||||
|
||||
for (const visual of context.nodeVisuals.values()) {
|
||||
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
|
||||
const animatedLocalPosition = resolveNodeRenderPosition(context, visual, worldTimeSeconds, renderMode);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
@@ -148,7 +168,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
for (const visual of context.stationVisuals.values()) {
|
||||
const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds);
|
||||
const animatedLocalPosition = resolveStructureRenderPosition(context, visual, worldTimeSeconds, renderMode);
|
||||
const displayPosition = context.toDisplayLocalPosition(animatedLocalPosition);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
@@ -165,7 +185,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
for (const visual of context.claimVisuals.values()) {
|
||||
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
const animatedLocalPosition = visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
@@ -173,12 +193,41 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
for (const visual of context.constructionSiteVisuals.values()) {
|
||||
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone();
|
||||
const animatedLocalPosition = visual.localPosition.clone();
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
visual.icon.setVisible(visual.systemId === context.activeSystemId);
|
||||
}
|
||||
|
||||
for (const visual of context.localNodeVisuals.values()) {
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(renderMode === "local");
|
||||
visual.icon.setVisible(renderMode === "local");
|
||||
}
|
||||
|
||||
for (const visual of context.localStationVisuals.values()) {
|
||||
const displayPosition = context.toDisplayLocalPosition(visual.localPosition.clone());
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(renderMode === "local");
|
||||
visual.icon.setVisible(renderMode === "local");
|
||||
}
|
||||
|
||||
for (const visual of context.localClaimVisuals.values()) {
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(renderMode === "local");
|
||||
visual.icon.setVisible(renderMode === "local");
|
||||
}
|
||||
|
||||
for (const visual of context.localConstructionSiteVisuals.values()) {
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(renderMode === "local");
|
||||
visual.icon.setVisible(renderMode === "local");
|
||||
}
|
||||
}
|
||||
|
||||
type RenderSpaceMode = "galaxy" | "system" | "local";
|
||||
@@ -213,6 +262,60 @@ export function resolveShipWorldPosition(
|
||||
return context.toDisplayLocalPosition(animatedLocalPosition);
|
||||
}
|
||||
|
||||
function resolveAnchorSystemPosition(context: WorldOrbitalContext, anchorId?: string | null) {
|
||||
if (!anchorId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const anchor = context.world?.anchors.get(anchorId);
|
||||
return anchor ? toThreeVector(anchor.systemPosition) : undefined;
|
||||
}
|
||||
|
||||
function resolveShipRenderPosition(
|
||||
context: WorldPresentationContext,
|
||||
ship: ShipSnapshot,
|
||||
visual: ShipVisual,
|
||||
now: number,
|
||||
renderMode: RenderSpaceMode,
|
||||
) {
|
||||
const animatedLocalPosition = getAnimatedShipLocalPosition(visual, now);
|
||||
const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId ?? visual.anchorId;
|
||||
const anchoredSystemPosition = resolveAnchorSystemPosition(context, currentAnchorId)
|
||||
?? (ship.spatialState.systemPosition ? toThreeVector(ship.spatialState.systemPosition) : undefined);
|
||||
|
||||
if (renderMode === "local" && currentAnchorId && currentAnchorId === context.focusedAnchorId) {
|
||||
return animatedLocalPosition;
|
||||
}
|
||||
|
||||
return anchoredSystemPosition ?? animatedLocalPosition;
|
||||
}
|
||||
|
||||
function resolveNodeRenderPosition(
|
||||
context: WorldPresentationContext,
|
||||
visual: NodeVisual,
|
||||
timeSeconds: number,
|
||||
renderMode: RenderSpaceMode,
|
||||
) {
|
||||
if (renderMode === "local" && visual.anchorId === context.focusedAnchorId) {
|
||||
return computeNodeLocalPosition(context, visual, timeSeconds);
|
||||
}
|
||||
|
||||
return resolveAnchorSystemPosition(context, visual.anchorId) ?? visual.localPosition.clone();
|
||||
}
|
||||
|
||||
function resolveStructureRenderPosition(
|
||||
context: WorldPresentationContext,
|
||||
visual: StructureVisual,
|
||||
timeSeconds: number,
|
||||
renderMode: RenderSpaceMode,
|
||||
) {
|
||||
if (renderMode === "local" && visual.anchorId && visual.anchorId === context.focusedAnchorId) {
|
||||
return resolveStructureAnimatedLocalPosition(context, visual, timeSeconds);
|
||||
}
|
||||
|
||||
return resolveAnchorSystemPosition(context, visual.anchorId) ?? visual.localPosition.clone();
|
||||
}
|
||||
|
||||
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) {
|
||||
if (!world) {
|
||||
return;
|
||||
@@ -310,9 +413,9 @@ export function describeGameStatus(params: GameStatusParams) {
|
||||
? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
|
||||
: "";
|
||||
// Local space: position relative to the focused celestial's orbital anchor in km
|
||||
const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||
const celestialAnchor = focusedCelestialId
|
||||
? world?.celestials.get(focusedCelestialId)?.orbitalAnchor
|
||||
const focusedAnchorId = resolveFocusedAnchorId(world, selectedItems);
|
||||
const celestialAnchor = focusedAnchorId
|
||||
? (world?.anchors.get(focusedAnchorId)?.systemPosition ?? world?.celestials.get(focusedAnchorId)?.orbitalAnchor)
|
||||
: undefined;
|
||||
const locPos = systemAnchor && celestialAnchor
|
||||
? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
|
||||
@@ -415,7 +518,14 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
|
||||
return bestAnchor;
|
||||
}
|
||||
|
||||
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null) {
|
||||
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null, anchorId?: string | null) {
|
||||
if (anchorId) {
|
||||
const anchor = context.world?.anchors.get(anchorId);
|
||||
if (anchor) {
|
||||
return toThreeVector(anchor.systemPosition);
|
||||
}
|
||||
}
|
||||
|
||||
if (celestialId) {
|
||||
const celestial = context.world?.celestials.get(celestialId);
|
||||
if (celestial) {
|
||||
@@ -446,17 +556,17 @@ export function computeCelestialLocalPositionById(
|
||||
}
|
||||
|
||||
const basePosition = toThreeVector(celestial.orbitalAnchor);
|
||||
if (!celestial.parentNodeId) {
|
||||
if (!celestial.parentAnchorId) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
const parentCelestial = context.world.celestials.get(celestial.parentNodeId);
|
||||
const parentCelestial = context.world.celestials.get(celestial.parentAnchorId);
|
||||
if (!parentCelestial) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
visiting.add(celestialId);
|
||||
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting);
|
||||
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentAnchorId, timeSeconds, visiting);
|
||||
visiting.delete(celestialId);
|
||||
if (!parentCurrentPosition) {
|
||||
return basePosition;
|
||||
@@ -548,9 +658,16 @@ function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, vis
|
||||
}
|
||||
|
||||
const station = context.world.stations.get(visual.id);
|
||||
if (!station?.celestialId) {
|
||||
if (!station) {
|
||||
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
|
||||
}
|
||||
|
||||
return computeCelestialLocalPositionById(context, station.celestialId, timeSeconds) ?? visual.localPosition.clone();
|
||||
if (station.anchorId) {
|
||||
const anchor = context.world.anchors.get(station.anchorId);
|
||||
if (anchor) {
|
||||
return toThreeVector(anchor.systemPosition);
|
||||
}
|
||||
}
|
||||
|
||||
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
allowedHosts: ["sobina.local"],
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:5080",
|
||||
"/api": "http://127.0.0.1:5079",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user