@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,98 @@
|
|
|
1
|
+
// Named gameplay-VFX presets: 'default' is the baseline; 'call_me_sensei' is
|
|
2
|
+
// the studio-managed signature look, curated and updated over releases.
|
|
3
|
+
// Community presets register alongside them via registerVfxPreset().
|
|
4
|
+
// Preset settings are nested partials over DEFAULT_VFX_SETTINGS.
|
|
5
|
+
|
|
6
|
+
function cleanObject(value) {
|
|
7
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const vfxPresetRegistry = new Map([
|
|
11
|
+
['default', Object.freeze({
|
|
12
|
+
description: 'Baseline gameplay VFX.',
|
|
13
|
+
label: 'Default',
|
|
14
|
+
settings: Object.freeze({}),
|
|
15
|
+
})],
|
|
16
|
+
['call_me_sensei', Object.freeze({
|
|
17
|
+
description: 'Studio-managed signature combat feel, curated by Call Me Sensei and updated over releases. Tuned to the reference-class action-RPG (Genshin-style) hit language: smooth luminous sword arcs with a white leading edge, four-point star glints with a fast shockwave circle on every hit, and a hard-saturated two-tone pyro fireball.',
|
|
18
|
+
label: 'Call Me Sensei',
|
|
19
|
+
settings: Object.freeze({
|
|
20
|
+
// Smooth, luminous arc — many fade bands read as the reference games'
|
|
21
|
+
// gradient trails; the crisp cel look stays available in 'default'.
|
|
22
|
+
// Physical-attack gold with the white leading band — the reference
|
|
23
|
+
// trail. Element variants are one `look.color` swap at spawn.
|
|
24
|
+
slash: Object.freeze({
|
|
25
|
+
bands: 6,
|
|
26
|
+
color: Object.freeze([1.0, 0.78, 0.2]),
|
|
27
|
+
coreColor: Object.freeze([1.0, 1.0, 1.0]),
|
|
28
|
+
intensity: 1.05,
|
|
29
|
+
lifetime: 0.45,
|
|
30
|
+
sparkle: 90,
|
|
31
|
+
}),
|
|
32
|
+
// Four-point glint + shockwave circle: the signature hit read.
|
|
33
|
+
impact: Object.freeze({
|
|
34
|
+
flashColor: Object.freeze([1.0, 0.98, 0.9]),
|
|
35
|
+
flashSize: 1.2,
|
|
36
|
+
intensity: 1.15,
|
|
37
|
+
shockwave: true,
|
|
38
|
+
sparkColor: Object.freeze([1.0, 0.85, 0.35]),
|
|
39
|
+
sparkCount: 34,
|
|
40
|
+
spikes: 4,
|
|
41
|
+
}),
|
|
42
|
+
// Pyro: near-white core, deep saturated orange skin, dense embers.
|
|
43
|
+
fireball: Object.freeze({
|
|
44
|
+
coreColor: Object.freeze([1.0, 0.97, 0.72]),
|
|
45
|
+
emberRate: 150,
|
|
46
|
+
explosionPower: 2.0,
|
|
47
|
+
flameColor: Object.freeze([1.0, 0.34, 0.08]),
|
|
48
|
+
intensity: 1.45,
|
|
49
|
+
ringColor: Object.freeze([1.0, 0.5, 0.15]),
|
|
50
|
+
}),
|
|
51
|
+
// Ground feedback stays subtle and cool so the glows own the frame.
|
|
52
|
+
footstep: Object.freeze({
|
|
53
|
+
color: Object.freeze([0.72, 0.7, 0.66]),
|
|
54
|
+
puffCount: 6,
|
|
55
|
+
}),
|
|
56
|
+
landing: Object.freeze({
|
|
57
|
+
color: Object.freeze([0.72, 0.7, 0.66]),
|
|
58
|
+
puffCount: 16,
|
|
59
|
+
ringRadius: 1.3,
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
62
|
+
})],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registers a named VFX preset so it resolves in
|
|
67
|
+
* `createVfxSystem({ preset })` exactly like the built-ins. Accepts
|
|
68
|
+
* `{ label?, description?, settings? }` or flat (nested) settings.
|
|
69
|
+
*/
|
|
70
|
+
export function registerVfxPreset(name, preset = {}, { overwrite = false } = {}) {
|
|
71
|
+
const id = String(name ?? '').trim();
|
|
72
|
+
if (!id) throw new Error('VFX preset name is required.');
|
|
73
|
+
if (!overwrite && vfxPresetRegistry.has(id)) {
|
|
74
|
+
throw new Error(`VFX preset "${id}" already exists.`);
|
|
75
|
+
}
|
|
76
|
+
const { label, description, settings, ...flat } = cleanObject(preset);
|
|
77
|
+
const entry = Object.freeze({
|
|
78
|
+
description: typeof description === 'string' ? description : '',
|
|
79
|
+
label: typeof label === 'string' && label ? label : id,
|
|
80
|
+
settings: Object.freeze({ ...cleanObject(settings ?? flat) }),
|
|
81
|
+
});
|
|
82
|
+
vfxPresetRegistry.set(id, entry);
|
|
83
|
+
return { description: entry.description, id, label: entry.label };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Lists registered presets as `{ id, label, description }` (for HUDs). */
|
|
87
|
+
export function getVfxPresetOptions() {
|
|
88
|
+
return Array.from(vfxPresetRegistry.entries()).map(([id, preset]) => ({
|
|
89
|
+
description: preset.description,
|
|
90
|
+
id,
|
|
91
|
+
label: preset.label,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Nested settings partial for a preset name; `{}` for unknown names. */
|
|
96
|
+
export function resolveVfxPreset(name) {
|
|
97
|
+
return vfxPresetRegistry.get(name)?.settings ?? {};
|
|
98
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// Canonical settings and field metadata for the gameplay-VFX cluster
|
|
2
|
+
// (weapon: slash trails + impacts; magic: fireballs; movement: footstep and
|
|
3
|
+
// landing dust). Follows the library-wide convention: DEFAULT_VFX_SETTINGS
|
|
4
|
+
// holds grouped values, VFX_SETTING_GROUPS + VFX_SETTING_FIELD_SCHEMA drive
|
|
5
|
+
// the debug panel and the generated settings reference.
|
|
6
|
+
//
|
|
7
|
+
// Unlike ambientfx (a steady windowed atmosphere), every effect here is
|
|
8
|
+
// EVENT-DRIVEN: the host spawns it at a gameplay moment (a swing, a hit, a
|
|
9
|
+
// cast, a footfall) and it runs its lifetime out. Settings hold the LOOK of
|
|
10
|
+
// each effect; per-spawn options (position, direction, power, follow target)
|
|
11
|
+
// are runtime arguments to createVfxSystem().spawn — see vfxSystem.js.
|
|
12
|
+
|
|
13
|
+
/** Effect ids in backbone kind order where applicable. */
|
|
14
|
+
export const VFX_EFFECT_IDS = Object.freeze(['slash', 'impact', 'fireball', 'footstep', 'landing']);
|
|
15
|
+
|
|
16
|
+
/** Gameplay categories — the organizing axis for docs, panels, and presets. */
|
|
17
|
+
export const VFX_CATEGORIES = Object.freeze({
|
|
18
|
+
magic: Object.freeze(['fireball']),
|
|
19
|
+
movement: Object.freeze(['footstep', 'landing']),
|
|
20
|
+
weapon: Object.freeze(['slash', 'impact']),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** Category of an effect id ('weapon' | 'magic' | 'movement'). */
|
|
24
|
+
export function vfxCategoryOf(effectId) {
|
|
25
|
+
for (const [category, ids] of Object.entries(VFX_CATEGORIES)) {
|
|
26
|
+
if (ids.includes(effectId)) return category;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default gameplay-VFX settings. Budgets: the one-shot backbone renders in
|
|
33
|
+
* two draw calls (glow + puff) regardless of how many effects are live;
|
|
34
|
+
* maxParticles bounds the ring buffers, maxTrails/maxProjectiles bound the
|
|
35
|
+
* pooled ribbon and projectile meshes (construction-only).
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_VFX_SETTINGS = Object.freeze({
|
|
38
|
+
shared: Object.freeze({
|
|
39
|
+
maxParticles: 4096,
|
|
40
|
+
maxProjectiles: 8,
|
|
41
|
+
maxTrails: 8,
|
|
42
|
+
timeScale: 1.0,
|
|
43
|
+
}),
|
|
44
|
+
// --- weapon ---------------------------------------------------------------
|
|
45
|
+
slash: Object.freeze({
|
|
46
|
+
enabled: true,
|
|
47
|
+
color: Object.freeze([0.55, 0.8, 1.0]),
|
|
48
|
+
coreColor: Object.freeze([1.0, 1.0, 1.0]),
|
|
49
|
+
lifetime: 0.28,
|
|
50
|
+
bands: 3,
|
|
51
|
+
intensity: 1.0,
|
|
52
|
+
sparkle: 60,
|
|
53
|
+
segments: 96,
|
|
54
|
+
}),
|
|
55
|
+
impact: Object.freeze({
|
|
56
|
+
enabled: true,
|
|
57
|
+
sparkColor: Object.freeze([1.0, 0.85, 0.45]),
|
|
58
|
+
flashColor: Object.freeze([1.0, 0.97, 0.88]),
|
|
59
|
+
sparkCount: 26,
|
|
60
|
+
sparkSpeed: 7.0,
|
|
61
|
+
gravity: 18,
|
|
62
|
+
flashSize: 0.9,
|
|
63
|
+
spikes: 6,
|
|
64
|
+
shockwave: true,
|
|
65
|
+
lifetime: 0.5,
|
|
66
|
+
intensity: 1.0,
|
|
67
|
+
}),
|
|
68
|
+
// --- magic ------------------------------------------------------------------
|
|
69
|
+
fireball: Object.freeze({
|
|
70
|
+
enabled: true,
|
|
71
|
+
coreSize: 0.42,
|
|
72
|
+
coreColor: Object.freeze([1.0, 0.95, 0.6]),
|
|
73
|
+
flameColor: Object.freeze([1.0, 0.45, 0.12]),
|
|
74
|
+
emberRate: 90,
|
|
75
|
+
emberSize: Object.freeze([0.05, 0.12]),
|
|
76
|
+
emberLifetime: 0.55,
|
|
77
|
+
intensity: 1.2,
|
|
78
|
+
explosionPower: 1.6,
|
|
79
|
+
scorchRing: true,
|
|
80
|
+
ringColor: Object.freeze([1.0, 0.55, 0.2]),
|
|
81
|
+
}),
|
|
82
|
+
// --- movement ---------------------------------------------------------------
|
|
83
|
+
footstep: Object.freeze({
|
|
84
|
+
enabled: true,
|
|
85
|
+
puffCount: 5,
|
|
86
|
+
sizeRange: Object.freeze([0.1, 0.22]),
|
|
87
|
+
color: Object.freeze([0.78, 0.72, 0.62]),
|
|
88
|
+
lifetime: 0.55,
|
|
89
|
+
rise: 0.5,
|
|
90
|
+
spread: 0.22,
|
|
91
|
+
}),
|
|
92
|
+
landing: Object.freeze({
|
|
93
|
+
enabled: true,
|
|
94
|
+
puffCount: 14,
|
|
95
|
+
ringRadius: 1.1,
|
|
96
|
+
sizeRange: Object.freeze([0.18, 0.38]),
|
|
97
|
+
color: Object.freeze([0.78, 0.72, 0.62]),
|
|
98
|
+
lifetime: 0.7,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const VFX_SETTING_GROUPS = Object.freeze([
|
|
103
|
+
Object.freeze({
|
|
104
|
+
id: 'shared',
|
|
105
|
+
label: 'Shared',
|
|
106
|
+
description: 'Budgets and global pacing for every effect. The one-shot backbone renders all bursts in two draw calls; these bound its ring buffers and the pooled trail/projectile meshes.',
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
id: 'slash',
|
|
110
|
+
label: 'Slash Trail',
|
|
111
|
+
category: 'weapon',
|
|
112
|
+
description: 'Weapon-swing ribbon sampled from a followed blade (base + tip anchors), with a stepped toon fade and edge sparkle. The anime arc smear.',
|
|
113
|
+
}),
|
|
114
|
+
Object.freeze({
|
|
115
|
+
id: 'impact',
|
|
116
|
+
label: 'Impact Burst',
|
|
117
|
+
category: 'weapon',
|
|
118
|
+
description: 'Hit feedback: a radial star flash plus ballistic sparks with gravity. `power` at spawn scales count, speed, and flash size.',
|
|
119
|
+
}),
|
|
120
|
+
Object.freeze({
|
|
121
|
+
id: 'fireball',
|
|
122
|
+
label: 'Fireball',
|
|
123
|
+
category: 'magic',
|
|
124
|
+
description: 'Projectile: a flame-shaded core billboard shedding embers in flight; explodes into an impact burst, smoke puffs, and an expanding scorch ring.',
|
|
125
|
+
}),
|
|
126
|
+
Object.freeze({
|
|
127
|
+
id: 'footstep',
|
|
128
|
+
label: 'Footstep Dust',
|
|
129
|
+
category: 'movement',
|
|
130
|
+
description: 'Small chunky dust puffs kicked up at a footfall. Cheap enough to fire every step.',
|
|
131
|
+
}),
|
|
132
|
+
Object.freeze({
|
|
133
|
+
id: 'landing',
|
|
134
|
+
label: 'Landing Ring',
|
|
135
|
+
category: 'movement',
|
|
136
|
+
description: 'The classic landing hit: a radial ring of dust puffs expanding outward from the touch-down point. `power` at spawn scales radius and count.',
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const enabledField = { label: 'Enabled', description: 'Master toggle for the effect.', type: 'boolean' };
|
|
141
|
+
const intensityField = {
|
|
142
|
+
label: 'Intensity',
|
|
143
|
+
description: 'Emissive brightness multiplier on the glow parts.',
|
|
144
|
+
range: { min: 0, max: 4, step: 0.05 },
|
|
145
|
+
};
|
|
146
|
+
const lifetimeField = (max, caption) => ({
|
|
147
|
+
label: 'Lifetime',
|
|
148
|
+
description: caption,
|
|
149
|
+
range: { min: 0.05, max, step: 0.01 },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const FIELD_DEFINITIONS = Object.freeze({
|
|
153
|
+
shared: Object.freeze({
|
|
154
|
+
maxParticles: {
|
|
155
|
+
label: 'Max Particles',
|
|
156
|
+
description: 'Ring-buffer capacity of the one-shot backbone (sparks, embers, puffs, rings, flashes). Oldest instances are overwritten first. Construction-only.',
|
|
157
|
+
range: { min: 256, max: 32768, step: 256 },
|
|
158
|
+
},
|
|
159
|
+
maxProjectiles: {
|
|
160
|
+
label: 'Max Projectiles',
|
|
161
|
+
description: 'Pooled projectile core meshes (fireballs in flight). Spawns beyond this reuse the oldest. Construction-only.',
|
|
162
|
+
range: { min: 1, max: 32, step: 1 },
|
|
163
|
+
},
|
|
164
|
+
maxTrails: {
|
|
165
|
+
label: 'Max Trails',
|
|
166
|
+
description: 'Pooled slash-trail ribbons live at once. Spawns beyond this reuse the oldest. Construction-only.',
|
|
167
|
+
range: { min: 1, max: 32, step: 1 },
|
|
168
|
+
},
|
|
169
|
+
timeScale: {
|
|
170
|
+
label: 'Time Scale',
|
|
171
|
+
description: 'Global VFX clock multiplier — hit-stop and slow-motion hooks feed this.',
|
|
172
|
+
range: { min: 0, max: 2, step: 0.01 },
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
slash: Object.freeze({
|
|
176
|
+
enabled: enabledField,
|
|
177
|
+
color: { label: 'Body Color', description: 'The solid body of the arc — the flat saturated fill.', type: 'color' },
|
|
178
|
+
coreColor: { label: 'Edge Band', description: 'Leading-edge band color along the blade-tip side; white body+edge banding is the reference action-RPG read.', type: 'color' },
|
|
179
|
+
lifetime: lifetimeField(1.5, 'Seconds a ribbon segment persists before the tail erodes over it.'),
|
|
180
|
+
bands: {
|
|
181
|
+
label: 'Erosion Bands',
|
|
182
|
+
description: 'Cel quantization of the tail erosion sweep — fewer bands, chunkier stepped tail.',
|
|
183
|
+
range: { min: 1, max: 8, step: 1 },
|
|
184
|
+
},
|
|
185
|
+
intensity: intensityField,
|
|
186
|
+
sparkle: {
|
|
187
|
+
label: 'Sparkle Rate',
|
|
188
|
+
description: 'Sparks per second shed from the blade tip while the trail is active.',
|
|
189
|
+
range: { min: 0, max: 300, step: 5 },
|
|
190
|
+
},
|
|
191
|
+
segments: {
|
|
192
|
+
label: 'Segments',
|
|
193
|
+
description: 'Ribbon history capacity in spline points — longer fast swings need more. Construction-only.',
|
|
194
|
+
range: { min: 8, max: 256, step: 4 },
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
impact: Object.freeze({
|
|
198
|
+
enabled: enabledField,
|
|
199
|
+
sparkColor: { label: 'Spark Color', description: 'Ballistic spark color (additive).', type: 'color' },
|
|
200
|
+
flashColor: { label: 'Flash Color', description: 'Radial star-flash color at the hit point.', type: 'color' },
|
|
201
|
+
sparkCount: {
|
|
202
|
+
label: 'Spark Count',
|
|
203
|
+
description: 'Sparks per burst at power 1; spawn `power` scales this.',
|
|
204
|
+
range: { min: 0, max: 120, step: 1 },
|
|
205
|
+
},
|
|
206
|
+
sparkSpeed: {
|
|
207
|
+
label: 'Spark Speed',
|
|
208
|
+
description: 'Initial spark speed in m/s, biased along the hit normal.',
|
|
209
|
+
range: { min: 0, max: 30, step: 0.5 },
|
|
210
|
+
},
|
|
211
|
+
gravity: {
|
|
212
|
+
label: 'Gravity',
|
|
213
|
+
description: 'Downward pull on sparks in m/s² — high values read as metal chips.',
|
|
214
|
+
range: { min: 0, max: 60, step: 1 },
|
|
215
|
+
},
|
|
216
|
+
flashSize: {
|
|
217
|
+
label: 'Flash Size',
|
|
218
|
+
description: 'Star-flash quad size in meters at power 1.',
|
|
219
|
+
range: { min: 0, max: 4, step: 0.05 },
|
|
220
|
+
},
|
|
221
|
+
spikes: {
|
|
222
|
+
label: 'Flash Spikes',
|
|
223
|
+
description: 'Point count of the star flash — 4 reads as an action-RPG glint, 6–8 as an anime hit star.',
|
|
224
|
+
range: { min: 3, max: 12, step: 1 },
|
|
225
|
+
},
|
|
226
|
+
shockwave: {
|
|
227
|
+
label: 'Shockwave Ring',
|
|
228
|
+
description: 'Camera-facing expanding ring at the hit point — the action-RPG hit circle. Tinted by Flash Color.',
|
|
229
|
+
type: 'boolean',
|
|
230
|
+
},
|
|
231
|
+
lifetime: lifetimeField(2, 'Seconds sparks live (the flash pops in about a quarter of this).'),
|
|
232
|
+
intensity: intensityField,
|
|
233
|
+
}),
|
|
234
|
+
fireball: Object.freeze({
|
|
235
|
+
enabled: enabledField,
|
|
236
|
+
coreSize: {
|
|
237
|
+
label: 'Core Size',
|
|
238
|
+
description: 'Flame-core billboard radius in meters.',
|
|
239
|
+
range: { min: 0.05, max: 2, step: 0.01 },
|
|
240
|
+
},
|
|
241
|
+
coreColor: { label: 'Core Color', description: 'Hot center of the flame shader.', type: 'color' },
|
|
242
|
+
flameColor: { label: 'Flame Color', description: 'Outer flame licks and ember tint.', type: 'color' },
|
|
243
|
+
emberRate: {
|
|
244
|
+
label: 'Ember Rate',
|
|
245
|
+
description: 'Embers shed per second while the projectile flies.',
|
|
246
|
+
range: { min: 0, max: 400, step: 5 },
|
|
247
|
+
},
|
|
248
|
+
emberSize: { label: 'Ember Size', description: 'Min/max ember size in meters.', type: 'vector2' },
|
|
249
|
+
emberLifetime: lifetimeField(2, 'Seconds each shed ember lives.'),
|
|
250
|
+
intensity: intensityField,
|
|
251
|
+
explosionPower: {
|
|
252
|
+
label: 'Explosion Power',
|
|
253
|
+
description: '`power` handed to the impact burst + smoke on detonation.',
|
|
254
|
+
range: { min: 0, max: 5, step: 0.05 },
|
|
255
|
+
},
|
|
256
|
+
scorchRing: { label: 'Scorch Ring', description: 'Expanding ground ring on detonation.', type: 'boolean' },
|
|
257
|
+
ringColor: { label: 'Ring Color', description: 'Scorch-ring glow color.', type: 'color' },
|
|
258
|
+
}),
|
|
259
|
+
footstep: Object.freeze({
|
|
260
|
+
enabled: enabledField,
|
|
261
|
+
puffCount: {
|
|
262
|
+
label: 'Puff Count',
|
|
263
|
+
description: 'Dust puffs per footfall.',
|
|
264
|
+
range: { min: 0, max: 20, step: 1 },
|
|
265
|
+
},
|
|
266
|
+
sizeRange: { label: 'Size Range', description: 'Min/max puff size in meters (puffs grow ~2× over life).', type: 'vector2' },
|
|
267
|
+
color: { label: 'Color', description: 'Dust color — sample the ground palette.', type: 'color' },
|
|
268
|
+
lifetime: lifetimeField(2, 'Seconds a puff lives.'),
|
|
269
|
+
rise: {
|
|
270
|
+
label: 'Rise',
|
|
271
|
+
description: 'Upward drift in m/s — heavier dust settles faster.',
|
|
272
|
+
range: { min: 0, max: 2, step: 0.05 },
|
|
273
|
+
},
|
|
274
|
+
spread: {
|
|
275
|
+
label: 'Spread',
|
|
276
|
+
description: 'Horizontal scatter radius in meters around the footfall.',
|
|
277
|
+
range: { min: 0, max: 1, step: 0.01 },
|
|
278
|
+
},
|
|
279
|
+
}),
|
|
280
|
+
landing: Object.freeze({
|
|
281
|
+
enabled: enabledField,
|
|
282
|
+
puffCount: {
|
|
283
|
+
label: 'Puff Count',
|
|
284
|
+
description: 'Puffs around the ring at power 1; spawn `power` scales this.',
|
|
285
|
+
range: { min: 0, max: 40, step: 1 },
|
|
286
|
+
},
|
|
287
|
+
ringRadius: {
|
|
288
|
+
label: 'Ring Radius',
|
|
289
|
+
description: 'Meters the dust ring expands to at power 1.',
|
|
290
|
+
range: { min: 0.2, max: 5, step: 0.05 },
|
|
291
|
+
},
|
|
292
|
+
sizeRange: { label: 'Size Range', description: 'Min/max puff size in meters.', type: 'vector2' },
|
|
293
|
+
color: { label: 'Color', description: 'Dust color — sample the ground palette.', type: 'color' },
|
|
294
|
+
lifetime: lifetimeField(2, 'Seconds the ring takes to expand and fade.'),
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
function createVfxFieldMetadata(group, key, field) {
|
|
299
|
+
const defaultValue = DEFAULT_VFX_SETTINGS[group.id][key];
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
defaultValue: Array.isArray(defaultValue) ? [...defaultValue] : defaultValue,
|
|
302
|
+
description: field.description,
|
|
303
|
+
group: group.id,
|
|
304
|
+
id: `${group.id}.${key}`,
|
|
305
|
+
key,
|
|
306
|
+
label: field.label,
|
|
307
|
+
optionLabels: field.optionLabels ?? null,
|
|
308
|
+
options: field.options ?? null,
|
|
309
|
+
range: field.range ?? null,
|
|
310
|
+
serializable: true,
|
|
311
|
+
type: field.type ?? (typeof defaultValue === 'boolean' ? 'boolean'
|
|
312
|
+
: typeof defaultValue === 'number' ? 'number' : 'text'),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const VFX_SETTING_FIELD_SCHEMA = Object.freeze(
|
|
317
|
+
Object.fromEntries(
|
|
318
|
+
VFX_SETTING_GROUPS.map((group) => [
|
|
319
|
+
group.id,
|
|
320
|
+
Object.freeze(
|
|
321
|
+
Object.fromEntries(
|
|
322
|
+
Object.entries(FIELD_DEFINITIONS[group.id] ?? {})
|
|
323
|
+
.map(([key, field]) => [key, createVfxFieldMetadata(group, key, field)]),
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
]),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
function cloneValue(value) {
|
|
331
|
+
if (Array.isArray(value)) return value.map(cloneValue);
|
|
332
|
+
if (value && typeof value === 'object') return Object.fromEntries(
|
|
333
|
+
Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]),
|
|
334
|
+
);
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function cloneVfxSettings(settings = DEFAULT_VFX_SETTINGS) {
|
|
339
|
+
return cloneValue(settings);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function clampNumber(value, fallback, min = -Infinity, max = Infinity) {
|
|
343
|
+
const number = Number(value);
|
|
344
|
+
if (!Number.isFinite(number)) return fallback;
|
|
345
|
+
return Math.min(max, Math.max(min, number));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Normalizes nested partial overrides over the defaults; unknown keys are
|
|
350
|
+
* dropped, malformed values fall back. Presets resolve in
|
|
351
|
+
* `createVfxSystem({ preset })` (see vfxPresets.js) — this function is the
|
|
352
|
+
* plain merge/validate step.
|
|
353
|
+
*/
|
|
354
|
+
export function createVfxSettings(overrides = {}) {
|
|
355
|
+
const result = cloneVfxSettings(DEFAULT_VFX_SETTINGS);
|
|
356
|
+
for (const groupId of Object.keys(result)) {
|
|
357
|
+
const group = overrides?.[groupId];
|
|
358
|
+
if (!group || typeof group !== 'object') continue;
|
|
359
|
+
for (const key of Object.keys(result[groupId])) {
|
|
360
|
+
if (group[key] === undefined) continue;
|
|
361
|
+
result[groupId][key] = cloneValue(group[key]);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const shared = result.shared;
|
|
365
|
+
shared.maxParticles = Math.round(clampNumber(shared.maxParticles, 4096, 64, 131072));
|
|
366
|
+
shared.maxProjectiles = Math.round(clampNumber(shared.maxProjectiles, 8, 1, 64));
|
|
367
|
+
shared.maxTrails = Math.round(clampNumber(shared.maxTrails, 8, 1, 64));
|
|
368
|
+
shared.timeScale = clampNumber(shared.timeScale, 1, 0, 8);
|
|
369
|
+
for (const id of VFX_EFFECT_IDS) {
|
|
370
|
+
const effect = result[id];
|
|
371
|
+
effect.enabled = Boolean(effect.enabled);
|
|
372
|
+
if ('lifetime' in effect) effect.lifetime = clampNumber(effect.lifetime, DEFAULT_VFX_SETTINGS[id].lifetime, 0.02, 10);
|
|
373
|
+
if ('intensity' in effect) effect.intensity = clampNumber(effect.intensity, 1, 0);
|
|
374
|
+
}
|
|
375
|
+
result.slash.segments = Math.round(clampNumber(result.slash.segments, 96, 4, 256));
|
|
376
|
+
result.slash.bands = Math.round(clampNumber(result.slash.bands, 3, 1, 12));
|
|
377
|
+
result.impact.spikes = Math.round(clampNumber(result.impact.spikes, 6, 3, 16));
|
|
378
|
+
result.impact.sparkCount = Math.round(clampNumber(result.impact.sparkCount, 26, 0, 500));
|
|
379
|
+
result.fireball.emberRate = clampNumber(result.fireball.emberRate, 90, 0, 2000);
|
|
380
|
+
result.fireball.emberLifetime = clampNumber(result.fireball.emberLifetime, 0.55, 0.05, 5);
|
|
381
|
+
result.footstep.puffCount = Math.round(clampNumber(result.footstep.puffCount, 5, 0, 100));
|
|
382
|
+
result.landing.puffCount = Math.round(clampNumber(result.landing.puffCount, 14, 0, 200));
|
|
383
|
+
return result;
|
|
384
|
+
}
|