@netforge/engine 0.0.0-stage → 0.2.0

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 (245) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +100 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +215 -0
  76. package/dist/net/protocol.js +10 -0
  77. package/dist/net/room-client.d.ts +65 -0
  78. package/dist/net/room-client.js +156 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms-full.txt +460 -0
  244. package/llms.txt +183 -0
  245. package/package.json +104 -3
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Voxel worlds: the play field's floor, terraced ground past the wall, a
3
+ * stone (or steel) wall, pillars with a voxel flame, and ground clutter.
4
+ *
5
+ * Static voxel scenery is greedy-meshed with occlusion baked into vertex
6
+ * colours, and the material adds a per-voxel shade from the voxel's world
7
+ * cell (a hash), so large merged faces still read as stacked cubes without
8
+ * costing a quad per voxel. The floor is one plane painted per cell the same
9
+ * way, and every placement is seeded, so every client sees the same world.
10
+ */
11
+ import { Color, InstancedMesh, Matrix4, Mesh, MeshStandardNodeMaterial, PlaneGeometry, Quaternion, Vector3 } from "three/webgpu";
12
+ import { abs, dot, float, floor, fract, max, mix, normalWorld, positionWorld, sin, smoothstep, step, vec3, vertexColor } from "three/tsl";
13
+ import { fillWhere, greedyMesh, hash3, jitter, shadeRgb, VoxelGrid } from "./core.js";
14
+ import { voxelGeometry, voxelMaterial } from "./geometry.js";
15
+ import { flameGrid } from "./shapes.js";
16
+ /** Floor cell (and terrain voxel), m. */
17
+ export const WORLD_CELL = 0.5;
18
+ // A moonlit meadow: deep greens, cool stone and earth that stay brown under the blue night light.
19
+ const MEADOW = {
20
+ grass: [0x36603a, 0x3b6740, 0x325736, 0x447a45],
21
+ dirt: [0x5a4631, 0x634d35, 0x4d3c2b],
22
+ stone: [0x8c8679, 0x7c776b, 0x9a9486, 0x6c675d],
23
+ moss: 0x46703a,
24
+ path: [0x5f4f39, 0x67563e, 0x574833],
25
+ flowers: [0xf2d64b, 0xe8584a, 0xf4f1e6, 0xa77be0, 0xff9a3c],
26
+ };
27
+ const YARD = {
28
+ grass: [0x3d4552, 0x434c5a, 0x394150, 0x48515f],
29
+ dirt: [0x2e333c, 0x353a44, 0x2a2e36],
30
+ stone: [0x4a5260, 0x535c6b, 0x3f4653, 0x5c6573],
31
+ moss: 0xd9a12a,
32
+ path: [0x5a6272, 0x626b7b, 0x535a68],
33
+ flowers: [0xffb020, 0x16e0ff, 0xff4fd8],
34
+ };
35
+ export function paletteFor(theme) {
36
+ return theme === "yard" ? YARD : MEADOW;
37
+ }
38
+ const pick = (list, h) => list[Math.min(list.length - 1, Math.floor(h * list.length))] ?? 0x808080;
39
+ /** A per-cell 0..1 hash in TSL (sin-hash; stable to a few hundred metres from the origin). */
40
+ function cellHash(c) {
41
+ return fract(sin(dot(c, vec3(12.9898, 78.233, 37.719))).mul(43758.5453));
42
+ }
43
+ /**
44
+ * The material for greedy-meshed static voxels: vertex colours (occlusion
45
+ * baked) times a per-voxel shade from the voxel's world cell, and a faint
46
+ * darkening along each voxel's edges so merged faces still show their cubes.
47
+ */
48
+ export function voxelWorldMaterial(opts = { cell: WORLD_CELL }) {
49
+ const m = voxelMaterial({ roughness: opts.roughness ?? 0.9 });
50
+ const cell = float(opts.cell);
51
+ // The voxel a face belongs to: half a cell in from the face, along its normal.
52
+ const inside = positionWorld.sub(normalWorld.mul(cell.mul(0.5))).div(cell);
53
+ const id = floor(inside);
54
+ const shade = float(1).add(cellHash(id).sub(0.5).mul(2 * (opts.jitter ?? 0.07)));
55
+ // Distance to the nearest voxel edge across the face (in cells): 0 at an edge, 0.5 in the middle.
56
+ const f = fract(positionWorld.div(cell));
57
+ const g = abs(f.sub(0.5));
58
+ const n = abs(normalWorld);
59
+ // Use the two axes in the face's plane.
60
+ const a = mix(g.x, g.z, step(0.5, n.x));
61
+ const b = mix(g.y, g.z, step(0.5, n.y));
62
+ const edge = smoothstep(0.5, 0.44, max(a, b));
63
+ const lines = float(1).sub(float(1).sub(edge).mul(opts.edge ?? 0.12));
64
+ m.colorNode = vertexColor().mul(shade).mul(lines);
65
+ // colorNode already carries the vertex colours; with the flag on as well, NodeMaterial multiplies them in twice (dark, muddy stone).
66
+ m.vertexColors = false;
67
+ return m;
68
+ }
69
+ /** The floor plane's material: grass cells with dirt and path patches, one colour per cell. */
70
+ function floorMaterial(p, half) {
71
+ const m = new MeshStandardNodeMaterial();
72
+ m.roughness = 0.95;
73
+ m.metalness = 0;
74
+ const cell = float(WORLD_CELL);
75
+ const q = positionWorld.xz.div(cell);
76
+ const c = floor(q);
77
+ const h = cellHash(vec3(c.x, 0, c.y));
78
+ const h2 = cellHash(vec3(c.y, 3, c.x));
79
+ // Blocky patches: a coarse hash per 4x4 cells, blended by the cell's own hash so patch edges stay ragged.
80
+ const big = floor(c.div(4));
81
+ const patch = cellHash(vec3(big.x, 7, big.y));
82
+ const col = (hex) => {
83
+ const r = ((hex >> 16) & 255) / 255;
84
+ const g = ((hex >> 8) & 255) / 255;
85
+ const b = (hex & 255) / 255;
86
+ // sRGB to linear (square approximation is enough for a palette).
87
+ return vec3(r * r, g * g, b * b);
88
+ };
89
+ const grassA = col(p.grass[0] ?? 0x4c7a3b);
90
+ const grassB = col(p.grass[1] ?? 0x568a41);
91
+ const grassC = col(p.grass[2] ?? 0x44703a);
92
+ const dirt = col(p.dirt[1] ?? 0x7c5c40);
93
+ const path = col(p.path[0] ?? 0x8a6d4a);
94
+ // Soft patches of two greens (one per 4 x 4 cells) and a sprinkle of a third, so the floor reads as blocks
95
+ // without a checkerboard competing with the crowd.
96
+ let base = mix(grassA, grassB, smoothstep(0.3, 0.7, patch.add(h.sub(0.5).mul(0.35))));
97
+ base = mix(base, grassC, step(0.9, h2));
98
+ // Dirt patches (about 1 in 9 blocks of 4x4), ragged at the edges.
99
+ const isDirt = step(0.89, patch.add(h.sub(0.5).mul(0.12)));
100
+ base = mix(base, dirt, isDirt);
101
+ // A worn ring path around the centre reads as a place people walk.
102
+ const r = q.mul(WORLD_CELL).length();
103
+ const ring = step(abs(r.sub(half * 0.42)), float(1.6).add(h.mul(0.8)));
104
+ base = mix(base, path, ring.mul(step(0.25, h2)));
105
+ const shade = float(1).add(h.sub(0.5).mul(0.08));
106
+ const f = fract(q);
107
+ const edge = smoothstep(0.0, 0.06, f.x).mul(smoothstep(1.0, 0.94, f.x)).mul(smoothstep(0.0, 0.06, f.y)).mul(smoothstep(1.0, 0.94, f.y));
108
+ m.colorNode = base.mul(shade).mul(float(0.86).add(edge.mul(0.14)));
109
+ return m;
110
+ }
111
+ /** Deterministic value noise in 2D, 0..1. */
112
+ function noise2(x, z, seed) {
113
+ const x0 = Math.floor(x);
114
+ const z0 = Math.floor(z);
115
+ const fx = x - x0;
116
+ const fz = z - z0;
117
+ const sx = fx * fx * (3 - 2 * fx);
118
+ const sz = fz * fz * (3 - 2 * fz);
119
+ const a = hash3(x0, 0, z0, seed);
120
+ const b = hash3(x0 + 1, 0, z0, seed);
121
+ const c = hash3(x0, 0, z0 + 1, seed);
122
+ const d = hash3(x0 + 1, 0, z0 + 1, seed);
123
+ return a + (b - a) * sx + (c - a) * sz + (a - b - c + d) * sx * sz;
124
+ }
125
+ /**
126
+ * Height of the voxel ground at (x, z), m: flat inside the field and for a
127
+ * strip past the wall, then terraces that climb away from it in whole
128
+ * voxels. Scenery standing outside the wall uses it to stand on the ground.
129
+ */
130
+ export function terrainHeight(spec, x, z) {
131
+ const d = Math.max(Math.abs(x), Math.abs(z)) - spec.half - 1;
132
+ if (d < 3)
133
+ return 0;
134
+ const n = noise2(x * 0.07, z * 0.07, 11) * 0.65 + noise2(x * 0.19, z * 0.19, 12) * 0.35;
135
+ const climb = spec.theme === "yard" ? 0.06 : 0.16;
136
+ const raw = (d - 3) * climb + (n - 0.35) * (spec.theme === "yard" ? 1.2 : 3.2);
137
+ const steps = Math.max(0, Math.round(raw / WORLD_CELL));
138
+ return Math.min(12, steps) * WORLD_CELL;
139
+ }
140
+ /** Terraced ground in a rectangle of cells (one greedy-meshed chunk). */
141
+ function terrainChunk(spec, p, x0, z0, nx, nz) {
142
+ const heights = [];
143
+ let top = 0;
144
+ for (let k = 0; k < nz; k++)
145
+ for (let i = 0; i < nx; i++) {
146
+ const cx = x0 + (i + 0.5) * WORLD_CELL;
147
+ const cz = z0 + (k + 0.5) * WORLD_CELL;
148
+ const h = Math.round(terrainHeight(spec, cx, cz) / WORLD_CELL);
149
+ heights.push(h);
150
+ top = Math.max(top, h);
151
+ }
152
+ if (top === 0)
153
+ return null;
154
+ const grid = new VoxelGrid(nx, top, nz, WORLD_CELL, [x0, 0, z0]);
155
+ for (let k = 0; k < nz; k++)
156
+ for (let i = 0; i < nx; i++) {
157
+ const h = heights[i + k * nx] ?? 0;
158
+ for (let y = 0; y < h; y++) {
159
+ const wx = Math.floor(x0 / WORLD_CELL) + i;
160
+ const wz = Math.floor(z0 / WORLD_CELL) + k;
161
+ const hh = hash3(wx, y, wz, 21);
162
+ // Grass on top, dirt under it, stone deep down; the colour is per material, the shader adds per-voxel shade.
163
+ const c = y === h - 1 ? (hh < 0.85 ? (p.grass[1] ?? 0) : p.moss) : y >= h - 3 ? (p.dirt[0] ?? 0) : hh < 0.5 ? (p.stone[1] ?? 0) : (p.dirt[2] ?? 0);
164
+ grid.set(i, y, k, c);
165
+ }
166
+ }
167
+ // Neighbours outside the chunk count as solid where the terrain is, so chunk seams have no faces or occlusion gaps.
168
+ const solidOutside = (x, y, z) => {
169
+ if (y < 0)
170
+ return true;
171
+ const wx = x0 + (x + 0.5) * WORLD_CELL;
172
+ const wz = z0 + (z + 0.5) * WORLD_CELL;
173
+ return y < Math.round(terrainHeight(spec, wx, wz) / WORLD_CELL);
174
+ };
175
+ return voxelGeometry(greedyMesh(grid, { splitParts: false, skip: ["-y"], solidOutside }));
176
+ }
177
+ /** A seeded PRNG. */
178
+ function rng(seed) {
179
+ let s = seed >>> 0;
180
+ return () => {
181
+ s = (s + 0x6d2b79f5) >>> 0;
182
+ let t = s;
183
+ t = Math.imul(t ^ (t >>> 15), t | 1);
184
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
185
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
186
+ };
187
+ }
188
+ /** The wall around the field: dry stone blocks (meadow) or steel barrier blocks (yard), an uneven top. */
189
+ function wallGeometry(spec, p) {
190
+ const out = [];
191
+ const cell = WORLD_CELL;
192
+ const rows = Math.max(2, Math.round(Math.min(spec.wallHeight, spec.theme === "yard" ? 1.5 : 2) / cell));
193
+ const len = Math.ceil((spec.half * 2 + 2) / cell);
194
+ const thick = 2;
195
+ // Two long sides (north, south) and two short (east, west) fitted between them.
196
+ const sides = [
197
+ [-spec.half - 1, spec.half, len, thick],
198
+ [-spec.half - 1, -spec.half - 1, len, thick],
199
+ [spec.half, -spec.half, thick, len - 4],
200
+ [-spec.half - 1, -spec.half, thick, len - 4],
201
+ ];
202
+ sides.forEach(([x0, z0, nx, nz], s) => {
203
+ const grid = new VoxelGrid(nx, rows + 1, nz, cell, [x0, 0, z0]);
204
+ fillWhere(grid, (x, y, z) => {
205
+ const i = Math.floor(x), k = Math.floor(z);
206
+ if (y < rows)
207
+ return true;
208
+ // An uneven top: a few raised blocks.
209
+ return hash3(i, 99, k, s) < 0.18;
210
+ }, (x, y, z) => {
211
+ const wx = Math.floor(x0 / cell) + x;
212
+ const wz = Math.floor(z0 / cell) + z;
213
+ const h = hash3(wx, y, wz, 31);
214
+ if (spec.theme === "yard")
215
+ return y === rows - 1 && (Math.floor((wx + wz) / 2) & 1) === 0 ? p.moss : pick(p.stone, h);
216
+ if (y >= rows - 1 && h < 0.45)
217
+ return p.moss;
218
+ return pick(p.stone, h);
219
+ });
220
+ out.push(voxelGeometry(greedyMesh(grid, { splitParts: false, skip: ["-y"] })));
221
+ });
222
+ return out;
223
+ }
224
+ /** Ground clutter models: a grass tuft, a flower, a pebble (tiny voxel models at 0.1 m). */
225
+ function clutterModels(p, theme) {
226
+ const s = 0.1;
227
+ const tuft = new VoxelGrid(3, 4, 3, s, [-0.15, 0, -0.15]);
228
+ const g0 = p.grass[3] ?? 0x5d9447;
229
+ const g1 = p.grass[0] ?? 0x4c7a3b;
230
+ tuft.set(1, 0, 1, g1);
231
+ tuft.set(1, 1, 1, g0);
232
+ tuft.set(1, 2, 1, g0);
233
+ tuft.set(0, 0, 1, g1);
234
+ tuft.set(0, 1, 1, g0);
235
+ tuft.set(2, 0, 0, g1);
236
+ tuft.set(2, 1, 0, g0);
237
+ tuft.set(1, 0, 2, g1);
238
+ tuft.set(1, 3, 1, shadeRgb(g0, 1.15));
239
+ const flower = new VoxelGrid(3, 4, 3, s, [-0.15, 0, -0.15]);
240
+ flower.set(1, 0, 1, g1);
241
+ flower.set(1, 1, 1, g0);
242
+ flower.set(1, 2, 1, g0);
243
+ const petal = 0xffffff;
244
+ for (const [x, z] of [[0, 1], [2, 1], [1, 0], [1, 2]])
245
+ flower.set(x, 3, z, petal);
246
+ flower.set(1, 3, 1, 0xf2d64b);
247
+ const pebble = new VoxelGrid(3, 2, 2, s, [-0.15, 0, -0.1]);
248
+ for (let x = 0; x < 3; x++)
249
+ for (let z = 0; z < 2; z++)
250
+ pebble.set(x, 0, z, pick(p.stone, hash3(x, 0, z, 5)));
251
+ pebble.set(1, 1, 0, pick(p.stone, 0.2));
252
+ if (theme === "yard") {
253
+ // The lot: bolts, cable stubs and hazard studs instead of grass.
254
+ return [
255
+ { geometry: voxelGeometry(greedyMesh(pebble, { splitParts: false, skip: ["-y"] })), count: 900, seed: 3 },
256
+ ];
257
+ }
258
+ return [
259
+ { geometry: voxelGeometry(greedyMesh(tuft, { splitParts: false, skip: ["-y"] })), count: 2600, seed: 1 },
260
+ { geometry: voxelGeometry(greedyMesh(flower, { splitParts: false, skip: ["-y"] })), count: 420, seed: 2 },
261
+ { geometry: voxelGeometry(greedyMesh(pebble, { splitParts: false, skip: ["-y"] })), count: 500, seed: 3 },
262
+ ];
263
+ }
264
+ /** Every flower gets one of the palette's colours: the petals are white in the model and tinted per instance. */
265
+ function scatterClutter(spec, p, avoid) {
266
+ const out = [];
267
+ const reach = spec.half + spec.outside * 0.6;
268
+ const m = new Matrix4();
269
+ const q = new Quaternion();
270
+ const axis = new Vector3(0, 1, 0);
271
+ const pos = new Vector3();
272
+ const scl = new Vector3();
273
+ for (const c of clutterModels(p, spec.theme)) {
274
+ const mat = voxelMaterial({ roughness: 0.95 });
275
+ const mesh = new InstancedMesh(c.geometry, mat, c.count);
276
+ const r = rng(0x9e37 + c.seed * 7919);
277
+ let n = 0;
278
+ for (let i = 0; i < c.count * 3 && n < c.count; i++) {
279
+ // Clumped: pick a clump centre, then scatter around it.
280
+ const x = (r() * 2 - 1) * reach;
281
+ const z = (r() * 2 - 1) * reach;
282
+ const inField = Math.abs(x) < spec.half - 0.6 && Math.abs(z) < spec.half - 0.6;
283
+ const outside = Math.max(Math.abs(x), Math.abs(z)) > spec.half + 1.5;
284
+ if (!inField && !outside)
285
+ continue;
286
+ if (avoid.some((pl) => (pl.x - x) ** 2 + (pl.z - z) ** 2 < (pl.radius + 0.6) ** 2))
287
+ continue;
288
+ // Thinner in the middle of the field, where the fights are.
289
+ if (inField && r() < 0.35)
290
+ continue;
291
+ const y = outside ? terrainHeight(spec, x, z) : 0;
292
+ // Snap to the floor's cells so clutter sits on blocks.
293
+ const sx = Math.round(x / 0.1) * 0.1;
294
+ const sz = Math.round(z / 0.1) * 0.1;
295
+ q.setFromAxisAngle(axis, Math.floor(r() * 4) * (Math.PI / 2));
296
+ const k = 0.8 + r() * 0.6;
297
+ pos.set(sx, y, sz);
298
+ scl.set(k, k * (0.8 + r() * 0.5), k);
299
+ mesh.setMatrixAt(n, m.compose(pos, q, scl));
300
+ if (c.seed === 2)
301
+ mesh.setColorAt(n, new Color(pick(p.flowers, r())));
302
+ n++;
303
+ }
304
+ mesh.count = n;
305
+ mesh.instanceMatrix.needsUpdate = true;
306
+ if (mesh.instanceColor)
307
+ mesh.instanceColor.needsUpdate = true;
308
+ mesh.receiveShadow = true;
309
+ mesh.castShadow = false;
310
+ mesh.computeBoundingSphere();
311
+ mesh.name = "VoxelClutter";
312
+ out.push(mesh);
313
+ }
314
+ return out;
315
+ }
316
+ /**
317
+ * The whole field: the painted floor, terraced ground past the wall, the
318
+ * wall, and ground clutter. Returns meshes to add to the scene.
319
+ */
320
+ export function voxelField(spec, pillars = []) {
321
+ const p = paletteFor(spec.theme);
322
+ const out = [];
323
+ const size = (spec.half + spec.outside) * 2;
324
+ const floor = new Mesh(new PlaneGeometry(size, size), floorMaterial(p, spec.half));
325
+ floor.rotation.x = -Math.PI / 2;
326
+ floor.receiveShadow = true;
327
+ floor.name = "VoxelFloor";
328
+ out.push(floor);
329
+ const worldMat = voxelWorldMaterial({ cell: WORLD_CELL, jitter: 0.08, edge: 0.14 });
330
+ // Terraces in chunks of 32 x 32 cells around the field.
331
+ const edge = spec.half + Math.min(spec.outside, 28);
332
+ const chunk = 32 * WORLD_CELL;
333
+ const start = -Math.ceil(edge / chunk) * chunk;
334
+ for (let z0 = start; z0 < edge; z0 += chunk)
335
+ for (let x0 = start; x0 < edge; x0 += chunk) {
336
+ // Skip chunks entirely inside the flat strip.
337
+ const far = Math.max(Math.abs(x0), Math.abs(x0 + chunk), Math.abs(z0), Math.abs(z0 + chunk));
338
+ if (far < spec.half + 4)
339
+ continue;
340
+ const g = terrainChunk(spec, p, x0, z0, 32, 32);
341
+ if (!g)
342
+ continue;
343
+ const mesh = new Mesh(g, worldMat);
344
+ mesh.receiveShadow = true;
345
+ mesh.castShadow = true;
346
+ mesh.name = "VoxelTerrain";
347
+ out.push(mesh);
348
+ }
349
+ if (spec.wall !== false) {
350
+ for (const g of wallGeometry(spec, p)) {
351
+ const mesh = new Mesh(g, worldMat);
352
+ mesh.castShadow = true;
353
+ mesh.receiveShadow = true;
354
+ mesh.name = "VoxelWall";
355
+ out.push(mesh);
356
+ }
357
+ }
358
+ out.push(...scatterClutter(spec, p, pillars));
359
+ return out;
360
+ }
361
+ /** A stone column of rough courses with a capital, an iron brazier and a voxel flame (meadow); a steel post with a lamp (yard). */
362
+ export function voxelPillar(spec, theme, index) {
363
+ const p = paletteFor(theme);
364
+ const cell = 0.25;
365
+ const r = spec.radius;
366
+ const n = Math.ceil((r * 2.3) / cell) + 1;
367
+ const rows = Math.max(4, Math.round(spec.height / cell));
368
+ const grid = new VoxelGrid(n, rows + 2, n, cell, [spec.x - (n * cell) / 2, 0, spec.z - (n * cell) / 2]);
369
+ const c = n / 2;
370
+ const yard = theme === "yard";
371
+ fillWhere(grid, (x, y, z) => {
372
+ const d = Math.hypot(x - c, z - c) * cell;
373
+ const course = Math.floor(y / 2);
374
+ // Rough courses: each ring of stones juts a little in or out.
375
+ const wobble = (hash3(Math.floor(Math.atan2(z - c, x - c) * 3), course, index, 41) - 0.5) * cell * 1.2;
376
+ if (y < 2)
377
+ return d < r * 1.08 + (yard ? 0 : wobble * 0.5);
378
+ if (y >= rows - 1)
379
+ return d < r * 1.02;
380
+ return d < r * 0.86 + (yard ? 0 : wobble);
381
+ }, (x, y, z) => {
382
+ const course = Math.floor(y / 2);
383
+ const stone = pick(p.stone, hash3(Math.floor(Math.atan2(z - c, x - c) * 2.5), course, index, 43));
384
+ if (yard)
385
+ return y >= rows - 1 || y < 2 ? (p.stone[3] ?? 0) : (y & 3) === 0 ? p.moss : (p.stone[0] ?? 0);
386
+ if (y < 2 && hash3(x, y, z, 45) < 0.4)
387
+ return p.moss;
388
+ // The capital a top-down camera mostly sees: darker, weathered stone with moss, so a pillar is not a bright disc.
389
+ if (y >= rows - 1)
390
+ return hash3(x, y, z, 49) < 0.3 ? shadeRgb(p.moss, 0.8) : shadeRgb(stone, 0.62);
391
+ // Alternate courses a step apart read as stacked blocks.
392
+ return shadeRgb(stone, (course & 1) === 0 ? 0.86 : 0.76);
393
+ });
394
+ const body = new Mesh(voxelGeometry(greedyMesh(grid, { splitParts: false, skip: ["-y"] })), voxelWorldMaterial({ cell, jitter: 0.06, edge: 0.1 }));
395
+ body.castShadow = true;
396
+ body.receiveShadow = true;
397
+ body.name = "VoxelPillar";
398
+ const top = [];
399
+ const topY = (rows + 1) * cell;
400
+ // The brazier: a dark iron bowl on the capital.
401
+ const bowlN = Math.max(4, Math.round((r * 1.1) / 0.125));
402
+ const bowl = new VoxelGrid(bowlN, 3, bowlN, 0.125, [spec.x - (bowlN * 0.125) / 2, topY, spec.z - (bowlN * 0.125) / 2]);
403
+ const bc = bowlN / 2;
404
+ fillWhere(bowl, (x, y, z) => {
405
+ const d = Math.hypot(x - bc, z - bc);
406
+ return y < 1 ? d < bc * 0.7 : d < bc && d > bc - 1.3;
407
+ }, (x, y, z) => jitter(yard ? 0x2a2f38 : 0x3a3330, x, y, z, 0.1, 47));
408
+ const bowlMesh = new Mesh(voxelGeometry(greedyMesh(bowl, { splitParts: false })), voxelMaterial({ roughness: 0.6 }));
409
+ bowlMesh.castShadow = true;
410
+ top.push(bowlMesh);
411
+ let flame = null;
412
+ if (!yard) {
413
+ const fg = flameGrid(index + 1);
414
+ const flameMesh = new Mesh(voxelGeometry(greedyMesh(fg, { splitParts: false, ao: false })), voxelMaterial({ emissive: 0xff7a1c, emissiveIntensity: 2.4, roughness: 1 }));
415
+ flameMesh.position.set(spec.x, topY + 0.1, spec.z);
416
+ flameMesh.scale.setScalar(Math.max(0.8, r * 0.45));
417
+ flameMesh.name = "VoxelFlame";
418
+ top.push(flameMesh);
419
+ flame = [spec.x, topY + 0.5, spec.z];
420
+ }
421
+ return { body, fade: [body.material], top, flame };
422
+ }