@call-me-sensei/toonlab 0.2.0 → 0.3.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 (205) hide show
  1. package/AGENTS.md +127 -4
  2. package/ATTRIBUTION.md +41 -3
  3. package/README.md +234 -34
  4. package/docs/characters.md +144 -0
  5. package/docs/debug-panel.md +126 -0
  6. package/docs/docs.css +589 -0
  7. package/docs/environment.md +186 -0
  8. package/docs/getting-started.md +180 -0
  9. package/docs/index.html +14 -0
  10. package/docs/lab-architecture.md +100 -0
  11. package/docs/lighting.md +825 -0
  12. package/docs/main.jsx +739 -0
  13. package/docs/mcp.md +97 -0
  14. package/docs/post-processing.md +98 -0
  15. package/docs/settings-reference.md +1878 -0
  16. package/docs/shader-constants.md +77 -0
  17. package/docs/sky.md +182 -0
  18. package/docs/style-labs.md +309 -0
  19. package/docs/texture-lab.md +135 -0
  20. package/docs/toon-shading.md +183 -0
  21. package/docs/tsl-conventions.md +167 -0
  22. package/docs/vegetation-sky.md +275 -0
  23. package/docs/water.md +430 -0
  24. package/docs/weather.md +200 -0
  25. package/docs/world-scale.md +111 -0
  26. package/mcp/server.mjs +610 -0
  27. package/mcp/style-lab-tools.mjs +371 -0
  28. package/mcp/vite-plugin.mjs +174 -0
  29. package/mcp/workspace.mjs +397 -0
  30. package/package.json +64 -5
  31. package/src/ambientfx/INTEGRATION.md +164 -0
  32. package/src/ambientfx/ambientFxPresets.js +83 -0
  33. package/src/ambientfx/ambientFxSettings.js +368 -0
  34. package/src/ambientfx/emitters.js +169 -0
  35. package/src/ambientfx/index.js +5 -0
  36. package/src/ambientfx/particleBackbone.js +493 -0
  37. package/src/ambientfx/stylizedAmbientFx.js +450 -0
  38. package/src/assetlib/ambientcg.js +117 -0
  39. package/src/assetlib/assetRef.js +91 -0
  40. package/src/assetlib/importedEntry.js +40 -0
  41. package/src/assetlib/index.js +23 -0
  42. package/src/assetlib/kaykit.js +192 -0
  43. package/src/assetlib/kaykitStaticIndex.js +700 -0
  44. package/src/assetlib/loadImported.js +143 -0
  45. package/src/assetlib/opensource3d.js +113 -0
  46. package/src/assetlib/polyhaven.js +182 -0
  47. package/src/assetlib/polypizza.js +115 -0
  48. package/src/assetlib/smithsonian.js +214 -0
  49. package/src/assetlib/sources.js +279 -0
  50. package/src/assetlib/zip.js +58 -0
  51. package/src/biome/biomeGenerator.js +385 -0
  52. package/src/biome/biomeRuntime.js +299 -0
  53. package/src/biome/index.js +2 -0
  54. package/src/buildinggen/buildingAsset.js +44 -0
  55. package/src/buildinggen/buildingGrammar.js +311 -0
  56. package/src/buildinggen/buildingMesh.js +451 -0
  57. package/src/buildinggen/buildingPresets.js +46 -0
  58. package/src/buildinggen/buildingRecipe.js +100 -0
  59. package/src/buildinggen/buildingSettings.js +238 -0
  60. package/src/buildinggen/index.js +6 -0
  61. package/src/camera/cameraDirector.js +157 -0
  62. package/src/camera/cameraGenerator.js +367 -0
  63. package/src/camera/cameraRig.js +570 -0
  64. package/src/camera/cameraSettings.js +236 -0
  65. package/src/camera/index.js +7 -0
  66. package/src/catalog/builtinEntries.js +245 -0
  67. package/src/catalog/catalog.js +210 -0
  68. package/src/catalog/index.js +3 -0
  69. package/src/catalog/manifest.js +84 -0
  70. package/src/core/generation.js +529 -0
  71. package/src/environment/environmentRigs.js +21 -1
  72. package/src/environment/environmentSettings.js +4 -0
  73. package/src/environment/environmentSunShadowPass.js +8 -0
  74. package/src/fauna/INTEGRATION.md +174 -0
  75. package/src/fauna/boids.js +861 -0
  76. package/src/fauna/faunaBodies.js +492 -0
  77. package/src/fauna/faunaPresets.js +52 -0
  78. package/src/fauna/faunaSettings.js +525 -0
  79. package/src/fauna/index.js +5 -0
  80. package/src/fauna/stylizedFauna.js +395 -0
  81. package/src/game-feel/gameFeelGenerator.js +402 -0
  82. package/src/game-feel/gameFeelRuntime.js +549 -0
  83. package/src/game-feel/index.js +2 -0
  84. package/src/index.js +17 -6
  85. package/src/lighting/colorIntensity.js +177 -0
  86. package/src/lighting/index.js +161 -0
  87. package/src/lighting/lightDescriptors.js +249 -0
  88. package/src/lighting/lightingCapabilities.js +79 -0
  89. package/src/lighting/lightingDocuments.js +247 -0
  90. package/src/lighting/lightingFixtures.js +446 -0
  91. package/src/lighting/lightingGenerator.js +449 -0
  92. package/src/lighting/lightingPresets.js +319 -0
  93. package/src/lighting/lightingRuntime.js +723 -0
  94. package/src/lighting/lightingStyle.js +386 -0
  95. package/src/lighting/lightingSystem.js +774 -0
  96. package/src/lighting/unrealExport.js +186 -0
  97. package/src/lighting/utils.js +87 -0
  98. package/src/motion/index.js +5 -0
  99. package/src/motion/motionClip.js +441 -0
  100. package/src/motion/motionController.js +628 -0
  101. package/src/motion/motionDocuments.js +225 -0
  102. package/src/motion/motionGraph.js +307 -0
  103. package/src/motion/motionSettings.js +222 -0
  104. package/src/pathgen/index.js +7 -0
  105. package/src/pathgen/pathBridge.js +232 -0
  106. package/src/pathgen/pathPresets.js +35 -0
  107. package/src/pathgen/pathRibbon.js +410 -0
  108. package/src/pathgen/pathRouter.js +380 -0
  109. package/src/pathgen/pathSettings.js +335 -0
  110. package/src/pathgen/pathTextures.js +123 -0
  111. package/src/pathgen/stylizedPaths.js +453 -0
  112. package/src/post/index.js +1 -0
  113. package/src/post/postGenerator.js +177 -0
  114. package/src/post/postProcessing.js +41 -0
  115. package/src/propgen/generatorsWave1.js +379 -0
  116. package/src/propgen/generatorsWave2.js +462 -0
  117. package/src/propgen/index.js +5 -0
  118. package/src/propgen/propAsset.js +323 -0
  119. package/src/propgen/propParts.js +170 -0
  120. package/src/propgen/propPlacement.js +459 -0
  121. package/src/propgen/propPresets.js +82 -0
  122. package/src/propgen/propSettings.js +395 -0
  123. package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
  124. package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
  125. package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
  126. package/src/shaders-tsl/chunks/water-waves.js +90 -11
  127. package/src/shaders-tsl/environment.js +15 -1
  128. package/src/shaders-tsl/flower.js +279 -30
  129. package/src/shaders-tsl/grass.js +60 -33
  130. package/src/shaders-tsl/sky.js +125 -31
  131. package/src/shaders-tsl/tree-leaf.js +61 -25
  132. package/src/shaders-tsl/water-breaker.js +7 -4
  133. package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
  134. package/src/shaders-tsl/water.js +439 -49
  135. package/src/shaders-tsl/woody-surface.js +154 -0
  136. package/src/sky/sceneOverrideLayers.js +10 -0
  137. package/src/sky/skyQuality.js +26 -0
  138. package/src/sky/stylizedSky.js +753 -45
  139. package/src/soundscape/index.js +4 -0
  140. package/src/soundscape/soundscapeGenerator.js +179 -0
  141. package/src/soundscape/soundscapeRuntime.js +806 -0
  142. package/src/soundscape/soundscapeSettings.js +292 -0
  143. package/src/styles/index.js +13 -0
  144. package/src/styles/styleBundle.js +325 -0
  145. package/src/stylizedTerrain.js +32 -2
  146. package/src/stylizedWorld.js +423 -20
  147. package/src/texgen/evaluateTexture.js +675 -0
  148. package/src/texgen/index.js +60 -0
  149. package/src/texgen/noise2.js +210 -0
  150. package/src/texgen/textureAi.js +436 -0
  151. package/src/texgen/textureGenerators.js +516 -0
  152. package/src/texgen/texturePresets.js +490 -0
  153. package/src/texgen/textureSettings.js +342 -0
  154. package/src/texgen/textureThree.js +59 -0
  155. package/src/vegetation/flowerSpecies.js +15 -3
  156. package/src/vegetation/grassPalettes.js +153 -0
  157. package/src/vegetation/index.js +6 -0
  158. package/src/vegetation/stylizedBush.js +2 -0
  159. package/src/vegetation/stylizedFlower.js +82 -0
  160. package/src/vegetation/stylizedFlowers.js +48 -7
  161. package/src/vegetation/stylizedForest.js +29 -1
  162. package/src/vegetation/stylizedGrass.js +291 -56
  163. package/src/vegetation/stylizedTree.js +38 -2
  164. package/src/vegetation/stylizedTreeFoliage.js +2 -1
  165. package/src/vegetation/vegetationShaders.js +1110 -0
  166. package/src/vfxgen/INTEGRATION.md +145 -0
  167. package/src/vfxgen/core/burstBackbone.js +380 -0
  168. package/src/vfxgen/core/projectileCore.js +92 -0
  169. package/src/vfxgen/core/spriteShapes.js +98 -0
  170. package/src/vfxgen/core/trailRibbon.js +272 -0
  171. package/src/vfxgen/core/vfxRandom.js +29 -0
  172. package/src/vfxgen/effects/emitHelpers.js +37 -0
  173. package/src/vfxgen/effects/magicEffects.js +162 -0
  174. package/src/vfxgen/effects/movementEffects.js +87 -0
  175. package/src/vfxgen/effects/weaponEffects.js +118 -0
  176. package/src/vfxgen/index.js +18 -0
  177. package/src/vfxgen/moves/moveController.js +146 -0
  178. package/src/vfxgen/moves/moveLibrary.js +335 -0
  179. package/src/vfxgen/vfxPresets.js +98 -0
  180. package/src/vfxgen/vfxSettings.js +384 -0
  181. package/src/vfxgen/vfxSystem.js +449 -0
  182. package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
  183. package/src/villagegen/index.js +4 -0
  184. package/src/villagegen/stylizedVillage.js +490 -0
  185. package/src/villagegen/villageArchetypes.js +160 -0
  186. package/src/villagegen/villageNames.js +40 -0
  187. package/src/villagegen/villageSites.js +105 -0
  188. package/src/water/sceneOverrideLayers.js +23 -0
  189. package/src/water/water.js +5 -0
  190. package/src/water/waterBreakerSystem.js +15 -1
  191. package/src/water/waterCurrentField.js +447 -0
  192. package/src/water/waterMaterial.js +12 -0
  193. package/src/water/waterNearshorePhase.js +320 -0
  194. package/src/water/waterScenePasses.js +83 -30
  195. package/src/water/waterSettings.js +325 -13
  196. package/src/water/waterShoreMaterial.js +322 -0
  197. package/src/water/waterShoreStateField.js +605 -0
  198. package/src/water/waterSurface.js +797 -28
  199. package/src/weather/index.js +6 -0
  200. package/src/weather/weatherPrecipitation.js +221 -0
  201. package/src/weather/weatherPresets.js +258 -0
  202. package/src/weather/weatherSettings.js +269 -0
  203. package/src/weather/weatherSystem.js +871 -0
  204. package/src/worldMinimap.js +62 -0
  205. package/src/worldPresets.js +4 -1
@@ -0,0 +1,449 @@
1
+ // The gameplay-VFX layer. Import from '@call-me-sensei/toonlab/vfxgen'.
2
+ //
3
+ // const vfx = createVfxSystem({ seed, preset: 'call_me_sensei', heightAt });
4
+ // scene.add(vfx.root);
5
+ // vfx.update(delta, camera); // each frame
6
+ //
7
+ // // weapon
8
+ // const trail = vfx.spawn('slash', { follow: sword, base: [0, 0, 0], tip: [0, 1.1, 0] });
9
+ // trail.stop(); // arc fades out, ribbon pools
10
+ // vfx.spawn('impact', { at: [x, y, z], normal: [0, 1, 0], power: 1 });
11
+ //
12
+ // // magic
13
+ // const bolt = vfx.spawn('fireball', { from: [x, y, z], velocity: [6, 3, 0],
14
+ // onHit: (at) => damage(at) });
15
+ // bolt.explode(); // or auto on ground/lifetime
16
+ //
17
+ // // movement
18
+ // vfx.spawn('footstep', { at, dir: velocity });
19
+ // vfx.spawn('landing', { at, power: 1.5 });
20
+ //
21
+ // vfx.setDistanceFog({ color, density, falloff, floorY }); // height-fog layer
22
+ //
23
+ // Unlike ambientfx (a steady follow-window), everything here is EVENT-DRIVEN
24
+ // and pooled: one-shot bursts write into the two-draw-call ring-buffer
25
+ // backbone (zero CPU after the spawn write), slash ribbons and fireball
26
+ // cores come from small mesh pools bounded by shared.maxTrails /
27
+ // maxProjectiles. Per-spawn `look` overrides re-tint any effect without
28
+ // touching settings. Fully seeded: same seed + same spawn sequence + same
29
+ // update cadence → bit-identical emission (what verify-vfxgen.mjs asserts).
30
+
31
+ import * as THREE from 'three';
32
+
33
+ import { createVfxSettings, VFX_EFFECT_IDS } from './vfxSettings.js';
34
+ import { resolveVfxPreset } from './vfxPresets.js';
35
+ import { createBurstBackbone } from './core/burstBackbone.js';
36
+ import { createTrailRibbon } from './core/trailRibbon.js';
37
+ import { createProjectileCore } from './core/projectileCore.js';
38
+ import { hashCombine, mulberry32 } from './core/vfxRandom.js';
39
+ import { emitImpact, emitSlashSparkle } from './effects/weaponEffects.js';
40
+ import { emitFireballEmbers, emitFireballExplosion } from './effects/magicEffects.js';
41
+ import { emitFootstep, emitLanding } from './effects/movementEffects.js';
42
+
43
+ function cleanObject(value) {
44
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
45
+ }
46
+
47
+ function mergeGroupOverrides(...layers) {
48
+ const merged = {};
49
+ for (const layer of layers) {
50
+ for (const [group, values] of Object.entries(cleanObject(layer))) {
51
+ merged[group] = { ...merged[group], ...cleanObject(values) };
52
+ }
53
+ }
54
+ return merged;
55
+ }
56
+
57
+ function toArray3(value, fallback = [0, 0, 0]) {
58
+ if (Array.isArray(value)) return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
59
+ if (value?.isVector3) return [value.x, value.y, value.z];
60
+ return [...fallback];
61
+ }
62
+
63
+ export function createVfxSystem({
64
+ seed = 1,
65
+ preset = null,
66
+ settings = {},
67
+ effects = {},
68
+ heightAt = null,
69
+ } = {}) {
70
+ const worldSeed = Math.max(1, Math.round(Number(seed) || 1)) >>> 0;
71
+
72
+ // Per-effect overrides from `effects` (same idiom as ambientfx): `false`
73
+ // disables, an object layers over preset + settings.
74
+ const effectOverrides = {};
75
+ for (const id of VFX_EFFECT_IDS) {
76
+ const raw = effects?.[id];
77
+ if (raw === false) effectOverrides[id] = { enabled: false };
78
+ else if (raw === true) effectOverrides[id] = { enabled: true };
79
+ else if (raw && typeof raw === 'object') effectOverrides[id] = { enabled: true, ...raw };
80
+ }
81
+ const resolved = createVfxSettings(
82
+ mergeGroupOverrides(resolveVfxPreset(preset), settings, effectOverrides));
83
+
84
+ const backbone = createBurstBackbone(resolved);
85
+ const u = backbone.uniforms;
86
+ const root = new THREE.Group();
87
+ root.name = 'StylizedVfx';
88
+ for (const group of Object.values(backbone.groups)) root.add(group.mesh);
89
+ // Gameplay flashes are transient — keep them out of the water scene passes
90
+ // (a half-second spark shower isn't worth re-rendering three water grabs).
91
+ for (const group of Object.values(backbone.groups)) group.mesh.userData.waterExclude = true;
92
+
93
+ // --- pools -----------------------------------------------------------------
94
+ const trailPool = [];
95
+ const projectilePool = [];
96
+ const liveTrails = [];
97
+ const liveProjectiles = [];
98
+
99
+ const acquireTrail = () => {
100
+ const free = trailPool.find((r) => !r.active && r.isDead(time));
101
+ if (free) return free;
102
+ if (trailPool.length < resolved.shared.maxTrails) {
103
+ const ribbon = createTrailRibbon({
104
+ segments: resolved.slash.segments,
105
+ sharedUniforms: u,
106
+ });
107
+ ribbon.mesh.userData.waterExclude = true;
108
+ trailPool.push(ribbon);
109
+ root.add(ribbon.mesh);
110
+ return ribbon;
111
+ }
112
+ // Pool exhausted: steal the oldest live ribbon (combat > correctness).
113
+ const oldest = liveTrails.shift();
114
+ if (oldest) oldest.ribbon.reset();
115
+ return oldest?.ribbon ?? trailPool[0];
116
+ };
117
+
118
+ const acquireProjectile = () => {
119
+ const free = projectilePool.find((core) => !core.mesh.visible);
120
+ if (free) return free;
121
+ if (projectilePool.length < resolved.shared.maxProjectiles) {
122
+ const core = createProjectileCore({ sharedUniforms: u });
123
+ core.mesh.userData.waterExclude = true;
124
+ projectilePool.push(core);
125
+ root.add(core.mesh);
126
+ return core;
127
+ }
128
+ const oldest = liveProjectiles.shift();
129
+ if (oldest) oldest.finish(false);
130
+ return oldest?.core ?? projectilePool[0];
131
+ };
132
+
133
+ // --- clock + determinism -----------------------------------------------------
134
+ let time = 0;
135
+ let spawnCounter = 0;
136
+ let spawnsTotal = 0;
137
+ const nextRng = () => {
138
+ spawnCounter += 1;
139
+ return mulberry32(hashCombine(worldSeed, spawnCounter));
140
+ };
141
+
142
+ const emitRecords = ({ glow = [], puff = [] }) => {
143
+ if (glow.length > 0) backbone.groups.glow.emit(glow);
144
+ if (puff.length > 0) backbone.groups.puff.emit(puff);
145
+ return glow.length + puff.length;
146
+ };
147
+
148
+ // --- effect spawners, by category -------------------------------------------
149
+
150
+ const spawnSlash = (options, rng) => {
151
+ if (!resolved.slash.enabled) return null;
152
+ const ribbon = acquireTrail();
153
+ const look = cleanObject(options.look);
154
+ ribbon.begin({
155
+ base: toArray3(options.base, [0, 0, 0]),
156
+ bands: look.bands ?? resolved.slash.bands,
157
+ color: look.color ?? resolved.slash.color,
158
+ coreColor: look.coreColor ?? resolved.slash.coreColor,
159
+ follow: options.follow,
160
+ intensity: look.intensity ?? resolved.slash.intensity,
161
+ lifetime: look.lifetime ?? resolved.slash.lifetime,
162
+ tip: toArray3(options.tip, [0, 1, 0]),
163
+ });
164
+ const entry = {
165
+ look,
166
+ ribbon,
167
+ rng,
168
+ sparkleAccum: 0,
169
+ stopped: false,
170
+ };
171
+ liveTrails.push(entry);
172
+ return {
173
+ type: 'slash',
174
+ get active() { return entry.ribbon.active; },
175
+ stop() {
176
+ entry.stopped = true;
177
+ entry.ribbon.stop();
178
+ return this;
179
+ },
180
+ };
181
+ };
182
+
183
+ const spawnFireball = (options, rng) => {
184
+ if (!resolved.fireball.enabled) return null;
185
+ const core = acquireProjectile();
186
+ const look = cleanObject(options.look);
187
+ core.arm({
188
+ coreColor: look.coreColor ?? resolved.fireball.coreColor,
189
+ coreSize: look.coreSize ?? resolved.fireball.coreSize,
190
+ flameColor: look.flameColor ?? resolved.fireball.flameColor,
191
+ intensity: look.intensity ?? resolved.fireball.intensity,
192
+ seed: rng(),
193
+ });
194
+ const from = toArray3(options.from ?? options.at, [0, 1, 0]);
195
+ const entry = {
196
+ core,
197
+ done: false,
198
+ emberAccum: 0,
199
+ gravity: Number.isFinite(options.gravity) ? options.gravity : 2.0,
200
+ life: 0,
201
+ look,
202
+ maxLife: Number.isFinite(options.maxLife) ? options.maxLife : 3.0,
203
+ onHit: typeof options.onHit === 'function' ? options.onHit : null,
204
+ position: new THREE.Vector3(...from),
205
+ rng,
206
+ velocity: new THREE.Vector3(...toArray3(options.velocity, [6, 2, 0])),
207
+ finish(detonate = true, at = null) {
208
+ if (entry.done) return;
209
+ entry.done = true;
210
+ const where = at ?? [entry.position.x, entry.position.y, entry.position.z];
211
+ if (detonate) {
212
+ emitRecords(emitFireballExplosion({
213
+ at: where,
214
+ normal: [0, 1, 0],
215
+ now: time,
216
+ overrides: entry.look,
217
+ rng: entry.rng,
218
+ settings: resolved,
219
+ }));
220
+ entry.onHit?.(where);
221
+ }
222
+ entry.core.reset();
223
+ },
224
+ };
225
+ core.setCenter(entry.position);
226
+ liveProjectiles.push(entry);
227
+ return {
228
+ type: 'fireball',
229
+ position: entry.position,
230
+ velocity: entry.velocity,
231
+ get alive() { return !entry.done; },
232
+ explode(at = null) {
233
+ entry.finish(true, at ? toArray3(at) : null);
234
+ return this;
235
+ },
236
+ cancel() {
237
+ entry.finish(false);
238
+ return this;
239
+ },
240
+ };
241
+ };
242
+
243
+ const oneShot = (builder, options, rng, extraArgs = {}) => {
244
+ emitRecords(builder({
245
+ now: time,
246
+ overrides: cleanObject(options.look),
247
+ rng,
248
+ settings: resolved,
249
+ ...extraArgs,
250
+ }));
251
+ return null;
252
+ };
253
+
254
+ // --- public surface ----------------------------------------------------------
255
+
256
+ const vfx = {
257
+ root,
258
+ settings: resolved,
259
+
260
+ /** Live pool/backbone state (also what verify-vfxgen.mjs asserts). */
261
+ get stats() {
262
+ const glow = backbone.groups.glow;
263
+ const puff = backbone.groups.puff;
264
+ return {
265
+ capacity: glow.capacity + puff.capacity,
266
+ drawCalls: (glow.liveCount(time) > 0 ? 1 : 0)
267
+ + (puff.liveCount(time) > 0 ? 1 : 0)
268
+ + liveTrails.filter((t) => t.ribbon.samples > 1).length
269
+ + liveProjectiles.filter((b) => !b.done).length,
270
+ live: {
271
+ glow: glow.liveCount(time),
272
+ projectiles: liveProjectiles.filter((b) => !b.done).length,
273
+ puff: puff.liveCount(time),
274
+ trails: liveTrails.length,
275
+ },
276
+ pooled: { projectiles: projectilePool.length, trails: trailPool.length },
277
+ spawnsTotal,
278
+ time,
279
+ };
280
+ },
281
+
282
+ /**
283
+ * Spawns an effect at a gameplay moment. Types by category —
284
+ * weapon: 'slash' (returns a handle; stop() when the swing ends),
285
+ * 'impact' ({ at, normal?, power? });
286
+ * magic: 'fireball' ({ from, velocity, gravity?, maxLife?, onHit? } →
287
+ * handle with .position/.explode()/.cancel());
288
+ * movement: 'footstep' ({ at, dir? }), 'landing' ({ at, power? }).
289
+ * All accept `look: { ...group overrides }` for per-spawn re-tints and
290
+ * `seed` to pin the spawn's randomness.
291
+ */
292
+ spawn(type, options = {}) {
293
+ const rng = Number.isFinite(options.seed)
294
+ ? mulberry32(hashCombine(worldSeed, Math.round(options.seed)))
295
+ : nextRng();
296
+ spawnsTotal += 1;
297
+ switch (type) {
298
+ case 'slash':
299
+ return spawnSlash(options, rng);
300
+ case 'impact':
301
+ return oneShot(emitImpact, options, rng, {
302
+ at: toArray3(options.at),
303
+ normal: toArray3(options.normal, [0, 1, 0]),
304
+ power: options.power,
305
+ });
306
+ case 'fireball':
307
+ return spawnFireball(options, rng);
308
+ case 'footstep':
309
+ return oneShot(emitFootstep, options, rng, {
310
+ at: toArray3(options.at),
311
+ dir: options.dir ? toArray3(options.dir) : null,
312
+ });
313
+ case 'landing':
314
+ return oneShot(emitLanding, options, rng, {
315
+ at: toArray3(options.at),
316
+ power: options.power,
317
+ });
318
+ default:
319
+ spawnsTotal -= 1;
320
+ console.warn(`[vfxgen] Unknown effect type "${type}".`);
321
+ return null;
322
+ }
323
+ },
324
+
325
+ /**
326
+ * Per frame. Ticks the shared clock (scaled by shared.timeScale — feed
327
+ * hit-stop here), samples live ribbons, integrates projectiles, and
328
+ * sheds their continuous particles.
329
+ */
330
+ update(delta, camera) {
331
+ const dt = Math.min(Math.max((delta ?? 0.016) * resolved.shared.timeScale, 0), 0.1);
332
+ time += dt;
333
+ u.uTime.value = time;
334
+ if (camera?.isObject3D) {
335
+ const e = camera.matrixWorld.elements;
336
+ u.uCamRight.value.set(e[0], e[1], e[2]);
337
+ u.uCamUp.value.set(e[4], e[5], e[6]);
338
+ }
339
+
340
+ // Weapon trails: sample the followed blade, sparkle along the tip.
341
+ for (let i = liveTrails.length - 1; i >= 0; i -= 1) {
342
+ const entry = liveTrails[i];
343
+ const tip = entry.ribbon.sample(time);
344
+ if (tip && resolved.slash.sparkle > 0 && dt > 0) {
345
+ entry.sparkleAccum += (entry.look.sparkle ?? resolved.slash.sparkle) * dt;
346
+ const count = Math.floor(entry.sparkleAccum);
347
+ if (count > 0) {
348
+ entry.sparkleAccum -= count;
349
+ emitRecords(emitSlashSparkle({
350
+ count,
351
+ now: time,
352
+ overrides: entry.look,
353
+ rng: entry.rng,
354
+ settings: resolved,
355
+ tip: [tip.x, tip.y, tip.z],
356
+ }));
357
+ }
358
+ }
359
+ if (entry.ribbon.isDead(time)) {
360
+ entry.ribbon.reset();
361
+ liveTrails.splice(i, 1);
362
+ }
363
+ }
364
+
365
+ // Magic projectiles: integrate, shed embers, detonate on ground/lifetime.
366
+ for (let i = liveProjectiles.length - 1; i >= 0; i -= 1) {
367
+ const entry = liveProjectiles[i];
368
+ if (entry.done) {
369
+ liveProjectiles.splice(i, 1);
370
+ continue;
371
+ }
372
+ entry.life += dt;
373
+ entry.velocity.y -= entry.gravity * dt;
374
+ entry.position.addScaledVector(entry.velocity, dt);
375
+ entry.core.setCenter(entry.position);
376
+
377
+ if (dt > 0 && resolved.fireball.emberRate > 0) {
378
+ entry.emberAccum += (entry.look.emberRate ?? resolved.fireball.emberRate) * dt;
379
+ const count = Math.floor(entry.emberAccum);
380
+ if (count > 0) {
381
+ entry.emberAccum -= count;
382
+ emitRecords(emitFireballEmbers({
383
+ at: [entry.position.x, entry.position.y, entry.position.z],
384
+ count,
385
+ now: time,
386
+ overrides: entry.look,
387
+ rng: entry.rng,
388
+ settings: resolved,
389
+ velocity: [entry.velocity.x, entry.velocity.y, entry.velocity.z],
390
+ }));
391
+ }
392
+ }
393
+
394
+ const groundY = typeof heightAt === 'function'
395
+ ? Number(heightAt(entry.position.x, entry.position.z) || 0)
396
+ : null;
397
+ if (groundY !== null && entry.position.y <= groundY + 0.1) {
398
+ entry.finish(true, [entry.position.x, groundY + 0.05, entry.position.z]);
399
+ liveProjectiles.splice(i, 1);
400
+ } else if (entry.life >= entry.maxLife) {
401
+ entry.finish(true);
402
+ liveProjectiles.splice(i, 1);
403
+ }
404
+ }
405
+ return vfx;
406
+ },
407
+
408
+ /**
409
+ * Joins every burst and puff to the environment shader's height-fog
410
+ * layer — same parameters and formula as ambientfx.setDistanceFog, so
411
+ * distant explosions haze out with the terrain. Density 0 disables.
412
+ */
413
+ setDistanceFog({ color, density, falloff, floorY } = {}) {
414
+ if (density !== undefined) u.uFogDensity.value = Math.max(Number(density) || 0, 0);
415
+ if (falloff !== undefined) u.uFogFalloff.value = Math.max(Number(falloff) || 0, 0.001);
416
+ if (floorY !== undefined) u.uFogFloorY.value = Number(floorY) || 0;
417
+ if (color !== undefined) {
418
+ const next = Array.isArray(color) ? new THREE.Color(...color) : new THREE.Color(color);
419
+ u.uFogColor.value.copy(next);
420
+ }
421
+ return vfx;
422
+ },
423
+
424
+ /** Global VFX clock multiplier — 0 freezes (hit-stop), 1 is realtime. */
425
+ setTimeScale(scale) {
426
+ resolved.shared.timeScale = Math.min(Math.max(Number(scale) || 0, 0), 8);
427
+ return vfx;
428
+ },
429
+
430
+ /** Drops every live effect (scene transitions). Pools stay warm. */
431
+ clear() {
432
+ for (const entry of liveTrails) entry.ribbon.reset();
433
+ liveTrails.length = 0;
434
+ for (const entry of liveProjectiles) entry.finish(false);
435
+ liveProjectiles.length = 0;
436
+ for (const group of Object.values(backbone.groups)) group.clear();
437
+ return vfx;
438
+ },
439
+
440
+ dispose() {
441
+ vfx.clear();
442
+ for (const ribbon of trailPool) ribbon.dispose();
443
+ for (const core of projectilePool) core.dispose();
444
+ for (const group of Object.values(backbone.groups)) group.dispose();
445
+ root.parent?.remove(root);
446
+ },
447
+ };
448
+ return vfx;
449
+ }
@@ -0,0 +1,137 @@
1
+ // Procedural stylized weapons — the "extensive range of weapons" the VFX
2
+ // rides on. Each weapon is a small flat-shaded mesh group (grip at the
3
+ // group origin, blade/head along local +Y) plus the two things the VFX
4
+ // system needs from ANY weapon:
5
+ // anchors — trail ribbon base/tip points in weapon-local space
6
+ // profile — length + weight; weight scales move timing and hit power
7
+ // (a dagger flicks, a hammer commits)
8
+ // External weapon models slot in the same way: wrap the mesh, give it
9
+ // anchors + a profile, and every move in moves/ works unchanged.
10
+
11
+ import * as THREE from 'three';
12
+
13
+ export const WEAPON_IDS = Object.freeze(['sword', 'greatsword', 'spear', 'dagger', 'hammer']);
14
+
15
+ const PALETTE = {
16
+ blade: 0xc7d2e4,
17
+ bladeDark: 0x9aa8bf,
18
+ grip: 0x3d3630,
19
+ guard: 0x8a7748,
20
+ shaft: 0x6b5138,
21
+ };
22
+
23
+ function part(geometry, color) {
24
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color }));
25
+ return mesh;
26
+ }
27
+
28
+ /** Tapered blade: a 4-sided cylinder reads as a stylized flat blade. */
29
+ function blade(length, width, thickness, color = PALETTE.blade) {
30
+ const geometry = new THREE.CylinderGeometry(width * 0.18, width, length, 4, 1);
31
+ geometry.scale(1, 1, thickness / width);
32
+ geometry.translate(0, length / 2, 0);
33
+ return part(geometry, color);
34
+ }
35
+
36
+ const BUILDERS = {
37
+ sword({ length = 1.05 } = {}) {
38
+ const root = new THREE.Group();
39
+ const edge = blade(length, 0.085, 0.02);
40
+ edge.position.y = 0.16;
41
+ const guard = part(new THREE.BoxGeometry(0.26, 0.045, 0.06), PALETTE.guard);
42
+ guard.position.y = 0.16;
43
+ const grip = part(new THREE.CylinderGeometry(0.028, 0.032, 0.22, 8), PALETTE.grip);
44
+ grip.position.y = 0.03;
45
+ const pommel = part(new THREE.SphereGeometry(0.045, 8, 6), PALETTE.guard);
46
+ pommel.position.y = -0.09;
47
+ root.add(edge, guard, grip, pommel);
48
+ return {
49
+ anchors: { base: [0, 0.28, 0], tip: [0, 0.16 + length, 0] },
50
+ profile: { label: 'Sword', length, weight: 1.0 },
51
+ root,
52
+ };
53
+ },
54
+ greatsword({ length = 1.5 } = {}) {
55
+ const root = new THREE.Group();
56
+ const edge = blade(length, 0.15, 0.03);
57
+ edge.position.y = 0.2;
58
+ const guard = part(new THREE.BoxGeometry(0.42, 0.06, 0.08), PALETTE.guard);
59
+ guard.position.y = 0.2;
60
+ const grip = part(new THREE.CylinderGeometry(0.034, 0.038, 0.34, 8), PALETTE.grip);
61
+ grip.position.y = 0.01;
62
+ root.add(edge, guard, grip);
63
+ return {
64
+ anchors: { base: [0, 0.42, 0], tip: [0, 0.2 + length, 0] },
65
+ profile: { label: 'Greatsword', length, weight: 1.4 },
66
+ root,
67
+ };
68
+ },
69
+ spear({ length = 1.9 } = {}) {
70
+ const root = new THREE.Group();
71
+ const shaft = part(new THREE.CylinderGeometry(0.024, 0.028, length, 8), PALETTE.shaft);
72
+ shaft.position.y = length / 2 - 0.35;
73
+ const head = blade(0.38, 0.075, 0.018);
74
+ head.position.y = length - 0.35;
75
+ const collar = part(new THREE.CylinderGeometry(0.04, 0.04, 0.05, 8), PALETTE.guard);
76
+ collar.position.y = length - 0.36;
77
+ root.add(shaft, head, collar);
78
+ return {
79
+ // Spear trails ride the head, not the whole shaft.
80
+ anchors: { base: [0, length - 0.5, 0], tip: [0, length + 0.03, 0] },
81
+ profile: { label: 'Spear', length, weight: 1.1 },
82
+ root,
83
+ };
84
+ },
85
+ dagger({ length = 0.48 } = {}) {
86
+ const root = new THREE.Group();
87
+ const edge = blade(length, 0.06, 0.016);
88
+ edge.position.y = 0.1;
89
+ const guard = part(new THREE.BoxGeometry(0.14, 0.03, 0.045), PALETTE.guard);
90
+ guard.position.y = 0.1;
91
+ const grip = part(new THREE.CylinderGeometry(0.024, 0.028, 0.16, 8), PALETTE.grip);
92
+ grip.position.y = 0;
93
+ root.add(edge, guard, grip);
94
+ return {
95
+ anchors: { base: [0, 0.16, 0], tip: [0, 0.1 + length, 0] },
96
+ profile: { label: 'Dagger', length, weight: 0.65 },
97
+ root,
98
+ };
99
+ },
100
+ hammer({ length = 1.0 } = {}) {
101
+ const root = new THREE.Group();
102
+ const shaft = part(new THREE.CylinderGeometry(0.03, 0.035, length, 8), PALETTE.shaft);
103
+ shaft.position.y = length / 2 - 0.15;
104
+ const head = part(new THREE.BoxGeometry(0.42, 0.24, 0.24), PALETTE.bladeDark);
105
+ head.position.y = length - 0.15;
106
+ const band = part(new THREE.BoxGeometry(0.44, 0.06, 0.26), PALETTE.guard);
107
+ band.position.y = length - 0.15;
108
+ root.add(shaft, head, band);
109
+ return {
110
+ // Hammer trails hug the head so the arc reads as mass, not edge.
111
+ anchors: { base: [0, length - 0.3, 0], tip: [0, length + 0.02, 0] },
112
+ profile: { label: 'Hammer', length, weight: 1.6 },
113
+ root,
114
+ };
115
+ },
116
+ };
117
+
118
+ /**
119
+ * Builds a stylized weapon: `{ root, anchors, profile }`. Grip sits at the
120
+ * group origin with the business end along +Y — the pose convention every
121
+ * move in moves/moveLibrary.js is authored against.
122
+ */
123
+ export function createStylizedWeapon({ type = 'sword', ...options } = {}) {
124
+ const builder = BUILDERS[type] ?? BUILDERS.sword;
125
+ const weapon = builder(options);
126
+ weapon.root.name = `StylizedWeapon:${type}`;
127
+ weapon.type = BUILDERS[type] ? type : 'sword';
128
+ return weapon;
129
+ }
130
+
131
+ /** `{ id, label, weight }` list for pickers. */
132
+ export function getWeaponOptions() {
133
+ return WEAPON_IDS.map((id) => {
134
+ const { profile } = BUILDERS[id]();
135
+ return { id, label: profile.label, weight: profile.weight };
136
+ });
137
+ }
@@ -0,0 +1,4 @@
1
+ export * from './villageNames.js';
2
+ export * from './villageArchetypes.js';
3
+ export * from './villageSites.js';
4
+ export * from './stylizedVillage.js';