@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.
- package/AGENTS.md +127 -4
- package/ATTRIBUTION.md +41 -3
- package/README.md +234 -34
- package/docs/characters.md +144 -0
- package/docs/debug-panel.md +126 -0
- package/docs/docs.css +589 -0
- package/docs/environment.md +186 -0
- package/docs/getting-started.md +180 -0
- package/docs/index.html +14 -0
- package/docs/lab-architecture.md +100 -0
- package/docs/lighting.md +825 -0
- package/docs/main.jsx +739 -0
- package/docs/mcp.md +97 -0
- package/docs/post-processing.md +98 -0
- package/docs/settings-reference.md +1878 -0
- package/docs/shader-constants.md +77 -0
- package/docs/sky.md +182 -0
- package/docs/style-labs.md +309 -0
- package/docs/texture-lab.md +135 -0
- package/docs/toon-shading.md +183 -0
- package/docs/tsl-conventions.md +167 -0
- package/docs/vegetation-sky.md +275 -0
- package/docs/water.md +430 -0
- package/docs/weather.md +200 -0
- package/docs/world-scale.md +111 -0
- package/mcp/server.mjs +610 -0
- package/mcp/style-lab-tools.mjs +371 -0
- package/mcp/vite-plugin.mjs +174 -0
- package/mcp/workspace.mjs +397 -0
- package/package.json +64 -5
- package/src/ambientfx/INTEGRATION.md +164 -0
- package/src/ambientfx/ambientFxPresets.js +83 -0
- package/src/ambientfx/ambientFxSettings.js +368 -0
- package/src/ambientfx/emitters.js +169 -0
- package/src/ambientfx/index.js +5 -0
- package/src/ambientfx/particleBackbone.js +493 -0
- package/src/ambientfx/stylizedAmbientFx.js +450 -0
- package/src/assetlib/ambientcg.js +117 -0
- package/src/assetlib/assetRef.js +91 -0
- package/src/assetlib/importedEntry.js +40 -0
- package/src/assetlib/index.js +23 -0
- package/src/assetlib/kaykit.js +192 -0
- package/src/assetlib/kaykitStaticIndex.js +700 -0
- package/src/assetlib/loadImported.js +143 -0
- package/src/assetlib/opensource3d.js +113 -0
- package/src/assetlib/polyhaven.js +182 -0
- package/src/assetlib/polypizza.js +115 -0
- package/src/assetlib/smithsonian.js +214 -0
- package/src/assetlib/sources.js +279 -0
- package/src/assetlib/zip.js +58 -0
- package/src/biome/biomeGenerator.js +385 -0
- package/src/biome/biomeRuntime.js +299 -0
- package/src/biome/index.js +2 -0
- package/src/buildinggen/buildingAsset.js +44 -0
- package/src/buildinggen/buildingGrammar.js +311 -0
- package/src/buildinggen/buildingMesh.js +451 -0
- package/src/buildinggen/buildingPresets.js +46 -0
- package/src/buildinggen/buildingRecipe.js +100 -0
- package/src/buildinggen/buildingSettings.js +238 -0
- package/src/buildinggen/index.js +6 -0
- package/src/camera/cameraDirector.js +157 -0
- package/src/camera/cameraGenerator.js +367 -0
- package/src/camera/cameraRig.js +570 -0
- package/src/camera/cameraSettings.js +236 -0
- package/src/camera/index.js +7 -0
- package/src/catalog/builtinEntries.js +245 -0
- package/src/catalog/catalog.js +210 -0
- package/src/catalog/index.js +3 -0
- package/src/catalog/manifest.js +84 -0
- package/src/core/generation.js +529 -0
- package/src/environment/environmentRigs.js +21 -1
- package/src/environment/environmentSettings.js +4 -0
- package/src/environment/environmentSunShadowPass.js +8 -0
- package/src/fauna/INTEGRATION.md +174 -0
- package/src/fauna/boids.js +861 -0
- package/src/fauna/faunaBodies.js +492 -0
- package/src/fauna/faunaPresets.js +52 -0
- package/src/fauna/faunaSettings.js +525 -0
- package/src/fauna/index.js +5 -0
- package/src/fauna/stylizedFauna.js +395 -0
- package/src/game-feel/gameFeelGenerator.js +402 -0
- package/src/game-feel/gameFeelRuntime.js +549 -0
- package/src/game-feel/index.js +2 -0
- package/src/index.js +17 -6
- package/src/lighting/colorIntensity.js +177 -0
- package/src/lighting/index.js +161 -0
- package/src/lighting/lightDescriptors.js +249 -0
- package/src/lighting/lightingCapabilities.js +79 -0
- package/src/lighting/lightingDocuments.js +247 -0
- package/src/lighting/lightingFixtures.js +446 -0
- package/src/lighting/lightingGenerator.js +449 -0
- package/src/lighting/lightingPresets.js +319 -0
- package/src/lighting/lightingRuntime.js +723 -0
- package/src/lighting/lightingStyle.js +386 -0
- package/src/lighting/lightingSystem.js +774 -0
- package/src/lighting/unrealExport.js +186 -0
- package/src/lighting/utils.js +87 -0
- package/src/motion/index.js +5 -0
- package/src/motion/motionClip.js +441 -0
- package/src/motion/motionController.js +628 -0
- package/src/motion/motionDocuments.js +225 -0
- package/src/motion/motionGraph.js +307 -0
- package/src/motion/motionSettings.js +222 -0
- package/src/pathgen/index.js +7 -0
- package/src/pathgen/pathBridge.js +232 -0
- package/src/pathgen/pathPresets.js +35 -0
- package/src/pathgen/pathRibbon.js +410 -0
- package/src/pathgen/pathRouter.js +380 -0
- package/src/pathgen/pathSettings.js +335 -0
- package/src/pathgen/pathTextures.js +123 -0
- package/src/pathgen/stylizedPaths.js +453 -0
- package/src/post/index.js +1 -0
- package/src/post/postGenerator.js +177 -0
- package/src/post/postProcessing.js +41 -0
- package/src/propgen/generatorsWave1.js +379 -0
- package/src/propgen/generatorsWave2.js +462 -0
- package/src/propgen/index.js +5 -0
- package/src/propgen/propAsset.js +323 -0
- package/src/propgen/propParts.js +170 -0
- package/src/propgen/propPlacement.js +459 -0
- package/src/propgen/propPresets.js +82 -0
- package/src/propgen/propSettings.js +395 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +242 -0
- package/src/shaders-tsl/chunks/vegetation-style.js +360 -0
- package/src/shaders-tsl/chunks/water-shore-state.js +31 -0
- package/src/shaders-tsl/chunks/water-waves.js +90 -11
- package/src/shaders-tsl/environment.js +15 -1
- package/src/shaders-tsl/flower.js +279 -30
- package/src/shaders-tsl/grass.js +60 -33
- package/src/shaders-tsl/sky.js +125 -31
- package/src/shaders-tsl/tree-leaf.js +61 -25
- package/src/shaders-tsl/water-breaker.js +7 -4
- package/src/shaders-tsl/water-shore-state-simulation.js +523 -0
- package/src/shaders-tsl/water.js +439 -49
- package/src/shaders-tsl/woody-surface.js +154 -0
- package/src/sky/sceneOverrideLayers.js +10 -0
- package/src/sky/skyQuality.js +26 -0
- package/src/sky/stylizedSky.js +753 -45
- package/src/soundscape/index.js +4 -0
- package/src/soundscape/soundscapeGenerator.js +179 -0
- package/src/soundscape/soundscapeRuntime.js +806 -0
- package/src/soundscape/soundscapeSettings.js +292 -0
- package/src/styles/index.js +13 -0
- package/src/styles/styleBundle.js +325 -0
- package/src/stylizedTerrain.js +32 -2
- package/src/stylizedWorld.js +423 -20
- package/src/texgen/evaluateTexture.js +675 -0
- package/src/texgen/index.js +60 -0
- package/src/texgen/noise2.js +210 -0
- package/src/texgen/textureAi.js +436 -0
- package/src/texgen/textureGenerators.js +516 -0
- package/src/texgen/texturePresets.js +490 -0
- package/src/texgen/textureSettings.js +342 -0
- package/src/texgen/textureThree.js +59 -0
- package/src/vegetation/flowerSpecies.js +15 -3
- package/src/vegetation/grassPalettes.js +153 -0
- package/src/vegetation/index.js +6 -0
- package/src/vegetation/stylizedBush.js +2 -0
- package/src/vegetation/stylizedFlower.js +82 -0
- package/src/vegetation/stylizedFlowers.js +48 -7
- package/src/vegetation/stylizedForest.js +29 -1
- package/src/vegetation/stylizedGrass.js +291 -56
- package/src/vegetation/stylizedTree.js +38 -2
- package/src/vegetation/stylizedTreeFoliage.js +2 -1
- package/src/vegetation/vegetationShaders.js +1110 -0
- package/src/vfxgen/INTEGRATION.md +145 -0
- package/src/vfxgen/core/burstBackbone.js +380 -0
- package/src/vfxgen/core/projectileCore.js +92 -0
- package/src/vfxgen/core/spriteShapes.js +98 -0
- package/src/vfxgen/core/trailRibbon.js +272 -0
- package/src/vfxgen/core/vfxRandom.js +29 -0
- package/src/vfxgen/effects/emitHelpers.js +37 -0
- package/src/vfxgen/effects/magicEffects.js +162 -0
- package/src/vfxgen/effects/movementEffects.js +87 -0
- package/src/vfxgen/effects/weaponEffects.js +118 -0
- package/src/vfxgen/index.js +18 -0
- package/src/vfxgen/moves/moveController.js +146 -0
- package/src/vfxgen/moves/moveLibrary.js +335 -0
- package/src/vfxgen/vfxPresets.js +98 -0
- package/src/vfxgen/vfxSettings.js +384 -0
- package/src/vfxgen/vfxSystem.js +449 -0
- package/src/vfxgen/weapons/stylizedWeapons.js +137 -0
- package/src/villagegen/index.js +4 -0
- package/src/villagegen/stylizedVillage.js +490 -0
- package/src/villagegen/villageArchetypes.js +160 -0
- package/src/villagegen/villageNames.js +40 -0
- package/src/villagegen/villageSites.js +105 -0
- package/src/water/sceneOverrideLayers.js +23 -0
- package/src/water/water.js +5 -0
- package/src/water/waterBreakerSystem.js +15 -1
- package/src/water/waterCurrentField.js +447 -0
- package/src/water/waterMaterial.js +12 -0
- package/src/water/waterNearshorePhase.js +320 -0
- package/src/water/waterScenePasses.js +83 -30
- package/src/water/waterSettings.js +325 -13
- package/src/water/waterShoreMaterial.js +322 -0
- package/src/water/waterShoreStateField.js +605 -0
- package/src/water/waterSurface.js +797 -28
- package/src/weather/index.js +6 -0
- package/src/weather/weatherPrecipitation.js +221 -0
- package/src/weather/weatherPresets.js +258 -0
- package/src/weather/weatherSettings.js +269 -0
- package/src/weather/weatherSystem.js +871 -0
- package/src/worldMinimap.js +62 -0
- package/src/worldPresets.js +4 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Weapon-category effects: the impact burst (star flash + ballistic sparks)
|
|
2
|
+
// and the slash trail's edge sparkle. The slash ribbon itself is geometry,
|
|
3
|
+
// not particles — see core/trailRibbon.js; vfxSystem owns the ribbon pool
|
|
4
|
+
// and calls emitSlashSparkle along the sampled tip path.
|
|
5
|
+
//
|
|
6
|
+
// Every export is a pure builder: (settings, rng, options, clock) → backbone
|
|
7
|
+
// records grouped by blend state ({ glow: [...], puff: [...] }).
|
|
8
|
+
|
|
9
|
+
import { BURST_KIND } from '../core/burstBackbone.js';
|
|
10
|
+
import { jitterColor, lerp, normalized, randUnitVector, withOverrides } from './emitHelpers.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hit feedback at a point: one N-spike star flash plus `sparkCount · power`
|
|
14
|
+
* ballistic sparks biased along the hit normal. `power` scales count, speed,
|
|
15
|
+
* and flash size — 0.3 reads as a parry tick, 2 as a heavy finisher.
|
|
16
|
+
*/
|
|
17
|
+
export function emitImpact({ settings, rng, at, normal = [0, 1, 0], power = 1, now = 0, overrides = null }) {
|
|
18
|
+
const e = withOverrides(settings.impact, overrides);
|
|
19
|
+
if (!e.enabled) return { glow: [], puff: [] };
|
|
20
|
+
const p = Math.max(Number(power) || 1, 0);
|
|
21
|
+
const n = normalized(normal);
|
|
22
|
+
const glow = [];
|
|
23
|
+
|
|
24
|
+
const count = Math.round(e.sparkCount * p);
|
|
25
|
+
for (let i = 0; i < count; i += 1) {
|
|
26
|
+
const scatter = randUnitVector(rng);
|
|
27
|
+
// Hemisphere bias: fold scatter toward the hit normal so sparks spray
|
|
28
|
+
// OFF the surface, with enough spread to read as a shower.
|
|
29
|
+
const dir = normalized([
|
|
30
|
+
n[0] + scatter[0] * 0.85,
|
|
31
|
+
Math.abs(n[1] + scatter[1] * 0.85),
|
|
32
|
+
n[2] + scatter[2] * 0.85,
|
|
33
|
+
]);
|
|
34
|
+
const speed = e.sparkSpeed * lerp(0.4, 1.2, rng()) * Math.sqrt(p);
|
|
35
|
+
const size = 0.05 * lerp(0.7, 1.3, rng()) * p ** 0.3;
|
|
36
|
+
const color = jitterColor(e.sparkColor, rng);
|
|
37
|
+
glow.push({
|
|
38
|
+
birth: now,
|
|
39
|
+
extra: 3.5, // streak stretch along velocity
|
|
40
|
+
gravity: e.gravity,
|
|
41
|
+
kind: BURST_KIND.spark,
|
|
42
|
+
lifetime: e.lifetime * lerp(0.5, 1.0, rng()),
|
|
43
|
+
r: color[0] * e.intensity, g: color[1] * e.intensity, b: color[2] * e.intensity,
|
|
44
|
+
seed: rng(),
|
|
45
|
+
size0: size, size1: size * 0.35,
|
|
46
|
+
vx: dir[0] * speed, vy: dir[1] * speed, vz: dir[2] * speed,
|
|
47
|
+
x: at[0], y: at[1], z: at[2],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (e.flashSize > 0) {
|
|
52
|
+
glow.push({
|
|
53
|
+
birth: now,
|
|
54
|
+
extra: e.spikes,
|
|
55
|
+
gravity: 0,
|
|
56
|
+
kind: BURST_KIND.flash,
|
|
57
|
+
lifetime: Math.max(e.lifetime * 0.45, 0.08),
|
|
58
|
+
r: e.flashColor[0] * e.intensity * 1.4,
|
|
59
|
+
g: e.flashColor[1] * e.intensity * 1.4,
|
|
60
|
+
b: e.flashColor[2] * e.intensity * 1.4,
|
|
61
|
+
seed: rng(),
|
|
62
|
+
size0: e.flashSize * Math.sqrt(p), size1: 0,
|
|
63
|
+
vx: 0, vy: 0, vz: 0,
|
|
64
|
+
x: at[0], y: at[1], z: at[2],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The action-RPG hit circle: a camera-facing ring that pops outward and
|
|
69
|
+
// dies fast — reads as the hit "landing" even before the sparks resolve.
|
|
70
|
+
if (e.shockwave) {
|
|
71
|
+
glow.push({
|
|
72
|
+
birth: now,
|
|
73
|
+
extra: 0.22, // starting band thickness
|
|
74
|
+
gravity: 0,
|
|
75
|
+
kind: BURST_KIND.shockwave,
|
|
76
|
+
lifetime: 0.3,
|
|
77
|
+
r: e.flashColor[0] * e.intensity * 0.9,
|
|
78
|
+
g: e.flashColor[1] * e.intensity * 0.9,
|
|
79
|
+
b: e.flashColor[2] * e.intensity * 0.9,
|
|
80
|
+
seed: rng(),
|
|
81
|
+
size0: 0.12, size1: 1.25 * Math.sqrt(p),
|
|
82
|
+
vx: 0, vy: 0, vz: 0,
|
|
83
|
+
x: at[0], y: at[1], z: at[2],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { glow, puff: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sparks shed from the blade tip while a slash trail is live. vfxSystem
|
|
92
|
+
* accumulates `sparkle · dt` and calls this with the whole-spark count each
|
|
93
|
+
* frame, so emission is framerate-independent and deterministic.
|
|
94
|
+
*/
|
|
95
|
+
export function emitSlashSparkle({ settings, rng, tip, count, now = 0, overrides = null }) {
|
|
96
|
+
const e = withOverrides(settings.slash, overrides);
|
|
97
|
+
if (!e.enabled || count <= 0) return { glow: [], puff: [] };
|
|
98
|
+
const glow = [];
|
|
99
|
+
for (let i = 0; i < count; i += 1) {
|
|
100
|
+
const dir = randUnitVector(rng);
|
|
101
|
+
const speed = lerp(0.4, 1.6, rng());
|
|
102
|
+
const size = lerp(0.02, 0.05, rng());
|
|
103
|
+
const color = jitterColor(e.color, rng, 0.12);
|
|
104
|
+
glow.push({
|
|
105
|
+
birth: now,
|
|
106
|
+
extra: 2.0,
|
|
107
|
+
gravity: 4,
|
|
108
|
+
kind: BURST_KIND.spark,
|
|
109
|
+
lifetime: lerp(0.12, 0.3, rng()),
|
|
110
|
+
r: color[0] * e.intensity, g: color[1] * e.intensity, b: color[2] * e.intensity,
|
|
111
|
+
seed: rng(),
|
|
112
|
+
size0: size, size1: size * 0.3,
|
|
113
|
+
vx: dir[0] * speed, vy: dir[1] * speed, vz: dir[2] * speed,
|
|
114
|
+
x: tip[0], y: tip[1], z: tip[2],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { glow, puff: [] };
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Gameplay-VFX cluster — event-driven combat/movement effects, organized by
|
|
2
|
+
// category (weapon / magic / movement). Entry point: createVfxSystem
|
|
3
|
+
// (vfxSystem.js). core/vfxRandom.js is deliberately NOT re-exported (its
|
|
4
|
+
// names would collide with ambientfx/fauna on the root package surface).
|
|
5
|
+
export * from './vfxSettings.js';
|
|
6
|
+
export * from './vfxPresets.js';
|
|
7
|
+
export * from './vfxSystem.js';
|
|
8
|
+
export { BURST_KIND, GROUP_FOR_BURST_KIND, createBurstBackbone } from './core/burstBackbone.js';
|
|
9
|
+
export { createTrailRibbon } from './core/trailRibbon.js';
|
|
10
|
+
export { createProjectileCore } from './core/projectileCore.js';
|
|
11
|
+
export { emitImpact, emitSlashSparkle } from './effects/weaponEffects.js';
|
|
12
|
+
export { emitFireballEmbers, emitFireballExplosion } from './effects/magicEffects.js';
|
|
13
|
+
export { emitFootstep, emitLanding } from './effects/movementEffects.js';
|
|
14
|
+
export { createStylizedWeapon, getWeaponOptions, WEAPON_IDS } from './weapons/stylizedWeapons.js';
|
|
15
|
+
export {
|
|
16
|
+
collectMoveEvents, getMove, getMoveOptions, MOVE_IDS, moveDuration, sampleMovePose,
|
|
17
|
+
} from './moves/moveLibrary.js';
|
|
18
|
+
export { createMoveController } from './moves/moveController.js';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Drives a weapon through a move and translates the move's event track into
|
|
2
|
+
// VFX spawns — the runtime glue a game uses for held-weapon attacks:
|
|
3
|
+
//
|
|
4
|
+
// const weapon = createStylizedWeapon({ type: 'greatsword' });
|
|
5
|
+
// actorGroup.add(weapon.root);
|
|
6
|
+
// const attack = createMoveController({ weapon, vfx });
|
|
7
|
+
// attack.play('overhead'); // on the attack input
|
|
8
|
+
// attack.update(delta); // per frame, before vfx.update
|
|
9
|
+
//
|
|
10
|
+
// The controller owns the trail lifecycle (start/stop beats live in the
|
|
11
|
+
// move data), fires impacts at the blade tip with the swing's real travel
|
|
12
|
+
// direction, and grounds landing/dust beats under the actor. Bring-your-own
|
|
13
|
+
// animation stays possible: skip the controller and drive vfx.spawn('slash',
|
|
14
|
+
// { follow: bone }) from your own clips — the moves are the batteries
|
|
15
|
+
// included, not a cage.
|
|
16
|
+
|
|
17
|
+
import * as THREE from 'three';
|
|
18
|
+
|
|
19
|
+
import { collectMoveEvents, getMove, sampleMovePose } from './moveLibrary.js';
|
|
20
|
+
|
|
21
|
+
const tipScratch = new THREE.Vector3();
|
|
22
|
+
const prevTipScratch = new THREE.Vector3();
|
|
23
|
+
const normalScratch = new THREE.Vector3();
|
|
24
|
+
|
|
25
|
+
export function createMoveController({ weapon, vfx, onEvent = null, groundY = 0 } = {}) {
|
|
26
|
+
if (!weapon?.root) throw new Error('createMoveController needs a weapon from createStylizedWeapon().');
|
|
27
|
+
const weight = weapon.profile?.weight ?? 1;
|
|
28
|
+
|
|
29
|
+
let move = null;
|
|
30
|
+
let time = 0;
|
|
31
|
+
let trail = null;
|
|
32
|
+
let hasPrevTip = false;
|
|
33
|
+
|
|
34
|
+
const applyPose = (pose) => {
|
|
35
|
+
weapon.root.position.set(pose.p[0], pose.p[1], pose.p[2]);
|
|
36
|
+
weapon.root.rotation.set(pose.r[0], pose.r[1], pose.r[2], 'XYZ');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const tipWorld = (target) => {
|
|
40
|
+
weapon.root.updateWorldMatrix(true, false);
|
|
41
|
+
return target.fromArray(weapon.anchors.tip).applyMatrix4(weapon.root.matrixWorld);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const stopTrail = () => {
|
|
45
|
+
trail?.stop();
|
|
46
|
+
trail = null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const controller = {
|
|
50
|
+
get playing() { return move !== null; },
|
|
51
|
+
get phase() { return move ? sampleMovePose(move, time, weight).phaseId : null; },
|
|
52
|
+
|
|
53
|
+
/** Starts a move by id (restarts if one is already playing). */
|
|
54
|
+
play(moveId) {
|
|
55
|
+
const next = getMove(moveId);
|
|
56
|
+
if (!next) {
|
|
57
|
+
console.warn(`[vfxgen] Unknown move "${moveId}".`);
|
|
58
|
+
return controller;
|
|
59
|
+
}
|
|
60
|
+
stopTrail();
|
|
61
|
+
move = next;
|
|
62
|
+
time = 0;
|
|
63
|
+
hasPrevTip = false;
|
|
64
|
+
applyPose(sampleMovePose(move, 0, weight));
|
|
65
|
+
return controller;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
stop() {
|
|
69
|
+
stopTrail();
|
|
70
|
+
move = null;
|
|
71
|
+
return controller;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/** Per frame. Returns true while a move is playing. */
|
|
75
|
+
update(delta) {
|
|
76
|
+
if (!move) return false;
|
|
77
|
+
const dt = Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
78
|
+
const prevTime = time;
|
|
79
|
+
time += dt;
|
|
80
|
+
|
|
81
|
+
const pose = sampleMovePose(move, time, weight);
|
|
82
|
+
applyPose(pose);
|
|
83
|
+
|
|
84
|
+
// Track the tip across frames so impacts inherit the swing's real
|
|
85
|
+
// travel direction (the spark spray leaves ALONG the blow).
|
|
86
|
+
const tip = tipWorld(tipScratch);
|
|
87
|
+
const travel = hasPrevTip
|
|
88
|
+
? normalScratch.copy(tip).sub(prevTipScratch)
|
|
89
|
+
: normalScratch.set(0, 1, 0);
|
|
90
|
+
if (travel.lengthSq() < 1e-8) travel.set(0, 1, 0);
|
|
91
|
+
travel.normalize();
|
|
92
|
+
|
|
93
|
+
for (const event of collectMoveEvents(move, prevTime, time, weight)) {
|
|
94
|
+
onEvent?.(event);
|
|
95
|
+
if (!vfx) continue;
|
|
96
|
+
switch (event.do) {
|
|
97
|
+
case 'trailStart': {
|
|
98
|
+
stopTrail();
|
|
99
|
+
// The arc reaches a touch past the physical tip — reference
|
|
100
|
+
// trails overshoot the blade slightly.
|
|
101
|
+
const { base, tip } = weapon.anchors;
|
|
102
|
+
trail = vfx.spawn('slash', {
|
|
103
|
+
base: [...base],
|
|
104
|
+
follow: weapon.root,
|
|
105
|
+
tip: base.map((b, i) => b + (tip[i] - b) * 1.15),
|
|
106
|
+
});
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'trailStop':
|
|
110
|
+
stopTrail();
|
|
111
|
+
break;
|
|
112
|
+
case 'impact':
|
|
113
|
+
vfx.spawn('impact', {
|
|
114
|
+
at: [tip.x, Math.max(tip.y, groundY + 0.08), tip.z],
|
|
115
|
+
normal: [travel.x, travel.y, travel.z],
|
|
116
|
+
power: event.power ?? 1,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
case 'landing':
|
|
120
|
+
vfx.spawn('landing', {
|
|
121
|
+
at: [weapon.root.position.x, groundY, weapon.root.position.z],
|
|
122
|
+
power: event.power ?? 1,
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
case 'dust':
|
|
126
|
+
vfx.spawn('footstep', {
|
|
127
|
+
at: [weapon.root.position.x, groundY, weapon.root.position.z],
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
prevTipScratch.copy(tip);
|
|
136
|
+
hasPrevTip = true;
|
|
137
|
+
|
|
138
|
+
if (pose.done) {
|
|
139
|
+
stopTrail();
|
|
140
|
+
move = null;
|
|
141
|
+
}
|
|
142
|
+
return move !== null;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return controller;
|
|
146
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// The weapon-move library: authored default motions the VFX rides on, so a
|
|
2
|
+
// designer only tunes the LOOK — the swing itself is provided. Every move is
|
|
3
|
+
// PURE DATA broken into phases (windup → strike → recover …), and each phase
|
|
4
|
+
// carries its own VFX event track (trail on/off, impact, landing, dust).
|
|
5
|
+
// Elaborate moves decompose the same way — `plunge` is the Dragoon jump:
|
|
6
|
+
// crouch → leap → apex → dive → landfall → recover, six phases, each with
|
|
7
|
+
// its beats.
|
|
8
|
+
//
|
|
9
|
+
// Conventions (what weapons/stylizedWeapons.js provides):
|
|
10
|
+
// - actor origin at (0,0,0), facing −X; grip pose is relative to it
|
|
11
|
+
// - pose = { p: [x, y, z] grip position, r: [rx, ry, rz] euler 'XYZ' },
|
|
12
|
+
// weapon blade along local +Y
|
|
13
|
+
// - phase durations are seconds at weight 1; a weapon profile's `weight`
|
|
14
|
+
// multiplies time (dagger flicks, hammer commits) and impact power
|
|
15
|
+
//
|
|
16
|
+
// This module is pure math — no THREE — so verify-vfxgen.mjs exercises the
|
|
17
|
+
// full move set in Node. moves/moveController.js is the THREE-side driver.
|
|
18
|
+
|
|
19
|
+
const HALF_PI = Math.PI / 2;
|
|
20
|
+
const REST = { p: [0.35, 1.0, 0.25], r: [0, 0, -0.35] };
|
|
21
|
+
|
|
22
|
+
/** Grip on a circle around the actor, blade pointing outward, horizontal. */
|
|
23
|
+
function arcPose(theta, { height = 1.15, radius = 0.55 } = {}) {
|
|
24
|
+
return {
|
|
25
|
+
p: [Math.cos(theta) * radius, height, Math.sin(theta) * radius],
|
|
26
|
+
r: [0, -theta, -HALF_PI],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = (t, pose) => ({ t, ...pose });
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Grip pose for a PLANE swing: the blade rotates about a shoulder pivot in
|
|
34
|
+
* the actor's X/Y plane (a real cut crosses the body in a plane — orbiting
|
|
35
|
+
* the actor like a cylinder folds the trail into a chevron from most
|
|
36
|
+
* cameras). rz is the blade angle: 0 = straight up, +ve sweeps toward the
|
|
37
|
+
* actor's left/down.
|
|
38
|
+
*/
|
|
39
|
+
function swingPose(rz, { pivot = [0, 1.25, 0.1], reach = 0.35 } = {}) {
|
|
40
|
+
const dir = [-Math.sin(rz), Math.cos(rz), 0];
|
|
41
|
+
return {
|
|
42
|
+
p: [pivot[0] + dir[0] * reach, pivot[1] + dir[1] * reach, pivot[2] + dir[2] * reach],
|
|
43
|
+
r: [0, 0, rz],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MOVES = {
|
|
48
|
+
slash: {
|
|
49
|
+
id: 'slash',
|
|
50
|
+
label: 'Slash',
|
|
51
|
+
description: 'A wide plane cut across the body — the bread-and-butter swing; the trail draws one continuous ~200° crescent.',
|
|
52
|
+
phases: [
|
|
53
|
+
{
|
|
54
|
+
id: 'windup',
|
|
55
|
+
duration: 0.22,
|
|
56
|
+
keys: [key(0, REST), key(1, swingPose(-1.7))],
|
|
57
|
+
events: [],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'strike',
|
|
61
|
+
duration: 0.18,
|
|
62
|
+
// Four keys keep the interpolated arc round across the wide sweep.
|
|
63
|
+
keys: [
|
|
64
|
+
key(0, swingPose(-1.7)),
|
|
65
|
+
key(0.35, swingPose(-0.5)),
|
|
66
|
+
key(0.7, swingPose(0.9)),
|
|
67
|
+
key(1, swingPose(2.3)),
|
|
68
|
+
],
|
|
69
|
+
events: [
|
|
70
|
+
{ at: 0, do: 'trailStart' },
|
|
71
|
+
{ at: 0.6, do: 'impact', power: 1.0 },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'recover',
|
|
76
|
+
duration: 0.3,
|
|
77
|
+
keys: [key(0, swingPose(2.3)), key(1, REST)],
|
|
78
|
+
events: [{ at: 0.15, do: 'trailStop' }],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
overhead: {
|
|
84
|
+
id: 'overhead',
|
|
85
|
+
label: 'Overhead',
|
|
86
|
+
description: 'Raise over the shoulder, chop down through the front.',
|
|
87
|
+
phases: [
|
|
88
|
+
{
|
|
89
|
+
id: 'windup',
|
|
90
|
+
duration: 0.28,
|
|
91
|
+
keys: [key(0, REST), key(1, { p: [0.15, 1.62, 0.15], r: [0, 0, -0.5] })],
|
|
92
|
+
events: [],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'strike',
|
|
96
|
+
duration: 0.16,
|
|
97
|
+
keys: [
|
|
98
|
+
key(0, { p: [0.15, 1.62, 0.15], r: [0, 0, -0.5] }),
|
|
99
|
+
key(0.55, { p: [-0.3, 1.25, 0.12], r: [0, 0, HALF_PI] }),
|
|
100
|
+
key(1, { p: [-0.52, 0.72, 0.1], r: [0, 0, 2.62] }),
|
|
101
|
+
],
|
|
102
|
+
events: [
|
|
103
|
+
{ at: 0, do: 'trailStart' },
|
|
104
|
+
{ at: 0.8, do: 'impact', power: 1.25 },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'recover',
|
|
109
|
+
duration: 0.32,
|
|
110
|
+
keys: [key(0, { p: [-0.52, 0.72, 0.1], r: [0, 0, 2.62] }), key(1, REST)],
|
|
111
|
+
events: [{ at: 0.15, do: 'trailStop' }],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
thrust: {
|
|
117
|
+
id: 'thrust',
|
|
118
|
+
label: 'Thrust',
|
|
119
|
+
description: 'Coil back and lunge the point straight forward — spears live here.',
|
|
120
|
+
phases: [
|
|
121
|
+
{
|
|
122
|
+
id: 'windup',
|
|
123
|
+
duration: 0.18,
|
|
124
|
+
keys: [key(0, REST), key(1, { p: [0.5, 1.12, 0.12], r: [0, 0, HALF_PI] })],
|
|
125
|
+
events: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'strike',
|
|
129
|
+
duration: 0.12,
|
|
130
|
+
keys: [
|
|
131
|
+
key(0, { p: [0.5, 1.12, 0.12], r: [0, 0, HALF_PI] }),
|
|
132
|
+
key(1, { p: [-0.78, 1.12, 0.05], r: [0, 0, HALF_PI] }),
|
|
133
|
+
],
|
|
134
|
+
events: [
|
|
135
|
+
{ at: 0, do: 'trailStart' },
|
|
136
|
+
{ at: 0.85, do: 'impact', power: 0.9 },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'recover',
|
|
141
|
+
duration: 0.26,
|
|
142
|
+
keys: [key(0, { p: [-0.78, 1.12, 0.05], r: [0, 0, HALF_PI] }), key(1, REST)],
|
|
143
|
+
events: [{ at: 0.2, do: 'trailStop' }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
spin: {
|
|
149
|
+
id: 'spin',
|
|
150
|
+
label: 'Spin',
|
|
151
|
+
description: 'Full 360° sweep — the trail draws a complete circle.',
|
|
152
|
+
phases: [
|
|
153
|
+
{
|
|
154
|
+
id: 'windup',
|
|
155
|
+
duration: 0.2,
|
|
156
|
+
keys: [key(0, REST), key(1, arcPose(Math.PI + 0.3, { height: 1.18 }))],
|
|
157
|
+
events: [],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'strike',
|
|
161
|
+
duration: 0.5,
|
|
162
|
+
// Five keys around the circle keep the interpolated arc round.
|
|
163
|
+
keys: [0, 0.25, 0.5, 0.75, 1].map((t) =>
|
|
164
|
+
key(t, arcPose(Math.PI + 0.3 - t * Math.PI * 2, { height: 1.18, radius: 0.62 }))),
|
|
165
|
+
events: [
|
|
166
|
+
{ at: 0, do: 'trailStart' },
|
|
167
|
+
{ at: 0.45, do: 'impact', power: 0.8 },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'recover',
|
|
172
|
+
duration: 0.26,
|
|
173
|
+
keys: [key(0, arcPose(Math.PI + 0.3 - Math.PI * 2, { height: 1.18, radius: 0.62 })), key(1, REST)],
|
|
174
|
+
events: [{ at: 0.1, do: 'trailStop' }],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
plunge: {
|
|
180
|
+
id: 'plunge',
|
|
181
|
+
label: 'Plunge',
|
|
182
|
+
description: 'The Dragoon jump, decomposed: crouch → leap → apex → dive → landfall → recover.',
|
|
183
|
+
phases: [
|
|
184
|
+
{
|
|
185
|
+
id: 'crouch',
|
|
186
|
+
duration: 0.28,
|
|
187
|
+
keys: [key(0, REST), key(1, { p: [0.3, 0.68, 0.2], r: [0, 0, -0.6] })],
|
|
188
|
+
events: [{ at: 0.2, do: 'dust' }],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'leap',
|
|
192
|
+
duration: 0.4,
|
|
193
|
+
keys: [
|
|
194
|
+
key(0, { p: [0.3, 0.68, 0.2], r: [0, 0, -0.6] }),
|
|
195
|
+
key(1, { p: [0.15, 3.4, 0.12], r: [0, 0, -0.15] }),
|
|
196
|
+
],
|
|
197
|
+
events: [
|
|
198
|
+
{ at: 0.1, do: 'trailStart' },
|
|
199
|
+
{ at: 0.85, do: 'trailStop' },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'apex',
|
|
204
|
+
duration: 0.15,
|
|
205
|
+
// Hang beat: flip the blade to point straight down for the dive.
|
|
206
|
+
keys: [
|
|
207
|
+
key(0, { p: [0.15, 3.4, 0.12], r: [0, 0, -0.15] }),
|
|
208
|
+
key(1, { p: [-0.45, 3.28, 0.08], r: [0, 0, Math.PI] }),
|
|
209
|
+
],
|
|
210
|
+
events: [],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'dive',
|
|
214
|
+
duration: 0.22,
|
|
215
|
+
keys: [
|
|
216
|
+
key(0, { p: [-0.45, 3.28, 0.08], r: [0, 0, Math.PI] }),
|
|
217
|
+
key(1, { p: [-1.7, 0.45, 0], r: [0, 0, Math.PI] }),
|
|
218
|
+
],
|
|
219
|
+
events: [{ at: 0, do: 'trailStart' }],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'landfall',
|
|
223
|
+
duration: 0.3,
|
|
224
|
+
keys: [
|
|
225
|
+
key(0, { p: [-1.7, 0.45, 0], r: [0, 0, Math.PI] }),
|
|
226
|
+
key(1, { p: [-1.6, 0.55, 0.05], r: [0, 0, 2.6] }),
|
|
227
|
+
],
|
|
228
|
+
events: [
|
|
229
|
+
{ at: 0, do: 'impact', power: 2.2 },
|
|
230
|
+
{ at: 0, do: 'landing', power: 2.0 },
|
|
231
|
+
{ at: 0.12, do: 'dust' },
|
|
232
|
+
{ at: 0.15, do: 'trailStop' },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'recover',
|
|
237
|
+
duration: 0.35,
|
|
238
|
+
keys: [key(0, { p: [-1.6, 0.55, 0.05], r: [0, 0, 2.6] }), key(1, REST)],
|
|
239
|
+
events: [],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const MOVE_IDS = Object.freeze(Object.keys(MOVES));
|
|
246
|
+
|
|
247
|
+
export function getMove(id) {
|
|
248
|
+
return MOVES[id] ?? null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `{ id, label, description, phases: [ids] }` list for pickers/docs. */
|
|
252
|
+
export function getMoveOptions() {
|
|
253
|
+
return MOVE_IDS.map((id) => ({
|
|
254
|
+
description: MOVES[id].description,
|
|
255
|
+
id,
|
|
256
|
+
label: MOVES[id].label,
|
|
257
|
+
phases: MOVES[id].phases.map((phase) => phase.id),
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Total seconds of a move for a weapon weight (heavier = slower). */
|
|
262
|
+
export function moveDuration(move, weight = 1) {
|
|
263
|
+
return move.phases.reduce((sum, phase) => sum + phase.duration, 0) * Math.max(weight, 0.05);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function smooth(t) {
|
|
267
|
+
const u = Math.min(Math.max(t, 0), 1);
|
|
268
|
+
return u * u * (3 - 2 * u);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function lerp3(a, b, t) {
|
|
272
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function interpolateKeys(keys, t) {
|
|
276
|
+
if (t <= keys[0].t) return { p: [...keys[0].p], r: [...keys[0].r] };
|
|
277
|
+
for (let i = 1; i < keys.length; i += 1) {
|
|
278
|
+
if (t <= keys[i].t) {
|
|
279
|
+
const a = keys[i - 1];
|
|
280
|
+
const b = keys[i];
|
|
281
|
+
const u = smooth((t - a.t) / Math.max(b.t - a.t, 1e-6));
|
|
282
|
+
return { p: lerp3(a.p, b.p, u), r: lerp3(a.r, b.r, u) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const last = keys[keys.length - 1];
|
|
286
|
+
return { p: [...last.p], r: [...last.r] };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Grip pose at `time` seconds into a move for a weapon weight. Returns
|
|
291
|
+
* `{ p, r, phaseId, done }`; after the move ends it holds the final pose
|
|
292
|
+
* with `done: true` so controllers can settle and release.
|
|
293
|
+
*/
|
|
294
|
+
export function sampleMovePose(move, time, weight = 1) {
|
|
295
|
+
const scale = Math.max(weight, 0.05);
|
|
296
|
+
let cursor = 0;
|
|
297
|
+
for (const phase of move.phases) {
|
|
298
|
+
const span = phase.duration * scale;
|
|
299
|
+
if (time < cursor + span) {
|
|
300
|
+
const local = (time - cursor) / span;
|
|
301
|
+
return { ...interpolateKeys(phase.keys, local), done: false, phaseId: phase.id };
|
|
302
|
+
}
|
|
303
|
+
cursor += span;
|
|
304
|
+
}
|
|
305
|
+
const lastPhase = move.phases[move.phases.length - 1];
|
|
306
|
+
return { ...interpolateKeys(lastPhase.keys, 1), done: true, phaseId: lastPhase.id };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Events whose scaled absolute time falls in (t0, t1] — controllers call
|
|
311
|
+
* this once per frame with the previous/current clock so every beat fires
|
|
312
|
+
* exactly once regardless of frame rate. Impact power scales with weight
|
|
313
|
+
* (a hammer landing carries more than a dagger flick).
|
|
314
|
+
*/
|
|
315
|
+
export function collectMoveEvents(move, t0, t1, weight = 1) {
|
|
316
|
+
const scale = Math.max(weight, 0.05);
|
|
317
|
+
const fired = [];
|
|
318
|
+
let cursor = 0;
|
|
319
|
+
for (const phase of move.phases) {
|
|
320
|
+
const span = phase.duration * scale;
|
|
321
|
+
for (const event of phase.events) {
|
|
322
|
+
const at = cursor + event.at * span;
|
|
323
|
+
if (at > t0 && at <= t1) {
|
|
324
|
+
fired.push({
|
|
325
|
+
...event,
|
|
326
|
+
phaseId: phase.id,
|
|
327
|
+
power: event.power !== undefined ? event.power * (0.6 + 0.4 * scale) : undefined,
|
|
328
|
+
time: at,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
cursor += span;
|
|
333
|
+
}
|
|
334
|
+
return fired.sort((a, b) => a.time - b.time);
|
|
335
|
+
}
|