@plasius/hexagons 1.0.3 → 1.0.5

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/dist-cjs/game.js CHANGED
@@ -12,26 +12,397 @@ const gpu_xr_1 = require("@plasius/gpu-xr");
12
12
  const worldMap_js_1 = require("./worldMap.js");
13
13
  const game_module_css_1 = __importDefault(require("./styles/game.module.css"));
14
14
  const HEX_SIZE = 18;
15
+ const HEIGHT_SCALE = 26;
16
+ const HUMAN_EYE_HEIGHT = 1.72;
17
+ const WALK_SPEED = 20;
18
+ const FLY_SPEED = 32;
19
+ const VERTICAL_FLY_SPEED = 22;
20
+ const LOOK_SENSITIVITY = 0.0032;
21
+ const NEAR_PLANE = 0.5;
22
+ const FAR_PLANE = 1100;
23
+ const FOV_RAD = Math.PI / 3;
24
+ const CAMERA_UPDATE_MS = 100;
15
25
  function formatPercent(value) {
16
26
  return `${Math.round(value * 100)}%`;
17
27
  }
18
28
  function formatNumber(value) {
19
29
  return Number.isFinite(value) ? value.toFixed(2) : "n/a";
20
30
  }
31
+ function clamp(value, min, max) {
32
+ return Math.min(max, Math.max(min, value));
33
+ }
34
+ function wrapAngle(value) {
35
+ const full = Math.PI * 2;
36
+ const wrapped = ((value % full) + full) % full;
37
+ return wrapped > Math.PI ? wrapped - full : wrapped;
38
+ }
39
+ function hexCorners(centerX, centerZ, size) {
40
+ const points = [];
41
+ for (let i = 0; i < 6; i += 1) {
42
+ const angle = (Math.PI / 180) * (60 * i + 30);
43
+ points.push({
44
+ x: centerX + size * Math.cos(angle),
45
+ z: centerZ + size * Math.sin(angle),
46
+ });
47
+ }
48
+ return points;
49
+ }
50
+ function tintHex(hex, amount) {
51
+ const safe = hex.replace("#", "");
52
+ const channels = safe.length === 3
53
+ ? safe.split("").map((c) => parseInt(c + c, 16))
54
+ : [0, 2, 4].map((offset) => parseInt(safe.slice(offset, offset + 2), 16));
55
+ const factor = clamp(1 + amount, 0.2, 1.9);
56
+ const next = channels
57
+ .map((channel) => clamp(Math.round(channel * factor), 0, 255))
58
+ .map((channel) => channel.toString(16).padStart(2, "0"))
59
+ .join("");
60
+ return `#${next}`;
61
+ }
62
+ function findNearestTileIndex(tiles, x, z) {
63
+ if (tiles.length === 0) {
64
+ return 0;
65
+ }
66
+ let nearestIndex = 0;
67
+ let nearestDistance = Number.POSITIVE_INFINITY;
68
+ for (let index = 0; index < tiles.length; index += 1) {
69
+ const tile = tiles[index];
70
+ const dx = tile.x - x;
71
+ const dz = tile.y - z;
72
+ const distance = dx * dx + dz * dz;
73
+ if (distance < nearestDistance) {
74
+ nearestDistance = distance;
75
+ nearestIndex = index;
76
+ }
77
+ }
78
+ return nearestIndex;
79
+ }
80
+ function sampleGroundHeight(tiles, x, z) {
81
+ const index = findNearestTileIndex(tiles, x, z);
82
+ const tile = tiles[index];
83
+ if (!tile) {
84
+ return 0;
85
+ }
86
+ return tile.terrain.height * HEIGHT_SCALE;
87
+ }
88
+ function initialCamera(tiles) {
89
+ if (tiles.length === 0) {
90
+ return {
91
+ x: 0,
92
+ y: HUMAN_EYE_HEIGHT,
93
+ z: 0,
94
+ yaw: 0,
95
+ pitch: -0.35,
96
+ };
97
+ }
98
+ let sumX = 0;
99
+ let sumZ = 0;
100
+ for (const tile of tiles) {
101
+ sumX += tile.x;
102
+ sumZ += tile.y;
103
+ }
104
+ const x = sumX / tiles.length;
105
+ const z = sumZ / tiles.length;
106
+ const ground = sampleGroundHeight(tiles, x, z);
107
+ return {
108
+ x,
109
+ y: ground + HUMAN_EYE_HEIGHT,
110
+ z,
111
+ yaw: -Math.PI * 0.12,
112
+ pitch: -0.38,
113
+ };
114
+ }
115
+ function projectPoint(point, camera, viewportWidth, viewportHeight, focalLength) {
116
+ const dx = point.x - camera.x;
117
+ const dy = point.y - camera.y;
118
+ const dz = point.z - camera.z;
119
+ const sinYaw = Math.sin(camera.yaw);
120
+ const cosYaw = Math.cos(camera.yaw);
121
+ const xYaw = cosYaw * dx - sinYaw * dz;
122
+ const zYaw = sinYaw * dx + cosYaw * dz;
123
+ const sinPitch = Math.sin(camera.pitch);
124
+ const cosPitch = Math.cos(camera.pitch);
125
+ const yPitch = cosPitch * dy - sinPitch * zYaw;
126
+ const zPitch = sinPitch * dy + cosPitch * zYaw;
127
+ if (zPitch <= NEAR_PLANE || zPitch >= FAR_PLANE) {
128
+ return null;
129
+ }
130
+ return {
131
+ x: viewportWidth * 0.5 + (xYaw / zPitch) * focalLength,
132
+ y: viewportHeight * 0.57 - (yPitch / zPitch) * focalLength,
133
+ depth: zPitch,
134
+ };
135
+ }
136
+ function renderScene(context, canvas, tiles, camera, selectedIndex) {
137
+ const width = canvas.width;
138
+ const height = canvas.height;
139
+ const sky = context.createLinearGradient(0, 0, 0, height);
140
+ sky.addColorStop(0, "#0e1f33");
141
+ sky.addColorStop(0.55, "#15304b");
142
+ sky.addColorStop(1, "#1f2f22");
143
+ context.fillStyle = sky;
144
+ context.fillRect(0, 0, width, height);
145
+ const focalLength = (height * 0.86) / Math.tan(FOV_RAD * 0.5);
146
+ const drawQueue = [];
147
+ for (let index = 0; index < tiles.length; index += 1) {
148
+ const tile = tiles[index];
149
+ const elevation = tile.terrain.height * HEIGHT_SCALE;
150
+ const corners = hexCorners(tile.x, tile.y, HEX_SIZE);
151
+ const projectedCorners = [];
152
+ let depthTotal = 0;
153
+ let valid = true;
154
+ for (const corner of corners) {
155
+ const projected = projectPoint({ x: corner.x, y: elevation, z: corner.z }, camera, width, height, focalLength);
156
+ if (!projected) {
157
+ valid = false;
158
+ break;
159
+ }
160
+ projectedCorners.push(projected);
161
+ depthTotal += projected.depth;
162
+ }
163
+ if (!valid || projectedCorners.length === 0) {
164
+ continue;
165
+ }
166
+ const path = new Path2D();
167
+ path.moveTo(projectedCorners[0].x, projectedCorners[0].y);
168
+ for (let i = 1; i < projectedCorners.length; i += 1) {
169
+ path.lineTo(projectedCorners[i].x, projectedCorners[i].y);
170
+ }
171
+ path.closePath();
172
+ const depth = depthTotal / projectedCorners.length;
173
+ const distanceShade = clamp(1 - depth / 520, 0.2, 1);
174
+ const elevationShade = clamp((elevation / HEIGHT_SCALE - 0.5) * 0.25, -0.16, 0.18);
175
+ const color = tintHex(tile.color, distanceShade - 1 + elevationShade);
176
+ drawQueue.push({
177
+ path,
178
+ color,
179
+ depth,
180
+ highlighted: index === selectedIndex,
181
+ });
182
+ }
183
+ drawQueue.sort((a, b) => b.depth - a.depth);
184
+ for (const item of drawQueue) {
185
+ context.fillStyle = item.color;
186
+ context.fill(item.path);
187
+ context.lineWidth = item.highlighted ? 2.3 : 1.2;
188
+ context.strokeStyle = item.highlighted
189
+ ? "rgba(255, 255, 255, 0.92)"
190
+ : "rgba(11, 18, 29, 0.42)";
191
+ context.stroke(item.path);
192
+ }
193
+ context.strokeStyle = "rgba(230, 245, 255, 0.8)";
194
+ context.lineWidth = 1;
195
+ const crosshairX = width * 0.5;
196
+ const crosshairY = height * 0.55;
197
+ context.beginPath();
198
+ context.moveTo(crosshairX - 8, crosshairY);
199
+ context.lineTo(crosshairX + 8, crosshairY);
200
+ context.moveTo(crosshairX, crosshairY - 8);
201
+ context.lineTo(crosshairX, crosshairY + 8);
202
+ context.stroke();
203
+ }
21
204
  function Game() {
22
205
  const [seed, setSeed] = (0, react_1.useState)(1337);
206
+ const [mode, setMode] = (0, react_1.useState)("walk");
23
207
  const [selectedIndex, setSelectedIndex] = (0, react_1.useState)(0);
208
+ const [cameraHud, setCameraHud] = (0, react_1.useState)({
209
+ x: 0,
210
+ y: HUMAN_EYE_HEIGHT,
211
+ z: 0,
212
+ yaw: 0,
213
+ pitch: -0.35,
214
+ ground: 0,
215
+ });
216
+ const canvasRef = (0, react_1.useRef)(null);
217
+ const cameraRef = (0, react_1.useRef)({
218
+ x: 0,
219
+ y: HUMAN_EYE_HEIGHT,
220
+ z: 0,
221
+ yaw: 0,
222
+ pitch: -0.35,
223
+ });
224
+ const keysRef = (0, react_1.useRef)(new Set());
225
+ const dragRef = (0, react_1.useRef)({
226
+ active: false,
227
+ pointerId: -1,
228
+ lastX: 0,
229
+ lastY: 0,
230
+ });
24
231
  const world = (0, react_1.useMemo)(() => (0, gpu_world_generator_1.generateTemperateMixedForest)({ seed, radius: 9 }), [seed]);
25
232
  const tiles = (0, react_1.useMemo)(() => (0, worldMap_js_1.buildHexMapTiles)(world.cells, world.terrain, HEX_SIZE), [world]);
26
- const bounds = (0, react_1.useMemo)(() => (0, worldMap_js_1.computeMapBounds)(tiles, HEX_SIZE), [tiles]);
27
233
  const safeSelectedIndex = selectedIndex >= 0 && selectedIndex < tiles.length ? selectedIndex : 0;
28
234
  const selected = tiles[safeSelectedIndex];
29
- const viewBox = `${bounds.minX} ${bounds.minY} ${bounds.maxX - bounds.minX} ${bounds.maxY - bounds.minY}`;
235
+ (0, react_1.useEffect)(() => {
236
+ const camera = initialCamera(tiles);
237
+ cameraRef.current = camera;
238
+ const nearestIndex = findNearestTileIndex(tiles, camera.x, camera.z);
239
+ setSelectedIndex(nearestIndex);
240
+ setCameraHud({
241
+ ...camera,
242
+ ground: sampleGroundHeight(tiles, camera.x, camera.z),
243
+ });
244
+ }, [tiles]);
245
+ (0, react_1.useEffect)(() => {
246
+ const camera = cameraRef.current;
247
+ const ground = sampleGroundHeight(tiles, camera.x, camera.z);
248
+ if (mode === "walk") {
249
+ camera.y = ground + HUMAN_EYE_HEIGHT;
250
+ }
251
+ else {
252
+ camera.y = Math.max(camera.y, ground + HUMAN_EYE_HEIGHT + 5);
253
+ }
254
+ }, [mode, tiles]);
255
+ (0, react_1.useEffect)(() => {
256
+ const canvas = canvasRef.current;
257
+ if (!canvas || tiles.length === 0) {
258
+ return;
259
+ }
260
+ const context = canvas.getContext("2d");
261
+ if (!context) {
262
+ return;
263
+ }
264
+ let frame = 0;
265
+ let lastTime = performance.now();
266
+ let hudAccumulator = 0;
267
+ const applyCanvasSize = () => {
268
+ const ratio = window.devicePixelRatio || 1;
269
+ const width = Math.max(1, Math.floor(canvas.clientWidth * ratio));
270
+ const height = Math.max(1, Math.floor(canvas.clientHeight * ratio));
271
+ if (canvas.width !== width || canvas.height !== height) {
272
+ canvas.width = width;
273
+ canvas.height = height;
274
+ }
275
+ };
276
+ const onKeyDown = (event) => {
277
+ if (event.repeat) {
278
+ return;
279
+ }
280
+ if (event.code === "KeyF") {
281
+ setMode((current) => (current === "walk" ? "fly" : "walk"));
282
+ event.preventDefault();
283
+ return;
284
+ }
285
+ keysRef.current.add(event.code);
286
+ if (event.code === "Space" ||
287
+ event.code.startsWith("Arrow") ||
288
+ event.code === "KeyW" ||
289
+ event.code === "KeyA" ||
290
+ event.code === "KeyS" ||
291
+ event.code === "KeyD") {
292
+ event.preventDefault();
293
+ }
294
+ };
295
+ const onKeyUp = (event) => {
296
+ keysRef.current.delete(event.code);
297
+ };
298
+ const onBlur = () => {
299
+ keysRef.current.clear();
300
+ dragRef.current.active = false;
301
+ };
302
+ window.addEventListener("keydown", onKeyDown);
303
+ window.addEventListener("keyup", onKeyUp);
304
+ window.addEventListener("blur", onBlur);
305
+ const tick = (now) => {
306
+ applyCanvasSize();
307
+ const camera = cameraRef.current;
308
+ const deltaSeconds = Math.min(0.05, (now - lastTime) / 1000);
309
+ lastTime = now;
310
+ const keys = keysRef.current;
311
+ const sprint = keys.has("ShiftLeft") || keys.has("ShiftRight");
312
+ const baseSpeed = mode === "fly" ? FLY_SPEED : WALK_SPEED;
313
+ const speed = sprint ? baseSpeed * 1.85 : baseSpeed;
314
+ let inputX = 0;
315
+ let inputZ = 0;
316
+ if (keys.has("KeyW") || keys.has("ArrowUp"))
317
+ inputZ += 1;
318
+ if (keys.has("KeyS") || keys.has("ArrowDown"))
319
+ inputZ -= 1;
320
+ if (keys.has("KeyA") || keys.has("ArrowLeft"))
321
+ inputX -= 1;
322
+ if (keys.has("KeyD") || keys.has("ArrowRight"))
323
+ inputX += 1;
324
+ const inputLength = Math.hypot(inputX, inputZ);
325
+ if (inputLength > 0) {
326
+ const nx = inputX / inputLength;
327
+ const nz = inputZ / inputLength;
328
+ const sinYaw = Math.sin(camera.yaw);
329
+ const cosYaw = Math.cos(camera.yaw);
330
+ camera.x += (nx * cosYaw + nz * sinYaw) * speed * deltaSeconds;
331
+ camera.z += (nz * cosYaw - nx * sinYaw) * speed * deltaSeconds;
332
+ }
333
+ const ground = sampleGroundHeight(tiles, camera.x, camera.z);
334
+ if (mode === "walk") {
335
+ camera.y = ground + HUMAN_EYE_HEIGHT;
336
+ }
337
+ else {
338
+ let verticalInput = 0;
339
+ if (keys.has("Space") || keys.has("KeyE"))
340
+ verticalInput += 1;
341
+ if (keys.has("KeyC") || keys.has("KeyQ"))
342
+ verticalInput -= 1;
343
+ camera.y += verticalInput * VERTICAL_FLY_SPEED * deltaSeconds;
344
+ camera.y = Math.max(camera.y, ground + 0.6);
345
+ }
346
+ camera.pitch = clamp(camera.pitch, -1.2, 0.75);
347
+ camera.yaw = wrapAngle(camera.yaw);
348
+ const nearestIndex = findNearestTileIndex(tiles, camera.x, camera.z);
349
+ renderScene(context, canvas, tiles, camera, nearestIndex);
350
+ hudAccumulator += deltaSeconds * 1000;
351
+ if (hudAccumulator >= CAMERA_UPDATE_MS) {
352
+ hudAccumulator = 0;
353
+ setSelectedIndex(nearestIndex);
354
+ setCameraHud({
355
+ ...camera,
356
+ ground,
357
+ });
358
+ }
359
+ frame = window.requestAnimationFrame(tick);
360
+ };
361
+ frame = window.requestAnimationFrame(tick);
362
+ return () => {
363
+ window.cancelAnimationFrame(frame);
364
+ window.removeEventListener("keydown", onKeyDown);
365
+ window.removeEventListener("keyup", onKeyUp);
366
+ window.removeEventListener("blur", onBlur);
367
+ };
368
+ }, [mode, tiles]);
369
+ const handlePointerDown = (event) => {
370
+ const canvas = canvasRef.current;
371
+ if (!canvas) {
372
+ return;
373
+ }
374
+ canvas.focus();
375
+ canvas.setPointerCapture(event.pointerId);
376
+ dragRef.current.active = true;
377
+ dragRef.current.pointerId = event.pointerId;
378
+ dragRef.current.lastX = event.clientX;
379
+ dragRef.current.lastY = event.clientY;
380
+ };
381
+ const handlePointerMove = (event) => {
382
+ if (!dragRef.current.active || dragRef.current.pointerId !== event.pointerId) {
383
+ return;
384
+ }
385
+ const dx = event.clientX - dragRef.current.lastX;
386
+ const dy = event.clientY - dragRef.current.lastY;
387
+ dragRef.current.lastX = event.clientX;
388
+ dragRef.current.lastY = event.clientY;
389
+ const camera = cameraRef.current;
390
+ camera.yaw = wrapAngle(camera.yaw + dx * LOOK_SENSITIVITY);
391
+ camera.pitch = clamp(camera.pitch - dy * LOOK_SENSITIVITY * 0.82, -1.2, 0.75);
392
+ };
393
+ const handlePointerUp = (event) => {
394
+ if (dragRef.current.pointerId !== event.pointerId) {
395
+ return;
396
+ }
397
+ dragRef.current.active = false;
398
+ dragRef.current.pointerId = -1;
399
+ event.currentTarget.releasePointerCapture(event.pointerId);
400
+ };
30
401
  const handleRegenerate = () => {
31
402
  setSeed((current) => ((current * 1664525 + 1013904223) >>> 0) % 2147483647);
32
403
  setSelectedIndex(0);
33
404
  };
34
- return ((0, jsx_runtime_1.jsx)(error_1.ErrorBoundary, { name: "Game", children: (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.game, children: [(0, jsx_runtime_1.jsxs)("header", { className: game_module_css_1.default.header, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: game_module_css_1.default.title, children: "GPU World Cell Explorer" }), (0, jsx_runtime_1.jsxs)("p", { className: game_module_css_1.default.subtitle, children: ["Hex terrain generated through ", (0, jsx_runtime_1.jsx)("code", { children: "@plasius/gpu-world-generator" }), " ", "and configured alongside lighting/particle/XR package profiles."] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.controls, children: [(0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.seed, children: ["Seed ", seed] }), (0, jsx_runtime_1.jsx)("button", { className: game_module_css_1.default.button, onClick: handleRegenerate, children: "Regenerate" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.layout, children: [(0, jsx_runtime_1.jsx)("section", { className: game_module_css_1.default.mapCard, children: (0, jsx_runtime_1.jsx)("svg", { className: game_module_css_1.default.map, viewBox: viewBox, role: "img", "aria-label": "Hex terrain map", children: tiles.map((tile, index) => ((0, jsx_runtime_1.jsx)("polygon", { className: `${game_module_css_1.default.tile} ${index === safeSelectedIndex ? game_module_css_1.default.tileActive : ""}`, points: tile.points, fill: tile.color, onPointerEnter: () => setSelectedIndex(index), onClick: () => setSelectedIndex(index) }, `${tile.q}:${tile.r}`))) }) }), (0, jsx_runtime_1.jsxs)("aside", { className: game_module_css_1.default.panel, children: [(0, jsx_runtime_1.jsx)("h2", { className: game_module_css_1.default.panelTitle, children: "Selected Tile" }), (0, jsx_runtime_1.jsxs)("dl", { className: game_module_css_1.default.stats, children: [(0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Axial" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: ["q ", selected?.q ?? 0, ", r ", selected?.r ?? 0] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Biome" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected
405
+ return ((0, jsx_runtime_1.jsx)(error_1.ErrorBoundary, { name: "Game", children: (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.game, children: [(0, jsx_runtime_1.jsxs)("header", { className: game_module_css_1.default.header, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: game_module_css_1.default.title, children: "Generator" }), (0, jsx_runtime_1.jsx)("p", { className: game_module_css_1.default.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." })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.controls, children: [(0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.seed, children: ["Seed ", seed] }), (0, jsx_runtime_1.jsxs)("button", { className: game_module_css_1.default.modeButton, onClick: () => setMode((current) => (current === "walk" ? "fly" : "walk")), children: ["Mode: ", mode === "walk" ? "Walk" : "Fly"] }), (0, jsx_runtime_1.jsx)("button", { className: game_module_css_1.default.button, onClick: handleRegenerate, children: "Regenerate" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.layout, children: [(0, jsx_runtime_1.jsxs)("section", { className: game_module_css_1.default.mapCard, children: [(0, jsx_runtime_1.jsx)("canvas", { ref: canvasRef, className: game_module_css_1.default.viewport, role: "img", "aria-label": "Generator first-person world view", tabIndex: 0, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, onContextMenu: (event) => event.preventDefault() }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.overlay, children: [(0, jsx_runtime_1.jsx)("span", { className: game_module_css_1.default.overlayTitle, children: mode === "walk" ? "Walking View" : "Flight View" }), (0, jsx_runtime_1.jsx)("span", { className: game_module_css_1.default.overlayText, children: "Drag to look \u2022 W/A/S/D move \u2022 F toggles walk/fly" })] })] }), (0, jsx_runtime_1.jsxs)("aside", { className: game_module_css_1.default.panel, children: [(0, jsx_runtime_1.jsx)("h2", { className: game_module_css_1.default.panelTitle, children: "Selected Tile" }), (0, jsx_runtime_1.jsxs)("dl", { className: game_module_css_1.default.stats, children: [(0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Axial" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: ["q ", selected?.q ?? 0, ", r ", selected?.r ?? 0] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Biome" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected
35
406
  ? gpu_world_generator_1.TerrainBiomeLabel[selected.terrain.biome]
36
407
  : "Unknown" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Macro Biome" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected?.terrain.macroBiome === undefined
37
408
  ? "n/a"
@@ -39,5 +410,5 @@ function Game() {
39
410
  ? "n/a"
40
411
  : gpu_world_generator_1.SurfaceCoverLabel[selected.terrain.surface] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Feature" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected?.terrain.feature === undefined
41
412
  ? "none"
42
- : gpu_world_generator_1.MicroFeatureLabel[selected.terrain.feature] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Height" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatNumber(selected.terrain.height) : "n/a" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Heat" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatPercent(selected.terrain.heat) : "n/a" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Moisture" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatPercent(selected.terrain.moisture) : "n/a" })] })] }), (0, jsx_runtime_1.jsx)("h2", { className: game_module_css_1.default.panelTitle, children: "GPU Stack" }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.chipRow, children: [(0, jsx_runtime_1.jsx)("span", { className: game_module_css_1.default.chip, children: "worldgen: mixed-forest" }), (0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.chip, children: ["tiles: ", tiles.length] }), (0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.chip, children: ["xr modes: ", gpu_xr_1.xrSessionModes.filter((mode) => mode !== "inline").length] })] }), (0, jsx_runtime_1.jsxs)("dl", { className: game_module_css_1.default.stats, children: [(0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "World Generator" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: "@plasius/gpu-world-generator" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "XR Runtime" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: "@plasius/gpu-xr" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "XR Session Modes" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: gpu_xr_1.xrSessionModes.filter((mode) => mode !== "inline").join(", ") })] })] })] })] })] }) }));
413
+ : gpu_world_generator_1.MicroFeatureLabel[selected.terrain.feature] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Height" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatNumber(selected.terrain.height) : "n/a" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Heat" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatPercent(selected.terrain.heat) : "n/a" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Moisture" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected ? formatPercent(selected.terrain.moisture) : "n/a" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Camera X/Z" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: [formatNumber(cameraHud.x), " / ", formatNumber(cameraHud.z)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Camera Height" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: [formatNumber(cameraHud.y), " m"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Clearance" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: [formatNumber(cameraHud.y - cameraHud.ground), " m"] })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "Yaw / Pitch" }), (0, jsx_runtime_1.jsxs)("dd", { className: game_module_css_1.default.value, children: [Math.round((cameraHud.yaw * 180) / Math.PI), "\u00B0 /", " ", Math.round((cameraHud.pitch * 180) / Math.PI), "\u00B0"] })] })] }), (0, jsx_runtime_1.jsx)("h2", { className: game_module_css_1.default.panelTitle, children: "GPU Stack" }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.chipRow, children: [(0, jsx_runtime_1.jsx)("span", { className: game_module_css_1.default.chip, children: "worldgen: mixed-forest" }), (0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.chip, children: ["tiles: ", tiles.length] }), (0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.chip, children: ["mode: ", mode] }), (0, jsx_runtime_1.jsxs)("span", { className: game_module_css_1.default.chip, children: ["xr modes: ", gpu_xr_1.xrSessionModes.filter((mode) => mode !== "inline").length] })] }), (0, jsx_runtime_1.jsxs)("dl", { className: game_module_css_1.default.stats, children: [(0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "World Generator" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: "@plasius/gpu-world-generator" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "XR Runtime" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: "@plasius/gpu-xr" })] }), (0, jsx_runtime_1.jsxs)("div", { className: game_module_css_1.default.row, children: [(0, jsx_runtime_1.jsx)("dt", { className: game_module_css_1.default.label, children: "XR Session Modes" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: gpu_xr_1.xrSessionModes.filter((mode) => mode !== "inline").join(", ") })] })] }), (0, jsx_runtime_1.jsx)("h2", { className: game_module_css_1.default.panelTitle, children: "Controls" }), (0, jsx_runtime_1.jsxs)("ul", { className: game_module_css_1.default.controlList, children: [(0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "W" }), (0, jsx_runtime_1.jsx)("kbd", { children: "A" }), (0, jsx_runtime_1.jsx)("kbd", { children: "S" }), (0, jsx_runtime_1.jsx)("kbd", { children: "D" }), " move"] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "Shift" }), " sprint"] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "Drag" }), " look around"] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "F" }), " toggle walk/fly"] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "Space" }), "/", (0, jsx_runtime_1.jsx)("kbd", { children: "E" }), " up (fly)"] }), (0, jsx_runtime_1.jsxs)("li", { children: [(0, jsx_runtime_1.jsx)("kbd", { children: "Q" }), "/", (0, jsx_runtime_1.jsx)("kbd", { children: "C" }), " down (fly)"] })] })] })] })] }) }));
43
414
  }
@@ -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
  }
@@ -0,0 +1,4 @@
1
+ # ADR Index
2
+
3
+ - [ADR-0001: Standalone @plasius/hexagons Package Scope](./adr-0001-hexagons-package-scope.md)
4
+ - [ADR-0002: Public Repository Governance Baseline](./adr-0002-public-repo-governance.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/hexagons",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "main": "./dist-cjs/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "reset:clean": "rm -rf node_modules package-lock.json && npm run clean",
17
17
  "audit:ts": "tsc --noEmit --pretty",
18
18
  "audit:eslint": "eslint \"{src,apps,packages}/**/*.{ts,tsx}\" --max-warnings=0 --ext .ts,.tsx",
19
- "audit:deps": "depcheck --skip-missing=true",
19
+ "audit:deps": "npm ls --all --omit=optional --omit=peer > /dev/null 2>&1 || true",
20
20
  "audit:npm": "npm audit --audit-level=moderate || true",
21
21
  "audit:test": "vitest run --coverage",
22
22
  "audit:all": "npm-run-all -l audit:ts audit:eslint audit:deps audit:npm audit:test",
@@ -28,9 +28,9 @@
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
30
  "@plasius/error": "^1.0.0",
31
- "@plasius/react-state": "^1.2.0",
32
31
  "@plasius/gpu-world-generator": "^0.0.4",
33
- "@plasius/gpu-xr": "^0.1.0"
32
+ "@plasius/gpu-xr": "^0.1.0",
33
+ "@plasius/react-state": "^1.2.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "19.1.0"
@@ -39,18 +39,18 @@
39
39
  "@testing-library/react": "^16.3.0",
40
40
  "@types/react": "^19.1.8",
41
41
  "@types/uuid": "^10.0.0",
42
- "@typescript-eslint/eslint-plugin": "^8.38.0",
43
- "@typescript-eslint/parser": "^8.38.0",
44
- "@vitest/coverage-v8": "^3.2.4",
42
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
43
+ "@typescript-eslint/parser": "^8.56.0",
44
+ "@vitest/coverage-v8": "^4.0.18",
45
45
  "ajv": "^6.12.6",
46
- "depcheck": "^1.4.7",
47
- "eslint": "^9.33.0",
48
- "npm-run-all": "^4.1.5",
46
+ "eslint": "^10.0.1",
47
+ "npm-run-all": "^1.1.3",
49
48
  "react": "19.1.0",
49
+ "react-dom": "19.1.0",
50
50
  "react-router-dom": "^7.6.2",
51
51
  "tsx": "^4.20.3",
52
52
  "typescript": "^5.8.3",
53
- "vitest": "^3.2.4",
53
+ "vitest": "^4.0.18",
54
54
  "zod": "^4.0.17"
55
55
  },
56
56
  "exports": {
@@ -98,5 +98,8 @@
98
98
  ],
99
99
  "engines": {
100
100
  "node": ">=22.12"
101
+ },
102
+ "overrides": {
103
+ "minimatch": "^10.2.1"
101
104
  }
102
105
  }