Refine viewer status panel layout
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import { fetchWorldSnapshot, openWorldStream, resetWorld } from "./api";
|
||||
import { fetchWorldSnapshot, openWorldStream } from "./api";
|
||||
import type {
|
||||
FactionDelta,
|
||||
FactionSnapshot,
|
||||
@@ -86,9 +86,7 @@ export class GameViewer {
|
||||
private readonly detailBodyEl: HTMLDivElement;
|
||||
private readonly factionStripEl: HTMLDivElement;
|
||||
private readonly networkPanelEl: HTMLDivElement;
|
||||
private readonly resetButton: HTMLButtonElement;
|
||||
private readonly errorEl: HTMLDivElement;
|
||||
private readonly streamEl: HTMLDivElement;
|
||||
private world?: WorldState;
|
||||
private stream?: EventSource;
|
||||
private readonly networkStats: NetworkStats = {
|
||||
@@ -126,15 +124,8 @@ export class GameViewer {
|
||||
hud.className = "viewer-shell";
|
||||
hud.innerHTML = `
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">Frontend Viewer</p>
|
||||
<h1>Space Game Observer</h1>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<div class="status-pill">Bootstrapping</div>
|
||||
<div class="status-pill stream-pill">Stream Offline</div>
|
||||
<button type="button" class="reset-button">Reset World</button>
|
||||
</div>
|
||||
<h2>Game</h2>
|
||||
<div class="topbar-body">Bootstrapping</div>
|
||||
</header>
|
||||
<aside class="details-panel">
|
||||
<h2>Selection</h2>
|
||||
@@ -149,18 +140,15 @@ export class GameViewer {
|
||||
<section class="faction-strip"></section>
|
||||
`;
|
||||
|
||||
this.statusEl = hud.querySelector(".status-pill") as HTMLDivElement;
|
||||
this.streamEl = hud.querySelector(".stream-pill") as HTMLDivElement;
|
||||
this.statusEl = hud.querySelector(".topbar-body") as HTMLDivElement;
|
||||
this.detailTitleEl = hud.querySelector(".detail-title") as HTMLHeadingElement;
|
||||
this.detailBodyEl = hud.querySelector(".detail-body") as HTMLDivElement;
|
||||
this.factionStripEl = hud.querySelector(".faction-strip") as HTMLDivElement;
|
||||
this.networkPanelEl = hud.querySelector(".network-body") as HTMLDivElement;
|
||||
this.resetButton = hud.querySelector(".reset-button") as HTMLButtonElement;
|
||||
this.errorEl = hud.querySelector(".error-strip") as HTMLDivElement;
|
||||
|
||||
this.container.append(this.renderer.domElement, hud);
|
||||
|
||||
this.resetButton.addEventListener("click", () => void this.handleReset());
|
||||
this.renderer.domElement.addEventListener("pointerdown", this.onPointerDown);
|
||||
this.renderer.domElement.addEventListener("pointermove", this.onPointerMove);
|
||||
this.renderer.domElement.addEventListener("pointerup", this.onPointerUp);
|
||||
@@ -181,14 +169,13 @@ export class GameViewer {
|
||||
const snapshot = await fetchWorldSnapshot();
|
||||
this.world = this.createWorldState(snapshot);
|
||||
this.networkStats.snapshotBytes = new Blob([JSON.stringify(snapshot)]).size;
|
||||
this.statusEl.textContent = `Snapshot ${snapshot.sequence}`;
|
||||
this.updateGamePanel("Bootstrapped");
|
||||
this.errorEl.hidden = true;
|
||||
this.applySnapshot(snapshot);
|
||||
this.openDeltaStream(snapshot.sequence);
|
||||
this.updatePanels();
|
||||
} catch (error) {
|
||||
this.statusEl.textContent = "Backend offline";
|
||||
this.streamEl.textContent = "Stream Offline";
|
||||
this.updateGamePanel("Backend offline");
|
||||
this.errorEl.hidden = false;
|
||||
this.errorEl.textContent = error instanceof Error ? error.message : "Unable to bootstrap the backend snapshot.";
|
||||
}
|
||||
@@ -200,12 +187,12 @@ export class GameViewer {
|
||||
onOpen: () => {
|
||||
this.networkStats.streamConnected = true;
|
||||
this.networkStats.streamOpenedAtMs = performance.now();
|
||||
this.streamEl.textContent = "Stream Live";
|
||||
this.updateGamePanel("Stream live");
|
||||
this.updateNetworkPanel();
|
||||
},
|
||||
onError: () => {
|
||||
this.networkStats.streamConnected = false;
|
||||
this.streamEl.textContent = "Stream Reconnecting";
|
||||
this.updateGamePanel("Stream reconnecting");
|
||||
this.updateNetworkPanel();
|
||||
},
|
||||
onDelta: (delta, rawBytes) => {
|
||||
@@ -226,26 +213,11 @@ export class GameViewer {
|
||||
|
||||
this.applyDelta(delta);
|
||||
this.recordDeltaStats(delta, rawBytes);
|
||||
this.statusEl.textContent = `Seq ${delta.sequence} · ${new Date(delta.generatedAtUtc).toLocaleTimeString()}`;
|
||||
this.updateGamePanel("Live");
|
||||
this.updatePanels();
|
||||
this.updateNetworkPanel();
|
||||
}
|
||||
|
||||
private async handleReset() {
|
||||
this.resetButton.disabled = true;
|
||||
try {
|
||||
const snapshot = await resetWorld();
|
||||
this.world = this.createWorldState(snapshot);
|
||||
this.networkStats.snapshotBytes = new Blob([JSON.stringify(snapshot)]).size;
|
||||
this.applySnapshot(snapshot);
|
||||
this.openDeltaStream(snapshot.sequence);
|
||||
this.updatePanels();
|
||||
this.updateNetworkPanel();
|
||||
} finally {
|
||||
this.resetButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private createWorldState(snapshot: WorldSnapshot): WorldState {
|
||||
return {
|
||||
label: snapshot.label,
|
||||
@@ -701,6 +673,18 @@ export class GameViewer {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private updateGamePanel(mode: string) {
|
||||
const sequence = this.world?.sequence ?? 0;
|
||||
const generatedAt = this.world?.generatedAtUtc
|
||||
? new Date(this.world.generatedAtUtc).toLocaleTimeString()
|
||||
: "n/a";
|
||||
this.statusEl.textContent = [
|
||||
`mode: ${mode}`,
|
||||
`sequence: ${sequence}`,
|
||||
`snapshot: ${generatedAt}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private toThreeVector(vector: Vector3Dto) {
|
||||
return new THREE.Vector3(vector.x, vector.y, vector.z);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user