@plasius/hexagons 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,20 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ - **Added**
12
+ - (placeholder)
13
+
14
+ - **Changed**
15
+ - (placeholder)
16
+
17
+ - **Fixed**
18
+ - (placeholder)
19
+
20
+ - **Security**
21
+ - (placeholder)
22
+
23
+ ## [1.0.4] - 2026-02-21
24
+
11
25
  - **Added**
12
26
  - (placeholder)
13
27
 
@@ -48,7 +62,7 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
48
62
 
49
63
  ---
50
64
 
51
- [Unreleased]: https://github.com/Plasius-LTD/hexagons/compare/v1.0.3...HEAD
65
+ [Unreleased]: https://github.com/Plasius-LTD/hexagons/compare/v1.0.4...HEAD
52
66
 
53
67
  ## [1.0.0] - 2026-02-11
54
68
 
@@ -64,3 +78,4 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
64
78
  - **Security**
65
79
  - (placeholder)
66
80
  [1.0.3]: https://github.com/Plasius-LTD/hexagons/releases/tag/v1.0.3
81
+ [1.0.4]: https://github.com/Plasius-LTD/hexagons/releases/tag/v1.0.4
package/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  Public package containing hexagonal architecture utilities and game/domain helpers for Plasius applications.
12
12
 
13
+ Primary demo surface name: **Generator**.
14
+
13
15
 
14
16
  ## Install
15
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.tsx"],"names":[],"mappings":"AA0BA,wBAAgB,IAAI,4CAgKnB"}
1
+ {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.tsx"],"names":[],"mappings":"AA4RA,wBAAgB,IAAI,4CAuZnB"}
package/dist/game.js CHANGED
@@ -1,31 +1,402 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useState } from "react";
2
+ import { useEffect, useMemo, useRef, useState, } from "react";
3
3
  import { ErrorBoundary } from "@plasius/error";
4
4
  import { MacroBiomeLabel, MicroFeatureLabel, SurfaceCoverLabel, TerrainBiomeLabel, generateTemperateMixedForest, } from "@plasius/gpu-world-generator";
5
5
  import { xrSessionModes } from "@plasius/gpu-xr";
6
- import { buildHexMapTiles, computeMapBounds, } from "./worldMap.js";
6
+ import { buildHexMapTiles, } from "./worldMap.js";
7
7
  import styles from "./styles/game.module.css";
8
8
  const HEX_SIZE = 18;
9
+ const HEIGHT_SCALE = 26;
10
+ const HUMAN_EYE_HEIGHT = 1.72;
11
+ const WALK_SPEED = 20;
12
+ const FLY_SPEED = 32;
13
+ const VERTICAL_FLY_SPEED = 22;
14
+ const LOOK_SENSITIVITY = 0.0032;
15
+ const NEAR_PLANE = 0.5;
16
+ const FAR_PLANE = 1100;
17
+ const FOV_RAD = Math.PI / 3;
18
+ const CAMERA_UPDATE_MS = 100;
9
19
  function formatPercent(value) {
10
20
  return `${Math.round(value * 100)}%`;
11
21
  }
12
22
  function formatNumber(value) {
13
23
  return Number.isFinite(value) ? value.toFixed(2) : "n/a";
14
24
  }
25
+ function clamp(value, min, max) {
26
+ return Math.min(max, Math.max(min, value));
27
+ }
28
+ function wrapAngle(value) {
29
+ const full = Math.PI * 2;
30
+ const wrapped = ((value % full) + full) % full;
31
+ return wrapped > Math.PI ? wrapped - full : wrapped;
32
+ }
33
+ function hexCorners(centerX, centerZ, size) {
34
+ const points = [];
35
+ for (let i = 0; i < 6; i += 1) {
36
+ const angle = (Math.PI / 180) * (60 * i + 30);
37
+ points.push({
38
+ x: centerX + size * Math.cos(angle),
39
+ z: centerZ + size * Math.sin(angle),
40
+ });
41
+ }
42
+ return points;
43
+ }
44
+ function tintHex(hex, amount) {
45
+ const safe = hex.replace("#", "");
46
+ const channels = safe.length === 3
47
+ ? safe.split("").map((c) => parseInt(c + c, 16))
48
+ : [0, 2, 4].map((offset) => parseInt(safe.slice(offset, offset + 2), 16));
49
+ const factor = clamp(1 + amount, 0.2, 1.9);
50
+ const next = channels
51
+ .map((channel) => clamp(Math.round(channel * factor), 0, 255))
52
+ .map((channel) => channel.toString(16).padStart(2, "0"))
53
+ .join("");
54
+ return `#${next}`;
55
+ }
56
+ function findNearestTileIndex(tiles, x, z) {
57
+ if (tiles.length === 0) {
58
+ return 0;
59
+ }
60
+ let nearestIndex = 0;
61
+ let nearestDistance = Number.POSITIVE_INFINITY;
62
+ for (let index = 0; index < tiles.length; index += 1) {
63
+ const tile = tiles[index];
64
+ const dx = tile.x - x;
65
+ const dz = tile.y - z;
66
+ const distance = dx * dx + dz * dz;
67
+ if (distance < nearestDistance) {
68
+ nearestDistance = distance;
69
+ nearestIndex = index;
70
+ }
71
+ }
72
+ return nearestIndex;
73
+ }
74
+ function sampleGroundHeight(tiles, x, z) {
75
+ const index = findNearestTileIndex(tiles, x, z);
76
+ const tile = tiles[index];
77
+ if (!tile) {
78
+ return 0;
79
+ }
80
+ return tile.terrain.height * HEIGHT_SCALE;
81
+ }
82
+ function initialCamera(tiles) {
83
+ if (tiles.length === 0) {
84
+ return {
85
+ x: 0,
86
+ y: HUMAN_EYE_HEIGHT,
87
+ z: 0,
88
+ yaw: 0,
89
+ pitch: -0.35,
90
+ };
91
+ }
92
+ let sumX = 0;
93
+ let sumZ = 0;
94
+ for (const tile of tiles) {
95
+ sumX += tile.x;
96
+ sumZ += tile.y;
97
+ }
98
+ const x = sumX / tiles.length;
99
+ const z = sumZ / tiles.length;
100
+ const ground = sampleGroundHeight(tiles, x, z);
101
+ return {
102
+ x,
103
+ y: ground + HUMAN_EYE_HEIGHT,
104
+ z,
105
+ yaw: -Math.PI * 0.12,
106
+ pitch: -0.38,
107
+ };
108
+ }
109
+ function projectPoint(point, camera, viewportWidth, viewportHeight, focalLength) {
110
+ const dx = point.x - camera.x;
111
+ const dy = point.y - camera.y;
112
+ const dz = point.z - camera.z;
113
+ const sinYaw = Math.sin(camera.yaw);
114
+ const cosYaw = Math.cos(camera.yaw);
115
+ const xYaw = cosYaw * dx - sinYaw * dz;
116
+ const zYaw = sinYaw * dx + cosYaw * dz;
117
+ const sinPitch = Math.sin(camera.pitch);
118
+ const cosPitch = Math.cos(camera.pitch);
119
+ const yPitch = cosPitch * dy - sinPitch * zYaw;
120
+ const zPitch = sinPitch * dy + cosPitch * zYaw;
121
+ if (zPitch <= NEAR_PLANE || zPitch >= FAR_PLANE) {
122
+ return null;
123
+ }
124
+ return {
125
+ x: viewportWidth * 0.5 + (xYaw / zPitch) * focalLength,
126
+ y: viewportHeight * 0.57 - (yPitch / zPitch) * focalLength,
127
+ depth: zPitch,
128
+ };
129
+ }
130
+ function renderScene(context, canvas, tiles, camera, selectedIndex) {
131
+ const width = canvas.width;
132
+ const height = canvas.height;
133
+ const sky = context.createLinearGradient(0, 0, 0, height);
134
+ sky.addColorStop(0, "#0e1f33");
135
+ sky.addColorStop(0.55, "#15304b");
136
+ sky.addColorStop(1, "#1f2f22");
137
+ context.fillStyle = sky;
138
+ context.fillRect(0, 0, width, height);
139
+ const focalLength = (height * 0.86) / Math.tan(FOV_RAD * 0.5);
140
+ const drawQueue = [];
141
+ for (let index = 0; index < tiles.length; index += 1) {
142
+ const tile = tiles[index];
143
+ const elevation = tile.terrain.height * HEIGHT_SCALE;
144
+ const corners = hexCorners(tile.x, tile.y, HEX_SIZE);
145
+ const projectedCorners = [];
146
+ let depthTotal = 0;
147
+ let valid = true;
148
+ for (const corner of corners) {
149
+ const projected = projectPoint({ x: corner.x, y: elevation, z: corner.z }, camera, width, height, focalLength);
150
+ if (!projected) {
151
+ valid = false;
152
+ break;
153
+ }
154
+ projectedCorners.push(projected);
155
+ depthTotal += projected.depth;
156
+ }
157
+ if (!valid || projectedCorners.length === 0) {
158
+ continue;
159
+ }
160
+ const path = new Path2D();
161
+ path.moveTo(projectedCorners[0].x, projectedCorners[0].y);
162
+ for (let i = 1; i < projectedCorners.length; i += 1) {
163
+ path.lineTo(projectedCorners[i].x, projectedCorners[i].y);
164
+ }
165
+ path.closePath();
166
+ const depth = depthTotal / projectedCorners.length;
167
+ const distanceShade = clamp(1 - depth / 520, 0.2, 1);
168
+ const elevationShade = clamp((elevation / HEIGHT_SCALE - 0.5) * 0.25, -0.16, 0.18);
169
+ const color = tintHex(tile.color, distanceShade - 1 + elevationShade);
170
+ drawQueue.push({
171
+ path,
172
+ color,
173
+ depth,
174
+ highlighted: index === selectedIndex,
175
+ });
176
+ }
177
+ drawQueue.sort((a, b) => b.depth - a.depth);
178
+ for (const item of drawQueue) {
179
+ context.fillStyle = item.color;
180
+ context.fill(item.path);
181
+ context.lineWidth = item.highlighted ? 2.3 : 1.2;
182
+ context.strokeStyle = item.highlighted
183
+ ? "rgba(255, 255, 255, 0.92)"
184
+ : "rgba(11, 18, 29, 0.42)";
185
+ context.stroke(item.path);
186
+ }
187
+ context.strokeStyle = "rgba(230, 245, 255, 0.8)";
188
+ context.lineWidth = 1;
189
+ const crosshairX = width * 0.5;
190
+ const crosshairY = height * 0.55;
191
+ context.beginPath();
192
+ context.moveTo(crosshairX - 8, crosshairY);
193
+ context.lineTo(crosshairX + 8, crosshairY);
194
+ context.moveTo(crosshairX, crosshairY - 8);
195
+ context.lineTo(crosshairX, crosshairY + 8);
196
+ context.stroke();
197
+ }
15
198
  export function Game() {
16
199
  const [seed, setSeed] = useState(1337);
200
+ const [mode, setMode] = useState("walk");
17
201
  const [selectedIndex, setSelectedIndex] = useState(0);
202
+ const [cameraHud, setCameraHud] = useState({
203
+ x: 0,
204
+ y: HUMAN_EYE_HEIGHT,
205
+ z: 0,
206
+ yaw: 0,
207
+ pitch: -0.35,
208
+ ground: 0,
209
+ });
210
+ const canvasRef = useRef(null);
211
+ const cameraRef = useRef({
212
+ x: 0,
213
+ y: HUMAN_EYE_HEIGHT,
214
+ z: 0,
215
+ yaw: 0,
216
+ pitch: -0.35,
217
+ });
218
+ const keysRef = useRef(new Set());
219
+ const dragRef = useRef({
220
+ active: false,
221
+ pointerId: -1,
222
+ lastX: 0,
223
+ lastY: 0,
224
+ });
18
225
  const world = useMemo(() => generateTemperateMixedForest({ seed, radius: 9 }), [seed]);
19
226
  const tiles = useMemo(() => buildHexMapTiles(world.cells, world.terrain, HEX_SIZE), [world]);
20
- const bounds = useMemo(() => computeMapBounds(tiles, HEX_SIZE), [tiles]);
21
227
  const safeSelectedIndex = selectedIndex >= 0 && selectedIndex < tiles.length ? selectedIndex : 0;
22
228
  const selected = tiles[safeSelectedIndex];
23
- const viewBox = `${bounds.minX} ${bounds.minY} ${bounds.maxX - bounds.minX} ${bounds.maxY - bounds.minY}`;
229
+ useEffect(() => {
230
+ const camera = initialCamera(tiles);
231
+ cameraRef.current = camera;
232
+ const nearestIndex = findNearestTileIndex(tiles, camera.x, camera.z);
233
+ setSelectedIndex(nearestIndex);
234
+ setCameraHud({
235
+ ...camera,
236
+ ground: sampleGroundHeight(tiles, camera.x, camera.z),
237
+ });
238
+ }, [tiles]);
239
+ useEffect(() => {
240
+ const camera = cameraRef.current;
241
+ const ground = sampleGroundHeight(tiles, camera.x, camera.z);
242
+ if (mode === "walk") {
243
+ camera.y = ground + HUMAN_EYE_HEIGHT;
244
+ }
245
+ else {
246
+ camera.y = Math.max(camera.y, ground + HUMAN_EYE_HEIGHT + 5);
247
+ }
248
+ }, [mode, tiles]);
249
+ useEffect(() => {
250
+ const canvas = canvasRef.current;
251
+ if (!canvas || tiles.length === 0) {
252
+ return;
253
+ }
254
+ const context = canvas.getContext("2d");
255
+ if (!context) {
256
+ return;
257
+ }
258
+ let frame = 0;
259
+ let lastTime = performance.now();
260
+ let hudAccumulator = 0;
261
+ const applyCanvasSize = () => {
262
+ const ratio = window.devicePixelRatio || 1;
263
+ const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
264
+ const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
265
+ if (canvas.width !== width || canvas.height !== height) {
266
+ canvas.width = width;
267
+ canvas.height = height;
268
+ }
269
+ };
270
+ const onKeyDown = (event) => {
271
+ if (event.repeat) {
272
+ return;
273
+ }
274
+ if (event.code === "KeyF") {
275
+ setMode((current) => (current === "walk" ? "fly" : "walk"));
276
+ event.preventDefault();
277
+ return;
278
+ }
279
+ keysRef.current.add(event.code);
280
+ if (event.code === "Space" ||
281
+ event.code.startsWith("Arrow") ||
282
+ event.code === "KeyW" ||
283
+ event.code === "KeyA" ||
284
+ event.code === "KeyS" ||
285
+ event.code === "KeyD") {
286
+ event.preventDefault();
287
+ }
288
+ };
289
+ const onKeyUp = (event) => {
290
+ keysRef.current.delete(event.code);
291
+ };
292
+ const onBlur = () => {
293
+ keysRef.current.clear();
294
+ dragRef.current.active = false;
295
+ };
296
+ window.addEventListener("keydown", onKeyDown);
297
+ window.addEventListener("keyup", onKeyUp);
298
+ window.addEventListener("blur", onBlur);
299
+ const tick = (now) => {
300
+ applyCanvasSize();
301
+ const camera = cameraRef.current;
302
+ const deltaSeconds = Math.min(0.05, (now - lastTime) / 1000);
303
+ lastTime = now;
304
+ const keys = keysRef.current;
305
+ const sprint = keys.has("ShiftLeft") || keys.has("ShiftRight");
306
+ const baseSpeed = mode === "fly" ? FLY_SPEED : WALK_SPEED;
307
+ const speed = sprint ? baseSpeed * 1.85 : baseSpeed;
308
+ let inputX = 0;
309
+ let inputZ = 0;
310
+ if (keys.has("KeyW") || keys.has("ArrowUp"))
311
+ inputZ += 1;
312
+ if (keys.has("KeyS") || keys.has("ArrowDown"))
313
+ inputZ -= 1;
314
+ if (keys.has("KeyA") || keys.has("ArrowLeft"))
315
+ inputX -= 1;
316
+ if (keys.has("KeyD") || keys.has("ArrowRight"))
317
+ inputX += 1;
318
+ const inputLength = Math.hypot(inputX, inputZ);
319
+ if (inputLength > 0) {
320
+ const nx = inputX / inputLength;
321
+ const nz = inputZ / inputLength;
322
+ const sinYaw = Math.sin(camera.yaw);
323
+ const cosYaw = Math.cos(camera.yaw);
324
+ camera.x += (nx * cosYaw + nz * sinYaw) * speed * deltaSeconds;
325
+ camera.z += (nz * cosYaw - nx * sinYaw) * speed * deltaSeconds;
326
+ }
327
+ const ground = sampleGroundHeight(tiles, camera.x, camera.z);
328
+ if (mode === "walk") {
329
+ camera.y = ground + HUMAN_EYE_HEIGHT;
330
+ }
331
+ else {
332
+ let verticalInput = 0;
333
+ if (keys.has("Space") || keys.has("KeyE"))
334
+ verticalInput += 1;
335
+ if (keys.has("KeyC") || keys.has("KeyQ"))
336
+ verticalInput -= 1;
337
+ camera.y += verticalInput * VERTICAL_FLY_SPEED * deltaSeconds;
338
+ camera.y = Math.max(camera.y, ground + 0.6);
339
+ }
340
+ camera.pitch = clamp(camera.pitch, -1.2, 0.75);
341
+ camera.yaw = wrapAngle(camera.yaw);
342
+ const nearestIndex = findNearestTileIndex(tiles, camera.x, camera.z);
343
+ renderScene(context, canvas, tiles, camera, nearestIndex);
344
+ hudAccumulator += deltaSeconds * 1000;
345
+ if (hudAccumulator >= CAMERA_UPDATE_MS) {
346
+ hudAccumulator = 0;
347
+ setSelectedIndex(nearestIndex);
348
+ setCameraHud({
349
+ ...camera,
350
+ ground,
351
+ });
352
+ }
353
+ frame = window.requestAnimationFrame(tick);
354
+ };
355
+ frame = window.requestAnimationFrame(tick);
356
+ return () => {
357
+ window.cancelAnimationFrame(frame);
358
+ window.removeEventListener("keydown", onKeyDown);
359
+ window.removeEventListener("keyup", onKeyUp);
360
+ window.removeEventListener("blur", onBlur);
361
+ };
362
+ }, [mode, tiles]);
363
+ const handlePointerDown = (event) => {
364
+ const canvas = canvasRef.current;
365
+ if (!canvas) {
366
+ return;
367
+ }
368
+ canvas.focus();
369
+ canvas.setPointerCapture(event.pointerId);
370
+ dragRef.current.active = true;
371
+ dragRef.current.pointerId = event.pointerId;
372
+ dragRef.current.lastX = event.clientX;
373
+ dragRef.current.lastY = event.clientY;
374
+ };
375
+ const handlePointerMove = (event) => {
376
+ if (!dragRef.current.active || dragRef.current.pointerId !== event.pointerId) {
377
+ return;
378
+ }
379
+ const dx = event.clientX - dragRef.current.lastX;
380
+ const dy = event.clientY - dragRef.current.lastY;
381
+ dragRef.current.lastX = event.clientX;
382
+ dragRef.current.lastY = event.clientY;
383
+ const camera = cameraRef.current;
384
+ camera.yaw = wrapAngle(camera.yaw + dx * LOOK_SENSITIVITY);
385
+ camera.pitch = clamp(camera.pitch - dy * LOOK_SENSITIVITY * 0.82, -1.2, 0.75);
386
+ };
387
+ const handlePointerUp = (event) => {
388
+ if (dragRef.current.pointerId !== event.pointerId) {
389
+ return;
390
+ }
391
+ dragRef.current.active = false;
392
+ dragRef.current.pointerId = -1;
393
+ event.currentTarget.releasePointerCapture(event.pointerId);
394
+ };
24
395
  const handleRegenerate = () => {
25
396
  setSeed((current) => ((current * 1664525 + 1013904223) >>> 0) % 2147483647);
26
397
  setSelectedIndex(0);
27
398
  };
28
- return (_jsx(ErrorBoundary, { name: "Game", children: _jsxs("div", { className: styles.game, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { children: [_jsx("h1", { className: styles.title, children: "GPU World Cell Explorer" }), _jsxs("p", { className: styles.subtitle, children: ["Hex terrain generated through ", _jsx("code", { children: "@plasius/gpu-world-generator" }), " ", "and configured alongside lighting/particle/XR package profiles."] })] }), _jsxs("div", { className: styles.controls, children: [_jsxs("span", { className: styles.seed, children: ["Seed ", seed] }), _jsx("button", { className: styles.button, onClick: handleRegenerate, children: "Regenerate" })] })] }), _jsxs("div", { className: styles.layout, children: [_jsx("section", { className: styles.mapCard, children: _jsx("svg", { className: styles.map, viewBox: viewBox, role: "img", "aria-label": "Hex terrain map", children: tiles.map((tile, index) => (_jsx("polygon", { className: `${styles.tile} ${index === safeSelectedIndex ? styles.tileActive : ""}`, points: tile.points, fill: tile.color, onPointerEnter: () => setSelectedIndex(index), onClick: () => setSelectedIndex(index) }, `${tile.q}:${tile.r}`))) }) }), _jsxs("aside", { className: styles.panel, children: [_jsx("h2", { className: styles.panelTitle, children: "Selected Tile" }), _jsxs("dl", { className: styles.stats, children: [_jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Axial" }), _jsxs("dd", { className: styles.value, children: ["q ", selected?.q ?? 0, ", r ", selected?.r ?? 0] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Biome" }), _jsx("dd", { className: styles.value, children: selected
399
+ return (_jsx(ErrorBoundary, { name: "Game", children: _jsxs("div", { className: styles.game, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { children: [_jsx("h1", { className: styles.title, children: "Generator" }), _jsx("p", { className: styles.subtitle, children: "Explore procedurally generated terrain in first-person. Walk at human eye height above the surface or switch to fly mode for aerial scouting." })] }), _jsxs("div", { className: styles.controls, children: [_jsxs("span", { className: styles.seed, children: ["Seed ", seed] }), _jsxs("button", { className: styles.modeButton, onClick: () => setMode((current) => (current === "walk" ? "fly" : "walk")), children: ["Mode: ", mode === "walk" ? "Walk" : "Fly"] }), _jsx("button", { className: styles.button, onClick: handleRegenerate, children: "Regenerate" })] })] }), _jsxs("div", { className: styles.layout, children: [_jsxs("section", { className: styles.mapCard, children: [_jsx("canvas", { ref: canvasRef, className: styles.viewport, role: "img", "aria-label": "Generator first-person world view", tabIndex: 0, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, onContextMenu: (event) => event.preventDefault() }), _jsxs("div", { className: styles.overlay, children: [_jsx("span", { className: styles.overlayTitle, children: mode === "walk" ? "Walking View" : "Flight View" }), _jsx("span", { className: styles.overlayText, children: "Drag to look \u2022 W/A/S/D move \u2022 F toggles walk/fly" })] })] }), _jsxs("aside", { className: styles.panel, children: [_jsx("h2", { className: styles.panelTitle, children: "Selected Tile" }), _jsxs("dl", { className: styles.stats, children: [_jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Axial" }), _jsxs("dd", { className: styles.value, children: ["q ", selected?.q ?? 0, ", r ", selected?.r ?? 0] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Biome" }), _jsx("dd", { className: styles.value, children: selected
29
400
  ? TerrainBiomeLabel[selected.terrain.biome]
30
401
  : "Unknown" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Macro Biome" }), _jsx("dd", { className: styles.value, children: selected?.terrain.macroBiome === undefined
31
402
  ? "n/a"
@@ -33,5 +404,5 @@ export function Game() {
33
404
  ? "n/a"
34
405
  : SurfaceCoverLabel[selected.terrain.surface] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Feature" }), _jsx("dd", { className: styles.value, children: selected?.terrain.feature === undefined
35
406
  ? "none"
36
- : MicroFeatureLabel[selected.terrain.feature] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Height" }), _jsx("dd", { className: styles.value, children: selected ? formatNumber(selected.terrain.height) : "n/a" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Heat" }), _jsx("dd", { className: styles.value, children: selected ? formatPercent(selected.terrain.heat) : "n/a" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Moisture" }), _jsx("dd", { className: styles.value, children: selected ? formatPercent(selected.terrain.moisture) : "n/a" })] })] }), _jsx("h2", { className: styles.panelTitle, children: "GPU Stack" }), _jsxs("div", { className: styles.chipRow, children: [_jsx("span", { className: styles.chip, children: "worldgen: mixed-forest" }), _jsxs("span", { className: styles.chip, children: ["tiles: ", tiles.length] }), _jsxs("span", { className: styles.chip, children: ["xr modes: ", xrSessionModes.filter((mode) => mode !== "inline").length] })] }), _jsxs("dl", { className: styles.stats, children: [_jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "World Generator" }), _jsx("dd", { className: styles.value, children: "@plasius/gpu-world-generator" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "XR Runtime" }), _jsx("dd", { className: styles.value, children: "@plasius/gpu-xr" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "XR Session Modes" }), _jsx("dd", { className: styles.value, children: xrSessionModes.filter((mode) => mode !== "inline").join(", ") })] })] })] })] })] }) }));
407
+ : MicroFeatureLabel[selected.terrain.feature] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Height" }), _jsx("dd", { className: styles.value, children: selected ? formatNumber(selected.terrain.height) : "n/a" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Heat" }), _jsx("dd", { className: styles.value, children: selected ? formatPercent(selected.terrain.heat) : "n/a" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Moisture" }), _jsx("dd", { className: styles.value, children: selected ? formatPercent(selected.terrain.moisture) : "n/a" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Camera X/Z" }), _jsxs("dd", { className: styles.value, children: [formatNumber(cameraHud.x), " / ", formatNumber(cameraHud.z)] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Camera Height" }), _jsxs("dd", { className: styles.value, children: [formatNumber(cameraHud.y), " m"] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Clearance" }), _jsxs("dd", { className: styles.value, children: [formatNumber(cameraHud.y - cameraHud.ground), " m"] })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "Yaw / Pitch" }), _jsxs("dd", { className: styles.value, children: [Math.round((cameraHud.yaw * 180) / Math.PI), "\u00B0 /", " ", Math.round((cameraHud.pitch * 180) / Math.PI), "\u00B0"] })] })] }), _jsx("h2", { className: styles.panelTitle, children: "GPU Stack" }), _jsxs("div", { className: styles.chipRow, children: [_jsx("span", { className: styles.chip, children: "worldgen: mixed-forest" }), _jsxs("span", { className: styles.chip, children: ["tiles: ", tiles.length] }), _jsxs("span", { className: styles.chip, children: ["mode: ", mode] }), _jsxs("span", { className: styles.chip, children: ["xr modes: ", xrSessionModes.filter((mode) => mode !== "inline").length] })] }), _jsxs("dl", { className: styles.stats, children: [_jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "World Generator" }), _jsx("dd", { className: styles.value, children: "@plasius/gpu-world-generator" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "XR Runtime" }), _jsx("dd", { className: styles.value, children: "@plasius/gpu-xr" })] }), _jsxs("div", { className: styles.row, children: [_jsx("dt", { className: styles.label, children: "XR Session Modes" }), _jsx("dd", { className: styles.value, children: xrSessionModes.filter((mode) => mode !== "inline").join(", ") })] })] }), _jsx("h2", { className: styles.panelTitle, children: "Controls" }), _jsxs("ul", { className: styles.controlList, children: [_jsxs("li", { children: [_jsx("kbd", { children: "W" }), _jsx("kbd", { children: "A" }), _jsx("kbd", { children: "S" }), _jsx("kbd", { children: "D" }), " move"] }), _jsxs("li", { children: [_jsx("kbd", { children: "Shift" }), " sprint"] }), _jsxs("li", { children: [_jsx("kbd", { children: "Drag" }), " look around"] }), _jsxs("li", { children: [_jsx("kbd", { children: "F" }), " toggle walk/fly"] }), _jsxs("li", { children: [_jsx("kbd", { children: "Space" }), "/", _jsx("kbd", { children: "E" }), " up (fly)"] }), _jsxs("li", { children: [_jsx("kbd", { children: "Q" }), "/", _jsx("kbd", { children: "C" }), " down (fly)"] })] })] })] })] }) }));
37
408
  }
@@ -45,7 +45,9 @@
45
45
  .controls {
46
46
  display: flex;
47
47
  align-items: center;
48
+ flex-wrap: wrap;
48
49
  gap: 0.65rem;
50
+ justify-content: flex-end;
49
51
  }
50
52
 
51
53
  .seed {
@@ -57,6 +59,27 @@
57
59
  padding: 0.3rem 0.7rem;
58
60
  }
59
61
 
62
+ .modeButton {
63
+ border: 1px solid rgba(120, 201, 255, 0.44);
64
+ color: var(--text);
65
+ background: rgba(120, 201, 255, 0.16);
66
+ border-radius: 999px;
67
+ padding: 0.5rem 0.9rem;
68
+ font-weight: 600;
69
+ letter-spacing: 0.01em;
70
+ cursor: pointer;
71
+ transition: transform 120ms ease, filter 120ms ease;
72
+ }
73
+
74
+ .modeButton:hover {
75
+ transform: translateY(-1px);
76
+ filter: brightness(1.06);
77
+ }
78
+
79
+ .modeButton:active {
80
+ transform: translateY(0);
81
+ }
82
+
60
83
  .button {
61
84
  border: 0;
62
85
  color: #001323;
@@ -93,13 +116,55 @@
93
116
  }
94
117
 
95
118
  .mapCard {
119
+ position: relative;
96
120
  padding: 0.65rem;
97
121
  }
98
122
 
99
- .map {
123
+ .viewport {
100
124
  display: block;
101
125
  width: 100%;
102
126
  height: min(72vh, 760px);
127
+ border-radius: 0.8rem;
128
+ background:
129
+ radial-gradient(90% 85% at 50% 100%, rgba(86, 118, 70, 0.35), transparent 70%),
130
+ linear-gradient(180deg, #0b1c2f, #112943 56%, #1b3428);
131
+ cursor: grab;
132
+ touch-action: none;
133
+ }
134
+
135
+ .viewport:active {
136
+ cursor: grabbing;
137
+ }
138
+
139
+ .viewport:focus-visible {
140
+ outline: 2px solid rgba(138, 214, 255, 0.8);
141
+ outline-offset: 2px;
142
+ }
143
+
144
+ .overlay {
145
+ position: absolute;
146
+ left: 1.2rem;
147
+ bottom: 1.15rem;
148
+ display: grid;
149
+ gap: 0.18rem;
150
+ max-width: min(88%, 32rem);
151
+ padding: 0.6rem 0.78rem;
152
+ border-radius: 0.65rem;
153
+ border: 1px solid rgba(185, 218, 255, 0.26);
154
+ background: rgba(3, 9, 17, 0.58);
155
+ backdrop-filter: blur(4px);
156
+ }
157
+
158
+ .overlayTitle {
159
+ font-size: 0.77rem;
160
+ letter-spacing: 0.06em;
161
+ text-transform: uppercase;
162
+ color: #bde4ff;
163
+ }
164
+
165
+ .overlayText {
166
+ font-size: 0.78rem;
167
+ color: rgba(226, 239, 255, 0.92);
103
168
  }
104
169
 
105
170
  .tile {
@@ -173,6 +238,31 @@
173
238
  border: 1px solid rgba(120, 201, 255, 0.34);
174
239
  }
175
240
 
241
+ .controlList {
242
+ margin: 0;
243
+ padding-left: 1rem;
244
+ display: grid;
245
+ gap: 0.35rem;
246
+ color: var(--text-muted);
247
+ font-size: 0.82rem;
248
+ }
249
+
250
+ .controlList li {
251
+ display: flex;
252
+ gap: 0.3rem;
253
+ flex-wrap: wrap;
254
+ align-items: center;
255
+ }
256
+
257
+ .controlList kbd {
258
+ font-size: 0.73rem;
259
+ border-radius: 0.32rem;
260
+ border: 1px solid rgba(157, 179, 209, 0.4);
261
+ padding: 0.12rem 0.32rem;
262
+ color: #e7f0ff;
263
+ background: rgba(14, 24, 38, 0.72);
264
+ }
265
+
176
266
  @keyframes reveal {
177
267
  from {
178
268
  opacity: 0;
@@ -202,7 +292,13 @@
202
292
  align-items: flex-start;
203
293
  }
204
294
 
205
- .map {
295
+ .viewport {
206
296
  height: min(58vh, 540px);
207
297
  }
298
+
299
+ .overlay {
300
+ left: 1rem;
301
+ right: 1rem;
302
+ max-width: none;
303
+ }
208
304
  }
@@ -1 +1 @@
1
- {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.tsx"],"names":[],"mappings":"AA0BA,wBAAgB,IAAI,4CAgKnB"}
1
+ {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.tsx"],"names":[],"mappings":"AA4RA,wBAAgB,IAAI,4CAuZnB"}