{{ faction.label }}
+ faction +GOAP State
++ {{ line }} +
+Priorities
++ {{ priority.label }} + {{ priority.value }} +
+GOAP State
++ {{ line }} +
+Priorities
++ {{ priority.label }} + {{ priority.value }} +
++ {{ line }} +
++ {{ line }} +
++ {{ line }} +
++ {{ line }} +
+GOAP State
-Military ${state.militaryShipCount} · Miners ${state.minerShipCount}
-Transport ${state.transportShipCount} · Constructors ${state.constructorShipCount}
-Systems ${state.controlledSystemCount} / ${state.targetSystemCount}
-Factory ${state.hasShipFactory ? "yes" : "no"} · Ore ${state.oreStockpile.toFixed(0)}
-Priorities
- ${priorities.map(p => `${p.goalName} ${p.priority.toFixed(0)}
`).join("")} -${station.systemId}
-Docked ${station.dockedShips} / ${station.dockingPads}
-Cargo ${cargo.toFixed(0)} · Pop ${station.population.toFixed(0)}
-Modules ${station.installedModules.length}
- ${processes.length > 0 ? ` -${shipLocation.system}${shipLocation.local ? `
${shipLocation.local}` : ""}
Cargo ${cargo.toFixed(0)}
-State ${shipState}
- ${shipAction ? ` -Objective ${describeShipObjective(ship.commanderObjective)}
` : ""} -Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}
-Task ${ship.controllerTaskKind}
-Parent ${parent}
-Behavior ${shipBehavior}
-State ${shipState}
-Order ${shipOrder}
-Task ${ship.controllerTaskKind}
- ${shipAction ? ` -Parent ${parent}
+Behavior ${shipBehavior}
+State ${shipState}
+Order ${shipOrder}
+Task ${ship.controllerTaskKind}
+ ${shipAction ? ` +Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}
-Inventory ${formatInventory(ship.inventory)}
-Speed ${formatShipSpeed(ship)}
-Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}
Press C to toggle follow
Cargo ${cargoUsed.toFixed(0)} / ${ship.cargoCapacity.toFixed(0)}
+Inventory ${formatInventory(ship.inventory)}
+Speed ${formatShipSpeed(ship)}
+Camera ${cameraMode === "follow" && cameraTargetShipId === ship.id ? "camera-follow" : "tactical"}
Press C to toggle follow
${station.category} · ${station.systemId}
-Parent ${parent}
-Docked ${station.dockedShips} / ${station.dockingPads}
-
- ${dockedShipLabels}
Modules ${moduleList}
-Storage ${stationStorage}
- `; - return; + return { + title: station.label, + bodyHtml: ` +${station.category} · ${station.systemId}
+Parent ${parent}
+Docked ${station.dockedShips} / ${station.dockingPads}
+
+ ${dockedShipLabels}
Modules ${moduleList}
+Storage ${stationStorage}
+ `, + }; } if (selected.kind === "node") { const node = world.nodes.get(selected.id); if (!node) { - return; + return { title: "Missing node", bodyHtml: "" }; } const parent = describeSelectionParent(selected); const nodeLevel = node.maxOre > 0 ? Math.max(0, Math.min(node.oreRemaining / node.maxOre, 1)) : 0; - detailTitleEl.textContent = `Node ${node.id}`; - detailBodyEl.innerHTML = ` -${node.systemId}
-Parent ${parent}
-Source ${node.sourceKind}
Resource ${node.itemId}
${node.systemId}
+Parent ${parent}
+Source ${node.sourceKind}
Resource ${node.itemId}
Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}
- `; - return; +Stock ${node.oreRemaining.toFixed(0)} / ${node.maxOre.toFixed(0)}
+ `, + }; } if (selected.kind === "celestial") { const celestial = world.celestials.get(selected.id); if (!celestial) { - return; + return { title: "Missing celestial", bodyHtml: "" }; } - detailTitleEl.textContent = `${celestial.kind} celestial`; - detailBodyEl.innerHTML = ` -${celestial.systemId}
-Parent ${celestial.parentNodeId ?? "none"}
Orbit ref ${celestial.orbitReferenceId ?? "none"}
Occupying structure ${celestial.occupyingStructureId ?? "none"}
-Local space radius ${celestial.localSpaceRadius.toFixed(0)} km
- `; - return; + return { + title: `${celestial.kind} celestial`, + bodyHtml: ` +${celestial.systemId}
+Parent ${celestial.parentNodeId ?? "none"}
Orbit ref ${celestial.orbitReferenceId ?? "none"}
Occupying structure ${celestial.occupyingStructureId ?? "none"}
+Local space radius ${celestial.localSpaceRadius.toFixed(0)} km
+ `, + }; } if (selected.kind === "claim") { const claim = world.claims.get(selected.id); if (!claim) { - return; + return { title: "Missing claim", bodyHtml: "" }; } - detailTitleEl.textContent = `Claim ${claim.id}`; - detailBodyEl.innerHTML = ` -${claim.systemId}
-Celestial ${claim.celestialId}
-State ${claim.state}
Health ${claim.health.toFixed(0)}
Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}
- `; - return; + return { + title: `Claim ${claim.id}`, + bodyHtml: ` +${claim.systemId}
+Celestial ${claim.celestialId}
+State ${claim.state}
Health ${claim.health.toFixed(0)}
Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}
+ `, + }; } if (selected.kind === "construction-site") { const site = world.constructionSites.get(selected.id); if (!site) { - return; + return { title: "Missing construction", bodyHtml: "" }; } const orderCount = [...world.marketOrders.values()].filter((order) => order.constructionSiteId === site.id).length; - detailTitleEl.textContent = `Construction ${site.id}`; - detailBodyEl.innerHTML = ` -${site.systemId}
-Celestial ${site.celestialId}
-${site.targetKind} ${site.targetDefinitionId}
-State ${site.state}
Progress ${(site.progress * 100).toFixed(0)}%
Orders ${orderCount}
Assigned constructors ${site.assignedConstructorShipIds.length}
${site.systemId}
+Celestial ${site.celestialId}
+${site.targetKind} ${site.targetDefinitionId}
+State ${site.state}
Progress ${(site.progress * 100).toFixed(0)}%
Orders ${orderCount}
Assigned constructors ${site.assignedConstructorShipIds.length}
${system.label}
-Parent ${parent}
-${planet.planetType} · ${planet.shape} · Moons ${planet.moons.length}
-Orbit ${formatSystemDistance(planet.orbitRadius)}
Speed ${planet.orbitSpeed.toFixed(3)}
Ecc ${planet.orbitEccentricity.toFixed(3)}
Inc ${planet.orbitInclination.toFixed(1)}°
Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°
- `; - return; + return { + title: planet.label, + bodyHtml: ` +${system.label}
+Parent ${parent}
+${planet.planetType} · ${planet.shape} · Moons ${planet.moons.length}
+Orbit ${formatSystemDistance(planet.orbitRadius)}
Speed ${planet.orbitSpeed.toFixed(3)}
Ecc ${planet.orbitEccentricity.toFixed(3)}
Inc ${planet.orbitInclination.toFixed(1)}°
Phase ${planet.orbitPhaseAtEpoch.toFixed(1)}°
+ `, + }; } if (selected.kind === "moon") { @@ -375,51 +384,57 @@ export function updateDetailPanel( const planet = system?.planets[selected.planetIndex]; const moon = planet?.moons[selected.moonIndex]; if (moon) { - detailTitleEl.textContent = moon.label; - detailBodyEl.innerHTML = ` -${system?.label ?? selected.systemId} / ${planet?.label ?? `planet ${selected.planetIndex + 1}`}
-Orbit ${formatSystemDistance(moon.orbitRadius)}
Inc ${moon.orbitInclination.toFixed(1)}°
${system?.label ?? selected.systemId} / ${planet?.label ?? `planet ${selected.planetIndex + 1}`}
+Orbit ${formatSystemDistance(moon.orbitRadius)}
Inc ${moon.orbitInclination.toFixed(1)}°
Parent galaxy
- ${renderSystemDetails(world, system, false, cameraMode, cameraTargetShipId)} - `; + return { + title: system.label, + bodyHtml: ` +Parent galaxy
+ ${renderSystemDetails(world, system, false, cameraMode, cameraTargetShipId)} + `, + }; } -export function updateSystemPanel(params: SystemPanelParams) { +export function buildSystemPanelState(params: SystemPanelParams) { const { world, activeSystemId, - systemTitleEl, - systemBodyEl, - systemPanelEl, - cameraMode, - cameraTargetShipId, } = params; const activeSystem = activeSystemId ? world.systems.get(activeSystemId) : undefined; - systemPanelEl.hidden = !activeSystem; if (!activeSystem) { - systemTitleEl.textContent = "Deep Space"; - systemBodyEl.innerHTML = ""; - return; + return { + hidden: true, + title: "Deep Space", + bodyHtml: "", + }; } - systemTitleEl.textContent = activeSystem.label; - systemBodyEl.innerHTML = ` -${renderSystemOwnership(world, activeSystem.id)}
- `; + return { + hidden: false, + title: activeSystem.label, + bodyHtml: ` +${renderSystemOwnership(world, activeSystem.id)}
+ `, + }; } export function describeSelectionParent( diff --git a/apps/viewer/src/viewerPresentationController.ts b/apps/viewer/src/viewerPresentationController.ts index 6b42dcd..4c6740b 100644 --- a/apps/viewer/src/viewerPresentationController.ts +++ b/apps/viewer/src/viewerPresentationController.ts @@ -1,34 +1,27 @@ import * as THREE from "three"; import { - updateNetworkPanel as renderNetworkPanel, + describeNetworkPanel, + describePerformancePanel, recordPerformanceStats, summarizeNetworkStats, summarizePerformanceStats, - updatePerformancePanel as renderPerformancePanel, } from "./viewerTelemetry"; import { updatePlanetPresentation } from "./viewerPresentation"; -import { renderRecentEvents, updateGameStatus, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation"; -import { updateSystemPanel } from "./viewerPanels"; +import { buildSystemPanelState } from "./viewerPanels"; +import { describeGameStatus, renderRecentEvents, updateSystemSummaries, updateWorldPresentation } from "./viewerWorldPresentation"; import { createBackdropStars, createNebulaClouds, createNebulaTexture } from "./viewerSceneFactory"; +import type { ViewerHudState } from "./viewerHudState"; import type { Selectable } from "./viewerTypes"; export interface ViewerPresentationContext { renderer: THREE.WebGLRenderer; + hudState: ViewerHudState; galaxyScene: THREE.Scene; galaxyCamera: THREE.PerspectiveCamera; systemCamera: THREE.PerspectiveCamera; galaxyAnchor: THREE.Vector3; systemAnchor: THREE.Vector3; ambienceGroup: THREE.Group; - gameSummaryEl: HTMLSpanElement; - networkSummaryEl: HTMLSpanElement; - performanceSummaryEl: HTMLSpanElement; - statusEl: HTMLDivElement; - networkPanelEl: HTMLDivElement; - performancePanelEl: HTMLDivElement; - systemPanelEl: HTMLDivElement; - systemTitleEl: HTMLHeadingElement; - systemBodyEl: HTMLDivElement; networkStats: any; performanceStats: any; getWorld: () => any; @@ -61,7 +54,6 @@ export class ViewerPresentationController { } applyZoomPresentation() { - const activeSystemId = this.context.getActiveSystemId(); const povLevel = this.context.getPovLevel(); this.context.galaxyScene.fog = new THREE.FogExp2(0x040912, 0.000035); @@ -73,8 +65,8 @@ export class ViewerPresentationController { } updateNetworkPanel() { - renderNetworkPanel(this.context.networkPanelEl, this.context.networkStats); - this.context.networkSummaryEl.textContent = summarizeNetworkStats(this.context.networkStats); + this.context.hudState.networkPanel.bodyText = describeNetworkPanel(this.context.networkStats); + this.context.hudState.networkPanel.summary = summarizeNetworkStats(this.context.networkStats); } recordPerformanceStats(frameMs: number) { @@ -82,8 +74,11 @@ export class ViewerPresentationController { } updatePerformancePanel() { - renderPerformancePanel(this.context.performancePanelEl, this.context.performanceStats, this.context.renderer); - this.context.performanceSummaryEl.textContent = summarizePerformanceStats(this.context.performanceStats); + const bodyText = describePerformancePanel(this.context.performanceStats, this.context.renderer); + if (bodyText) { + this.context.hudState.performancePanel.bodyText = bodyText; + } + this.context.hudState.performancePanel.summary = summarizePerformanceStats(this.context.performanceStats); } updateShipPresentation() { @@ -109,9 +104,7 @@ export class ViewerPresentationController { } updateGamePanel(mode: string) { - updateGameStatus({ - statusEl: this.context.statusEl, - summaryEl: this.context.gameSummaryEl, + const state = describeGameStatus({ world: this.context.getWorld(), activeSystemId: this.context.getActiveSystemId(), cameraMode: this.context.getCameraMode(), @@ -121,6 +114,8 @@ export class ViewerPresentationController { galaxyAnchor: this.context.galaxyAnchor, systemAnchor: this.context.systemAnchor, }); + this.context.hudState.gamePanel.bodyText = state.bodyText; + this.context.hudState.gamePanel.summary = state.summaryText; } updateSystemPanel() { @@ -129,15 +124,15 @@ export class ViewerPresentationController { return; } - updateSystemPanel({ + const state = buildSystemPanelState({ world, activeSystemId: this.context.getActiveSystemId(), - systemTitleEl: this.context.systemTitleEl, - systemBodyEl: this.context.systemBodyEl, - systemPanelEl: this.context.systemPanelEl, cameraMode: this.context.getCameraMode(), cameraTargetShipId: this.context.getCameraTargetShipId(), }); + this.context.hudState.systemPanel.hidden = state.hidden; + this.context.hudState.systemPanel.title = state.title; + this.context.hudState.systemPanel.bodyHtml = state.bodyHtml; } screenPointFromClient(clientX: number, clientY: number) { diff --git a/apps/viewer/src/viewerRenderLoop.ts b/apps/viewer/src/viewerRenderLoop.ts index 261d6eb..4a261b3 100644 --- a/apps/viewer/src/viewerRenderLoop.ts +++ b/apps/viewer/src/viewerRenderLoop.ts @@ -25,10 +25,11 @@ export interface RenderFrameParams { } export interface ResizeParams { - renderer: THREE.WebGLRenderer; galaxyLayer: GalaxyLayer; systemLayer: SystemLayer; localLayer: LocalLayer; + width: number; + height: number; } export interface CameraStepParams { @@ -72,12 +73,10 @@ export function renderFrame(params: RenderFrameParams) { } export function resizeViewer(params: ResizeParams) { - const width = window.innerWidth; - const height = window.innerHeight; - params.galaxyLayer.onResize(width / height); - params.systemLayer.onResize(width / height); - params.localLayer.onResize(width / height); - params.renderer.setSize(width, height); + const aspect = params.width / params.height; + params.galaxyLayer.onResize(aspect); + params.systemLayer.onResize(aspect); + params.localLayer.onResize(aspect); } export function stepCamera(params: CameraStepParams) { diff --git a/apps/viewer/src/viewerTelemetry.ts b/apps/viewer/src/viewerTelemetry.ts index 103c29d..4839e40 100644 --- a/apps/viewer/src/viewerTelemetry.ts +++ b/apps/viewer/src/viewerTelemetry.ts @@ -6,6 +6,10 @@ import type { } from "./viewerTypes"; export function updateNetworkPanel(networkPanelEl: HTMLDivElement, networkStats: NetworkStats) { + networkPanelEl.textContent = describeNetworkPanel(networkStats); +} + +export function describeNetworkPanel(networkStats: NetworkStats) { const now = performance.now(); const uptimeSeconds = networkStats.streamOpenedAtMs ? (now - networkStats.streamOpenedAtMs) / 1000 @@ -22,7 +26,7 @@ export function updateNetworkPanel(networkPanelEl: HTMLDivElement, networkStats: ? ((now - networkStats.lastDeltaAtMs) / 1000).toFixed(1) : "n/a"; - networkPanelEl.textContent = [ + return [ `snapshot: ${formatBytes(networkStats.snapshotBytes)}`, `stream: ${networkStats.streamConnected ? "live" : "offline"}`, `deltas: ${networkStats.deltasReceived}`, @@ -59,13 +63,23 @@ export function updatePerformancePanel( performancePanelEl: HTMLDivElement, performanceStats: PerformanceStats, renderer: THREE.WebGLRenderer, +) { + const text = describePerformancePanel(performanceStats, renderer); + if (text) { + performancePanelEl.textContent = text; + } +} + +export function describePerformancePanel( + performanceStats: PerformanceStats, + renderer: THREE.WebGLRenderer, ) { const now = performance.now(); if ( performanceStats.lastPanelUpdateAtMs > 0 && now - performanceStats.lastPanelUpdateAtMs < 250 ) { - return; + return undefined; } const samples = performanceStats.frameSamples; @@ -84,7 +98,8 @@ export function updatePerformancePanel( const recentLowFps = averageFrameMs > 0 ? 1000 / Math.max(worstFrameMs, averageFrameMs) : 0; const renderInfo = renderer.info; - performancePanelEl.textContent = [ + performanceStats.lastPanelUpdateAtMs = now; + return [ `fps: ${fps.toFixed(1)}`, `frame avg: ${averageFrameMs.toFixed(2)} ms`, `frame last: ${performanceStats.lastFrameMs.toFixed(2)} ms`, @@ -98,7 +113,6 @@ export function updatePerformancePanel( `textures: ${renderInfo.memory.textures}`, `pixel ratio: ${renderer.getPixelRatio().toFixed(2)}`, ].join("\n"); - performanceStats.lastPanelUpdateAtMs = now; } export function summarizePerformanceStats(performanceStats: PerformanceStats): string { diff --git a/apps/viewer/src/viewerTypes.ts b/apps/viewer/src/viewerTypes.ts index e85e9c7..19fcdca 100644 --- a/apps/viewer/src/viewerTypes.ts +++ b/apps/viewer/src/viewerTypes.ts @@ -183,13 +183,3 @@ export interface PerformanceStats { lastFrameMs: number; lastPanelUpdateAtMs: number; } - -export interface HistoryWindowState { - id: string; - target: Selectable; - root: HTMLElement; - titleEl: HTMLHeadingElement; - bodyEl: HTMLDivElement; - copyButtonEl: HTMLButtonElement; - text: string; -} diff --git a/apps/viewer/src/viewerWorldLifecycle.ts b/apps/viewer/src/viewerWorldLifecycle.ts index 74d754e..9ea306e 100644 --- a/apps/viewer/src/viewerWorldLifecycle.ts +++ b/apps/viewer/src/viewerWorldLifecycle.ts @@ -1,6 +1,7 @@ import { fetchWorldSnapshot, openWorldStream } from "./api"; -import { renderOpsStrip } from "./viewerOpsStrip"; -import { updateDetailPanel } from "./viewerPanels"; +import type { ViewerHudState } from "./viewerHudState"; +import { buildOpsStripState } from "./viewerOpsStrip"; +import { buildDetailPanelState } from "./viewerPanels"; import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState"; import type { CelestialDelta, @@ -46,10 +47,7 @@ export interface ViewerWorldLifecycleContext { getCameraTargetShipId: () => string | undefined; getNetworkStats: () => NetworkStats; getSystemSummaryVisuals: () => Map