Six abilities to one hundred, plus a program-cache bug in the shader patcher - #3
Open
majidmanzarpour wants to merge 9 commits into
Open
Six abilities to one hundred, plus a program-cache bug in the shader patcher#3majidmanzarpour wants to merge 9 commits into
majidmanzarpour wants to merge 9 commits into
Conversation
…s gate The sandbox enumerated its six abilities in eight places. That does not scale to a hundred, so abilities become data and every consumer derives from it. - registry.js is the single declaration site. ELEMENTS/ELEMENT_META are now derived views over it, so AimController, HUD and App were untouched. - Ability classes load lazily; settings modules stay eager, because the editor, the presets and DEFAULT_SETTINGS all need the whole tree at boot. - Each ability owns src/config/abilities/<id>.js: its settings block and its editor schema. The six that shipped were moved verbatim and machine-verified byte-identical against HEAD. - The editor is schema-driven and builds an ability's controls on first open. Eager construction at 100 abilities cost 597ms and 107k DOM nodes at boot; it is now 7.7ms and 1.4k, with search indexing the schemas rather than the controllers so unbuilt controls are still findable. - Eight rebindable loadout slots over a Tab-key spellbook, persisted to localStorage. Fifty abilities do not fit on a keyboard. - scripts/check.mjs is the gate: it statically cross-checks every settings reference against its block, simulates every cast headlessly, and runs a pause test that mutates sliders on a zero-length frame and fails if nothing observable changes. That invariant is the whole character of this project, so it is now enforced rather than trusted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… pass on An ability should be beats and configuration, not a new renderer. These generalise the techniques the original six invented — the bolt's ribbon strip, the beam's three-layer tube, the crystal field, the raymarched wake — and add the ones the new schools need. GrowthField, FilamentPaths, VolumeHull, GroundField, Tube, Shell, Projectile, Swarm, ArcNetwork, ShatterField, Distortion, Portal, LiquidSurface, Curtain, Caustics, LightShaft, LensFlare, Mirror, HardSurface, BrushStroke, InkDiffusion, FoldMesh, Dissolve, TimeControl, Colony, SceneHooks. Two are worth calling out: - Distortion. The README listed "the distortion pass runs with nothing writing to it" as a known rough edge, costing a half-res clear per frame for nothing. It now has five emitter types, and Layers.js counts its writers so the pass skips itself entirely when none are visible — turning it on would be a poor trade if it then ran unconditionally. - SceneHooks. The first thing here that reaches outside its own group: the key light, the grade, the ground material, gravity. Four casts can be live at once and any of them can be destroyed mid-effect, so it is built on a borrow/restore discipline with tokens rather than direct mutation, and the world is restored exactly. Caustics is not the usual difference-of-worley recipe. That gives blobs with soft shoulders because worley is smooth except at its cell walls, and no exponent fixes it; this draws the fold of the refraction map, so the singular set is a curve and the filaments come out thin without being asked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Frost, Flame, Storm, Stone, Verdant, Void, Arcane, Blood, Aether, plus six new schools chosen because each forces a rendering technique the sandbox did not have: Tide (caustics), Forge (hard surface — involute gear teeth, brushed anisotropic specular, a real blackbody cooling locus), Lumen (light that behaves like light), Ink (the anti-glow school — matte, no bloom anywhere), Chrono (time as a driver), Hive (emergence). Every one has a named trick and none is a recolour of another slot. A few that show the range: - Dawnbreak borrows the scene's directional light and swings it, so every object in the world throws a real sweeping shadow. The ability draws almost nothing; the world doing the work is the effect. - Stasis Field draws nothing at all. It clamps other systems' clocks inside a radius, so a Storm Lance already in flight stops guttering. - Thunderclap's entire effect is the silence between the flash and the pressure front. - Silence renders a hole in the frame: not black, absent. - Spellbreak is the one ability aware of the others. - Mirage is the caster's own skinned rig, cloned and posed from a recorded track, drawn with nothing but refraction — you see it because the world bends into a human shape, and you lose it the moment it stops moving. All 100 pass the headless gate, including the paused-slider test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three presented identically — some abilities black out the screen, but only sometimes — and had nothing in common. 1. Program-cache collisions. patchOnBeforeCompile wraps every injection in an identically-worded closure, and three keys its program cache on onBeforeCompile.toString() by default. The injected code lives in the closed-over function, which never appears in that string, so ~60 patched materials shared one cache key and every one after the first silently rendered with another material's compiled program — its uniforms absent, the ones present holding someone else's values. Which ability broke depended on cast order. Fixed by deriving customProgramCacheKey from the injection's own source, composed down the chain; materials that genuinely share an injection still share a program. 2. pow() of a negative base. GroundField's SCOUR phase includes uTime, which grows without bound, so cos() was being asked for the cosine of ~10^4 radians; float32 loses its mantissa to argument reduction and returns fractionally outside [-1,1], and pow() of the resulting tiny negative is NaN at every exponent — which is why zeroing the sharpness slider never masked it. Folded the phase with fract() (exact: the cosine has period 1 there) and clamped the base. 3. The same class in six more places, pow(sin(x), e), where float32 sin() returns about -1e-7 at its zeroes. Two of them were in SnareMaterial and predate this expansion. One NaN fragment is not one bad pixel: the next bloom blur spreads it over its whole kernel and tone-mapping NaN gives black, so eighty-odd bad fragments on one spear turned the entire frame black. check.mjs now fails on pow() over a raw trig base, negative-controlled by reintroducing the bug. Also: post.distortion shipped at 0.045, a trim from when nothing wrote to the refraction buffer. Six abilities author their strength "in screen widths at post.distortion = 1", so every refraction effect was running at 4.5% of what it asked for. Set to 1.0, and the editor slider widened past its own default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…specs The README described six abilities, 938 sliders, and a five-step "adding an ability" list whose every step named a file that no longer holds a roster. All four are now true again, including the slider count (~16,000) and the fact that registering an ability is one descriptor rather than four edits. docs/ROSTER.md and docs/ROSTER-II.md are the briefs the ninety-four were built from — each entry names the single technique that makes it read, because an ability that is another slot with a different palette should not exist. docs/EXPANSION.md is the engineering contract: the invariants, the registry, the schema format, and what "done" means. docs/VFX_API.md and VFX_API_II.md are the tech library's signatures, verified against source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aims - The repo is MIT, © mohamedachrefelouafi, and the README did not say so — it said "code is provided as-is", which is both wrong and uncredited. It now names the licence, links LICENSE, and says plainly whose work this is built on. The heading loses its British spelling because it is now naming the MIT License, which is a proper noun. - The editor section still listed the old flat folder list. It is fifteen school folders now, built on first open, with a search box that indexes the schemas so it can find controls in folders that have never been opened. - The performance notes did not mention that abilities load lazily, which changes when shaders compile: compileAsync at boot only sees what is in the scene at boot, so selection warms an ability rather than the cast. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Your sandbox had six abilities. This takes it to one hundred, across fifteen schools, without changing the rules it was built under.
I started from your code because the discipline in it is unusual: no dimensions on the CPU, nothing is a texture, one technique per ability. Everything here obeys those rules, and there is now a headless test that enforces the first one mechanically instead of trusting it.
This is a big PR, and you should feel free to take part of it
318 files. I do not expect you to merge it as one lump, so the commits are ordered so each layer stands on its own and you can stop anywhere:
feat(core)feat(vfx)feat(abilities)fixdocsIf only commit 4 is interesting to you, say so and I will open it as its own small PR against a clean branch and close this one. Genuinely no hard feelings.
The bug that was already there
Three abilities were blacking out the entire screen. Chasing it turned up something systemic in the original code:
patchOnBeforeCompilewraps every shader injection in an identically worded closure. three.js keys its program cache ononBeforeCompile.toString()by default, and the injected code lives in the closed-over function, so it never appears in that string. The result is that every patched material in the project shares one cache key, and each one after the first silently renders with a different material's compiled program: its own uniforms absent, the ones present holding another shader's values. Which ability broke depended on which was cast first, which is why it looked intermittent.Fixed by deriving
customProgramCacheKeyfrom the injection's own source, composed down the chain. Materials that genuinely share an injection still share a program, so the cache keeps working.The other two were
pow()on a negative base. One wascos()of about 10,000 radians losing its mantissa to argument reduction and returning just outside [-1, 1]. The other waspow(sin(x), e), where float32sin()returns roughly -1e-7 at its zeroes. Two of those sites are inSnareMaterialand predate this work. One NaN fragment is not one bad pixel: the bloom blur spreads it across its whole kernel and tone mapping NaN gives black, so a few bad fragments on one object turn the whole frame black.check.mjsnow fails the build onpow()over a raw trig base, and I confirmed that check catches the bug by reintroducing it.Also worth knowing:
post.distortionshipped at0.045, which was correct back when nothing wrote to that buffer. Now that emitters exist, every refraction effect was running at 4.5% of its authored strength.Two things you flagged in the README, now closed
Layers.jscounts its writers so the clear, the draw and the resample are all skipped on frames where nothing is refracting. Turning it on would have been a poor trade if it then ran unconditionally.ELEMENTSandELEMENT_METAstill exist as derived views, which is whyAimController,HUDandAppneeded no changes at all.Verifying it
npm run checkruns headless, with no WebGL. For each ability it cross-checks everysettings.<id>.<key>reference against that ability's block, simulates the whole cast, and then mutates sliders on a zero-length frame and fails if nothing observable changes. That last one is your edit-while-paused invariant, turned into a test. It does not compile GLSL, so shader errors still need a browser; I verified all 100 cast in Chrome with zero console errors and no leaked scene state.The six original abilities are untouched apart from the settings-module move, which I verified is byte-identical in value against
HEAD.A few of the new ones
docs/ROSTER.mdanddocs/ROSTER-II.mdlist all 94 with the single technique each one exists to show off, since an ability that is another slot with a different palette should not exist.Thanks for publishing the original. The bolt's piecewise-linear noise and the beam's inverted core weighting are both lovely, and most of the new work is built on ideas taken straight from them.
🤖 Generated with Claude Code