@plasius/hexagons 1.0.3

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +21 -0
  5. package/README.md +43 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/game.d.ts +2 -0
  8. package/dist/game.d.ts.map +1 -0
  9. package/dist/game.js +37 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/state/gameActions.d.ts +2 -0
  14. package/dist/state/gameActions.d.ts.map +1 -0
  15. package/dist/state/gameActions.js +1 -0
  16. package/dist/state/gameStateProvider.d.ts +28 -0
  17. package/dist/state/gameStateProvider.d.ts.map +1 -0
  18. package/dist/state/gameStateProvider.js +21 -0
  19. package/dist/styles/game.module.css +208 -0
  20. package/dist/worldMap.d.ts +24 -0
  21. package/dist/worldMap.d.ts.map +1 -0
  22. package/dist/worldMap.js +120 -0
  23. package/dist-cjs/game.d.ts +2 -0
  24. package/dist-cjs/game.d.ts.map +1 -0
  25. package/dist-cjs/game.js +43 -0
  26. package/dist-cjs/index.d.ts +2 -0
  27. package/dist-cjs/index.d.ts.map +1 -0
  28. package/dist-cjs/index.js +17 -0
  29. package/dist-cjs/state/gameActions.d.ts +1 -0
  30. package/dist-cjs/state/gameActions.d.ts.map +1 -0
  31. package/dist-cjs/state/gameActions.js +1 -0
  32. package/dist-cjs/state/gameStateProvider.d.ts +28 -0
  33. package/dist-cjs/state/gameStateProvider.d.ts.map +1 -0
  34. package/dist-cjs/state/gameStateProvider.js +24 -0
  35. package/dist-cjs/styles/game.module.css +208 -0
  36. package/dist-cjs/worldMap.d.ts +24 -0
  37. package/dist-cjs/worldMap.d.ts.map +1 -0
  38. package/dist-cjs/worldMap.js +127 -0
  39. package/docs/adrs/adr-0001-hexagons-package-scope.md +21 -0
  40. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  41. package/docs/adrs/adr-template.md +35 -0
  42. package/legal/CLA-REGISTRY.csv +1 -0
  43. package/legal/CLA.md +22 -0
  44. package/legal/CORPORATE_CLA.md +57 -0
  45. package/legal/INDIVIDUAL_CLA.md +91 -0
  46. package/package.json +102 -0
  47. package/src/game.tsx +187 -0
  48. package/src/global.d.ts +4 -0
  49. package/src/index.ts +1 -0
  50. package/src/state/gameActions.ts +0 -0
  51. package/src/state/gameStateProvider.tsx +49 -0
  52. package/src/styles/game.module.css +208 -0
  53. package/src/worldMap.ts +158 -0
@@ -0,0 +1,120 @@
1
+ import { SurfaceCover, TerrainBiome, } from "@plasius/gpu-world-generator";
2
+ const SQRT3 = Math.sqrt(3);
3
+ const SURFACE_COLORS = {
4
+ [SurfaceCover.Grass]: "#5bb768",
5
+ [SurfaceCover.Dirt]: "#86604a",
6
+ [SurfaceCover.Sand]: "#d7b273",
7
+ [SurfaceCover.Rock]: "#7c879a",
8
+ [SurfaceCover.Gravel]: "#91a0b2",
9
+ [SurfaceCover.Snowpack]: "#d7ecff",
10
+ [SurfaceCover.Ice]: "#99e0ff",
11
+ [SurfaceCover.Mud]: "#69503c",
12
+ [SurfaceCover.Ash]: "#6f6476",
13
+ [SurfaceCover.Cobble]: "#9ba7b8",
14
+ [SurfaceCover.Road]: "#6f6a60",
15
+ [SurfaceCover.Water]: "#3f83d2",
16
+ [SurfaceCover.Basalt]: "#4f5a70",
17
+ [SurfaceCover.Crystal]: "#75ffd8",
18
+ [SurfaceCover.Sludge]: "#5d7948",
19
+ };
20
+ const BIOME_COLORS = {
21
+ [TerrainBiome.Plains]: "#89bf67",
22
+ [TerrainBiome.Tundra]: "#a5b8ce",
23
+ [TerrainBiome.Savanna]: "#c8b470",
24
+ [TerrainBiome.River]: "#498dd6",
25
+ [TerrainBiome.City]: "#9da9b8",
26
+ [TerrainBiome.Village]: "#b1906d",
27
+ [TerrainBiome.Ice]: "#b7f0ff",
28
+ [TerrainBiome.Snow]: "#e2f4ff",
29
+ [TerrainBiome.Mountainous]: "#7a8596",
30
+ [TerrainBiome.Volcanic]: "#8d6c69",
31
+ [TerrainBiome.Road]: "#7a7469",
32
+ [TerrainBiome.Town]: "#958774",
33
+ [TerrainBiome.Castle]: "#9aa7bb",
34
+ [TerrainBiome.MixedForest]: "#4f9464",
35
+ };
36
+ function clamp(value, min, max) {
37
+ return Math.min(max, Math.max(min, value));
38
+ }
39
+ function withLightness(hex, amount) {
40
+ const safe = hex.replace("#", "");
41
+ const channels = safe.length === 3
42
+ ? safe.split("").map((c) => parseInt(c + c, 16))
43
+ : [0, 2, 4].map((offset) => parseInt(safe.slice(offset, offset + 2), 16));
44
+ const factor = clamp(1 + amount, 0.3, 1.8);
45
+ const next = channels
46
+ .map((channel) => clamp(Math.round(channel * factor), 0, 255))
47
+ .map((channel) => channel.toString(16).padStart(2, "0"))
48
+ .join("");
49
+ return `#${next}`;
50
+ }
51
+ function defaultBiomeColor(terrain) {
52
+ if (terrain.biome in BIOME_COLORS) {
53
+ return BIOME_COLORS[terrain.biome];
54
+ }
55
+ return "#8b9eb6";
56
+ }
57
+ export function resolveTileColor(terrain) {
58
+ const base = typeof terrain.surface === "number" && terrain.surface in SURFACE_COLORS
59
+ ? SURFACE_COLORS[terrain.surface]
60
+ : defaultBiomeColor(terrain);
61
+ const elevationBoost = clamp(terrain.height * 0.28, -0.2, 0.22);
62
+ return withLightness(base, elevationBoost);
63
+ }
64
+ export function axialToPixel(q, r, size) {
65
+ return {
66
+ x: size * 1.5 * q,
67
+ y: size * SQRT3 * (r + q / 2),
68
+ };
69
+ }
70
+ export function hexPolygonPoints(x, y, size) {
71
+ const points = [];
72
+ for (let i = 0; i < 6; i += 1) {
73
+ const angle = (Math.PI / 180) * (60 * i + 30);
74
+ const px = x + size * Math.cos(angle);
75
+ const py = y + size * Math.sin(angle);
76
+ points.push(`${px.toFixed(2)},${py.toFixed(2)}`);
77
+ }
78
+ return points.join(" ");
79
+ }
80
+ export function buildHexMapTiles(cells, terrain, size) {
81
+ return cells.map((cell, index) => {
82
+ const terrainCell = terrain[index] ?? {
83
+ height: 0,
84
+ heat: 0,
85
+ moisture: 0,
86
+ biome: TerrainBiome.Plains,
87
+ };
88
+ const { x, y } = axialToPixel(cell.q, cell.r, size);
89
+ return {
90
+ q: cell.q,
91
+ r: cell.r,
92
+ x,
93
+ y,
94
+ points: hexPolygonPoints(x, y, size),
95
+ color: resolveTileColor(terrainCell),
96
+ terrain: terrainCell,
97
+ };
98
+ });
99
+ }
100
+ export function computeMapBounds(tiles, size) {
101
+ if (tiles.length === 0) {
102
+ return {
103
+ minX: -size,
104
+ maxX: size,
105
+ minY: -size,
106
+ maxY: size,
107
+ };
108
+ }
109
+ let minX = Number.POSITIVE_INFINITY;
110
+ let maxX = Number.NEGATIVE_INFINITY;
111
+ let minY = Number.POSITIVE_INFINITY;
112
+ let maxY = Number.NEGATIVE_INFINITY;
113
+ for (const tile of tiles) {
114
+ minX = Math.min(minX, tile.x - size);
115
+ maxX = Math.max(maxX, tile.x + size);
116
+ minY = Math.min(minY, tile.y - size);
117
+ maxY = Math.max(maxY, tile.y + size);
118
+ }
119
+ return { minX, maxX, minY, maxY };
120
+ }
@@ -0,0 +1,2 @@
1
+ export declare function Game(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=game.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.tsx"],"names":[],"mappings":"AA0BA,wBAAgB,IAAI,4CAgKnB"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Game = Game;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const error_1 = require("@plasius/error");
10
+ const gpu_world_generator_1 = require("@plasius/gpu-world-generator");
11
+ const gpu_xr_1 = require("@plasius/gpu-xr");
12
+ const worldMap_js_1 = require("./worldMap.js");
13
+ const game_module_css_1 = __importDefault(require("./styles/game.module.css"));
14
+ const HEX_SIZE = 18;
15
+ function formatPercent(value) {
16
+ return `${Math.round(value * 100)}%`;
17
+ }
18
+ function formatNumber(value) {
19
+ return Number.isFinite(value) ? value.toFixed(2) : "n/a";
20
+ }
21
+ function Game() {
22
+ const [seed, setSeed] = (0, react_1.useState)(1337);
23
+ const [selectedIndex, setSelectedIndex] = (0, react_1.useState)(0);
24
+ const world = (0, react_1.useMemo)(() => (0, gpu_world_generator_1.generateTemperateMixedForest)({ seed, radius: 9 }), [seed]);
25
+ 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
+ const safeSelectedIndex = selectedIndex >= 0 && selectedIndex < tiles.length ? selectedIndex : 0;
28
+ const selected = tiles[safeSelectedIndex];
29
+ const viewBox = `${bounds.minX} ${bounds.minY} ${bounds.maxX - bounds.minX} ${bounds.maxY - bounds.minY}`;
30
+ const handleRegenerate = () => {
31
+ setSeed((current) => ((current * 1664525 + 1013904223) >>> 0) % 2147483647);
32
+ setSelectedIndex(0);
33
+ };
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
35
+ ? gpu_world_generator_1.TerrainBiomeLabel[selected.terrain.biome]
36
+ : "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
+ ? "n/a"
38
+ : gpu_world_generator_1.MacroBiomeLabel[selected.terrain.macroBiome] })] }), (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: "Surface" }), (0, jsx_runtime_1.jsx)("dd", { className: game_module_css_1.default.value, children: selected?.terrain.surface === undefined
39
+ ? "n/a"
40
+ : 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
+ ? "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(", ") })] })] })] })] })] }) }));
43
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./game.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./game.js"), exports);
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=gameActions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gameActions.d.ts","sourceRoot":"","sources":["../../src/state/gameActions.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,28 @@
1
+ type GameAction = {
2
+ type: "INCREMENT_SCORE";
3
+ payload: number;
4
+ } | {
5
+ type: "SET_LEVEL";
6
+ payload: number;
7
+ } | {
8
+ type: "TOGGLE_PAUSE";
9
+ } | {
10
+ type: "RESET_GAME";
11
+ };
12
+ interface GameState {
13
+ score: number;
14
+ level: number;
15
+ isPaused: boolean;
16
+ }
17
+ export declare const GameStateStore: {
18
+ Context: import("react").Context<import("@plasius/react-state").Store<GameState, GameAction> | null>;
19
+ Provider: ({ children, initialState: override, }: {
20
+ children: React.ReactNode;
21
+ initialState?: GameState | undefined;
22
+ }) => import("react/jsx-runtime").JSX.Element;
23
+ useStore: () => GameState;
24
+ useDispatch: () => (action: GameAction) => void;
25
+ useSelector: <T>(selector: (state: GameState) => T, isEqual?: ((a: T, b: T) => boolean) | undefined) => T;
26
+ };
27
+ export {};
28
+ //# sourceMappingURL=gameStateProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gameStateProvider.d.ts","sourceRoot":"","sources":["../../src/state/gameStateProvider.tsx"],"names":[],"mappings":"AAEA,KAAK,UAAU,GACX;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,cAAc,CAAC;CACtB,GACD;IACE,IAAI,EAAE,YAAY,CAAC;CACpB,CAAC;AAEN,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAuBD,eAAO,MAAM,cAAc;;;;;;;;;CAG1B,CAAC"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GameStateStore = void 0;
4
+ const react_state_1 = require("@plasius/react-state");
5
+ const initialState = {
6
+ score: 0,
7
+ level: 1,
8
+ isPaused: false,
9
+ };
10
+ function gameReducer(state, action) {
11
+ switch (action.type) {
12
+ case "INCREMENT_SCORE":
13
+ return { ...state, score: state.score + action.payload };
14
+ case "SET_LEVEL":
15
+ return { ...state, level: action.payload };
16
+ case "TOGGLE_PAUSE":
17
+ return { ...state, isPaused: !state.isPaused };
18
+ case "RESET_GAME":
19
+ return { ...initialState };
20
+ default:
21
+ return state;
22
+ }
23
+ }
24
+ exports.GameStateStore = (0, react_state_1.createScopedStoreContext)(gameReducer, initialState);
@@ -0,0 +1,208 @@
1
+ .game {
2
+ --bg-deep: #070d17;
3
+ --bg-mid: #102035;
4
+ --bg-bright: #1b3a5a;
5
+ --panel: rgba(7, 13, 23, 0.72);
6
+ --panel-border: rgba(163, 198, 255, 0.24);
7
+ --text: #e7f0ff;
8
+ --text-muted: #9fb4d2;
9
+ --accent: #78c9ff;
10
+ min-height: calc(100vh - 8rem);
11
+ padding: 1.25rem;
12
+ color: var(--text);
13
+ font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
14
+ background:
15
+ radial-gradient(120% 120% at 0% 0%, rgba(120, 201, 255, 0.2), transparent 52%),
16
+ radial-gradient(100% 160% at 100% 20%, rgba(141, 255, 204, 0.15), transparent 48%),
17
+ linear-gradient(160deg, var(--bg-bright), var(--bg-mid) 46%, var(--bg-deep));
18
+ border-radius: 1rem;
19
+ overflow: hidden;
20
+ animation: reveal 350ms ease-out;
21
+ }
22
+
23
+ .header {
24
+ display: flex;
25
+ align-items: end;
26
+ justify-content: space-between;
27
+ gap: 1rem;
28
+ margin-bottom: 1rem;
29
+ }
30
+
31
+ .title {
32
+ margin: 0;
33
+ font-size: clamp(1.2rem, 1.4vw + 1rem, 2.1rem);
34
+ letter-spacing: 0.03em;
35
+ }
36
+
37
+ .subtitle {
38
+ margin: 0.25rem 0 0;
39
+ color: var(--text-muted);
40
+ max-width: 56ch;
41
+ line-height: 1.35;
42
+ font-size: 0.94rem;
43
+ }
44
+
45
+ .controls {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 0.65rem;
49
+ }
50
+
51
+ .seed {
52
+ font-size: 0.84rem;
53
+ color: var(--text-muted);
54
+ background: rgba(255, 255, 255, 0.08);
55
+ border: 1px solid rgba(255, 255, 255, 0.14);
56
+ border-radius: 999px;
57
+ padding: 0.3rem 0.7rem;
58
+ }
59
+
60
+ .button {
61
+ border: 0;
62
+ color: #001323;
63
+ background: linear-gradient(120deg, #8ad6ff, #87f6d1);
64
+ border-radius: 999px;
65
+ padding: 0.55rem 1rem;
66
+ font-weight: 700;
67
+ letter-spacing: 0.02em;
68
+ cursor: pointer;
69
+ transition: transform 120ms ease, filter 120ms ease;
70
+ }
71
+
72
+ .button:hover {
73
+ transform: translateY(-1px);
74
+ filter: brightness(1.08);
75
+ }
76
+
77
+ .button:active {
78
+ transform: translateY(0);
79
+ }
80
+
81
+ .layout {
82
+ display: grid;
83
+ grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
84
+ gap: 1rem;
85
+ }
86
+
87
+ .mapCard,
88
+ .panel {
89
+ background: var(--panel);
90
+ border: 1px solid var(--panel-border);
91
+ border-radius: 0.95rem;
92
+ backdrop-filter: blur(7px);
93
+ }
94
+
95
+ .mapCard {
96
+ padding: 0.65rem;
97
+ }
98
+
99
+ .map {
100
+ display: block;
101
+ width: 100%;
102
+ height: min(72vh, 760px);
103
+ }
104
+
105
+ .tile {
106
+ stroke: rgba(7, 11, 18, 0.35);
107
+ stroke-width: 1.3;
108
+ transition: filter 140ms ease, stroke 140ms ease, transform 140ms ease;
109
+ transform-origin: center;
110
+ }
111
+
112
+ .tile:hover {
113
+ filter: brightness(1.12);
114
+ stroke: rgba(240, 248, 255, 0.8);
115
+ }
116
+
117
+ .tileActive {
118
+ stroke: #ffffff;
119
+ stroke-width: 2.1;
120
+ filter: drop-shadow(0 0 6px rgba(138, 214, 255, 0.6));
121
+ }
122
+
123
+ .panel {
124
+ padding: 0.95rem;
125
+ display: flex;
126
+ flex-direction: column;
127
+ gap: 0.95rem;
128
+ }
129
+
130
+ .panelTitle {
131
+ margin: 0;
132
+ font-size: 0.96rem;
133
+ letter-spacing: 0.06em;
134
+ text-transform: uppercase;
135
+ color: var(--accent);
136
+ }
137
+
138
+ .stats {
139
+ margin: 0;
140
+ display: grid;
141
+ gap: 0.5rem;
142
+ }
143
+
144
+ .row {
145
+ display: flex;
146
+ justify-content: space-between;
147
+ gap: 0.75rem;
148
+ font-size: 0.87rem;
149
+ }
150
+
151
+ .label {
152
+ color: var(--text-muted);
153
+ }
154
+
155
+ .value {
156
+ text-align: right;
157
+ }
158
+
159
+ .chipRow {
160
+ display: flex;
161
+ flex-wrap: wrap;
162
+ gap: 0.45rem;
163
+ }
164
+
165
+ .chip {
166
+ border-radius: 999px;
167
+ padding: 0.3rem 0.58rem;
168
+ font-size: 0.74rem;
169
+ font-weight: 600;
170
+ letter-spacing: 0.01em;
171
+ color: #e8f2ff;
172
+ background: rgba(120, 201, 255, 0.16);
173
+ border: 1px solid rgba(120, 201, 255, 0.34);
174
+ }
175
+
176
+ @keyframes reveal {
177
+ from {
178
+ opacity: 0;
179
+ transform: translateY(6px);
180
+ }
181
+ to {
182
+ opacity: 1;
183
+ transform: translateY(0);
184
+ }
185
+ }
186
+
187
+ @media (max-width: 980px) {
188
+ .layout {
189
+ grid-template-columns: 1fr;
190
+ }
191
+ }
192
+
193
+ @media (max-width: 720px) {
194
+ .game {
195
+ min-height: auto;
196
+ padding: 0.8rem;
197
+ border-radius: 0.75rem;
198
+ }
199
+
200
+ .header {
201
+ flex-direction: column;
202
+ align-items: flex-start;
203
+ }
204
+
205
+ .map {
206
+ height: min(58vh, 540px);
207
+ }
208
+ }
@@ -0,0 +1,24 @@
1
+ import { type HexCell, type TerrainCell } from "@plasius/gpu-world-generator";
2
+ export interface HexMapTile {
3
+ q: number;
4
+ r: number;
5
+ x: number;
6
+ y: number;
7
+ points: string;
8
+ color: string;
9
+ terrain: TerrainCell;
10
+ }
11
+ export declare function resolveTileColor(terrain: TerrainCell): string;
12
+ export declare function axialToPixel(q: number, r: number, size: number): {
13
+ x: number;
14
+ y: number;
15
+ };
16
+ export declare function hexPolygonPoints(x: number, y: number, size: number): string;
17
+ export declare function buildHexMapTiles(cells: HexCell[], terrain: TerrainCell[], size: number): HexMapTile[];
18
+ export declare function computeMapBounds(tiles: HexMapTile[], size: number): {
19
+ minX: number;
20
+ maxX: number;
21
+ minY: number;
22
+ maxY: number;
23
+ };
24
+ //# sourceMappingURL=worldMap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worldMap.d.ts","sourceRoot":"","sources":["../src/worldMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAEtC,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AA+DD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAQ7D;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAKzF;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAS3E;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,EAAE,WAAW,EAAE,EACtB,IAAI,EAAE,MAAM,GACX,UAAU,EAAE,CAmBd;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAuB5D"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTileColor = resolveTileColor;
4
+ exports.axialToPixel = axialToPixel;
5
+ exports.hexPolygonPoints = hexPolygonPoints;
6
+ exports.buildHexMapTiles = buildHexMapTiles;
7
+ exports.computeMapBounds = computeMapBounds;
8
+ const gpu_world_generator_1 = require("@plasius/gpu-world-generator");
9
+ const SQRT3 = Math.sqrt(3);
10
+ const SURFACE_COLORS = {
11
+ [gpu_world_generator_1.SurfaceCover.Grass]: "#5bb768",
12
+ [gpu_world_generator_1.SurfaceCover.Dirt]: "#86604a",
13
+ [gpu_world_generator_1.SurfaceCover.Sand]: "#d7b273",
14
+ [gpu_world_generator_1.SurfaceCover.Rock]: "#7c879a",
15
+ [gpu_world_generator_1.SurfaceCover.Gravel]: "#91a0b2",
16
+ [gpu_world_generator_1.SurfaceCover.Snowpack]: "#d7ecff",
17
+ [gpu_world_generator_1.SurfaceCover.Ice]: "#99e0ff",
18
+ [gpu_world_generator_1.SurfaceCover.Mud]: "#69503c",
19
+ [gpu_world_generator_1.SurfaceCover.Ash]: "#6f6476",
20
+ [gpu_world_generator_1.SurfaceCover.Cobble]: "#9ba7b8",
21
+ [gpu_world_generator_1.SurfaceCover.Road]: "#6f6a60",
22
+ [gpu_world_generator_1.SurfaceCover.Water]: "#3f83d2",
23
+ [gpu_world_generator_1.SurfaceCover.Basalt]: "#4f5a70",
24
+ [gpu_world_generator_1.SurfaceCover.Crystal]: "#75ffd8",
25
+ [gpu_world_generator_1.SurfaceCover.Sludge]: "#5d7948",
26
+ };
27
+ const BIOME_COLORS = {
28
+ [gpu_world_generator_1.TerrainBiome.Plains]: "#89bf67",
29
+ [gpu_world_generator_1.TerrainBiome.Tundra]: "#a5b8ce",
30
+ [gpu_world_generator_1.TerrainBiome.Savanna]: "#c8b470",
31
+ [gpu_world_generator_1.TerrainBiome.River]: "#498dd6",
32
+ [gpu_world_generator_1.TerrainBiome.City]: "#9da9b8",
33
+ [gpu_world_generator_1.TerrainBiome.Village]: "#b1906d",
34
+ [gpu_world_generator_1.TerrainBiome.Ice]: "#b7f0ff",
35
+ [gpu_world_generator_1.TerrainBiome.Snow]: "#e2f4ff",
36
+ [gpu_world_generator_1.TerrainBiome.Mountainous]: "#7a8596",
37
+ [gpu_world_generator_1.TerrainBiome.Volcanic]: "#8d6c69",
38
+ [gpu_world_generator_1.TerrainBiome.Road]: "#7a7469",
39
+ [gpu_world_generator_1.TerrainBiome.Town]: "#958774",
40
+ [gpu_world_generator_1.TerrainBiome.Castle]: "#9aa7bb",
41
+ [gpu_world_generator_1.TerrainBiome.MixedForest]: "#4f9464",
42
+ };
43
+ function clamp(value, min, max) {
44
+ return Math.min(max, Math.max(min, value));
45
+ }
46
+ function withLightness(hex, amount) {
47
+ const safe = hex.replace("#", "");
48
+ const channels = safe.length === 3
49
+ ? safe.split("").map((c) => parseInt(c + c, 16))
50
+ : [0, 2, 4].map((offset) => parseInt(safe.slice(offset, offset + 2), 16));
51
+ const factor = clamp(1 + amount, 0.3, 1.8);
52
+ const next = channels
53
+ .map((channel) => clamp(Math.round(channel * factor), 0, 255))
54
+ .map((channel) => channel.toString(16).padStart(2, "0"))
55
+ .join("");
56
+ return `#${next}`;
57
+ }
58
+ function defaultBiomeColor(terrain) {
59
+ if (terrain.biome in BIOME_COLORS) {
60
+ return BIOME_COLORS[terrain.biome];
61
+ }
62
+ return "#8b9eb6";
63
+ }
64
+ function resolveTileColor(terrain) {
65
+ const base = typeof terrain.surface === "number" && terrain.surface in SURFACE_COLORS
66
+ ? SURFACE_COLORS[terrain.surface]
67
+ : defaultBiomeColor(terrain);
68
+ const elevationBoost = clamp(terrain.height * 0.28, -0.2, 0.22);
69
+ return withLightness(base, elevationBoost);
70
+ }
71
+ function axialToPixel(q, r, size) {
72
+ return {
73
+ x: size * 1.5 * q,
74
+ y: size * SQRT3 * (r + q / 2),
75
+ };
76
+ }
77
+ function hexPolygonPoints(x, y, size) {
78
+ const points = [];
79
+ for (let i = 0; i < 6; i += 1) {
80
+ const angle = (Math.PI / 180) * (60 * i + 30);
81
+ const px = x + size * Math.cos(angle);
82
+ const py = y + size * Math.sin(angle);
83
+ points.push(`${px.toFixed(2)},${py.toFixed(2)}`);
84
+ }
85
+ return points.join(" ");
86
+ }
87
+ function buildHexMapTiles(cells, terrain, size) {
88
+ return cells.map((cell, index) => {
89
+ const terrainCell = terrain[index] ?? {
90
+ height: 0,
91
+ heat: 0,
92
+ moisture: 0,
93
+ biome: gpu_world_generator_1.TerrainBiome.Plains,
94
+ };
95
+ const { x, y } = axialToPixel(cell.q, cell.r, size);
96
+ return {
97
+ q: cell.q,
98
+ r: cell.r,
99
+ x,
100
+ y,
101
+ points: hexPolygonPoints(x, y, size),
102
+ color: resolveTileColor(terrainCell),
103
+ terrain: terrainCell,
104
+ };
105
+ });
106
+ }
107
+ function computeMapBounds(tiles, size) {
108
+ if (tiles.length === 0) {
109
+ return {
110
+ minX: -size,
111
+ maxX: size,
112
+ minY: -size,
113
+ maxY: size,
114
+ };
115
+ }
116
+ let minX = Number.POSITIVE_INFINITY;
117
+ let maxX = Number.NEGATIVE_INFINITY;
118
+ let minY = Number.POSITIVE_INFINITY;
119
+ let maxY = Number.NEGATIVE_INFINITY;
120
+ for (const tile of tiles) {
121
+ minX = Math.min(minX, tile.x - size);
122
+ maxX = Math.max(maxX, tile.x + size);
123
+ minY = Math.min(minY, tile.y - size);
124
+ maxY = Math.max(maxY, tile.y + size);
125
+ }
126
+ return { minX, maxX, minY, maxY };
127
+ }
@@ -0,0 +1,21 @@
1
+ # ADR-0001: Standalone @plasius/hexagons Package Scope
2
+
3
+ - Date: 2026-02-11
4
+ - Status: Accepted
5
+
6
+ ## Context
7
+
8
+ This package was previously maintained as a workspace-only module inside
9
+ `plasius-ltd-site`. External consumers and remote builds require it to be
10
+ installable from npm without monorepo-local links.
11
+
12
+ ## Decision
13
+
14
+ Move `@plasius/hexagons` to a standalone root package with independent build,
15
+ test, governance, CI, and publish workflows.
16
+
17
+ ## Consequences
18
+
19
+ - The package can be versioned and released independently.
20
+ - `plasius-ltd-site` and other repositories can depend on npm-published versions.
21
+ - Build and lint rules must no longer rely on monorepo-relative tsconfig paths.