@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,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,158 @@
1
+ import {
2
+ SurfaceCover,
3
+ TerrainBiome,
4
+ type HexCell,
5
+ type TerrainCell,
6
+ } from "@plasius/gpu-world-generator";
7
+
8
+ export interface HexMapTile {
9
+ q: number;
10
+ r: number;
11
+ x: number;
12
+ y: number;
13
+ points: string;
14
+ color: string;
15
+ terrain: TerrainCell;
16
+ }
17
+
18
+ const SQRT3 = Math.sqrt(3);
19
+
20
+ const SURFACE_COLORS: Record<number, string> = {
21
+ [SurfaceCover.Grass]: "#5bb768",
22
+ [SurfaceCover.Dirt]: "#86604a",
23
+ [SurfaceCover.Sand]: "#d7b273",
24
+ [SurfaceCover.Rock]: "#7c879a",
25
+ [SurfaceCover.Gravel]: "#91a0b2",
26
+ [SurfaceCover.Snowpack]: "#d7ecff",
27
+ [SurfaceCover.Ice]: "#99e0ff",
28
+ [SurfaceCover.Mud]: "#69503c",
29
+ [SurfaceCover.Ash]: "#6f6476",
30
+ [SurfaceCover.Cobble]: "#9ba7b8",
31
+ [SurfaceCover.Road]: "#6f6a60",
32
+ [SurfaceCover.Water]: "#3f83d2",
33
+ [SurfaceCover.Basalt]: "#4f5a70",
34
+ [SurfaceCover.Crystal]: "#75ffd8",
35
+ [SurfaceCover.Sludge]: "#5d7948",
36
+ };
37
+
38
+ const BIOME_COLORS: Record<number, string> = {
39
+ [TerrainBiome.Plains]: "#89bf67",
40
+ [TerrainBiome.Tundra]: "#a5b8ce",
41
+ [TerrainBiome.Savanna]: "#c8b470",
42
+ [TerrainBiome.River]: "#498dd6",
43
+ [TerrainBiome.City]: "#9da9b8",
44
+ [TerrainBiome.Village]: "#b1906d",
45
+ [TerrainBiome.Ice]: "#b7f0ff",
46
+ [TerrainBiome.Snow]: "#e2f4ff",
47
+ [TerrainBiome.Mountainous]: "#7a8596",
48
+ [TerrainBiome.Volcanic]: "#8d6c69",
49
+ [TerrainBiome.Road]: "#7a7469",
50
+ [TerrainBiome.Town]: "#958774",
51
+ [TerrainBiome.Castle]: "#9aa7bb",
52
+ [TerrainBiome.MixedForest]: "#4f9464",
53
+ };
54
+
55
+ function clamp(value: number, min: number, max: number): number {
56
+ return Math.min(max, Math.max(min, value));
57
+ }
58
+
59
+ function withLightness(hex: string, amount: number): string {
60
+ const safe = hex.replace("#", "");
61
+ const channels = safe.length === 3
62
+ ? safe.split("").map((c) => parseInt(c + c, 16))
63
+ : [0, 2, 4].map((offset) => parseInt(safe.slice(offset, offset + 2), 16));
64
+ const factor = clamp(1 + amount, 0.3, 1.8);
65
+ const next = channels
66
+ .map((channel) => clamp(Math.round(channel * factor), 0, 255))
67
+ .map((channel) => channel.toString(16).padStart(2, "0"))
68
+ .join("");
69
+ return `#${next}`;
70
+ }
71
+
72
+ function defaultBiomeColor(terrain: TerrainCell): string {
73
+ if (terrain.biome in BIOME_COLORS) {
74
+ return BIOME_COLORS[terrain.biome];
75
+ }
76
+ return "#8b9eb6";
77
+ }
78
+
79
+ export function resolveTileColor(terrain: TerrainCell): string {
80
+ const base =
81
+ typeof terrain.surface === "number" && terrain.surface in SURFACE_COLORS
82
+ ? SURFACE_COLORS[terrain.surface]
83
+ : defaultBiomeColor(terrain);
84
+
85
+ const elevationBoost = clamp(terrain.height * 0.28, -0.2, 0.22);
86
+ return withLightness(base, elevationBoost);
87
+ }
88
+
89
+ export function axialToPixel(q: number, r: number, size: number): { x: number; y: number } {
90
+ return {
91
+ x: size * 1.5 * q,
92
+ y: size * SQRT3 * (r + q / 2),
93
+ };
94
+ }
95
+
96
+ export function hexPolygonPoints(x: number, y: number, size: number): string {
97
+ const points: string[] = [];
98
+ for (let i = 0; i < 6; i += 1) {
99
+ const angle = (Math.PI / 180) * (60 * i + 30);
100
+ const px = x + size * Math.cos(angle);
101
+ const py = y + size * Math.sin(angle);
102
+ points.push(`${px.toFixed(2)},${py.toFixed(2)}`);
103
+ }
104
+ return points.join(" ");
105
+ }
106
+
107
+ export function buildHexMapTiles(
108
+ cells: HexCell[],
109
+ terrain: TerrainCell[],
110
+ size: number
111
+ ): HexMapTile[] {
112
+ return cells.map((cell, index) => {
113
+ const terrainCell = terrain[index] ?? {
114
+ height: 0,
115
+ heat: 0,
116
+ moisture: 0,
117
+ biome: TerrainBiome.Plains,
118
+ };
119
+ const { x, y } = axialToPixel(cell.q, cell.r, size);
120
+ return {
121
+ q: cell.q,
122
+ r: cell.r,
123
+ x,
124
+ y,
125
+ points: hexPolygonPoints(x, y, size),
126
+ color: resolveTileColor(terrainCell),
127
+ terrain: terrainCell,
128
+ };
129
+ });
130
+ }
131
+
132
+ export function computeMapBounds(
133
+ tiles: HexMapTile[],
134
+ size: number
135
+ ): { minX: number; maxX: number; minY: number; maxY: number } {
136
+ if (tiles.length === 0) {
137
+ return {
138
+ minX: -size,
139
+ maxX: size,
140
+ minY: -size,
141
+ maxY: size,
142
+ };
143
+ }
144
+
145
+ let minX = Number.POSITIVE_INFINITY;
146
+ let maxX = Number.NEGATIVE_INFINITY;
147
+ let minY = Number.POSITIVE_INFINITY;
148
+ let maxY = Number.NEGATIVE_INFINITY;
149
+
150
+ for (const tile of tiles) {
151
+ minX = Math.min(minX, tile.x - size);
152
+ maxX = Math.max(maxX, tile.x + size);
153
+ minY = Math.min(minY, tile.y - size);
154
+ maxY = Math.max(maxY, tile.y + size);
155
+ }
156
+
157
+ return { minX, maxX, minY, maxY };
158
+ }