Update Healer Man server
This commit is contained in:
1 parent
71246e5b75
commit
a868401ebc
40 files changed
+1522
-161
No files matched your search
@@ -3860,3 +3860,38 @@ Vite's existing large-chunk advisory remains informational.
|
||||
unrelated dependency deprecation warning. Audio loop and release/cancel
|
||||
behavior passed focused unit tests, but the browser pass did not assess sound
|
||||
by listening. The generated map is `docs/ROM_SPELL_MEDIA_MAPPING.md`.
|
||||
|
||||
## RoM Priest target and Rising Tide media (2026-09-24)
|
||||
|
||||
- Traced Urgent Heal's MagicObject component 501543 to ImageObject 573048 and
|
||||
its target `act_benedictory_heal_t_down.ros`. Its `explode` motion requests a
|
||||
one-shot 3D `act_benedictory_heal_t_down.mp3` at the healed target. The
|
||||
browser graph renders two original sprite layers, the emitted 72-triangle
|
||||
rune cylinder, and the animated gold ground decal with source color and
|
||||
alpha tracks. It remains marked partial because the decal is placed on a
|
||||
level ground plane rather than projected onto nearby terrain triangles.
|
||||
- Traced Rising Tide's component 620408 to ImageObject 572062 and target
|
||||
`act_riptide_t_freeze_hit.ros`. Its five-sprite target graph and the
|
||||
`act_icecone01_c_hand.ros` cast and `act_icecone01_f.ros` projectile graphs
|
||||
are attached to their respective phases. ROS direct-audio events identify
|
||||
`act_icecone01_f.ros.mp3` on projectile release and
|
||||
`act_riptide_t_freeze_hit.mp3` on target impact. Impact audio is positioned
|
||||
at the target; release audio remains at the caster.
|
||||
- The focused combat audio suite passed 12 tests, including distinct release
|
||||
and target panning. The manifest test checked all three new MP3 paths against
|
||||
the source ROS EventTrigger ID 43 payloads and packaged files.
|
||||
- In the in-app RuneWaker preview, Urgent Heal's impact showed gold stars, a
|
||||
green rune cylinder, and a gold target-ground ring at the frozen 250 ms frame.
|
||||
Rising Tide's cast showed a blue ring, its release a bright blue flash, and
|
||||
its target impact a blue and magenta burst. All four graphs loaded without
|
||||
browser errors or warnings. Audio mapping was source and test verified; this
|
||||
browser pass did not assess the sound by listening.
|
||||
- Live Priest combat in Wailing Caverns confirmed Urgent Heal's original gold
|
||||
rune circle and sparkles around the selected party target Veyra. In Boss
|
||||
Testing against Verdan the Everliving, Rising Tide showed blue source
|
||||
particles around the Priest during the cast, a blue projectile on release,
|
||||
and a bright blue and magenta burst across Verdan's visible lower body at
|
||||
arrival. The impact presentation now follows the projectile's 180–550 ms
|
||||
travel time; large-target attachment uses visual bounds so the burst remains
|
||||
in view. The browser reported no original-media or WebGL errors. Audio
|
||||
timing and target panning are source and test verified, not listening tested.
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
Generated by `node scripts/combat-effects/audit-rom-media.mjs --write` from the player catalog and combat effect manifest. The five accepted pairings follow `src/game/romFiveIdentities.ts`. Castable skills are listed; passives have no cast media event.
|
||||
|
||||
The effect column records original client ROS/RAS references and their cast phase. 6 castable skills have browser-playable original phases; other source visuals use the game's generated runtime effect until their controller families are supported. The sound column lists client cues that are packaged and played on their mapped phase. A dash means the source record or matching sound file did not provide that media.
|
||||
The effect column records original client ROS/RAS references and their cast phase. 7 castable skills have browser-playable original phases; other source visuals use the game's generated runtime effect until their controller families are supported. A `partial original` phase renders verified source layers and declares its remaining fidelity limit. The sound column lists packaged client cues on their mapped phase. A dash means the source record or matching sound file did not provide that media.
|
||||
|
||||
| Primary class | Skill group | Castable skills | With source effect | With packaged sound | With playable original effect |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| Priest | Base | 25 | 19 | 18 | 3 |
|
||||
| Priest | Base | 25 | 19 | 18 | 4 |
|
||||
| Priest | Accepted elite pair | 6 | 6 | 4 | 0 |
|
||||
| Knight | Base | 19 | 17 | 14 | 2 |
|
||||
| Knight | Accepted elite pair | 6 | 6 | 3 | 0 |
|
||||
@@ -40,8 +40,8 @@ The effect column records original client ROS/RAS references and their cast phas
|
||||
| Cleanse | 491175 | impact: act_dispersion_t_freeze_hit_e.ros | impact: act_dispersion_t_freeze_hit_e.mp3 |
|
||||
| Advanced Resurrection | 491176 | cast-start: act_casting_c_hand.ros<br>impact: act_anabiosis_t_down.ros | cast-start: act_casting_c_hand.wav<br>impact: act_anabiosis_t_down.mp3 |
|
||||
| Supreme Resurrection | 491177 | cast-start: act_casting_c_hand.ros<br>impact: act_anabiosis_t_down.ros | cast-start: act_casting_c_hand.wav<br>impact: act_anabiosis_t_down.mp3 |
|
||||
| Urgent Heal | 491147 | cast-start: act_casting_c_hand.ros | cast-start: act_casting_c_hand.wav |
|
||||
| Rising Tide | 490256 | cast-start: act_icecone01_c_hand.ros<br>release: act_icecone01_f.ros | cast-start: act_icecone01_c_hand.wav |
|
||||
| Urgent Heal | 491147 | cast-start: act_casting_c_hand.ros<br>impact: act_benedictory_heal_t_down.ros (partial original) | cast-start: act_casting_c_hand.wav<br>impact: act_benedictory_heal_t_down.mp3 |
|
||||
| Rising Tide | 490256 | cast-start: act_icecone01_c_hand.ros<br>release: act_icecone01_f.ros<br>impact: act_riptide_t_freeze_hit.ros | cast-start: act_icecone01_c_hand.wav<br>release: act_icecone01_f.ros.mp3<br>impact: act_riptide_t_freeze_hit.mp3 |
|
||||
| Regenerate | 490269 | cast-start: act_casting_c_hand.ros<br>cast-start: act_holy_hand_wave_c_hand_e.ros | cast-start: act_casting_c_hand.wav |
|
||||
| Holy Aura | 490300 | cast-start: act_casting_c_hand.ros<br>cast-start: act_flash_candle_c_down_e.ros | cast-start: act_casting_c_hand.wav |
|
||||
| Magic Barrier | 491166 | — | — |
|
||||
@@ -199,8 +199,8 @@ The effect column records original client ROS/RAS references and their cast phas
|
||||
|
||||
## Decoded particle archive
|
||||
|
||||
The particle archive covers 123 referenced ROS models, with 7 fully decoded models and 116 partial models. 4 models are renderer-ready, including the Priest holy cast whose remaining EventTrigger is an audio callback. It exports 510 sprite controllers and 103 distinct PNG textures. See `public/assets/game/combat-effects/original/runewaker/manifest.json` for per-model details.
|
||||
The particle archive covers 124 referenced ROS models, with 7 fully decoded models and 117 partial models. 7 models are renderer-ready and 1 has a browser-playable partial original. The Priest holy cast's remaining EventTrigger is an audio callback; Urgent Heal's target renders its source sprites, emitted rune mesh, and animated ground decal. Its terrain-triangle decal projection remains approximated by a level plane. The archive exports 515 sprite controllers and 106 distinct PNG textures. See `public/assets/game/combat-effects/original/runewaker/manifest.json` for per-model details.
|
||||
|
||||
## Next conversion step
|
||||
|
||||
The WebGL sprite renderer plays Priest Urgent Heal, Heal, and Group Heal casting, Knight Charge cast and Whirlwind Shield impact, and Mage Electric Bolt projectile. The Priest holy model's EventTrigger starts a looping 3D WAV and is handled by phase-matched combat audio while its three sprite layers render. Next, verify more spell phases against the source motion and controller semantics before attaching their `effectGraphUrl`. The Direct3D source and packaged ROS data remain the reference for those ports.
|
||||
The WebGL sprite renderer plays Priest Urgent Heal, Heal, and Group Heal casting; Urgent Heal's target impact with its source rune mesh and decal; Rising Tide's cast, projectile release, and target impact; Knight Charge cast and Whirlwind Shield impact; and Mage Electric Bolt projectile. Urgent Heal's holy cast EventTrigger starts a looping 3D WAV, and its target impact triggers the source benedictory heal MP3. Rising Tide retains its packaged cast WAV and now plays its ROS-linked projectile release and target impact MP3s. Impact cues use the target position. Urgent Heal's decal uses a level plane in place of the client's terrain projection. The Direct3D source and packaged ROS data remain the reference for subsequent phases.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"generatedAt":null,"sources":[{"kind":"wow335","path":"../wow335a/Data/enUS/patch-enUS.MPQ","sha256":"9f0088da35c5ed0b5106cd20e422611489e435b74043e520e02221e0ce1b1af1"},{"kind":"ascension","path":"../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc","sha256":"2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce"},{"kind":"runewaker","path":"../Runewaker/Resource/data","sha256":"dd5ffcfd74b253dcecaa2f41a931a634dd45ef5f7c13e849d539ddc47fdee1f9"}],"coverage":{"requested":6841,"authentic":5569,"fallback":1272,"fallbackKeys":["wow335:20647","wow335:2687","wow335:64380","wow335:20187","wow335:25781","wow335:20185","wow335:20186","wow335:5502","wow335:20184","wow335:53651","wow335:31898","wow335:31804","wow335:53733","wow335:53726","wow335:75","wow335:13481","wow335:781","wow335:34026","wow335:34754","wow335:47633","wow335:46584","wow335:26372","wow335:49278","wow335:49279","wow335:44450","wow335:34913","wow335:43043","wow335:43044","wow335:55342","wow335:29886","wow335:58889","wow335:5225","wow335:52610","ascension:705153","ascension:705154","ascension:560456","ascension:800193","ascension:561322","ascension:804138","ascension:805821","ascension:801767","ascension:707583","ascension:705160","ascension:705184","ascension:705185","ascension:804746","ascension:706346","ascension:706347","ascension:705238","ascension:92081","ascension:706354","ascension:92083","ascension:560906","ascension:707207","ascension:706326","ascension:706327","ascension:92082","ascension:705209","ascension:705210","ascension:705215","ascension:707721","ascension:705196","ascension:706804","ascension:706350","ascension:706351","ascension:705206","ascension:561314","ascension:705172","ascension:561321","ascension:705171","ascension:807861","ascension:800943","ascension:561394","ascension:705202","ascension:705241","ascension:705174","ascension:705170","ascension:705169","ascension:706353","ascension:560564","ascension:503408","ascension:503409","ascension:503410","ascension:503411","ascension:503412","ascension:503413","ascension:503414","ascension:705921","ascension:301297","ascension:572870","ascension:92085","ascension:705923","ascension:712396","ascension:802756","ascension:705906","ascension:705888","ascension:705852","ascension:681465","ascension:705889","ascension:705890","ascension:705873","ascension:806283","ascension:705849","ascension:705907","ascension:706368","ascension:705936","ascension:705937","ascension:705861","ascension:705909","ascension:503748","ascension:92084","ascension:705840","ascension:705841","ascension:92086","ascension:705911","ascension:705878","ascension:706538","ascension:300663","ascension:705903","ascension:705904","ascension:501220","ascension:501221","ascension:501222","ascension:501223","ascension:501224","ascension:501225","ascension:501226","ascension:501227","ascension:501228","ascension:705843","ascension:706482","ascension:501162","ascension:705848","ascension:705844","ascension:807296","ascension:707617","ascension:704500","ascension:560822","ascension:706266","ascension:707844","ascension:92089","ascension:681376","ascension:300465","ascension:92087","ascension:520251","ascension:705102","ascension:707842","ascension:520257","ascension:705108","ascension:804613","ascension:704607","ascension:705120","ascension:705151","ascension:92088","ascension:705105","ascension:560573","ascension:704360","ascension:301287","ascension:707513","ascension:704374","ascension:804609","ascension:300468","ascension:705128","ascension:805244","ascension:704613","ascension:524947","ascension:704612","ascension:807430","ascension:705113","ascension:705125","ascension:705133","ascension:705521","ascension:705522","ascension:705523","ascension:705498","ascension:707862","ascension:92091","ascension:680529","ascension:681484","ascension:806195","ascension:802011","ascension:705488","ascension:92094","ascension:705466","ascension:92093","ascension:705493","ascension:705454","ascension:789256","ascension:707230","ascension:706242","ascension:706243","ascension:705528","ascension:681092","ascension:804024","ascension:680253","ascension:705526","ascension:803638","ascension:705534","ascension:800165","ascension:520211","ascension:520212","ascension:520213","ascension:578297","ascension:574319","ascension:574320","ascension:807462","ascension:807887","ascension:705690","ascension:705691","ascension:92097","ascension:705675","ascension:707053","ascension:705720","ascension:705701","ascension:570722","ascension:92098","ascension:705647","ascension:300595","ascension:92096","ascension:705640","ascension:567542","ascension:705693","ascension:705641","ascension:705642","ascension:705656","ascension:705648","ascension:705649","ascension:707652","ascension:705651","ascension:705696","ascension:705697","ascension:705698","ascension:800699","ascension:807233","ascension:705659","ascension:705660","ascension:804040","ascension:705677","ascension:705689","ascension:567533","ascension:705Line truncated
|
||||
{"schemaVersion":1,"generatedAt":null,"sources":[{"kind":"wow335","path":"../wow335a/Data/enUS/patch-enUS.MPQ","sha256":"9f0088da35c5ed0b5106cd20e422611489e435b74043e520e02221e0ce1b1af1"},{"kind":"ascension","path":"../LadiksMPQEditor/client-current/area-52/patch-D/DBFilesClient/Spell.dbc","sha256":"2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce"},{"kind":"runewaker","path":"../Runewaker/Resource/data","sha256":"dd5ffcfd74b253dcecaa2f41a931a634dd45ef5f7c13e849d539ddc47fdee1f9"}],"coverage":{"requested":6841,"authentic":5569,"fallback":1272,"fallbackKeys":["wow335:20647","wow335:2687","wow335:64380","wow335:20187","wow335:25781","wow335:20185","wow335:20186","wow335:5502","wow335:20184","wow335:53651","wow335:31898","wow335:31804","wow335:53733","wow335:53726","wow335:75","wow335:13481","wow335:781","wow335:34026","wow335:34754","wow335:47633","wow335:46584","wow335:26372","wow335:49278","wow335:49279","wow335:44450","wow335:34913","wow335:43043","wow335:43044","wow335:55342","wow335:29886","wow335:58889","wow335:5225","wow335:52610","ascension:705153","ascension:705154","ascension:560456","ascension:800193","ascension:561322","ascension:804138","ascension:805821","ascension:801767","ascension:707583","ascension:705160","ascension:705184","ascension:705185","ascension:804746","ascension:706346","ascension:706347","ascension:705238","ascension:92081","ascension:706354","ascension:92083","ascension:560906","ascension:707207","ascension:706326","ascension:706327","ascension:92082","ascension:705209","ascension:705210","ascension:705215","ascension:707721","ascension:705196","ascension:706804","ascension:706350","ascension:706351","ascension:705206","ascension:561314","ascension:705172","ascension:561321","ascension:705171","ascension:807861","ascension:800943","ascension:561394","ascension:705202","ascension:705241","ascension:705174","ascension:705170","ascension:705169","ascension:706353","ascension:560564","ascension:503408","ascension:503409","ascension:503410","ascension:503411","ascension:503412","ascension:503413","ascension:503414","ascension:705921","ascension:301297","ascension:572870","ascension:92085","ascension:705923","ascension:712396","ascension:802756","ascension:705906","ascension:705888","ascension:705852","ascension:681465","ascension:705889","ascension:705890","ascension:705873","ascension:806283","ascension:705849","ascension:705907","ascension:706368","ascension:705936","ascension:705937","ascension:705861","ascension:705909","ascension:503748","ascension:92084","ascension:705840","ascension:705841","ascension:92086","ascension:705911","ascension:705878","ascension:706538","ascension:300663","ascension:705903","ascension:705904","ascension:501220","ascension:501221","ascension:501222","ascension:501223","ascension:501224","ascension:501225","ascension:501226","ascension:501227","ascension:501228","ascension:705843","ascension:706482","ascension:501162","ascension:705848","ascension:705844","ascension:807296","ascension:707617","ascension:704500","ascension:560822","ascension:706266","ascension:707844","ascension:92089","ascension:681376","ascension:300465","ascension:92087","ascension:520251","ascension:705102","ascension:707842","ascension:520257","ascension:705108","ascension:804613","ascension:704607","ascension:705120","ascension:705151","ascension:92088","ascension:705105","ascension:560573","ascension:704360","ascension:301287","ascension:707513","ascension:704374","ascension:804609","ascension:300468","ascension:705128","ascension:805244","ascension:704613","ascension:524947","ascension:704612","ascension:807430","ascension:705113","ascension:705125","ascension:705133","ascension:705521","ascension:705522","ascension:705523","ascension:705498","ascension:707862","ascension:92091","ascension:680529","ascension:681484","ascension:806195","ascension:802011","ascension:705488","ascension:92094","ascension:705466","ascension:92093","ascension:705493","ascension:705454","ascension:789256","ascension:707230","ascension:706242","ascension:706243","ascension:705528","ascension:681092","ascension:804024","ascension:680253","ascension:705526","ascension:803638","ascension:705534","ascension:800165","ascension:520211","ascension:520212","ascension:520213","ascension:578297","ascension:574319","ascension:574320","ascension:807462","ascension:807887","ascension:705690","ascension:705691","ascension:92097","ascension:705675","ascension:707053","ascension:705720","ascension:705701","ascension:570722","ascension:92098","ascension:705647","ascension:300595","ascension:92096","ascension:705640","ascension:567542","ascension:705693","ascension:705641","ascension:705642","ascension:705656","ascension:705648","ascension:705649","ascension:707652","ascension:705651","ascension:705696","ascension:705697","ascension:705698","ascension:800699","ascension:807233","ascension:705659","ascension:705660","ascension:804040","ascension:705677","ascension:705689","ascension:567533","ascension:705Line truncated
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/holy/act_benedictory_heal_t_down.ros","sourceSha256":"1efe9db99e088770f75bbe4f2ab3bc9498ba9eb0aa9d26b4e464a9be1af2f26a","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":false,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_phy_smashsharp.dds","sourceSha256":"4f715a3db1a4a0586affc7566a15f0643a1b3104b92a0c18349383a80dfe81e1","url":"/assets/game/combat-effects/original/runewaker/textures/4f715a3db1a4a0586affc756.png"},{"sourcePath":"texture/fx/skill/nor_speedupstar.dds","sourceSha256":"94238b7d366ba5bfd1172b7a851eba28566f70a8ac99df22ba16e3c9f038d184","url":"/assets/game/combat-effects/original/runewaker/textures/94238b7d366ba5bfd1172b7a.png"}],"motions":[{"name":"explode","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"frame/holy_line","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":0,"particleSystemRadius":5,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,5,0]}],[{"time":0,"value":[5,15,5]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":80},{"time":0.600000024,"value":0}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":0}],[{"time":0,"value":1}],[{"time":0,"value":1}],[{"time":0,"value":8}],[{"time":0,"value":16}],[{"time":0,"value":2}],[{"time":0,"value":4}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"frame/holy_line","texture":"texture\\fx\\skill\\nor_phy_smashsharp.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":2,"billboardAxis":3,"orientToTravel":true,"randomRotation":false,"tileSizePixels":[32,256],"frameRate":8,"frameCount":8,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[1,0.474509805,0.0117647061,1]},{"time":0.633333325,"value":[1,0.752941191,0.0117647061,1]}],[{"time":0,"value":1},{"time":0.633333325,"value":0.800000012}],[{"time":0,"value":0.100000001},{"time":0.300000012,"value":1.5},{"time":0.633333325,"value":0.5}],[{"time":0,"value":1}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/4f715a3db1a4a0586affc756.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"frame/star","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":0,"particleSystemRadius":5,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,5,0]}],[{"time":0,"value":[4,15,4]}],[{"time":0,"value":5}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":50},{"time":0.633333325,"value":0}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":0.5}],[{"time":0,"value":1}],[{"time":0,"value":1.5}],[{"time":0,"value":3}],[{"time":0,"value":5}],[{"time":0,"value":2}],[{"time":0,"value":3}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"frame/star","texture":"texture\\fx\\skill\\nor_speedupstar.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":false,"raLine truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/ice/act_icecone01_c_hand.ros","sourceSha256":"fe979d958d6cedd5c5e6791c5165b2d47bac86375594477442c68daab505850d","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":true,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_ice_conering.dds","sourceSha256":"9d2e0d12c1c9f805d43ce752adcf8be464b071e570c3cbfe4a22d889da0d824f","url":"/assets/game/combat-effects/original/runewaker/textures/9d2e0d12c1c9f805d43ce752.png"},{"sourcePath":"texture/fx/skill/nor_ashpartical_snow.dds","sourceSha256":"51f2b18ce64e77b98c7299cc65f1577c9039d2682baa2fcc735991f3a3b045b8","url":"/assets/game/combat-effects/original/runewaker/textures/51f2b18ce64e77b98c7299cc.png"},{"sourcePath":"texture/fx/skill/nor_phy_smashdark.dds","sourceSha256":"a3dc99fa23017f03954e2fda7b0ab448cc70ea813c4466a7a871f4073fe0c1d3","url":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"sourcePath":"texture/fx/skill/omni_light.dds","sourceSha256":"9398b7ea5b72f9b125efc831119f8cc0619749a10e8524ad7aa7e6931392bc6e","url":"/assets/game/combat-effects/original/runewaker/textures/9398b7ea5b72f9b125efc831.png"}],"motions":[{"name":"idle","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/ad50ccba_41f7a5da_a3c055a0_d83e8ce","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":20,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":1,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":0}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":60}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":3.1400001}],[{"time":0,"value":0.100000001}],[{"time":0,"value":2}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":1.64999998}],[{"time":0,"value":1.79999995}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/ad50ccba_41f7a5da_a3c055a0_d83e8ce","texture":"texture\\fx\\skill\\nor_ice_conering.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":true,"randomRotation":false,"tileSizePixels":[256,256],"frameRate":1,"frameCount":1,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[0.184313729,0.447058827,0.800000012,1]},{"time":0.300000012,"value":[0.0392156877,0.00392156886,0.607843161,1]}],[{"time":0,"value":0.200000003},{"time":0.300000012,"value":0}],[{"time":0,"value":6},{"time":0.300000012,"value":9}],[{"time":0,"value":3}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/9d2e0d12c1c9f805d43ce752.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/b6f1b26d_4b47c096_c8da7881_f81748af","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":20,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":1,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[8,8,8]}],[{"time":0,"value":7}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":40}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":3.1400001}],[{"time":0,"value":0.5}],[{"time":0,"value":0.800000012}],[{"time":0,"value":10}],[{"time":0,"value":0}],[{"time":0Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/ice/act_icecone01_f.ros","sourceSha256":"613087503b6a2fe1b223e8e82f7797efcbd4d27034955654b6be4e5b3d283955","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":true,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_phy_smashdark.dds","sourceSha256":"a3dc99fa23017f03954e2fda7b0ab448cc70ea813c4466a7a871f4073fe0c1d3","url":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"sourcePath":"texture/fx/skill/nor_icecone_crystal.dds","sourceSha256":"b7dcdd74409fb8797e52656a4f37673deca438032ca2eecdb50f842660dccd7b","url":"/assets/game/combat-effects/original/runewaker/textures/b7dcdd74409fb8797e52656a.png"},{"sourcePath":"texture/fx/skill/nor_ashpartical_snow.dds","sourceSha256":"51f2b18ce64e77b98c7299cc65f1577c9039d2682baa2fcc735991f3a3b045b8","url":"/assets/game/combat-effects/original/runewaker/textures/51f2b18ce64e77b98c7299cc.png"}],"motions":[{"name":"idle","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/c75f9089_4e354dea_692934a4_271e0d8c","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":30,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[1,1,1]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":30}],[{"time":0,"value":[0,0,-1]}],[{"time":0,"value":0.200000003}],[{"time":0,"value":1}],[{"time":0,"value":1.5}],[{"time":0,"value":0}],[{"time":0,"value":8}],[{"time":0,"value":1}],[{"time":0,"value":2}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/c75f9089_4e354dea_692934a4_271e0d8c","texture":"texture\\fx\\skill\\nor_phy_smashdark.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":false,"randomRotation":true,"tileSizePixels":[128,128],"frameRate":1,"frameCount":1,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[0.200000003,0.392156869,0.749019623,1]},{"time":0.5,"value":[0.313725501,0.200000003,0.768627465,1]}],[{"time":0,"value":0},{"time":0.0666666701,"value":0.5},{"time":0.5,"value":0}],[{"time":0,"value":8},{"time":0.5,"value":6}],[{"time":0,"value":60}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/82294ce0_46134ac5_c2e1fd82_ef41c674","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":1,"particleSystemRadius":30,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[1,1,1]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":5}],[{"time":0,"value":[0,0,-1]}],[{"time":0,"value":0.5}],[{"time":0,"value":0.5}],[{"time":0,"value":1}],[{"time":0,"value":8}],[{"time":0,"value":15}],[{"time":0,"value":0.5}],[{"time":0,"value":2}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time"Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/holy/act_benedictory_heal_t_down.ros","sourceSha256":"1efe9db99e088770f75bbe4f2ab3bc9498ba9eb0aa9d26b4e464a9be1af2f26a","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":false,"partialRendererReady":true,"visualFidelity":"partial-original","omittedVisuals":["CRuEntity_Decal:terrain-projection"],"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/rune.dds","sourceSha256":"77850679b67e7a86a45fd290bf1f2658942bd641ef8987766fb241e63c3cd34a","url":"/assets/game/combat-effects/original/runewaker/textures/77850679b67e7a86a45fd290.png"},{"sourcePath":"texture/fx/skill/nor_phy_smashsharp.dds","sourceSha256":"4f715a3db1a4a0586affc7566a15f0643a1b3104b92a0c18349383a80dfe81e1","url":"/assets/game/combat-effects/original/runewaker/textures/4f715a3db1a4a0586affc756.png"},{"sourcePath":"texture/fx/skill/nor_speedupstar.dds","sourceSha256":"94238b7d366ba5bfd1172b7a851eba28566f70a8ac99df22ba16e3c9f038d184","url":"/assets/game/combat-effects/original/runewaker/textures/94238b7d366ba5bfd1172b7a.png"},{"sourcePath":"texture/fx/skill/kan_rune_circle.dds","sourceSha256":"4e6b6ae54598b182d245413a75fb47390e87de8dc32b5a6c78bf67adebe9f145","url":"/assets/game/combat-effects/original/runewaker/textures/4e6b6ae54598b182d245413a.png"}],"runeMesh":{"sourceModel":"model/fx/skill/holy/_polygon/rune_cylinder.ros","sourceSha256":"22fab52e583617550181fec6fd8844b3e20e5b293faba9198c4feb828b5a59d3","sourceTexture":"texture/fx/skill/rune.dds","textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/77850679b67e7a86a45fd290.png","positions":[7.89487362,1.21848297,-3.52884868e-8,7.87259483,-1.21848285,-1.07392617e-9,7.39781952,-1.21848297,2.69258356,7.41875458,1.21848273,2.70020342,7.89487362,1.21848297,-3.52884868e-8,7.39781952,-1.21848297,2.69258356,7.41875458,1.21848273,2.70020342,7.39781952,-1.21848297,2.69258356,6.03075886,-1.21848309,5.0604043,6.04782581,1.21848273,5.07472467,7.41875458,1.21848273,2.70020342,6.03075886,-1.21848309,5.0604043,6.04782581,1.21848273,5.07472467,6.03075886,-1.21848309,5.0604043,3.93629909,-1.21848321,6.81786585,3.94743824,1.21848273,6.83715963,6.04782581,1.21848273,5.07472467,3.93629909,-1.21848321,6.81786585,3.94743824,1.21848273,6.83715963,3.93629909,-1.21848321,6.81786585,1.36706364,-1.21848321,7.75299215,1.37093234,1.21848273,7.77493143,3.94743824,1.21848273,6.83715963,1.36706364,-1.21848321,7.75299215,1.37093234,1.21848273,7.77493143,1.36706364,-1.21848321,7.75299215,-1.36705995,-1.21848321,7.75299263,-1.37092853,1.21848273,7.77493238,1.37093234,1.21848273,7.77493143,-1.36705995,-1.21848321,7.75299263,-1.37092853,1.21848273,7.77493238,-1.36705995,-1.21848321,7.75299263,-3.93629599,-1.21848321,6.81786823,-3.9474349,1.21848273,6.83716202,-1.37092853,1.21848273,7.77493238,-3.93629599,-1.21848321,6.81786823,-3.9474349,1.21848273,6.83716202,-3.93629599,-1.21848321,6.81786823,-6.03075695,-1.21848309,5.06040764,-6.04782295,1.21848273,5.07472801,-3.9474349,1.21848273,6.83716202,-6.03075695,-1.21848309,5.06040764,-6.04782295,1.21848273,5.07472801,-6.03075695,-1.21848309,5.06040764,-7.39781857,-1.21848297,2.69258738,-7.41875362,1.21848273,2.70020723,-6.04782295,1.21848273,5.07472801,-7.39781857,-1.21848297,2.69258738,-7.41875362,1.21848273,2.70020723,-7.39781857,-1.21848297,2.69258738,-7.87259483,-1.21848285,0.00000139103497,-7.89487362,1.21848297,0.00000136076005,-7.41875362,1.21848273,2.70020723,-7.87259483,-1.21848285,0.00000139103497,-7.89487362,1.21848297,0.00000136076005,-7.87259483,-1.21848285,0.00000139103497,-7.39781952,-1.21848273,-2.69258475,-7.41875458,1.21848297,-2.70020437,-7.89487362,1.21848297,0.00000136076005,-7.39781952,-1.21848273,-2.69258475,-7.41875458,1.21848297,-2.70020437,-7.39781952,-1.21848273,-2.69258475,-6.03075886,-1.21848261,-5.06040573,-6.04782486,1.21848297,-5.07472563,-7.41875458,1.21848297,-2.70020437,-6.03075886,-1.21848261,-5.06040573,-6.04782486,1.21848297,-5.07472563,-6.03075886,-1.21848261,-5.06040573,-3.93629837,-1.21848261,-6.8178668,-3.94743752,1.21848321,-6.83716059,-6.04782486,1.21848297,-5.07472563,-3.93629837,-1.21848261,-6.8178668,-3.94743752,1.21848321Line truncated
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/ice/act_icecone01_f.ros","sourceSha256":"613087503b6a2fe1b223e8e82f7797efcbd4d27034955654b6be4e5b3d283955","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":false,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_phy_smashdark.dds","sourceSha256":"a3dc99fa23017f03954e2fda7b0ab448cc70ea813c4466a7a871f4073fe0c1d3","url":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"sourcePath":"texture/fx/skill/nor_icecone_crystal.dds","sourceSha256":"b7dcdd74409fb8797e52656a4f37673deca438032ca2eecdb50f842660dccd7b","url":"/assets/game/combat-effects/original/runewaker/textures/b7dcdd74409fb8797e52656a.png"},{"sourcePath":"texture/fx/skill/nor_ashpartical_snow.dds","sourceSha256":"51f2b18ce64e77b98c7299cc65f1577c9039d2682baa2fcc735991f3a3b045b8","url":"/assets/game/combat-effects/original/runewaker/textures/51f2b18ce64e77b98c7299cc.png"}],"motions":[{"name":"idle","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/c75f9089_4e354dea_692934a4_271e0d8c","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":30,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[1,1,1]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":30}],[{"time":0,"value":[0,0,-1]}],[{"time":0,"value":0.200000003}],[{"time":0,"value":1}],[{"time":0,"value":1.5}],[{"time":0,"value":0}],[{"time":0,"value":8}],[{"time":0,"value":1}],[{"time":0,"value":2}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/c75f9089_4e354dea_692934a4_271e0d8c","texture":"texture\\fx\\skill\\nor_phy_smashdark.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":false,"randomRotation":true,"tileSizePixels":[128,128],"frameRate":1,"frameCount":1,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[0.200000003,0.392156869,0.749019623,1]},{"time":0.5,"value":[0.313725501,0.200000003,0.768627465,1]}],[{"time":0,"value":0},{"time":0.0666666701,"value":0.5},{"time":0.5,"value":0}],[{"time":0,"value":8},{"time":0.5,"value":6}],[{"time":0,"value":60}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"46d13dfd_42ec21db_21c856b3_3a9e94f3/82294ce0_46134ac5_c2e1fd82_ef41c674","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":1,"particleSystemRadius":30,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[1,1,1]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":5}],[{"time":0,"value":[0,0,-1]}],[{"time":0,"value":0.5}],[{"time":0,"value":0.5}],[{"time":0,"value":1}],[{"time":0,"value":8}],[{"time":0,"value":15}],[{"time":0,"value":0.5}],[{"time":0,"value":2}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"timeLine truncated
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/ice/act_icecone01_c_hand.ros","sourceSha256":"fe979d958d6cedd5c5e6791c5165b2d47bac86375594477442c68daab505850d","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":false,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_ice_conering.dds","sourceSha256":"9d2e0d12c1c9f805d43ce752adcf8be464b071e570c3cbfe4a22d889da0d824f","url":"/assets/game/combat-effects/original/runewaker/textures/9d2e0d12c1c9f805d43ce752.png"},{"sourcePath":"texture/fx/skill/nor_ashpartical_snow.dds","sourceSha256":"51f2b18ce64e77b98c7299cc65f1577c9039d2682baa2fcc735991f3a3b045b8","url":"/assets/game/combat-effects/original/runewaker/textures/51f2b18ce64e77b98c7299cc.png"},{"sourcePath":"texture/fx/skill/nor_phy_smashdark.dds","sourceSha256":"a3dc99fa23017f03954e2fda7b0ab448cc70ea813c4466a7a871f4073fe0c1d3","url":"/assets/game/combat-effects/original/runewaker/textures/a3dc99fa23017f03954e2fda.png"},{"sourcePath":"texture/fx/skill/omni_light.dds","sourceSha256":"9398b7ea5b72f9b125efc831119f8cc0619749a10e8524ad7aa7e6931392bc6e","url":"/assets/game/combat-effects/original/runewaker/textures/9398b7ea5b72f9b125efc831.png"}],"motions":[{"name":"idle","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/ad50ccba_41f7a5da_a3c055a0_d83e8ce","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":20,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":1,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":0}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":60}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":3.1400001}],[{"time":0,"value":0.100000001}],[{"time":0,"value":2}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":1.64999998}],[{"time":0,"value":1.79999995}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/ad50ccba_41f7a5da_a3c055a0_d83e8ce","texture":"texture\\fx\\skill\\nor_ice_conering.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":true,"randomRotation":false,"tileSizePixels":[256,256],"frameRate":1,"frameCount":1,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[0.184313729,0.447058827,0.800000012,1]},{"time":0.300000012,"value":[0.0392156877,0.00392156886,0.607843161,1]}],[{"time":0,"value":0.200000003},{"time":0.300000012,"value":0}],[{"time":0,"value":6},{"time":0.300000012,"value":9}],[{"time":0,"value":3}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/9d2e0d12c1c9f805d43ce752.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"bb59dbd1_4692ad21_cbdc5198_edfe889/b6f1b26d_4b47c096_c8da7881_f81748af","particlePath":"","emitterPath":"","frameType":0,"pathFrameType":1,"particleSystemRadius":20,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":1,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[8,8,8]}],[{"time":0,"value":7}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":40}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":3.1400001}],[{"time":0,"value":0.5}],[{"time":0,"value":0.800000012}],[{"time":0,"value":10}],[{"time":0,"value":0}],[{"time":Line truncated
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"schemaVersion":1,"kind":"runewaker-ros-particle-archive","source":"runewaker","sourceModel":"model/fx/skill/ice/act_riptide_t_freeze_hit.ros","sourceSha256":"2430ee30dddf6465007963fb22d5b52c7b212a51ae219e7dfc0c7894f9a28beb","coordinateTransform":"three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)","rendererReady":true,"shader":{"name":"ruSHADER_PARTICLE_SPRITEEX","sourceImplementation":"Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp","sourceSha256":"fd40fe72aa310b7b5fe7cffa3d0afe6552e9dfcbdc8a7cc361c697a686317502","vertexSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.vertex.hlsl","pixelSourceUrl":"/assets/game/combat-effects/original/runewaker/shaders/particle-spriteex.pixel.hlsl","target":"Direct3D 9 source; WebGL sprite port for verified models","twoSided":true,"fog":false,"premultipliedOutput":true,"blendModes":{"0":"source-alpha / one-minus-source-alpha","1":"source-alpha / one","2":"zero / one-minus-source-alpha","3":"one / one-minus-source-alpha","4":"one / one","5":"one / zero"}},"textures":[{"sourcePath":"texture/fx/skill/nor_ice_icebuffwave.dds","sourceSha256":"50c646fc7b1f5f3da13a39b1936d07ab3ada3dd27fe86e5529bc0ca12783a7ab","url":"/assets/game/combat-effects/original/runewaker/textures/50c646fc7b1f5f3da13a39b1.png"},{"sourcePath":"texture/fx/skill/nor_ashpartical_snow.dds","sourceSha256":"51f2b18ce64e77b98c7299cc65f1577c9039d2682baa2fcc735991f3a3b045b8","url":"/assets/game/combat-effects/original/runewaker/textures/51f2b18ce64e77b98c7299cc.png"},{"sourcePath":"texture/fx/skill/nor_phy_line.dds","sourceSha256":"7f87e16f9b839ee1b74444ee89de398ee4e477e5109a8c6df0f2334a188365c8","url":"/assets/game/combat-effects/original/runewaker/textures/7f87e16f9b839ee1b74444ee.png"},{"sourcePath":"texture/fx/environment/nor_spiderdarkwave.dds","sourceSha256":"b97c5c77fd98393fa0554884d2a0de5f5144e085967c5a55f1c63eedf1f2e154","url":"/assets/game/combat-effects/original/runewaker/textures/b97c5c77fd98393fa0554884.png"},{"sourcePath":"texture/fx/skill/kan_ice_riptide.dds","sourceSha256":"f4a11fa1ae15a2c72f9a01d301410084fd2b83fb967a931b85a03e6a8592ddeb","url":"/assets/game/combat-effects/original/runewaker/textures/f4a11fa1ae15a2c72f9a01d3.png"}],"motions":[{"name":"explode","id":1,"controllers":[{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"act_/blue_wave","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":0,"particleSystemRadius":10,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":5}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[3,3,3]}],[{"time":0,"value":3}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":200},{"time":0.166666672,"value":0}],[{"time":0,"value":[0,1,0]}],[{"time":0,"value":0.200000003}],[{"time":0,"value":0.300000012}],[{"time":0,"value":0.800000012}],[{"time":0,"value":5}],[{"time":0,"value":8}],[{"time":0,"value":0.300000012}],[{"time":0,"value":1.20000005}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}]]},{"type":"sprite","sourceType":"CRuController_Particle_Visualizer_Sprite","anchor":"act_/blue_wave","texture":"texture\\fx\\skill\\nor_ice_icebuffwave.dds","blendMode":1,"shader":"ruSHADER_PARTICLE_SPRITEEX","allowWorldRotation":false,"orientation":3,"billboardType":1,"billboardAxis":2,"orientToTravel":true,"randomRotation":false,"tileSizePixels":[128,64],"frameRate":16,"frameCount":16,"quadScale":[1,1,0],"quadOffset":[0,0,0],"tracks":[[{"time":0,"value":[0.619607866,1,0.839215696,1]},{"time":0.633333325,"value":[0.00784313772,0.694117665,1,1]}],[{"time":0,"value":1},{"time":0.533333361,"value":1},{"time":0.633333325,"value":0}],[{"time":0,"value":5},{"time":0.633333325,"value":10}],[{"time":0,"value":0}]],"textureAssetUrl":"/assets/game/combat-effects/original/runewaker/textures/50c646fc7b1f5f3da13a39b1.png"},{"type":"emitter","sourceType":"CRuController_Particle_Emitter","anchor":"act_/spot","particlePath":"","emitterPath":"","frameType":1,"pathFrameType":0,"particleSystemRadius":10,"pathRandomMirror":false,"pathRandomMirrorPlane":1,"pathRotationAxis":[0,1,0],"pathRotationDeviation":0,"pathLoopDelay":-1,"emitterPathFrameType":1,"emitterPathLoopDelay":-1,"emitterOrientation":6,"prerunTime":0,"tracks":[[{"time":0,"value":200}],[{"time":0,"value":1}],[{"time":0,"value":[0,0,0]}],[{"time":0,"value":[3,3,3]}],[{"time":0,"value":20}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":0}],[{"time":0,"value":800},{"time":0.0666666701,"value":0}],[{"time":0,"valLine truncated
|
||||
@@ -2,10 +2,10 @@
|
||||
"schemaVersion": 1,
|
||||
"kind": "runewaker-ros-particle-archive-index",
|
||||
"scope": "primary-five-base-and-accepted-elite-pairs",
|
||||
"requestedModels": 123,
|
||||
"processedModels": 123,
|
||||
"requestedModels": 124,
|
||||
"processedModels": 124,
|
||||
"decodedModels": 7,
|
||||
"partialModels": 116,
|
||||
"partialModels": 117,
|
||||
"failedModels": 0,
|
||||
"results": [
|
||||
{
|
||||
@@ -13,6 +13,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e090fa17cd49978789f86bed.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 2,
|
||||
"unsupported": [],
|
||||
@@ -23,6 +24,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/be7720da98e84828a0b1827f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 5,
|
||||
"unsupported": [],
|
||||
@@ -33,6 +35,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/5667b51ea910c1de4dacaf8a.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 12,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -45,6 +48,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/75f3f97797399a0026c01826.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -58,6 +62,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/df83b93fb75960770e4b3ce3.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -74,6 +79,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d554b42ef95e9fb1fe0c46eb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -86,6 +92,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/234804e24d630cd4415098b5.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -98,6 +105,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/08ba946c69fa4ac89a060721.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -127,6 +135,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/98d77f21ff7601057a4b54f4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [],
|
||||
@@ -137,6 +146,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d642cb85ed13f0e7546e7d3c.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -149,6 +159,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/19ca1b3e4494cc63cb5d3ffb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -162,6 +173,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/964170a45dd885e8df9b3a5a.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -179,6 +191,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e0a8cd28646454303b835809.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -192,6 +205,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a968041acc9bfde2fecf6266.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -204,6 +218,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/311db29ae86c62a0d2eeefde.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -218,6 +233,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c65961e29ce5f8c285277dc5.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -231,6 +247,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/bf45f9834ef19e5316be6c30.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -248,6 +265,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a5fec4274fa4fbf4668db02a.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [],
|
||||
@@ -258,6 +276,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/220d99f74771f4321740fbb9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -271,6 +290,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c5b1129be66e9e390625390e.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -286,6 +306,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6908daf7bf70c351132272b1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -300,6 +321,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/0807b166970705e58f968d45.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -315,6 +337,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/2ffc388ef09ff800215ebd0e.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 12,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -340,6 +363,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d9448a6cf58b272d6d9cb9f4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -352,6 +376,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e20230ec22dd7b39a99302b1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -365,6 +390,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/f62e735344d47fb11fb7c2b7.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 12,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -392,6 +418,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d1e9c74e62571a6bd143de26.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -407,6 +434,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e82f8aaaefb40c56a0de1ea1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 8,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -419,6 +447,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/457e148f32619f5247ab8126.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -438,6 +467,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/1c4f95bffc77e44e5fe75645.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -451,6 +481,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/222d1e6e4fb9ec37b02429cc.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -466,6 +497,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/3df82cc72cd0f66f7b519837.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -484,6 +516,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6f9db1c88b41781a99090beb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 8,
|
||||
"textures": 6,
|
||||
"unsupported": [
|
||||
@@ -495,17 +528,13 @@
|
||||
{
|
||||
"model": "model/fx/skill/holy/act_benedictory_heal_t_down.ros",
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/25d68c4251779ce20dcde306.json",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/8dd9bcacd0f5feb65bd52d12.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": true,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
"CRuController_Particle_Visualizer_Entity",
|
||||
"CRuController_Decal",
|
||||
"CRuController_GlobalAlpha",
|
||||
"CRuController_GlobalColor",
|
||||
"CRuController_EventTrigger",
|
||||
"CRuEntity_Particle_Visualizer_Entity"
|
||||
"CRuController_EventTrigger"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
@@ -514,6 +543,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/071cd6773fd37ff2e7515339.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -526,6 +556,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/10af3c42129f27ce6263a939.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -540,6 +571,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a361cf0ed24660b34cfc3ac8.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -563,6 +595,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4641d64b13f793b2ec59e643.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 6,
|
||||
"unsupported": [
|
||||
@@ -575,6 +608,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6aeae5be261a170e0ab0370e.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -592,6 +626,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/ca746c2b3912631c36fab531.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -607,6 +642,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/14e269d87e267f206977be8d.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -619,6 +655,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4457e8ae70e688822f82391d.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -635,6 +672,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/1c668eabb9743489beeaa540.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -647,6 +685,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/9ee131c9295b43c6c4ca86cb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -660,6 +699,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/00f57b27bcae002a2038340f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -672,6 +712,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/3791aabbd98daa86506bf171.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -693,6 +734,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/f28d5759608237d617592a88.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 4,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -705,6 +747,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/b7d3389e6a44b45b25f2c2eb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -717,6 +760,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/af5802c9c9076a1cd04d5b70.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -740,6 +784,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a37c01cb17215879fde73a63.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -757,6 +802,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c44a59e78c845ef412c20ec9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -772,6 +818,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/5227647ab51e90334d89a027.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [],
|
||||
@@ -784,6 +831,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/ac064aee3422b89ca306d025.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -804,6 +852,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/cc20b6b82565466c677ea89b.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -818,6 +867,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/7fd85f9680e249402c7565f1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -832,6 +882,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a3db8cd9d570b85474e770bc.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -844,6 +895,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6388a9b5f8641e2886b2a7e4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -860,6 +912,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/853a0f487435665db20ec94f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -878,6 +931,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/17d8fd9116f380b960f325c7.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -895,6 +949,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e14c2bde166ae71d8d83c8ff.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 6,
|
||||
"unsupported": [
|
||||
@@ -907,6 +962,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/ac821669047622c5b864f2a1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 12,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -923,6 +979,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/397cc94366a4c51a0eac9cde.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -938,6 +995,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a6e6b78af72cb1da00922227.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -952,6 +1010,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/f1171bd6cd1c8dab4c858cd3.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -974,25 +1033,24 @@
|
||||
{
|
||||
"model": "model/fx/skill/ice/act_icecone01_c_hand.ros",
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d34ce38236573a68c02be790.json",
|
||||
"rendererReady": false,
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4fcf73ba81bbc515a4030544.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 12,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
"CRuController_EventTrigger",
|
||||
"CRuController_GlobalAlpha",
|
||||
"CRuController_EventTrigger",
|
||||
"CRuController_GlobalAlpha",
|
||||
"CRuController_EventTrigger",
|
||||
"CRuController_GlobalAlpha"
|
||||
"CRuController_EventTrigger"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"model": "model/fx/skill/ice/act_icecone01_f.ros",
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/ae8ce52b6c50e63ed754cf05.json",
|
||||
"rendererReady": false,
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6dd522645757b1e3694ffe21.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1003,11 +1061,26 @@
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"model": "model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/f8fbeadb894fc795581b10dc.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 5,
|
||||
"unsupported": [
|
||||
"CRuController_EventTrigger",
|
||||
"CRuController_EventTrigger"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"model": "model/fx/skill/ice/act_watershieldbuff_c_down_i.ros",
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/87fb3880b296a0a845cb685d.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1020,6 +1093,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/649470027a4d64510456992f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1033,6 +1107,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/200c43e790e87a46151f84a5.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1050,6 +1125,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/b58e1b6ac19262c9d9dd5f1e.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 4,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1062,6 +1138,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/29d3ca5b6a88ee337f042a71.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 5,
|
||||
"unsupported": [
|
||||
@@ -1082,6 +1159,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/1e08eb44ba367623a88dd177.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1110,6 +1188,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/5638bf5b5482728086cfc7c3.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1122,6 +1201,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/6fb78e867275d92f3dd38847.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -1134,6 +1214,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c63eae7a48188be0c35ce7e1.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1155,6 +1236,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c8e4797a43a2450f46d6a129.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 7,
|
||||
"textures": 7,
|
||||
"unsupported": [
|
||||
@@ -1167,6 +1249,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4f4d90292dc1f65f7f258bf5.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [],
|
||||
@@ -1177,6 +1260,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/291291de3f5aefd9f77c73e5.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1189,6 +1273,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/95df9f350c80e9b29097aa09.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1201,6 +1286,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c7a0f4a1c0f45c3bf5a5b61a.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1228,6 +1314,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4ab81c20c7d74958eb0fbf7f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1240,6 +1327,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/aaf151fa3ab8bdcf7ecc2e16.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -1252,6 +1340,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/2ee082809dc47463907a939d.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -1269,6 +1358,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/153a0df93c81f185faf0324f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1298,6 +1388,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/2c3d9de6125356d81bfae69c.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1310,6 +1401,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/bd5dda56795f3cec82845fe2.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 4,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1324,6 +1416,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/4041e8f84f7358ee8cc1f4d2.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 4,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -1336,6 +1429,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/d582e3355c5286158bc630e4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1348,6 +1442,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/97f42badd33077485f0cb98b.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1362,6 +1457,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/f2bf53cac27ef98cc1c3d838.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1376,6 +1472,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/7b7dd5fc47d1526fcedf2754.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1398,6 +1495,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/66079f115ab86eaa5fbb25f3.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1413,6 +1511,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c948963bdd612885ba84aac2.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1427,6 +1526,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/921d0f3ecbe91bb50ff64891.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1441,6 +1541,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/2b6d12cdd3a57031e5d80a2a.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1455,6 +1556,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/579ccbc92277d9d328a1b7d0.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -1471,6 +1573,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/550823913cd3bf13c2528a50.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1483,6 +1586,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/c28603666865b9072cfc39d4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 5,
|
||||
"textures": 5,
|
||||
"unsupported": [
|
||||
@@ -1498,6 +1602,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/df1153babc2a1dc8613c3a27.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1517,6 +1622,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/89588a8fe7751e72fa3ec75b.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 7,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -1532,6 +1638,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/72d97a3126effcaefb7441b8.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1559,6 +1666,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/0804e6ee485816e00a3e1791.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1571,6 +1679,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/74e3fbb84c9c01bb1834f4bb.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1583,6 +1692,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/b4f7d91d19f4fdebc0fe4cab.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1601,6 +1711,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/89d7847fa450541be3862291.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1617,6 +1728,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/3b965f7767599e23d4ebb063.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 7,
|
||||
"textures": 4,
|
||||
"unsupported": [
|
||||
@@ -1634,6 +1746,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/3654b65dcf248481abbc0c2f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -1647,6 +1760,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/03ca38ff0cf39c2242af78b5.json",
|
||||
"rendererReady": true,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [],
|
||||
@@ -1657,6 +1771,7 @@
|
||||
"status": "decoded",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/cc95ace4dd031e20d10d0c5e.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [],
|
||||
@@ -1667,6 +1782,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/849ccde11d4b5dce1f7655f9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 9,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1681,6 +1797,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/9c2532ca3f7f0a1960479749.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1693,6 +1810,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/877aae7a3cdac75c62e43d25.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1716,6 +1834,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/b7622027e0c79d8dae26dbb9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1728,6 +1847,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/0dadf58f017714709a5d78a9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 2,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1740,6 +1860,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/71355ca795b31a2cce23759f.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1753,6 +1874,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/5b4f7a78e35eb553af907493.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 0,
|
||||
"textures": 0,
|
||||
"unsupported": [
|
||||
@@ -1768,6 +1890,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/b34256bda32c5cef54b3769b.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 6,
|
||||
"textures": 2,
|
||||
"unsupported": [
|
||||
@@ -1783,6 +1906,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/9358501ce9313ad42b5999b3.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1795,6 +1919,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/beea16afdaa6f5d9fcb8e867.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
@@ -1822,6 +1947,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a565d331ff4c490ee376a023.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 3,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1835,6 +1961,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/a4f4974cefc640fd0f893884.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 8,
|
||||
"textures": 5,
|
||||
"unsupported": [
|
||||
@@ -1847,6 +1974,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/de99a8f0ba46257cc6038cc9.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 15,
|
||||
"textures": 3,
|
||||
"unsupported": [
|
||||
@@ -1866,6 +1994,7 @@
|
||||
"status": "partial",
|
||||
"graphUrl": "/assets/game/combat-effects/original/runewaker/graphs/e91dbe72d224789b0512aaf4.json",
|
||||
"rendererReady": false,
|
||||
"partialRendererReady": false,
|
||||
"spriteControllers": 1,
|
||||
"textures": 1,
|
||||
"unsupported": [
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -54,9 +54,9 @@ retain the verified media-pack download requirement.
|
||||
their sound cues without rebuilding WoW effects. It reads spell-to-image links
|
||||
from `romPlayerCatalog.generated.json` and `magiccollectobject.db`, effect model
|
||||
paths from `imageobject.db`, and exact same-name cues under the client's
|
||||
parallel `sound/fx` tree. The sound association is a path match, not an
|
||||
explicit sound ID from `imageobject.db`. Each cue is tagged with its source
|
||||
cast phase so a target impact does not replay a caster sound.
|
||||
parallel `sound/fx` tree. Verified ROS audio events supply cues when their
|
||||
embedded path differs from the mirrored model path. Each cue is tagged with
|
||||
its source cast phase so a target impact does not replay a caster sound.
|
||||
|
||||
`npm run effects:rom:particles` packages a decoded ROS particle archive for the
|
||||
five primary classes and their accepted elite pairs. Build the native exporter
|
||||
@@ -75,8 +75,9 @@ tile sizes are pixels, not atlas row and column counts.
|
||||
`npm run effects:rom:audit` writes the first-five class and accepted-pair map
|
||||
to `docs/ROM_SPELL_MEDIA_MAPPING.md`. It distinguishes source ROS references,
|
||||
packaged audio, and browser-playable original visual graphs. The WebGL sprite
|
||||
renderer plays Priest Urgent Heal, Heal, and Group Heal casts, Knight Charge
|
||||
cast and Whirlwind Shield impact, and Mage Electric Bolt projectile. The exact
|
||||
renderer plays Priest Urgent Heal, Heal, and Group Heal casts; Urgent Heal's
|
||||
target impact; Rising Tide cast, release, and impact; Knight Charge cast and
|
||||
Whirlwind Shield impact; and Mage Electric Bolt projectile. The exact
|
||||
spell, phase, and model allowlist lives in `rom-particle-readiness.mjs`. Other
|
||||
ROS phases remain in the archive until their controller families and motion
|
||||
sequence are checked in the browser. Open `scripts/combat-effects/rom-preview.html`
|
||||
@@ -94,6 +95,30 @@ parameters. Its packaged WAV loops only during the matching Priest cast and
|
||||
stops on release or cancellation. Other unsupported event triggers still
|
||||
block a graph from the ready allowlist.
|
||||
|
||||
Urgent Heal's target effect is linked through MagicCollect spell `491147` to
|
||||
MagicObject component `501543`, whose ImageObject `573048` supplies
|
||||
`ExtendACT=model/fx/skill/holy/act_benedictory_heal_t_down.ros` at `p_down`.
|
||||
The client creates the component's `ExtendACT` on the target after the heal
|
||||
resolves. Its `explode` motion contains two original sprite layers (holy line
|
||||
and gold stars), a nested `rune_cylinder.ros` particle entity with source rune
|
||||
texture, a `kan_rune_circle.dds` ground decal, and a one-shot direct 3D audio
|
||||
event for `sound/fx/skill/holy/act_benedictory_heal_t_down.mp3`. The browser
|
||||
plays the two sprite layers and source rune mesh emission, and animates the
|
||||
decal radius, rotation, color, and alpha. The decal lies on a level plane;
|
||||
the RuneWaker client projects it onto collision triangles. The graph remains
|
||||
`partial-original` for that terrain projection difference while replacing
|
||||
the generated target filler.
|
||||
|
||||
Rising Tide spell `490256` uses ImageObject `572061` for
|
||||
`act_icecone01_c_hand.ros` at cast start and `act_icecone01_f.ros` on release.
|
||||
MagicObject component `620408` links ImageObject `572062`, whose target
|
||||
`ExtendACT` is `act_riptide_t_freeze_hit.ros` at `freeze_hit`. The source
|
||||
audio events name the projectile cue
|
||||
`sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3` and target cue
|
||||
`sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3`; the cast hand has its
|
||||
mirrored WAV. The verified models are attached to their respective combat
|
||||
presentation phases after their controller support and source motion checks.
|
||||
|
||||
## Rendering
|
||||
|
||||
The `wow-m2-model` format composes source particles, animated/skinned geometry,
|
||||
|
||||
@@ -17,6 +17,10 @@ const cohorts = [
|
||||
["Mage", "rom-mage", "rom-warlock"],
|
||||
];
|
||||
const phases = ["cast-start", "release", "impact", "tick", "aura-start", "aura-end"];
|
||||
const phaseOrder = (phase) => {
|
||||
const index = phases.indexOf(phase);
|
||||
return index < 0 ? phases.length : index;
|
||||
};
|
||||
|
||||
function idsForSkill(skill) {
|
||||
return [...new Set([skill.sourceSkillId, ...(skill.ranks ?? []).map((rank) => rank.sourceSkillId)])]
|
||||
@@ -27,10 +31,15 @@ function skillMedia(skill) {
|
||||
const ids = idsForSkill(skill);
|
||||
const mapped = ids.map((id) => entries.get(id)).filter(Boolean);
|
||||
const models = [...new Set(mapped.flatMap((entry) => phases.flatMap((phase) =>
|
||||
(entry.phases?.[phase] ?? []).map((asset) => `${phase}: ${path.posix.basename(asset.model)}`))))];
|
||||
(entry.phases?.[phase] ?? []).map((asset) => (
|
||||
`${phase}: ${path.posix.basename(asset.model)}`
|
||||
+ (asset.visualCompleteness === "partial" ? " (partial original)" : "")
|
||||
)))))];
|
||||
const sounds = [...new Set(mapped.flatMap((entry) => (entry.sounds ?? [])
|
||||
.filter((sound) => sound.url)
|
||||
.map((sound) => `${sound.phase ?? "any"}: ${path.posix.basename(sound.sourcePath)}`)))];
|
||||
.map((sound) => `${sound.phase ?? "any"}: ${path.posix.basename(sound.sourcePath)}`)))].sort((left, right) => (
|
||||
phaseOrder(left.split(":", 1)[0]) - phaseOrder(right.split(":", 1)[0])
|
||||
));
|
||||
const playableVisuals = mapped.flatMap((entry) => Object.values(entry.phases ?? {}).flat())
|
||||
.filter((asset) => asset.effectGraphUrl).length;
|
||||
for (const entry of mapped) {
|
||||
@@ -75,7 +84,7 @@ const lines = [
|
||||
"",
|
||||
"Generated by `node scripts/combat-effects/audit-rom-media.mjs --write` from the player catalog and combat effect manifest. The five accepted pairings follow `src/game/romFiveIdentities.ts`. Castable skills are listed; passives have no cast media event.",
|
||||
"",
|
||||
`The effect column records original client ROS/RAS references and their cast phase. ${summary.reduce((total, row) => total + row.playableVisuals, 0)} castable skills have browser-playable original phases; other source visuals use the game's generated runtime effect until their controller families are supported. The sound column lists client cues that are packaged and played on their mapped phase. A dash means the source record or matching sound file did not provide that media.`,
|
||||
`The effect column records original client ROS/RAS references and their cast phase. ${summary.reduce((total, row) => total + row.playableVisuals, 0)} castable skills have browser-playable original phases; other source visuals use the game's generated runtime effect until their controller families are supported. A \`partial original\` phase renders verified source layers and declares its remaining fidelity limit. The sound column lists packaged client cues on their mapped phase. A dash means the source record or matching sound file did not provide that media.`,
|
||||
"",
|
||||
"| Primary class | Skill group | Castable skills | With source effect | With packaged sound | With playable original effect |",
|
||||
"| --- | --- | ---: | ---: | ---: | ---: |",
|
||||
@@ -85,12 +94,12 @@ const lines = [
|
||||
"## Decoded particle archive",
|
||||
"",
|
||||
particleArchive
|
||||
? `The particle archive covers ${particleArchive.processedModels} referenced ROS models, with ${particleArchive.decodedModels} fully decoded models and ${particleArchive.partialModels} partial models. ${particleArchive.results.filter((result) => result.rendererReady).length} models are renderer-ready, including the Priest holy cast whose remaining EventTrigger is an audio callback. It exports ${particleArchive.results.reduce((sum, result) => sum + result.spriteControllers, 0)} sprite controllers and ${new Set(particleArchive.results.flatMap((result) => result.graphUrl ? JSON.parse(fs.readFileSync(path.join(projectRoot, "public", result.graphUrl.slice(1)), "utf8")).textures.map((texture) => texture.url) : [])).size} distinct PNG textures. See \`public/assets/game/combat-effects/original/runewaker/manifest.json\` for per-model details.`
|
||||
? `The particle archive covers ${particleArchive.processedModels} referenced ROS models, with ${particleArchive.decodedModels} fully decoded models and ${particleArchive.partialModels} partial models. ${particleArchive.results.filter((result) => result.rendererReady).length} models are renderer-ready and ${particleArchive.results.filter((result) => result.partialRendererReady).length} has a browser-playable partial original. The Priest holy cast's remaining EventTrigger is an audio callback; Urgent Heal's target renders its source sprites, emitted rune mesh, and animated ground decal. Its terrain-triangle decal projection remains approximated by a level plane. The archive exports ${particleArchive.results.reduce((sum, result) => sum + result.spriteControllers, 0)} sprite controllers and ${new Set(particleArchive.results.flatMap((result) => result.graphUrl ? JSON.parse(fs.readFileSync(path.join(projectRoot, "public", result.graphUrl.slice(1)), "utf8")).textures.map((texture) => texture.url) : [])).size} distinct PNG textures. See \`public/assets/game/combat-effects/original/runewaker/manifest.json\` for per-model details.`
|
||||
: "The RuneWaker particle archive has not been packaged.",
|
||||
"",
|
||||
"## Next conversion step",
|
||||
"",
|
||||
"The WebGL sprite renderer plays Priest Urgent Heal, Heal, and Group Heal casting, Knight Charge cast and Whirlwind Shield impact, and Mage Electric Bolt projectile. The Priest holy model's EventTrigger starts a looping 3D WAV and is handled by phase-matched combat audio while its three sprite layers render. Next, verify more spell phases against the source motion and controller semantics before attaching their `effectGraphUrl`. The Direct3D source and packaged ROS data remain the reference for those ports.",
|
||||
"The WebGL sprite renderer plays Priest Urgent Heal, Heal, and Group Heal casting; Urgent Heal's target impact with its source rune mesh and decal; Rising Tide's cast, projectile release, and target impact; Knight Charge cast and Whirlwind Shield impact; and Mage Electric Bolt projectile. Urgent Heal's holy cast EventTrigger starts a looping 3D WAV, and its target impact triggers the source benedictory heal MP3. Rising Tide retains its packaged cast WAV and now plays its ROS-linked projectile release and target impact MP3s. Impact cues use the target position. Urgent Heal's decal uses a level plane in place of the client's terrain projection. The Direct3D source and packaged ROS data remain the reference for subsequent phases.",
|
||||
];
|
||||
const markdown = `${lines.join("\n").trimEnd()}\n`;
|
||||
if (process.argv.includes("--write")) {
|
||||
|
||||
@@ -534,6 +534,38 @@ export function runewakerSoundPathsForModel(modelPath, resourceRoot) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// These one-shot cues are direct 3D audio events (ID 43) in the named ROS
|
||||
// motions. The client places the impact models through the skill's MagicObject
|
||||
// component image, while the projectile model comes from ImageObject.DB.
|
||||
// The projectile's embedded cue differs from the parallel sound/fx filename.
|
||||
const verifiedRuneWakerEventSounds = [
|
||||
{
|
||||
spellId: 491147, phase: "impact",
|
||||
model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros",
|
||||
sourcePath: "sound/fx/skill/holy/act_benedictory_heal_t_down.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "release",
|
||||
model: "model/fx/skill/ice/act_icecone01_f.ros",
|
||||
sourcePath: "sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "impact",
|
||||
model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
sourcePath: "sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3",
|
||||
},
|
||||
];
|
||||
|
||||
function attachVerifiedRuneWakerEventSounds(entries) {
|
||||
const bySpellId = new Map(entries.map((entry) => [entry.spellId, entry]));
|
||||
for (const { spellId, phase, model, sourcePath } of verifiedRuneWakerEventSounds) {
|
||||
const entry = bySpellId.get(spellId);
|
||||
if (!entry?.phases?.[phase]?.some((asset) => asset.model?.toLowerCase() === model)) continue;
|
||||
if (entry.sounds?.some((sound) => sound.phase === phase && sound.sourcePath?.toLowerCase() === sourcePath)) continue;
|
||||
(entry.sounds ??= []).push({ sourcePath, phase, weight: 1, volume: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
const count = buffer.readInt32LE(132);
|
||||
const recordSize = 1032;
|
||||
@@ -548,27 +580,82 @@ function runewakerImageIdsFromMagicCollect(buffer, spellIds) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// The cast's MagicCollect ImageID omits the target-side ExtendACT. These two
|
||||
// component links are verified against MagicObject.ImageID and ImageObject.DB
|
||||
// before the target model is added to the combat presentation manifest.
|
||||
const verifiedRuneWakerTargetEffects = [
|
||||
{ spellId: 491147, componentId: 501543, imageId: 573048,
|
||||
model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros", point: "p_down" },
|
||||
{ spellId: 490256, componentId: 620408, imageId: 572062,
|
||||
model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros", point: "freeze_hit" },
|
||||
];
|
||||
|
||||
function verifiedRunewakerTargetModels(magicCollect, magicObject, imageObject, spellIds) {
|
||||
const wanted = verifiedRuneWakerTargetEffects.filter((effect) => spellIds.includes(effect.spellId));
|
||||
if (!wanted.length) return new Map();
|
||||
const collectOffsets = new Map();
|
||||
for (let offset = 140; offset + 1032 <= magicCollect.length; offset += 1032) {
|
||||
const spellId = magicCollect.readInt32LE(offset);
|
||||
if (wanted.some((effect) => effect.spellId === spellId)) collectOffsets.set(spellId, offset);
|
||||
}
|
||||
const componentImages = new Map();
|
||||
for (let offset = 140; offset + 992 <= magicObject.length; offset += 992) {
|
||||
const componentId = magicObject.readInt32LE(offset);
|
||||
if (wanted.some((effect) => effect.componentId === componentId)) {
|
||||
componentImages.set(componentId, magicObject.readInt32LE(offset + 0x20));
|
||||
}
|
||||
}
|
||||
const componentImageFields = new Map();
|
||||
for (let offset = 140; offset + 3712 <= imageObject.length; offset += 3712) {
|
||||
const imageId = imageObject.readInt32LE(offset);
|
||||
if (!wanted.some((effect) => effect.imageId === imageId)) continue;
|
||||
const textField = (fieldOffset, length) => imageObject.subarray(
|
||||
offset + fieldOffset, offset + fieldOffset + length,
|
||||
).toString("latin1").split("\0", 1)[0].replaceAll("\\", "/").toLowerCase();
|
||||
componentImageFields.set(imageId, {
|
||||
extendAct: textField(2132, 128),
|
||||
extendPoint: textField(2260, 32),
|
||||
});
|
||||
}
|
||||
for (const effect of wanted) {
|
||||
const collectOffset = collectOffsets.get(effect.spellId);
|
||||
const componentIds = collectOffset === undefined ? [] : Array.from(
|
||||
{ length: (0x2c0 - 0x180) / 4 + 1 }, (_, index) =>
|
||||
magicCollect.readInt32LE(collectOffset + 0x180 + index * 4),
|
||||
);
|
||||
if (!componentIds.includes(effect.componentId)
|
||||
|| componentImages.get(effect.componentId) !== effect.imageId
|
||||
|| componentImageFields.get(effect.imageId)?.extendAct !== effect.model
|
||||
|| componentImageFields.get(effect.imageId)?.extendPoint !== effect.point) {
|
||||
throw new Error(`RuneWaker target media source link changed for spell ${effect.spellId}`);
|
||||
}
|
||||
}
|
||||
return new Map(wanted.map((effect) => [effect.spellId, effect]));
|
||||
}
|
||||
|
||||
function attachVerifiedRuneWakerParticles(entries) {
|
||||
const archivePath = path.join(projectRoot, "public/assets/game/combat-effects/original/runewaker/manifest.json");
|
||||
if (!fs.existsSync(archivePath)) return;
|
||||
const archive = JSON.parse(fs.readFileSync(archivePath, "utf8"));
|
||||
const decoded = new Map((archive.results ?? [])
|
||||
.filter((result) => result.rendererReady && result.graphUrl)
|
||||
.filter((result) => (result.rendererReady || result.partialRendererReady) && result.graphUrl)
|
||||
.map((result) => [result.model.toLowerCase(), result]));
|
||||
const bySpellId = new Map(entries.map((entry) => [entry.spellId, entry]));
|
||||
for (const { spellId, phase, model, scale, loopCastSound } of verifiedRuneWakerParticlePhases) {
|
||||
for (const { spellId, phase, model, scale, loopCastSound, partialVisual } of verifiedRuneWakerParticlePhases) {
|
||||
const entry = bySpellId.get(spellId);
|
||||
const asset = entry?.phases?.[phase]?.find((candidate) => (
|
||||
candidate.model.toLowerCase() === model
|
||||
));
|
||||
const result = decoded.get(model);
|
||||
if (!asset || !result) continue;
|
||||
if (!asset || !result || (partialVisual ? !result.partialRendererReady : !result.rendererReady)) continue;
|
||||
const graphPath = path.join(projectRoot, "public", result.graphUrl.slice(1));
|
||||
if (!fs.existsSync(graphPath)) continue;
|
||||
const graph = JSON.parse(fs.readFileSync(graphPath, "utf8"));
|
||||
if (graph.sourceModel !== model || !graph.rendererReady) continue;
|
||||
if (graph.sourceModel !== model
|
||||
|| (partialVisual ? !graph.partialRendererReady : !graph.rendererReady)) continue;
|
||||
asset.effectKind = "runewaker-particles";
|
||||
asset.effectGraphUrl = result.graphUrl;
|
||||
if (partialVisual) asset.visualCompleteness = "partial";
|
||||
if (scale) asset.scale = scale;
|
||||
if (loopCastSound) for (const sound of entry.sounds ?? []) {
|
||||
if (sound.phase === "cast-start"
|
||||
@@ -582,15 +669,29 @@ function attachVerifiedRuneWakerParticles(entries) {
|
||||
function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
const resourceRoot = path.dirname(dataRoot);
|
||||
const magicCollect = fs.readFileSync(path.join(dataRoot, "magiccollectobject.db"));
|
||||
const magicObject = fs.readFileSync(path.join(dataRoot, "magicobject.db"));
|
||||
const imageObject = fs.readFileSync(path.join(dataRoot, "imageobject.db"));
|
||||
const targetEffects = verifiedRunewakerTargetModels(magicCollect, magicObject, imageObject, spellIds);
|
||||
const collectedImages = runewakerImageIdsFromMagicCollect(magicCollect, spellIds);
|
||||
for (const [spellId, imageId] of collectedImages) if (!imageBySpellId.has(spellId)) imageBySpellId.set(spellId, imageId);
|
||||
const requestedImages = [...new Set(spellIds.map((id) => imageBySpellId.get(id)).filter(Boolean))];
|
||||
const images = parseRunewakerImageRecords(fs.readFileSync(path.join(dataRoot, "imageobject.db")), requestedImages);
|
||||
const requestedImages = [...new Set([
|
||||
...spellIds.map((id) => imageBySpellId.get(id)).filter(Boolean),
|
||||
...[...targetEffects.values()].map((effect) => effect.imageId),
|
||||
])];
|
||||
const images = parseRunewakerImageRecords(imageObject, requestedImages);
|
||||
const entries = [];
|
||||
for (const spellId of spellIds) {
|
||||
const imageId = imageBySpellId.get(spellId);
|
||||
if (!imageId) continue;
|
||||
const paths = images.get(imageId) ?? [];
|
||||
const targetEffect = targetEffects.get(spellId);
|
||||
const targetImagePaths = targetEffect ? images.get(targetEffect.imageId) ?? [] : [];
|
||||
const targetModel = targetEffect && targetImagePaths.find((candidate) => (
|
||||
candidate.toLowerCase() === targetEffect.model
|
||||
));
|
||||
if (targetEffect && !targetModel) {
|
||||
throw new Error(`RuneWaker target ImageObject changed for spell ${spellId}`);
|
||||
}
|
||||
const paths = [...(images.get(imageId) ?? []), ...(targetModel ? [targetModel] : [])];
|
||||
const phases = {};
|
||||
const sounds = [];
|
||||
for (const sourcePath of paths) {
|
||||
@@ -628,6 +729,7 @@ function buildRunewakerEntries(spellIds, imageBySpellId, dataRoot) {
|
||||
...(uniqueSounds.length ? { sounds: uniqueSounds } : {}),
|
||||
});
|
||||
}
|
||||
attachVerifiedRuneWakerEventSounds(entries);
|
||||
attachVerifiedRuneWakerParticles(entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -353,12 +353,52 @@ test("primary-five RuneWaker spells retain source phases and packaged sound cues
|
||||
assert.ok(entry(490495).sounds.some((sound) => sound.sourcePath === "sound/fx/skill/holy/act_godshield_c_down.mp3"));
|
||||
});
|
||||
|
||||
test("Priest target and projectile sounds follow their verified ROS event phases", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(projectRoot, "public/assets/game/combat-effects/manifest.json"), "utf8"));
|
||||
const archive = JSON.parse(fs.readFileSync(path.join(projectRoot, "public/assets/game/combat-effects/original/runewaker/manifest.json"), "utf8"));
|
||||
for (const { spellId, phase, motion, model, sourcePath } of [
|
||||
{
|
||||
spellId: 491147, phase: "impact", motion: "explode",
|
||||
model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros",
|
||||
sourcePath: "sound/fx/skill/holy/act_benedictory_heal_t_down.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "release", motion: "appear",
|
||||
model: "model/fx/skill/ice/act_icecone01_f.ros",
|
||||
sourcePath: "sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3",
|
||||
},
|
||||
{
|
||||
spellId: 490256, phase: "impact", motion: "explode",
|
||||
model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
sourcePath: "sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3",
|
||||
},
|
||||
]) {
|
||||
const entry = manifest.entries.find((candidate) => candidate.key === `runewaker:${spellId}`);
|
||||
assert.ok(entry?.phases?.[phase]?.some((asset) => asset.model === model));
|
||||
const cues = entry.sounds.filter((sound) => sound.phase === phase && sound.sourcePath === sourcePath);
|
||||
assert.equal(cues.length, 1);
|
||||
assert.ok(cues[0].url?.startsWith("/assets/game/combat-effects/audio/"));
|
||||
assert.ok(fs.existsSync(path.join(projectRoot, "public", cues[0].url.slice(1))));
|
||||
const result = archive.results.find((candidate) => candidate.model === model);
|
||||
assert.ok(result?.graphUrl);
|
||||
const graph = JSON.parse(fs.readFileSync(path.join(projectRoot, "public", result.graphUrl.slice(1)), "utf8"));
|
||||
assert.ok(graph.motions.some((candidate) => candidate.name === motion
|
||||
&& candidate.controllers.some((controller) => controller.sourceType === "CRuController_EventTrigger"
|
||||
&& controller.eventID === 43 && controller.eventParameter0 === 0
|
||||
&& controller.eventString?.replaceAll("\\", "/").toLowerCase() === sourcePath)));
|
||||
}
|
||||
});
|
||||
|
||||
test("only browser-verified RuneWaker particle phases use original graphs", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(projectRoot, "public/assets/game/combat-effects/manifest.json"), "utf8"));
|
||||
const expected = new Set([
|
||||
"491147:cast-start:model/fx/skill/holy/act_casting_c_hand.ros",
|
||||
"491147:impact:model/fx/skill/holy/act_benedictory_heal_t_down.ros",
|
||||
"490261:cast-start:model/fx/skill/holy/act_casting_c_hand.ros",
|
||||
"490281:cast-start:model/fx/skill/holy/act_casting_c_hand.ros",
|
||||
"490256:cast-start:model/fx/skill/ice/act_icecone01_c_hand.ros",
|
||||
"490256:release:model/fx/skill/ice/act_icecone01_f.ros",
|
||||
"490256:impact:model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
"490150:cast-start:model/fx/skill/physical/act_charge02_c_down.ros",
|
||||
"490175:impact:model/fx/hit_fx/act_shieldstorm_t_freeze_hit_e.ros",
|
||||
"491168:release:model/fx/skill/wind/act_electric_ball_f_i.ros",
|
||||
@@ -372,7 +412,14 @@ test("only browser-verified RuneWaker particle phases use original graphs", () =
|
||||
assert.equal(asset.effectKind, "runewaker-particles");
|
||||
if (entry.spellId === 490175) assert.equal(asset.scale, 2.5);
|
||||
const graph = JSON.parse(fs.readFileSync(path.join(projectRoot, "public", asset.effectGraphUrl.slice(1)), "utf8"));
|
||||
assert.equal(graph.rendererReady, true);
|
||||
if (key === "491147:impact:model/fx/skill/holy/act_benedictory_heal_t_down.ros") {
|
||||
assert.equal(asset.visualCompleteness, "partial");
|
||||
assert.equal(graph.partialRendererReady, true);
|
||||
assert.equal(graph.rendererReady, false);
|
||||
} else {
|
||||
assert.equal(asset.visualCompleteness, undefined);
|
||||
assert.equal(graph.rendererReady, true);
|
||||
}
|
||||
assert.equal(graph.sourceModel, asset.model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,17 @@ function sourceFile(resourcePath) {
|
||||
const textureCache = new Map();
|
||||
const HOLY_CAST_MODEL = "model/fx/skill/holy/act_casting_c_hand.ros";
|
||||
const HOLY_CAST_SOUND = "sound/fx/skill/holy/act_casting_c_hand.wav";
|
||||
const ICE_CAST_MODEL = "model/fx/skill/ice/act_icecone01_c_hand.ros";
|
||||
const ICE_RELEASE_MODEL = "model/fx/skill/ice/act_icecone01_f.ros";
|
||||
const RIPTIDE_IMPACT_MODEL = "model/fx/skill/ice/act_riptide_t_freeze_hit.ros";
|
||||
const URGENT_IMPACT_MODEL = "model/fx/skill/holy/act_benedictory_heal_t_down.ros";
|
||||
const URGENT_RUNE_MODEL = "model/fx/skill/holy/_polygon/rune_cylinder.ros";
|
||||
const URGENT_RUNE_TEXTURE = "texture/fx/skill/rune.dds";
|
||||
const URGENT_DECAL_TEXTURE = "texture/fx/skill/kan_rune_circle.dds";
|
||||
|
||||
function normalizedEventString(controller) {
|
||||
return controller.eventString?.replaceAll("\\", "/").toLowerCase();
|
||||
}
|
||||
|
||||
function visualOnlyHolyCastTrigger(model, motion, controller) {
|
||||
return model === HOLY_CAST_MODEL && motion.name === "appear"
|
||||
@@ -100,11 +111,121 @@ function visualOnlyHolyCastTrigger(model, motion, controller) {
|
||||
&& controller.eventID === 43 // ruENTITYEVENT_AUDIO_3D_DIRECT
|
||||
&& controller.eventParameter0 === 1 // Loop during cast.
|
||||
&& controller.eventParameter1 === 0
|
||||
&& controller.eventString?.replaceAll("\\", "/").toLowerCase() === HOLY_CAST_SOUND
|
||||
&& normalizedEventString(controller) === HOLY_CAST_SOUND
|
||||
&& controller.startTime === 0
|
||||
&& Number.isFinite(controller.eventDuration) && controller.eventDuration > 0;
|
||||
}
|
||||
|
||||
function visualOnlyRisingTideTrigger(model, motion, controller) {
|
||||
if (controller.type !== "unsupported" || controller.sourceType !== "CRuController_EventTrigger"
|
||||
|| controller.startTime !== 0 || controller.eventParameter1 !== 0) return false;
|
||||
const eventString = normalizedEventString(controller);
|
||||
if (model === ICE_CAST_MODEL && ["idle", "appear", "disappear"].includes(motion.name)) {
|
||||
return controller.eventID === 43 && controller.eventParameter0 === 1
|
||||
&& eventString === "sound/fx/skill/ice/act_cone01_c_hand.mp3"
|
||||
&& Number.isFinite(controller.eventDuration) && controller.eventDuration > 0;
|
||||
}
|
||||
if (model === ICE_RELEASE_MODEL && ["idle", "appear", "disappear"].includes(motion.name)) {
|
||||
return controller.eventID === 44 && controller.eventParameter0 === 1
|
||||
&& eventString === "missile" && controller.eventDuration === 1
|
||||
|| motion.name === "appear" && controller.eventID === 43
|
||||
&& controller.eventParameter0 === 0 && controller.eventDuration === 0
|
||||
&& eventString === "sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3";
|
||||
}
|
||||
if (model === RIPTIDE_IMPACT_MODEL && motion.name === "explode") {
|
||||
// The source HIT callback is handled by combat; it contributes no visual geometry.
|
||||
return controller.eventID === 1 && controller.eventParameter0 === 0
|
||||
&& controller.eventDuration === 0 && eventString === ""
|
||||
|| controller.eventID === 43 && controller.eventParameter0 === 0
|
||||
&& controller.eventDuration === 0
|
||||
&& eventString === "sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function verifiedGlobalAlpha(model, motion, controller) {
|
||||
if (controller.type !== "global-alpha" || controller.sourceType !== "CRuController_GlobalAlpha"
|
||||
|| controller.startTime !== 0 || !Number.isFinite(controller.duration)
|
||||
|| !Array.isArray(controller.alphaTrack)
|
||||
|| !controller.alphaTrack.every((key) => Number.isFinite(key.time)
|
||||
&& Number.isFinite(key.value) && key.value >= 0 && key.value <= 1)) return false;
|
||||
if (model === ICE_CAST_MODEL && ["idle", "appear", "disappear"].includes(motion.name)) {
|
||||
return motion.name === "appear"
|
||||
? controller.duration > 0 && controller.alphaTrack.length === 2
|
||||
&& controller.alphaTrack[0].time === 0 && controller.alphaTrack[0].value === 0
|
||||
&& controller.alphaTrack[1].value === 1
|
||||
: controller.duration === 0 && controller.alphaTrack.length === 1
|
||||
&& controller.alphaTrack[0].time === 0 && controller.alphaTrack[0].value === 1;
|
||||
}
|
||||
if (model === URGENT_IMPACT_MODEL && motion.name === "explode") {
|
||||
return controller.anchor === "frame/decal" && controller.duration > 0
|
||||
&& controller.alphaTrack.length === 4
|
||||
&& controller.alphaTrack[0].value === 0 && controller.alphaTrack[1].value === 1
|
||||
&& controller.alphaTrack[2].value === 1 && controller.alphaTrack[3].value === 0;
|
||||
}
|
||||
return model === RIPTIDE_IMPACT_MODEL && motion.name === "explode"
|
||||
&& controller.duration === 0 && controller.alphaTrack.length === 1
|
||||
&& controller.alphaTrack[0].time === 0 && controller.alphaTrack[0].value === 1;
|
||||
}
|
||||
|
||||
const expectedSpecialControllerCounts = new Map([
|
||||
[HOLY_CAST_MODEL, [1, 0]],
|
||||
[ICE_CAST_MODEL, [3, 3]],
|
||||
[ICE_RELEASE_MODEL, [4, 0]],
|
||||
[RIPTIDE_IMPACT_MODEL, [2, 1]],
|
||||
]);
|
||||
|
||||
// The source projects onto nearby world triangles. The browser draws this
|
||||
// same animated texture on a target-ground plane until terrain projection is ported.
|
||||
const urgentOmittedVisuals = ["CRuEntity_Decal:terrain-projection"];
|
||||
|
||||
function verifiedUrgentPartial(model, decoded, result, unsupportedControllers, globalAlphaControllers, runeMesh) {
|
||||
if (model !== URGENT_IMPACT_MODEL || decoded.motions.length !== 1
|
||||
|| decoded.motions[0].name !== "explode" || result.spriteControllers !== 2
|
||||
|| globalAlphaControllers.length !== 1 || unsupportedControllers.length !== 1
|
||||
|| decoded.unsupported.length !== 1
|
||||
|| decoded.unsupported[0] !== "CRuEntity_Particle_Visualizer_Entity"
|
||||
|| runeMesh?.sourceModel !== URGENT_RUNE_MODEL
|
||||
|| runeMesh.sourceTexture !== URGENT_RUNE_TEXTURE
|
||||
|| runeMesh.positions.length !== 72 * 9
|
||||
|| runeMesh.uvs.length !== 72 * 6) return false;
|
||||
const controllers = decoded.motions[0].controllers;
|
||||
const sprites = controllers.filter((controller) => controller.type === "sprite");
|
||||
const emitters = controllers.filter((controller) => controller.type === "emitter");
|
||||
const entity = controllers.find((controller) => controller.type === "entity-visualizer");
|
||||
const decal = controllers.find((controller) => controller.type === "decal");
|
||||
const color = controllers.find((controller) => controller.type === "global-color");
|
||||
const expectedSprites = new Map([
|
||||
["frame/holy_line", "texture/fx/skill/nor_phy_smashsharp.dds"],
|
||||
["frame/star", "texture/fx/skill/nor_speedupstar.dds"],
|
||||
]);
|
||||
if (sprites.length !== 2 || emitters.length !== 3
|
||||
|| !sprites.every((sprite) => expectedSprites.get(sprite.anchor)
|
||||
=== sprite.texture?.replaceAll("\\", "/").toLowerCase())
|
||||
|| !["frame/holy_line", "frame/star", "frame/rune"].every((anchor) =>
|
||||
emitters.some((emitter) => emitter.anchor === anchor))) return false;
|
||||
const trigger = unsupportedControllers[0].controller;
|
||||
if (trigger.sourceType !== "CRuController_EventTrigger" || trigger.anchor !== "frame"
|
||||
|| trigger.eventID !== 43 || trigger.eventParameter0 !== 0 || trigger.eventParameter1 !== 0
|
||||
|| trigger.startTime !== 0 || trigger.eventDuration !== 0
|
||||
|| normalizedEventString(trigger) !== "sound/fx/skill/holy/act_benedictory_heal_t_down.mp3") return false;
|
||||
if (entity?.anchor !== "frame/rune" || entity.sourceType !== "CRuController_Particle_Visualizer_Entity"
|
||||
|| entity.entityName?.replaceAll("\\", "/").toLowerCase() !== URGENT_RUNE_MODEL
|
||||
|| entity.orientToTravel !== false || entity.tracks?.length !== 4
|
||||
|| !entity.tracks.every((track) => Array.isArray(track) && track.length > 0)
|
||||
|| decal?.anchor !== "frame/decal" || decal.sourceType !== "CRuController_Decal"
|
||||
|| decal.decalTexture?.replaceAll("\\", "/").toLowerCase() !== URGENT_DECAL_TEXTURE
|
||||
|| !decal.textureAssetUrl || decal.billboardType !== 0 || decal.tracks?.length !== 3
|
||||
|| decal.tracks[1]?.length !== 2 || decal.tracks[2]?.length !== 3
|
||||
|| color?.anchor !== "frame/decal" || color.sourceType !== "CRuController_GlobalColor"
|
||||
|| color.tracks?.length !== 1 || color.tracks[0]?.length !== 2) return false;
|
||||
const alpha = globalAlphaControllers[0].controller;
|
||||
const unsupportedTypes = new Set(result.unsupported);
|
||||
return alpha.anchor === "frame/decal" && alpha.sourceType === "CRuController_GlobalAlpha"
|
||||
&& alpha.alphaTrack?.length === 4 && alpha.startTime === 0
|
||||
&& unsupportedTypes.size === 1 && unsupportedTypes.has("CRuController_EventTrigger");
|
||||
}
|
||||
|
||||
function packageTexture(resourcePath, scratch) {
|
||||
const normalized = resourcePath.replaceAll("\\", "/").toLowerCase();
|
||||
if (textureCache.has(normalized)) return textureCache.get(normalized);
|
||||
@@ -132,11 +253,63 @@ function packageTexture(resourcePath, scratch) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
function packageUrgentRuneMesh(scratch) {
|
||||
const directory = path.join(scratch, "urgent-rune-mesh");
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
execFileSync(exporter, [resourceRoot, URGENT_RUNE_MODEL, directory], {
|
||||
cwd: root, stdio: "pipe", maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
const report = JSON.parse(fs.readFileSync(path.join(directory, "export-report.json"), "utf8"));
|
||||
const material = report.materials?.[0];
|
||||
if (report.stats?.meshes !== 1 || report.stats?.triangles !== 72
|
||||
|| report.stats?.missingTextures !== 0
|
||||
|| material?.resolvedTexture?.replaceAll("\\", "/").toLowerCase() !== URGENT_RUNE_TEXTURE) {
|
||||
throw new Error("Urgent Heal rune cylinder source mesh changed");
|
||||
}
|
||||
const vertices = [];
|
||||
const textureCoordinates = [];
|
||||
const positions = [];
|
||||
const uvs = [];
|
||||
for (const line of fs.readFileSync(path.join(directory, "scene.obj"), "utf8").split(/\r?\n/)) {
|
||||
if (line.startsWith("v ")) vertices.push(line.trim().split(/\s+/).slice(1).map(Number));
|
||||
else if (line.startsWith("vt ")) textureCoordinates.push(line.trim().split(/\s+/).slice(1).map(Number));
|
||||
else if (line.startsWith("f ")) {
|
||||
const corners = line.trim().split(/\s+/).slice(1);
|
||||
if (corners.length !== 3) throw new Error("Urgent rune mesh has a non-triangle face");
|
||||
for (const corner of corners) {
|
||||
const [vertexIndex, uvIndex] = corner.split("/").map(Number);
|
||||
const vertex = vertices[vertexIndex - 1];
|
||||
const uv = textureCoordinates[uvIndex - 1];
|
||||
if (vertex?.length !== 3 || uv?.length !== 2
|
||||
|| ![...vertex, ...uv].every(Number.isFinite)) {
|
||||
throw new Error("Urgent rune mesh has invalid vertex data");
|
||||
}
|
||||
positions.push(...vertex);
|
||||
uvs.push(...uv);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (positions.length !== 72 * 9 || uvs.length !== 72 * 6) {
|
||||
throw new Error("Urgent rune mesh triangle count changed");
|
||||
}
|
||||
const texture = packageTexture(material.resolvedTexture, scratch);
|
||||
return {
|
||||
sourceModel: URGENT_RUNE_MODEL,
|
||||
sourceSha256: sha256(fs.readFileSync(sourceFile(URGENT_RUNE_MODEL))),
|
||||
sourceTexture: URGENT_RUNE_TEXTURE,
|
||||
textureAssetUrl: texture.url,
|
||||
positions,
|
||||
uvs,
|
||||
texture,
|
||||
};
|
||||
}
|
||||
|
||||
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "healer-rom-fx-"));
|
||||
const results = [];
|
||||
try {
|
||||
for (const model of selectedModels) {
|
||||
const result = { model, status: "failed", graphUrl: null, rendererReady: false,
|
||||
partialRendererReady: false,
|
||||
spriteControllers: 0, textures: 0, unsupported: [], errors: [] };
|
||||
results.push(result);
|
||||
try {
|
||||
@@ -151,8 +324,37 @@ try {
|
||||
throw new Error(`Unexpected native particle report schema for ${model}`);
|
||||
}
|
||||
const textures = new Map();
|
||||
const runeMesh = model === URGENT_IMPACT_MODEL ? packageUrgentRuneMesh(scratch) : null;
|
||||
if (runeMesh) textures.set(runeMesh.texture.sourcePath.toLowerCase(), runeMesh.texture);
|
||||
for (const motion of decoded.motions) for (const controller of motion.controllers) {
|
||||
if (controller.type === "unsupported") result.unsupported.push(controller.sourceType);
|
||||
if (model !== URGENT_IMPACT_MODEL
|
||||
&& ["entity-visualizer", "decal", "global-color"].includes(controller.type)) {
|
||||
controller.type = "unsupported";
|
||||
delete controller.tracks;
|
||||
delete controller.textureAssetUrl;
|
||||
}
|
||||
if (model !== URGENT_IMPACT_MODEL && controller.type === "unsupported") {
|
||||
delete controller.entityName;
|
||||
delete controller.decalTexture;
|
||||
delete controller.orientToTravel;
|
||||
delete controller.billboardType;
|
||||
}
|
||||
if (controller.type === "global-alpha"
|
||||
&& ![ICE_CAST_MODEL, RIPTIDE_IMPACT_MODEL, URGENT_IMPACT_MODEL].includes(model)) {
|
||||
controller.type = "unsupported";
|
||||
delete controller.startTime;
|
||||
delete controller.duration;
|
||||
delete controller.alphaTrack;
|
||||
}
|
||||
if (controller.type === "unsupported"
|
||||
|| controller.type === "global-alpha" && !verifiedGlobalAlpha(model, motion, controller)) {
|
||||
result.unsupported.push(controller.sourceType);
|
||||
}
|
||||
if (model === URGENT_IMPACT_MODEL && controller.type === "decal") {
|
||||
const texture = packageTexture(controller.decalTexture, scratch);
|
||||
controller.textureAssetUrl = texture.url;
|
||||
textures.set(texture.sourcePath.toLowerCase(), texture);
|
||||
}
|
||||
if (controller.type !== "sprite") continue;
|
||||
result.spriteControllers++;
|
||||
if (!controller.texture) {
|
||||
@@ -168,7 +370,9 @@ try {
|
||||
}
|
||||
}
|
||||
if (result.spriteControllers === 0) result.errors.push("No sprite controllers decoded");
|
||||
result.unsupported.push(...decoded.unsupported);
|
||||
result.unsupported.push(...decoded.unsupported.filter((type) => !(
|
||||
model === URGENT_IMPACT_MODEL && type === "CRuEntity_Particle_Visualizer_Entity" && runeMesh
|
||||
)));
|
||||
result.textures = textures.size;
|
||||
result.status = result.errors.length || result.unsupported.length ? "partial" : "decoded";
|
||||
const unsupportedControllers = decoded.motions.flatMap((motion) => motion.controllers
|
||||
@@ -176,14 +380,26 @@ try {
|
||||
.map((controller) => ({ motion, controller })));
|
||||
// The holy casting graph's sole non-particle controller is a 3D WAV
|
||||
// callback. Its sprite layers are complete; audio is packaged separately.
|
||||
const audioOnlyTrigger = unsupportedControllers.length === 1
|
||||
const globalAlphaControllers = decoded.motions.flatMap((motion) => motion.controllers
|
||||
.filter((controller) => controller.type === "global-alpha")
|
||||
.map((controller) => ({ motion, controller })));
|
||||
const [expectedTriggers, expectedAlpha] = expectedSpecialControllerCounts.get(model) ?? [0, 0];
|
||||
const visualOnlyControllers = unsupportedControllers.length === expectedTriggers
|
||||
&& globalAlphaControllers.length === expectedAlpha
|
||||
&& unsupportedControllers.every(({ motion, controller }) => (
|
||||
visualOnlyHolyCastTrigger(model, motion, controller)
|
||||
|| visualOnlyRisingTideTrigger(model, motion, controller)
|
||||
))
|
||||
&& globalAlphaControllers.every(({ motion, controller }) => (
|
||||
verifiedGlobalAlpha(model, motion, controller)
|
||||
))
|
||||
&& result.unsupported.every((type) => type === "CRuController_EventTrigger");
|
||||
result.rendererReady = verifiedRuneWakerParticleModels.has(model)
|
||||
&& result.errors.length === 0
|
||||
&& (result.unsupported.length === 0 || audioOnlyTrigger);
|
||||
&& visualOnlyControllers;
|
||||
result.partialRendererReady = verifiedRuneWakerParticleModels.has(model)
|
||||
&& result.errors.length === 0
|
||||
&& verifiedUrgentPartial(model, decoded, result, unsupportedControllers, globalAlphaControllers, runeMesh);
|
||||
const graph = {
|
||||
schemaVersion: 1,
|
||||
kind: "runewaker-ros-particle-archive",
|
||||
@@ -192,6 +408,11 @@ try {
|
||||
sourceSha256,
|
||||
coordinateTransform: "three(x,y,z)=(runewaker.x,runewaker.y,-runewaker.z)",
|
||||
rendererReady: result.rendererReady,
|
||||
...(result.partialRendererReady ? {
|
||||
partialRendererReady: true,
|
||||
visualFidelity: "partial-original",
|
||||
omittedVisuals: urgentOmittedVisuals,
|
||||
} : {}),
|
||||
shader: {
|
||||
name: "ruSHADER_PARTICLE_SPRITEEX",
|
||||
sourceImplementation: "Runewaker/SourceCodeVS2019/Libraries/Rune Engine/Rune/Direct3D/Shader/SMUNI/RuShader_SMUNI_Particle.cpp",
|
||||
@@ -212,6 +433,14 @@ try {
|
||||
},
|
||||
},
|
||||
textures: [...textures.values()],
|
||||
...(runeMesh ? { runeMesh: {
|
||||
sourceModel: runeMesh.sourceModel,
|
||||
sourceSha256: runeMesh.sourceSha256,
|
||||
sourceTexture: runeMesh.sourceTexture,
|
||||
textureAssetUrl: runeMesh.textureAssetUrl,
|
||||
positions: runeMesh.positions,
|
||||
uvs: runeMesh.uvs,
|
||||
} } : {}),
|
||||
motions: decoded.motions,
|
||||
sprites: decoded.sprites,
|
||||
unsupported: [...new Set(result.unsupported)],
|
||||
|
||||
@@ -18,7 +18,7 @@ function assetFile(url) {
|
||||
}
|
||||
|
||||
test("primary-five RuneWaker particle archive resolves its decoded graphs and textures", () => {
|
||||
assert.equal(index.requestedModels, 123);
|
||||
assert.equal(index.requestedModels, 124);
|
||||
assert.equal(index.processedModels, index.requestedModels);
|
||||
assert.equal(index.failedModels, 0);
|
||||
assert.equal(index.results.length, index.requestedModels);
|
||||
@@ -30,7 +30,9 @@ test("primary-five RuneWaker particle archive resolves its decoded graphs and te
|
||||
assert.equal(graph.kind, "runewaker-ros-particle-archive");
|
||||
assert.equal(graph.sourceModel, result.model);
|
||||
assert.equal(graph.rendererReady, result.rendererReady);
|
||||
assert.equal(Boolean(graph.partialRendererReady), result.partialRendererReady);
|
||||
assert.equal(result.rendererReady, verifiedRuneWakerParticleModels.has(result.model)
|
||||
&& !result.partialRendererReady
|
||||
&& result.errors.length === 0
|
||||
&& result.unsupported.every((type) => type === "CRuController_EventTrigger"));
|
||||
assert.equal(graph.shader.name, "ruSHADER_PARTICLE_SPRITEEX");
|
||||
@@ -56,6 +58,37 @@ test("primary-five RuneWaker particle archive resolves its decoded graphs and te
|
||||
if (controller.type === "emitter") assert.equal(controller.tracks.length, 27);
|
||||
}
|
||||
}
|
||||
assert.equal(spriteControllers, 510);
|
||||
assert.ok(textureReferences >= 103);
|
||||
assert.equal(spriteControllers, 515);
|
||||
assert.ok(textureReferences >= 104);
|
||||
});
|
||||
|
||||
test("Rising Tide ships three original renderable phases and Urgent Heal declares its partial target layers", () => {
|
||||
for (const model of [
|
||||
"model/fx/skill/ice/act_icecone01_c_hand.ros",
|
||||
"model/fx/skill/ice/act_icecone01_f.ros",
|
||||
"model/fx/skill/ice/act_riptide_t_freeze_hit.ros",
|
||||
]) {
|
||||
const result = index.results.find((entry) => entry.model === model);
|
||||
assert.equal(result?.rendererReady, true, model);
|
||||
assert.equal(result?.partialRendererReady, false, model);
|
||||
}
|
||||
const urgent = index.results.find((entry) =>
|
||||
entry.model === "model/fx/skill/holy/act_benedictory_heal_t_down.ros");
|
||||
assert.equal(urgent?.rendererReady, false);
|
||||
assert.equal(urgent?.partialRendererReady, true);
|
||||
assert.equal(urgent?.spriteControllers, 2);
|
||||
const graph = JSON.parse(fs.readFileSync(assetFile(urgent.graphUrl), "utf8"));
|
||||
assert.equal(graph.visualFidelity, "partial-original");
|
||||
assert.deepEqual(graph.omittedVisuals, ["CRuEntity_Decal:terrain-projection"]);
|
||||
assert.equal(graph.runeMesh.sourceModel, "model/fx/skill/holy/_polygon/rune_cylinder.ros");
|
||||
assert.equal(graph.runeMesh.positions.length, 72 * 9);
|
||||
assert.equal(graph.runeMesh.uvs.length, 72 * 6);
|
||||
assert.ok(graph.textures.some((texture) => texture.url === graph.runeMesh.textureAssetUrl));
|
||||
const impactControllers = graph.motions.find((motion) => motion.name === "explode").controllers;
|
||||
assert.ok(impactControllers.some((controller) => controller.type === "entity-visualizer"));
|
||||
const decal = impactControllers.find((controller) => controller.type === "decal");
|
||||
assert.ok(decal);
|
||||
assert.ok(graph.textures.some((texture) => texture.url === decal.textureAssetUrl));
|
||||
assert.ok(impactControllers.some((controller) => controller.type === "global-color"));
|
||||
assert.ok(impactControllers.some((controller) => controller.type === "global-alpha"));
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
export const verifiedRuneWakerParticlePhases = [
|
||||
{ spellId: 491147, phase: "cast-start", model: "model/fx/skill/holy/act_casting_c_hand.ros", loopCastSound: true },
|
||||
{ spellId: 491147, phase: "impact", model: "model/fx/skill/holy/act_benedictory_heal_t_down.ros", partialVisual: true },
|
||||
{ spellId: 490261, phase: "cast-start", model: "model/fx/skill/holy/act_casting_c_hand.ros", loopCastSound: true },
|
||||
{ spellId: 490281, phase: "cast-start", model: "model/fx/skill/holy/act_casting_c_hand.ros", loopCastSound: true },
|
||||
{ spellId: 490256, phase: "cast-start", model: "model/fx/skill/ice/act_icecone01_c_hand.ros" },
|
||||
{ spellId: 490256, phase: "release", model: "model/fx/skill/ice/act_icecone01_f.ros" },
|
||||
{ spellId: 490256, phase: "impact", model: "model/fx/skill/ice/act_riptide_t_freeze_hit.ros" },
|
||||
{ spellId: 490150, phase: "cast-start", model: "model/fx/skill/physical/act_charge02_c_down.ros" },
|
||||
{ spellId: 490175, phase: "impact", model: "model/fx/hit_fx/act_shieldstorm_t_freeze_hit_e.ros", scale: 2.5 },
|
||||
{ spellId: 491168, phase: "release", model: "model/fx/skill/wind/act_electric_ball_f_i.ros" },
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,9 @@
|
||||
#include "Rune Engine/Rune/Scene/Base/RuHierarchy_GR2.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_Hierarchy.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_EventTrigger.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_GlobalAlpha.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_Decal.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_Particle_Visualizer_Entity.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_Particle_Emitter.h"
|
||||
#include "Rune Engine/Rune/Scene/Controller/RuController_Particle_Visualizer_Sprite.h"
|
||||
#include "Rune Engine/Rune/Scene/Terrain/RuWorld_Base.h"
|
||||
@@ -1507,6 +1510,20 @@ namespace
|
||||
{ WriteFxRealTrack(out, **track); return; }
|
||||
if(CRuAnimTrack<CRuColor4> **track = boost::get<CRuAnimTrack<CRuColor4> *>(&variant))
|
||||
{ WriteFxColorTrack(out, **track); return; }
|
||||
if(CRuAnimTrack<char *> **track = boost::get<CRuAnimTrack<char *> *>(&variant))
|
||||
{
|
||||
out << "[";
|
||||
for(INT32 key = 0; key < (*track)->GetNumKeys(); ++key)
|
||||
{
|
||||
REAL time = 0; char *value = NULL;
|
||||
(*track)->GetKeyTimeByIndex(key, &time); (*track)->GetKeyByIndex(key, &value);
|
||||
if(key) out << ",";
|
||||
out << "{\"time\":" << FiniteFxValue(time) << ",\"value\":\""
|
||||
<< JsonEscape(value ? value : "") << "\"}";
|
||||
}
|
||||
out << "]";
|
||||
return;
|
||||
}
|
||||
out << "[";
|
||||
if(CRuAnimTrack<INT32> **track = boost::get<CRuAnimTrack<INT32> *>(&variant))
|
||||
{
|
||||
@@ -1565,9 +1582,14 @@ namespace
|
||||
const bool isSprite = base->GetType().IsSubClassOf(CRuController_Particle_Visualizer_Sprite::Type());
|
||||
const bool isEmitter = base->GetType().IsSubClassOf(CRuController_Particle_Emitter::Type());
|
||||
const bool isEventTrigger = base->GetType().IsSubClassOf(CRuController_EventTrigger::Type());
|
||||
const bool isGlobalAlpha = base->GetType().IsSubClassOf(CRuController_GlobalAlpha::Type());
|
||||
const bool isEntityVisualizer = base->GetType().IsSubClassOf(CRuController_Particle_Visualizer_Entity::Type());
|
||||
const bool isDecal = base->GetType().IsSubClassOf(CRuController_Decal::Type());
|
||||
const bool isGlobalColor = base->GetType().IsSubClassOf(CRuController_GlobalColor::Type());
|
||||
if(wroteController) out << ",";
|
||||
wroteController = true;
|
||||
out << "{\"type\":\"" << (isSprite ? "sprite" : isEmitter ? "emitter" : "unsupported")
|
||||
out << "{\"type\":\"" << (isSprite ? "sprite" : isEmitter ? "emitter" : isGlobalAlpha ? "global-alpha"
|
||||
: isEntityVisualizer ? "entity-visualizer" : isDecal ? "decal" : isGlobalColor ? "global-color" : "unsupported")
|
||||
<< "\",\"sourceType\":\"" << JsonEscape(base->GetType().GetTypeName())
|
||||
<< "\",\"anchor\":\"" << JsonEscape(node->GetAnchorTarget() ? node->GetAnchorTarget() : "") << "\"";
|
||||
if(isEventTrigger)
|
||||
@@ -1580,6 +1602,37 @@ namespace
|
||||
<< ",\"eventDuration\":" << FiniteFxValue(trigger->GetEventDuration())
|
||||
<< ",\"startTime\":" << FiniteFxValue(trigger->GetStartTime());
|
||||
}
|
||||
if(isGlobalAlpha)
|
||||
{
|
||||
CRuController_GlobalAlpha *alpha = static_cast<CRuController_GlobalAlpha *>(base);
|
||||
out << ",\"startTime\":" << FiniteFxValue(alpha->GetStartTime())
|
||||
<< ",\"duration\":" << FiniteFxValue(alpha->GetDuration())
|
||||
<< ",\"alphaTrack\":";
|
||||
WriteFxControllerTrack(out, alpha, 1);
|
||||
}
|
||||
if(isEntityVisualizer)
|
||||
{
|
||||
CRuController_Particle_Visualizer_Entity *visualizer = static_cast<CRuController_Particle_Visualizer_Entity *>(base);
|
||||
out << ",\"entityName\":\"" << JsonEscape(visualizer->GetEntityName()) << "\""
|
||||
<< ",\"orientToTravel\":" << (visualizer->GetOrientToDirectionOfTravel() ? "true" : "false");
|
||||
}
|
||||
if(isDecal)
|
||||
{
|
||||
CRuController_Decal *decal = static_cast<CRuController_Decal *>(base);
|
||||
RuAnimTrackVariant textureVariant;
|
||||
const char *textureName = "";
|
||||
if(decal->GetTrackByIndex(0, textureVariant))
|
||||
{
|
||||
if(CRuAnimTrack<char *> **track = boost::get<CRuAnimTrack<char *> *>(&textureVariant))
|
||||
{
|
||||
char *firstKey = NULL;
|
||||
if((*track)->GetNumKeys() > 0 && (*track)->GetKeyByIndex(0, &firstKey) && firstKey)
|
||||
textureName = firstKey;
|
||||
}
|
||||
}
|
||||
out << ",\"decalTexture\":\"" << JsonEscape(textureName) << "\""
|
||||
<< ",\"billboardType\":" << static_cast<INT32>(decal->GetBillboardType());
|
||||
}
|
||||
if(isSprite)
|
||||
{
|
||||
CRuController_Particle_Visualizer_Sprite *sprite = static_cast<CRuController_Particle_Visualizer_Sprite *>(base);
|
||||
@@ -1616,7 +1669,7 @@ namespace
|
||||
out << ",\"emitterOrientation\":" << static_cast<INT32>(emitter->GetEmitterOrientation());
|
||||
out << ",\"prerunTime\":" << FiniteFxValue(emitter->GetEmitterPrerunTime());
|
||||
}
|
||||
if(isSprite || isEmitter)
|
||||
if(isSprite || isEmitter || isEntityVisualizer || isDecal || isGlobalColor)
|
||||
{
|
||||
IRuEntity_KeyableController *keyable = static_cast<IRuEntity_KeyableController *>(base);
|
||||
out << ",\"tracks\":[";
|
||||
|
||||
@@ -42,8 +42,10 @@ function Preview() {
|
||||
useEffect(() => {
|
||||
void fetch("/assets/game/combat-effects/original/runewaker/manifest.json")
|
||||
.then((response) => response.json())
|
||||
.then((index: { results: { model: string; graphUrl: string; status: string; rendererReady?: boolean }[] }) => {
|
||||
const available = index.results.filter((entry) => entry.status === "decoded" || entry.rendererReady)
|
||||
.then((index: { results: { model: string; graphUrl: string; status: string;
|
||||
rendererReady?: boolean; partialRendererReady?: boolean }[] }) => {
|
||||
const available = index.results.filter((entry) =>
|
||||
entry.status === "decoded" || entry.rendererReady || entry.partialRendererReady)
|
||||
.map(({ model, graphUrl }) => ({ model, graphUrl }));
|
||||
setChoices(available);
|
||||
setSelected(available.find((entry) => entry.model.includes("electric_ball_t_freeze_hit"))?.graphUrl
|
||||
@@ -83,8 +85,11 @@ function Preview() {
|
||||
<label><input type="checkbox" checked={freezeAt} onChange={(event) => { setFreezeAt(event.target.checked); setReplay(replay + 1); }} />Freeze at 250 ms</label>
|
||||
</div>
|
||||
<div role="status" style={{ padding: "0 20px 8px" }}>{error || (loaded
|
||||
? `Ready · ${runeLayers(loaded.graph, phase).length} sprite layers · ${loaded.graph.textures.length} textures`
|
||||
? `${loaded.graph.partialRendererReady ? "Partial original" : "Ready"} · ${runeLayers(loaded.graph, phase).length} sprite layers · ${loaded.graph.textures.length} textures`
|
||||
: "Loading RuneWaker media…")}</div>
|
||||
{loaded?.graph.partialRendererReady && <div style={{ padding: "0 20px 8px" }}>
|
||||
Fidelity limit: {loaded.graph.omittedVisuals?.join(", ")}
|
||||
</div>}
|
||||
<div style={{ height: "calc(100vh - 100px)" }}>
|
||||
<Canvas camera={{ position: [3, 2.5, 5], fov: 45 }}>
|
||||
<color attach="background" args={["#0b121b"]} />
|
||||
|
||||
@@ -208,6 +208,62 @@ describe("combat audio output policy", () => {
|
||||
audio.resetCombatAudio();
|
||||
});
|
||||
|
||||
it("positions release audio at the caster and impact audio at the target", async () => {
|
||||
vi.resetModules();
|
||||
const audio = await import("./combatAudio");
|
||||
const panners: Array<{
|
||||
positionX: { value: number };
|
||||
positionY: { value: number };
|
||||
positionZ: { value: number };
|
||||
}> = [];
|
||||
const audioContext = {
|
||||
currentTime: 0, state: "running", destination: {},
|
||||
createGain: () => ({ connect: vi.fn(), disconnect: vi.fn(), gain: {
|
||||
setTargetAtTime: vi.fn(), value: 1,
|
||||
} }),
|
||||
createPanner: () => {
|
||||
const panner = {
|
||||
connect: vi.fn(), disconnect: vi.fn(),
|
||||
positionX: { value: 0 }, positionY: { value: 0 }, positionZ: { value: 0 },
|
||||
panningModel: "HRTF", distanceModel: "inverse",
|
||||
refDistance: 0, maxDistance: 0, rolloffFactor: 0,
|
||||
};
|
||||
panners.push(panner);
|
||||
return panner;
|
||||
},
|
||||
createBufferSource: () => ({
|
||||
connect: vi.fn(), disconnect: vi.fn(), start: vi.fn(), stop: vi.fn(),
|
||||
buffer: null, loop: false, onended: null,
|
||||
}),
|
||||
decodeAudioData: vi.fn(async () => ({ duration: 0.2 })),
|
||||
resume: vi.fn(async () => undefined),
|
||||
};
|
||||
vi.stubGlobal("window", { AudioContext: function () { return audioContext; } });
|
||||
combatMedia.ready = true;
|
||||
combatMedia.entry.mockReturnValue({
|
||||
key: "runewaker:490256", source: "runewaker", spellId: 490256,
|
||||
sounds: [
|
||||
{ url: "/assets/game/combat-effects/audio/rising-release.mp3", phase: "release" },
|
||||
{ url: "/assets/game/combat-effects/audio/rising-impact.mp3", phase: "impact" },
|
||||
],
|
||||
});
|
||||
combatMedia.fetch.mockImplementation(async () => new Response(new Uint8Array([1, 2, 3])));
|
||||
audio.configureCombatAudio(DEFAULT_COMBAT_PRESENTATION_SETTINGS, 0.6);
|
||||
const release = {
|
||||
...fixtureEvent, source: "runewaker" as const, sourceSpellId: 490256,
|
||||
phase: "release" as const, origin: [1, 2, 3] as const,
|
||||
targetPosition: [9, 8, 7] as const,
|
||||
};
|
||||
audio.playCombatAudio(release);
|
||||
audio.playCombatAudio({ ...release, id: 2, phase: "impact" });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(panners.map((panner) => [
|
||||
panner.positionX.value, panner.positionY.value, panner.positionZ.value,
|
||||
])).toEqual([[1, 2, 3], [9, 8, 7]]);
|
||||
audio.resetCombatAudio();
|
||||
});
|
||||
|
||||
it("loops only a flagged cast sound and stops the matching actor and ability", async () => {
|
||||
vi.resetModules();
|
||||
const audio = await import("./combatAudio");
|
||||
|
||||
@@ -128,15 +128,18 @@ function positionPanner(
|
||||
event: CombatPresentationEvent,
|
||||
): PannerNode | StereoPannerNode {
|
||||
if (typeof audioContext.createPanner === "function") {
|
||||
const position = event.phase === "impact"
|
||||
? event.targetPosition ?? event.origin
|
||||
: event.origin;
|
||||
const panner = audioContext.createPanner();
|
||||
panner.panningModel = "HRTF";
|
||||
panner.distanceModel = "inverse";
|
||||
panner.refDistance = event.actorGroup === "player" ? 8 : 5;
|
||||
panner.maxDistance = 55;
|
||||
panner.rolloffFactor = event.actorGroup === "player" ? 0.15 : 0.8;
|
||||
panner.positionX.value = event.origin[0];
|
||||
panner.positionY.value = event.origin[1];
|
||||
panner.positionZ.value = event.origin[2];
|
||||
panner.positionX.value = position[0];
|
||||
panner.positionY.value = position[1];
|
||||
panner.positionZ.value = position[2];
|
||||
return panner;
|
||||
}
|
||||
return audioContext.createStereoPanner();
|
||||
|
||||
@@ -178,6 +178,8 @@ export interface CombatEffectPhaseAsset {
|
||||
/** Browser-ready graph generated from the original client asset. */
|
||||
readonly effectGraphUrl?: string;
|
||||
readonly effectKind?: "wow-m2-particles" | "wow-m2-static-mesh" | "wow-m2-model" | "runewaker-particles";
|
||||
/** A source graph with verified sprite layers but omitted unsupported visual controllers. */
|
||||
readonly visualCompleteness?: "partial";
|
||||
readonly originalSourceSha256?: string;
|
||||
}
|
||||
|
||||
|
||||
+50
-30
@@ -34,6 +34,7 @@ import {
|
||||
updateCombatEffectQueue,
|
||||
originalInstantCastEvent,
|
||||
originalInstantTerminalEvent,
|
||||
risingTideArrivalDelayMs,
|
||||
type ActiveCombatEffect,
|
||||
} from "./combatEffectRuntime";
|
||||
import { OriginalCombatEffect } from "./OriginalCombatEffect";
|
||||
@@ -296,37 +297,56 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
|
||||
activeRef.current = active;
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => subscribeCombatPresentation((message) => {
|
||||
if (message.kind === "reset") {
|
||||
setEffects([]);
|
||||
resetCombatAudio();
|
||||
return;
|
||||
}
|
||||
const event = message.event;
|
||||
if (activeRef.current) playCombatAudio(event);
|
||||
setEffects((current) => {
|
||||
const withoutCast = event.phase === "release" || event.phase === "cast-cancel"
|
||||
? current.filter((effect) => !(
|
||||
effect.event.phase === "cast-start"
|
||||
&& effect.event.sourceActorId === event.sourceActorId
|
||||
&& effect.event.abilityId === event.abilityId
|
||||
))
|
||||
: current;
|
||||
const presentationSettings = settingsRef.current;
|
||||
const quality = qualityRef.current;
|
||||
if (
|
||||
event.phase === "cast-cancel"
|
||||
|| !activeRef.current
|
||||
|| quality === "off"
|
||||
|| !presentationSettings.vfxEnabled[event.actorGroup]
|
||||
) return withoutCast;
|
||||
const withInstantSourceCast = effectiveCombatVfxMode(presentationSettings) === "original"
|
||||
&& originalInstantCastEvent(event)
|
||||
? addOriginalInstantCastEffect(withoutCast, event, quality)
|
||||
: withoutCast;
|
||||
return updateCombatEffectQueue(withInstantSourceCast, event, quality);
|
||||
useEffect(() => {
|
||||
const pendingArrivals = new Set<ReturnType<typeof setTimeout>>();
|
||||
const present = (event: CombatPresentationEvent) => {
|
||||
if (activeRef.current) playCombatAudio(event);
|
||||
setEffects((current) => {
|
||||
const withoutCast = event.phase === "release" || event.phase === "cast-cancel"
|
||||
? current.filter((effect) => !(
|
||||
effect.event.phase === "cast-start"
|
||||
&& effect.event.sourceActorId === event.sourceActorId
|
||||
&& effect.event.abilityId === event.abilityId
|
||||
))
|
||||
: current;
|
||||
const presentationSettings = settingsRef.current;
|
||||
const quality = qualityRef.current;
|
||||
if (
|
||||
event.phase === "cast-cancel"
|
||||
|| !activeRef.current
|
||||
|| quality === "off"
|
||||
|| !presentationSettings.vfxEnabled[event.actorGroup]
|
||||
) return withoutCast;
|
||||
const withInstantSourceCast = effectiveCombatVfxMode(presentationSettings) === "original"
|
||||
&& originalInstantCastEvent(event)
|
||||
? addOriginalInstantCastEffect(withoutCast, event, quality)
|
||||
: withoutCast;
|
||||
return updateCombatEffectQueue(withInstantSourceCast, event, quality);
|
||||
});
|
||||
};
|
||||
const unsubscribe = subscribeCombatPresentation((message) => {
|
||||
if (message.kind === "reset") {
|
||||
for (const timer of pendingArrivals) clearTimeout(timer);
|
||||
pendingArrivals.clear();
|
||||
setEffects([]);
|
||||
resetCombatAudio();
|
||||
return;
|
||||
}
|
||||
const event = message.event;
|
||||
const arrivalDelay = risingTideArrivalDelayMs(event);
|
||||
if (arrivalDelay > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
pendingArrivals.delete(timer);
|
||||
present(event);
|
||||
}, arrivalDelay);
|
||||
pendingArrivals.add(timer);
|
||||
} else present(event);
|
||||
});
|
||||
}), []);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
for (const timer of pendingArrivals) clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => resetCombatAudio(), []);
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
updateMobRuntimePosition,
|
||||
} from "../game/mobRuntimeRegistry";
|
||||
import { combatHasLineOfSight } from "../game/combatSpatial";
|
||||
import { registerCombatEffectTargetVisualSize } from "./combatEffectAnchorRegistry";
|
||||
import { registerDungeonRuntimeReset } from "../game/dungeonRuntimeReset";
|
||||
import { scaledDungeonEnemyLevel } from "../game/dungeonLevelScaling";
|
||||
import { dungeonEnemyScaling } from "../game/dungeonBossScaling";
|
||||
@@ -909,6 +910,8 @@ function ProxyMob({
|
||||
const proceduralModelAnimation = model?.animationMode === "procedural";
|
||||
const labelHeight = model?.labelHeight ?? (boss ? 3.25 : 2.65) * scale;
|
||||
const markerRadius = model?.markerRadius ?? 0.9 * scale;
|
||||
useEffect(() => registerCombatEffectTargetVisualSize(instanceId,
|
||||
{ height: labelHeight, radius: markerRadius }), [instanceId, labelHeight, markerRadius]);
|
||||
const dead = mob?.dead ?? false;
|
||||
const lootable = Boolean(dead && mob?.hasLoot && !mob.looted);
|
||||
const attackRevision = mob?.attackRevision ?? 0;
|
||||
|
||||
@@ -49,8 +49,10 @@ import {
|
||||
parseOriginalCombatMeshGraph,
|
||||
type OriginalCombatMeshGraph,
|
||||
} from "./originalCombatMeshRuntime";
|
||||
import { updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry";
|
||||
import { shouldUseGeneratedOriginalFallback } from "./combatEffectRuntime";
|
||||
import { combatEffectTargetVisualSize, updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry";
|
||||
import {
|
||||
projectileTravelDurationMs, risingTideImpactPlacement, shouldUseGeneratedOriginalFallback,
|
||||
} from "./combatEffectRuntime";
|
||||
import { OriginalWowModel } from "./OriginalWowModel";
|
||||
import { OriginalParticleGeometry } from "./OriginalParticleGeometry";
|
||||
import { RunewakerParticleEffect } from "./RunewakerParticleEffect";
|
||||
@@ -722,13 +724,7 @@ function LoadedOriginalCombatEffect({
|
||||
const projectile = event.phase === "release"
|
||||
&& event.delivery === "projectile"
|
||||
&& event.targetPosition !== undefined;
|
||||
const projectileDurationMs = projectile && event.targetPosition
|
||||
? Math.max(180, Math.min(550, Math.hypot(
|
||||
event.targetPosition[0] - event.origin[0],
|
||||
event.targetPosition[1] - event.origin[1],
|
||||
event.targetPosition[2] - event.origin[2],
|
||||
) / 28 * 1_000))
|
||||
: 0;
|
||||
const projectileDurationMs = projectile ? projectileTravelDurationMs(event) : 0;
|
||||
const anchor = event.phase === "cast-start" || projectile
|
||||
? event.origin
|
||||
: event.targetPosition ?? event.origin;
|
||||
@@ -796,11 +792,17 @@ function LoadedOriginalCombatEffect({
|
||||
<group ref={groupRef} position={anchor}>
|
||||
{loaded.map((item, loadedIndex) => {
|
||||
const graph = item.graph;
|
||||
const scale = effectScale(item.asset);
|
||||
const risingTideTarget = event.phase === "impact" && event.sourceSpellId === 490256
|
||||
&& item.asset.model?.toLowerCase() === "model/fx/skill/ice/act_riptide_t_freeze_hit.ros"
|
||||
? risingTideImpactPlacement(event, combatEffectTargetVisualSize(event.targetActorId))
|
||||
: null;
|
||||
const scale = effectScale(item.asset) * (risingTideTarget?.scale ?? 1);
|
||||
return (
|
||||
<group
|
||||
key={`${item.asset.effectGraphUrl}:${item.asset.model ?? "model"}`}
|
||||
position={[0, effectPlacementHeight(item.asset, graph, event), 0]}
|
||||
position={risingTideTarget
|
||||
? [risingTideTarget.offsetX, risingTideTarget.height, risingTideTarget.offsetZ]
|
||||
: [0, effectPlacementHeight(item.asset, graph, event), 0]}
|
||||
scale={scale}
|
||||
>
|
||||
{graph.kind === "wow-m2-model" && <OriginalWowModel graph={graph} textures={item.textures}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { memo, useEffect, useMemo, useRef, type MutableRefObject } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import {
|
||||
AddEquation, BufferAttribute, CustomBlending, DoubleSide, DynamicDrawUsage,
|
||||
AddEquation, AdditiveBlending, BufferAttribute, BufferGeometry, ClampToEdgeWrapping,
|
||||
CustomBlending, DoubleSide, DynamicDrawUsage,
|
||||
InstancedBufferAttribute, InstancedBufferGeometry, OneFactor, OneMinusSrcAlphaFactor,
|
||||
ShaderMaterial, Vector2, ZeroFactor, type Texture,
|
||||
Mesh, MeshBasicMaterial, PlaneGeometry, RepeatWrapping, ShaderMaterial, Vector2, ZeroFactor,
|
||||
type Texture,
|
||||
} from "three";
|
||||
import type { CombatPresentationPhase, CombatVfxQuality } from "../game/combatPresentation";
|
||||
import {
|
||||
runeEmissionsBetween, runeLayers, runeSample, type RuneParticleLayer, type RuneVector3,
|
||||
type RunewakerParticleGraph,
|
||||
runeEmissionsBetween, runeGlobalAlpha, runeLayers, runeMotion, runeSample,
|
||||
type RuneDecalController, type RuneEmitterController, type RuneEntityVisualizerController,
|
||||
type RuneGlobalAlphaController, type RuneGlobalColorController, type RuneParticleLayer,
|
||||
type RuneVector3, type RunewakerParticleGraph,
|
||||
} from "./runewakerParticleRuntime";
|
||||
|
||||
interface Particle {
|
||||
@@ -32,6 +36,7 @@ const vertexShader = /* glsl */ `
|
||||
varying vec4 vColor;
|
||||
varying float vFrame;
|
||||
uniform vec2 quadOffset;
|
||||
uniform float billboardType;
|
||||
|
||||
void main() {
|
||||
vec4 viewPosition = modelViewMatrix * vec4(instancePosition, 1.0);
|
||||
@@ -39,7 +44,16 @@ const vertexShader = /* glsl */ `
|
||||
float s = sin(instanceRotation);
|
||||
float c = cos(instanceRotation);
|
||||
vec2 rotated = vec2(local.x * c - local.y * s, local.x * s + local.y * c);
|
||||
viewPosition.xy += rotated * length(modelViewMatrix[0].xyz);
|
||||
if (billboardType == 2.0) {
|
||||
vec3 worldPosition = (modelMatrix * vec4(instancePosition, 1.0)).xyz;
|
||||
vec3 toCamera = normalize(cameraPosition - worldPosition);
|
||||
vec3 right = cross(vec3(0.0, 1.0, 0.0), toCamera);
|
||||
right = length(right) < 0.001 ? vec3(1.0, 0.0, 0.0) : normalize(right);
|
||||
vec3 worldOffset = right * rotated.x + vec3(0.0, 1.0, 0.0) * rotated.y;
|
||||
viewPosition.xyz += (viewMatrix * vec4(worldOffset, 0.0)).xyz * length(modelMatrix[0].xyz);
|
||||
} else {
|
||||
viewPosition.xy += rotated * length(modelViewMatrix[0].xyz);
|
||||
}
|
||||
gl_Position = projectionMatrix * viewPosition;
|
||||
vUv = uv;
|
||||
vColor = instanceColor;
|
||||
@@ -78,68 +92,74 @@ function random(seed: number): number {
|
||||
return ((value ^ value >>> 14) >>> 0) / 4_294_967_296;
|
||||
}
|
||||
|
||||
function vectorAt(layer: RuneParticleLayer, track: number, time: number, fallback: RuneVector3): RuneVector3 {
|
||||
return runeSample(layer.emitter.tracks[track] as { time: number; value: RuneVector3 }[], time, fallback);
|
||||
function vectorAt(emitter: RuneEmitterController, track: number, time: number, fallback: RuneVector3): RuneVector3 {
|
||||
return runeSample(emitter.tracks[track] as { time: number; value: RuneVector3 }[], time, fallback);
|
||||
}
|
||||
|
||||
function scalarAt(layer: RuneParticleLayer, track: number, time: number, fallback = 0): number {
|
||||
return runeSample(layer.emitter.tracks[track] as { time: number; value: number }[], time, fallback);
|
||||
function scalarAt(emitter: RuneEmitterController, track: number, time: number, fallback = 0): number {
|
||||
return runeSample(emitter.tracks[track] as { time: number; value: number }[], time, fallback);
|
||||
}
|
||||
|
||||
function spawn(layer: RuneParticleLayer, time: number, seed: number): Particle {
|
||||
function spawn(emitter: RuneEmitterController, position: RuneVector3,
|
||||
randomRotation: boolean, time: number, seed: number): Particle {
|
||||
const randomAt = (index: number) => random(seed + index * 1619);
|
||||
const kind = Math.round(scalarAt(layer, 1, time));
|
||||
const emitterPosition = vectorAt(layer, 2, time, [0, 0, 0]);
|
||||
const extents = vectorAt(layer, 3, time, [0, 0, 0]);
|
||||
const radius = scalarAt(layer, 4, time);
|
||||
const kind = Math.round(scalarAt(emitter, 1, time));
|
||||
const emitterPosition = vectorAt(emitter, 2, time, [0, 0, 0]);
|
||||
const extents = vectorAt(emitter, 3, time, [0, 0, 0]);
|
||||
const radius = scalarAt(emitter, 4, time);
|
||||
let offset: RuneVector3 = [0, 0, 0];
|
||||
let radialDirection: RuneVector3 | null = null;
|
||||
if (kind === 1) {
|
||||
offset = [(randomAt(1) * 2 - 1) * extents[0],
|
||||
(randomAt(2) * 2 - 1) * extents[1], (randomAt(3) * 2 - 1) * extents[2]];
|
||||
} else if (kind === 4) {
|
||||
} else if (kind === 4 || kind === 5) {
|
||||
const angle = randomAt(1) * Math.PI * 2;
|
||||
const distance = randomAt(2) * radius;
|
||||
const orientation = layer.emitter.emitterOrientation;
|
||||
offset = orientation === 1 || orientation === 4
|
||||
? [0, Math.sin(angle) * distance, Math.cos(angle) * distance]
|
||||
const distance = kind === 5 ? radius : randomAt(2) * radius;
|
||||
const orientation = emitter.emitterOrientation;
|
||||
const radial = orientation === 1 || orientation === 4
|
||||
? [0, Math.sin(angle), Math.cos(angle)] as RuneVector3
|
||||
: orientation === 2 || orientation === 5
|
||||
? [Math.sin(angle) * distance, 0, Math.cos(angle) * distance]
|
||||
: [Math.sin(angle) * distance, Math.cos(angle) * distance, 0];
|
||||
? [Math.sin(angle), 0, Math.cos(angle)] as RuneVector3
|
||||
: [Math.sin(angle), Math.cos(angle), 0] as RuneVector3;
|
||||
offset = [radial[0] * distance, radial[1] * distance, radial[2] * distance];
|
||||
radialDirection = radial;
|
||||
}
|
||||
const direction = vectorAt(layer, 10, time, [0, 1, 0]);
|
||||
const deviation = Math.min(Math.PI, Math.abs(scalarAt(layer, 11, time)));
|
||||
const direction = radialDirection ?? vectorAt(emitter, 10, time, [0, 1, 0]);
|
||||
const deviation = radialDirection ? 0 : Math.min(Math.PI, Math.abs(scalarAt(emitter, 11, time)));
|
||||
const spread = Math.tan(deviation * 0.5);
|
||||
const dx = direction[0] + (randomAt(4) * 2 - 1) * spread;
|
||||
const dy = direction[1] + (randomAt(5) * 2 - 1) * spread;
|
||||
const dz = direction[2] + (randomAt(6) * 2 - 1) * spread;
|
||||
const magnitude = Math.max(0.00001, Math.hypot(dx, dy, dz));
|
||||
const speed = scalarAt(layer, 14, time) + randomAt(7) * (
|
||||
scalarAt(layer, 15, time) - scalarAt(layer, 14, time));
|
||||
const force0 = vectorAt(layer, 20, time, [0, 0, 0]);
|
||||
const force1 = vectorAt(layer, 21, time, [0, 0, 0]);
|
||||
const minLife = scalarAt(layer, 12, time, 0.5);
|
||||
const maxLife = scalarAt(layer, 13, time, 0.5);
|
||||
const minScale = scalarAt(layer, 16, time, 1);
|
||||
const maxScale = scalarAt(layer, 17, time, 1);
|
||||
const minOrientation = vectorAt(layer, 18, time, [0, 0, 0]);
|
||||
const maxOrientation = vectorAt(layer, 19, time, [0, 0, 0]);
|
||||
const speed = scalarAt(emitter, 14, time) + randomAt(7) * (
|
||||
scalarAt(emitter, 15, time) - scalarAt(emitter, 14, time));
|
||||
const force0 = vectorAt(emitter, 20, time, [0, 0, 0]);
|
||||
const force1 = vectorAt(emitter, 21, time, [0, 0, 0]);
|
||||
const minLife = scalarAt(emitter, 12, time, 0.5);
|
||||
const maxLife = scalarAt(emitter, 13, time, 0.5);
|
||||
const minScale = scalarAt(emitter, 16, time, 1);
|
||||
const maxScale = scalarAt(emitter, 17, time, 1);
|
||||
const minOrientation = vectorAt(emitter, 18, time, [0, 0, 0]);
|
||||
const maxOrientation = vectorAt(emitter, 19, time, [0, 0, 0]);
|
||||
return {
|
||||
born: time,
|
||||
life: Math.max(0.02, minLife + randomAt(8) * (maxLife - minLife)),
|
||||
origin: [layer.position[0] + emitterPosition[0] + offset[0],
|
||||
layer.position[1] + emitterPosition[1] + offset[1],
|
||||
-layer.position[2] - emitterPosition[2] - offset[2]],
|
||||
origin: [position[0] + emitterPosition[0] + offset[0],
|
||||
position[1] + emitterPosition[1] + offset[1],
|
||||
-position[2] - emitterPosition[2] - offset[2]],
|
||||
velocity: [dx / magnitude * speed, dy / magnitude * speed, -dz / magnitude * speed],
|
||||
acceleration: [force0[0] + force1[0], force0[1] + force1[1], -force0[2] - force1[2]],
|
||||
scale: minScale + randomAt(9) * (maxScale - minScale),
|
||||
orientation: (minOrientation[0] + randomAt(10) * (maxOrientation[0] - minOrientation[0])) * Math.PI / 180,
|
||||
rotationSign: layer.sprite.randomRotation && randomAt(11) < 0.5 ? -1 : 1,
|
||||
rotationSign: randomRotation && randomAt(11) < 0.5 ? -1 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
const RuneEmitter = memo(function RuneEmitter({
|
||||
layer, texture, budget, active, eventId, timelineRef,
|
||||
graph, phase, layer, texture, budget, active, eventId, timelineRef,
|
||||
}: {
|
||||
readonly graph: RunewakerParticleGraph;
|
||||
readonly phase: CombatPresentationPhase;
|
||||
readonly layer: RuneParticleLayer;
|
||||
readonly texture: Texture;
|
||||
readonly budget: number;
|
||||
@@ -181,6 +201,7 @@ const RuneEmitter = memo(function RuneEmitter({
|
||||
atlasColumns: { value: Math.max(1, Math.floor(width / tileWidth)) },
|
||||
frameCount: { value: Math.max(1, layer.sprite.frameCount) },
|
||||
quadOffset: { value: new Vector2(layer.sprite.quadOffset[0], layer.sprite.quadOffset[1]) },
|
||||
billboardType: { value: layer.sprite.billboardType },
|
||||
},
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
@@ -198,6 +219,7 @@ const RuneEmitter = memo(function RuneEmitter({
|
||||
useFrame(() => {
|
||||
if (!active || budget === 0) return;
|
||||
const time = timelineRef.current.elapsedMs / 1_000;
|
||||
const globalAlpha = runeGlobalAlpha(graph, phase, time);
|
||||
const current = system.current;
|
||||
if (time < current.lastTime) {
|
||||
current.particles.fill(null); current.debt = 0; current.births = 0;
|
||||
@@ -214,7 +236,8 @@ const RuneEmitter = memo(function RuneEmitter({
|
||||
if (particle && time - particle.born >= particle.life) current.particles[index] = null;
|
||||
if (!current.particles[index] && births > 0) {
|
||||
const seed = eventId * 1009 + current.births * 7919 + index * 97;
|
||||
current.particles[index] = spawn(layer, time, seed);
|
||||
current.particles[index] = spawn(layer.emitter, layer.position,
|
||||
layer.sprite.randomRotation, time, seed);
|
||||
current.births++; births--;
|
||||
}
|
||||
}
|
||||
@@ -237,7 +260,8 @@ const RuneEmitter = memo(function RuneEmitter({
|
||||
particle.origin[0] + particle.velocity[0] * age + particle.acceleration[0] * ageSquared,
|
||||
particle.origin[1] + particle.velocity[1] * age + particle.acceleration[1] * ageSquared,
|
||||
particle.origin[2] + particle.velocity[2] * age + particle.acceleration[2] * ageSquared);
|
||||
colors.setXYZW(index, color[0], color[1], color[2], Math.max(0, Math.min(1, alpha * color[3])));
|
||||
colors.setXYZW(index, color[0], color[1], color[2],
|
||||
Math.max(0, Math.min(1, alpha * color[3] * globalAlpha)));
|
||||
sizes.setXY(index, Math.max(0.001, scale * layer.sprite.quadScale[0]),
|
||||
Math.max(0.001, scale * layer.sprite.quadScale[1]));
|
||||
rotations.setX(index, layer.sprite.orientToTravel
|
||||
@@ -251,6 +275,151 @@ const RuneEmitter = memo(function RuneEmitter({
|
||||
return <mesh name="original-runewaker-particle-layer" geometry={geometry} material={material} frustumCulled={false} />;
|
||||
});
|
||||
|
||||
const RuneEntityEmitter = memo(function RuneEntityEmitter({
|
||||
graph, emitter, visualizer, texture, budget, active, eventId, timelineRef,
|
||||
}: {
|
||||
readonly graph: RunewakerParticleGraph;
|
||||
readonly emitter: RuneEmitterController;
|
||||
readonly visualizer: RuneEntityVisualizerController;
|
||||
readonly texture: Texture;
|
||||
readonly budget: number;
|
||||
readonly active: boolean;
|
||||
readonly eventId: number;
|
||||
readonly timelineRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
}) {
|
||||
const system = useRef({ particles: Array<Particle | null>(budget).fill(null), debt: 0, births: 0, lastTime: 0 });
|
||||
const pool = useMemo(() => {
|
||||
const mesh = graph.runeMesh;
|
||||
if (!mesh) throw new Error("Urgent Heal rune geometry is missing.");
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new BufferAttribute(new Float32Array(mesh.positions), 3));
|
||||
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(mesh.uvs), 2));
|
||||
const tiledTexture = texture.clone();
|
||||
tiledTexture.wrapS = RepeatWrapping;
|
||||
tiledTexture.wrapT = ClampToEdgeWrapping;
|
||||
tiledTexture.needsUpdate = true;
|
||||
const meshes = Array.from({ length: budget }, (_, index) => {
|
||||
const material = new MeshBasicMaterial({
|
||||
name: `runewaker-urgent-rune-${index}`,
|
||||
map: tiledTexture,
|
||||
transparent: true,
|
||||
blending: AdditiveBlending,
|
||||
depthWrite: false,
|
||||
side: DoubleSide,
|
||||
toneMapped: false,
|
||||
});
|
||||
const instance = new Mesh(geometry, material);
|
||||
instance.name = "original-runewaker-rune-cylinder";
|
||||
instance.frustumCulled = false;
|
||||
instance.visible = false;
|
||||
return instance;
|
||||
});
|
||||
return { geometry, tiledTexture, meshes };
|
||||
}, [budget, graph.runeMesh, texture]);
|
||||
|
||||
useEffect(() => () => {
|
||||
pool.geometry.dispose();
|
||||
pool.tiledTexture.dispose();
|
||||
pool.meshes.forEach((mesh) => mesh.material.dispose());
|
||||
}, [pool]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!active) {
|
||||
pool.meshes.forEach((mesh) => { mesh.visible = false; });
|
||||
return;
|
||||
}
|
||||
const time = timelineRef.current.elapsedMs / 1_000;
|
||||
const current = system.current;
|
||||
if (time < current.lastTime) {
|
||||
current.particles.fill(null); current.debt = 0; current.births = 0;
|
||||
}
|
||||
const previousTime = current.lastTime;
|
||||
current.lastTime = time;
|
||||
current.debt = Math.min(budget, current.debt + runeEmissionsBetween(
|
||||
emitter.tracks[9] as { time: number; value: number }[], previousTime, time,
|
||||
));
|
||||
let births = Math.min(budget, Math.floor(current.debt));
|
||||
current.debt -= births;
|
||||
for (let index = 0; index < budget; index++) {
|
||||
const particle = current.particles[index];
|
||||
if (particle && time - particle.born >= particle.life) current.particles[index] = null;
|
||||
if (!current.particles[index] && births > 0) {
|
||||
const seed = eventId * 1009 + current.births * 7919 + index * 97;
|
||||
current.particles[index] = spawn(emitter, [0, 0, 0], false, time, seed);
|
||||
current.births++; births--;
|
||||
}
|
||||
const next = current.particles[index];
|
||||
const mesh = pool.meshes[index];
|
||||
if (!next) { mesh.visible = false; continue; }
|
||||
const age = Math.max(0, time - next.born);
|
||||
const lifetime = Math.min(1, age / next.life);
|
||||
const color = runeSample(visualizer.tracks[1], lifetime, [1, 1, 1, 1]);
|
||||
const alpha = runeSample(visualizer.tracks[2], lifetime, 1);
|
||||
const scale = runeSample(visualizer.tracks[3], lifetime, 1) * next.scale;
|
||||
const ageSquared = age * age * 0.5;
|
||||
mesh.position.set(
|
||||
next.origin[0] + next.velocity[0] * age + next.acceleration[0] * ageSquared,
|
||||
next.origin[1] + next.velocity[1] * age + next.acceleration[1] * ageSquared,
|
||||
next.origin[2] + next.velocity[2] * age + next.acceleration[2] * ageSquared,
|
||||
);
|
||||
mesh.scale.setScalar(Math.max(0.001, scale));
|
||||
mesh.rotation.y = next.orientation;
|
||||
mesh.material.color.setRGB(color[0], color[1], color[2]);
|
||||
mesh.material.opacity = Math.max(0, Math.min(1, alpha * color[3]));
|
||||
mesh.visible = scale > 0.001 && mesh.material.opacity > 0.002;
|
||||
}
|
||||
});
|
||||
|
||||
return <group name="original-runewaker-rune-entities">
|
||||
{pool.meshes.map((mesh, index) => <primitive key={index} object={mesh} />)}
|
||||
</group>;
|
||||
});
|
||||
|
||||
const RuneGroundDecal = memo(function RuneGroundDecal({
|
||||
decal, alpha, color, texture, active, timelineRef,
|
||||
}: {
|
||||
readonly decal: RuneDecalController;
|
||||
readonly alpha: RuneGlobalAlphaController;
|
||||
readonly color: RuneGlobalColorController;
|
||||
readonly texture: Texture;
|
||||
readonly active: boolean;
|
||||
readonly timelineRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
}) {
|
||||
const meshRef = useRef<Mesh<PlaneGeometry, MeshBasicMaterial>>(null);
|
||||
const geometry = useMemo(() => new PlaneGeometry(2, 2), []);
|
||||
const material = useMemo(() => new MeshBasicMaterial({
|
||||
name: "runewaker-urgent-ground-decal",
|
||||
map: texture,
|
||||
transparent: true,
|
||||
blending: AdditiveBlending,
|
||||
depthWrite: false,
|
||||
side: DoubleSide,
|
||||
toneMapped: false,
|
||||
}), [texture]);
|
||||
useEffect(() => () => { geometry.dispose(); material.dispose(); }, [geometry, material]);
|
||||
|
||||
useFrame(() => {
|
||||
const mesh = meshRef.current;
|
||||
if (!mesh) return;
|
||||
if (!active) { mesh.visible = false; return; }
|
||||
const time = timelineRef.current.elapsedMs / 1_000;
|
||||
const radius = runeSample(decal.tracks[2], time, 0);
|
||||
const rotation = runeSample(decal.tracks[1], time, 0);
|
||||
const opacity = runeSample(alpha.alphaTrack, Math.max(0, time - alpha.startTime), 0);
|
||||
const tint = runeSample(color.tracks[0], time, [1, 1, 1, 1]);
|
||||
material.color.setRGB(tint[0], tint[1], tint[2]);
|
||||
material.opacity = Math.max(0, Math.min(1, opacity * tint[3]));
|
||||
mesh.scale.set(radius, radius, 1);
|
||||
mesh.rotation.z = rotation;
|
||||
mesh.visible = time <= alpha.startTime + alpha.duration && material.opacity > 0.002;
|
||||
});
|
||||
|
||||
// The client projects this decal onto terrain triangles. The preview uses a level ground plane.
|
||||
return <group name="original-runewaker-ground-decal" position={[0, 0.1, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<mesh ref={meshRef} geometry={geometry} material={material} visible={false} frustumCulled={false} />
|
||||
</group>;
|
||||
});
|
||||
|
||||
export const RunewakerParticleEffect = memo(function RunewakerParticleEffect({
|
||||
graph, textures, phase, quality, active, eventId, playbackRef,
|
||||
}: {
|
||||
@@ -263,6 +432,22 @@ export const RunewakerParticleEffect = memo(function RunewakerParticleEffect({
|
||||
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
}) {
|
||||
const layers = useMemo(() => runeLayers(graph, phase), [graph, phase]);
|
||||
const motion = useMemo(() => runeMotion(graph, phase), [graph, phase]);
|
||||
const runeEntity = motion?.controllers.find(
|
||||
(controller): controller is RuneEntityVisualizerController => controller.type === "entity-visualizer",
|
||||
);
|
||||
const runeEmitter = motion?.controllers.find(
|
||||
(controller): controller is RuneEmitterController => controller.type === "emitter" && controller.anchor === runeEntity?.anchor,
|
||||
);
|
||||
const decal = motion?.controllers.find(
|
||||
(controller): controller is RuneDecalController => controller.type === "decal",
|
||||
);
|
||||
const decalAlpha = motion?.controllers.find(
|
||||
(controller): controller is RuneGlobalAlphaController => controller.type === "global-alpha" && controller.anchor === decal?.anchor,
|
||||
);
|
||||
const decalColor = motion?.controllers.find(
|
||||
(controller): controller is RuneGlobalColorController => controller.type === "global-color" && controller.anchor === decal?.anchor,
|
||||
);
|
||||
const limit = quality === "high" ? 256 : 96;
|
||||
return <group name="original-runewaker-particles" scale={0.1}>
|
||||
{layers.map((layer, index) => {
|
||||
@@ -270,9 +455,20 @@ export const RunewakerParticleEffect = memo(function RunewakerParticleEffect({
|
||||
if (!texture) return null;
|
||||
const sourceMaximum = Number(layer.emitter.tracks[0][0]?.value ?? 0);
|
||||
const budget = Math.max(1, Math.min(sourceMaximum, Math.floor(limit / Math.max(1, layers.length))));
|
||||
return <RuneEmitter key={`${layer.sprite.anchor}:${index}`} layer={layer} texture={texture}
|
||||
return <RuneEmitter key={`${layer.sprite.anchor}:${index}`} graph={graph} phase={phase}
|
||||
layer={layer} texture={texture}
|
||||
budget={budget} active={active} eventId={eventId + index * 271}
|
||||
timelineRef={playbackRef} />;
|
||||
})}
|
||||
{graph.runeMesh && runeEntity && runeEmitter && textures.get(graph.runeMesh.textureAssetUrl)
|
||||
? <RuneEntityEmitter graph={graph} emitter={runeEmitter} visualizer={runeEntity}
|
||||
texture={textures.get(graph.runeMesh.textureAssetUrl)!}
|
||||
budget={quality === "high" ? 12 : 6} active={active} eventId={eventId + 617}
|
||||
timelineRef={playbackRef} />
|
||||
: null}
|
||||
{decal && decalAlpha && decalColor && textures.get(decal.textureAssetUrl)
|
||||
? <RuneGroundDecal decal={decal} alpha={decalAlpha} color={decalColor}
|
||||
texture={textures.get(decal.textureAssetUrl)!} active={active} timelineRef={playbackRef} />
|
||||
: null}
|
||||
</group>;
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
clearCombatEffectAnchorRegistry,
|
||||
registerCombatEffectAnchor,
|
||||
updateCombatEffectAnchorPosition,
|
||||
registerCombatEffectTargetVisualSize,
|
||||
combatEffectTargetVisualSize,
|
||||
} from "./combatEffectAnchorRegistry";
|
||||
|
||||
afterEach(() => clearCombatEffectAnchorRegistry());
|
||||
@@ -49,4 +51,14 @@ describe("combat-effect visual anchors", () => {
|
||||
expect(updateCombatEffectAnchorPosition("missing", effect, new Vector3())).toBe(false);
|
||||
expect(effect.position.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("tracks creature display bounds without losing a replacement registration", () => {
|
||||
const unregisterOld = registerCombatEffectTargetVisualSize("verdan", { height: 10, radius: 3 });
|
||||
const current = { height: 15.85, radius: 5.3 };
|
||||
const unregisterCurrent = registerCombatEffectTargetVisualSize("verdan", current);
|
||||
unregisterOld();
|
||||
expect(combatEffectTargetVisualSize("verdan")).toBe(current);
|
||||
unregisterCurrent();
|
||||
expect(combatEffectTargetVisualSize("verdan")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,25 @@
|
||||
import { Vector3, type Object3D } from "three";
|
||||
|
||||
const anchors = new Map<string, Object3D>();
|
||||
export interface CombatEffectTargetVisualSize {
|
||||
readonly height: number;
|
||||
readonly radius: number;
|
||||
}
|
||||
const targetVisualSizes = new Map<string, CombatEffectTargetVisualSize>();
|
||||
|
||||
/** Model display bounds used to place target-attached spell effects on large creatures. */
|
||||
export function registerCombatEffectTargetVisualSize(
|
||||
actorId: string, size: CombatEffectTargetVisualSize,
|
||||
): () => void {
|
||||
targetVisualSizes.set(actorId, size);
|
||||
return () => {
|
||||
if (targetVisualSizes.get(actorId) === size) targetVisualSizes.delete(actorId);
|
||||
};
|
||||
}
|
||||
|
||||
export function combatEffectTargetVisualSize(actorId: string | undefined): CombatEffectTargetVisualSize | null {
|
||||
return actorId ? targetVisualSizes.get(actorId) ?? null : null;
|
||||
}
|
||||
|
||||
export function registerCombatEffectAnchor(actorId: string, object: Object3D): () => void {
|
||||
anchors.set(actorId, object);
|
||||
@@ -30,4 +49,5 @@ export function updateCombatEffectAnchorPosition(
|
||||
|
||||
export function clearCombatEffectAnchorRegistry(): void {
|
||||
anchors.clear();
|
||||
targetVisualSizes.clear();
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
advanceCombatEffectFrame,
|
||||
originalInstantCastEvent,
|
||||
originalInstantTerminalEvent,
|
||||
projectileTravelDurationMs,
|
||||
risingTideArrivalDelayMs,
|
||||
risingTideImpactPlacement,
|
||||
shouldUseGeneratedOriginalFallback,
|
||||
updateCombatEffectQueue,
|
||||
type ActiveCombatEffect,
|
||||
@@ -46,6 +49,35 @@ function enqueue(
|
||||
}
|
||||
|
||||
describe("combat-effect runtime budget", () => {
|
||||
it("delays only the Rising Tide target arrival by the projectile's travel time", () => {
|
||||
const release = event(1, { source: "runewaker", sourceSpellId: 490256,
|
||||
phase: "release", origin: [0, 0, 0], targetPosition: [14, 0, 0] });
|
||||
const impact = { ...release, id: 2, phase: "impact" as const };
|
||||
expect(projectileTravelDurationMs(release)).toBe(500);
|
||||
expect(risingTideArrivalDelayMs(release)).toBe(0);
|
||||
expect(risingTideArrivalDelayMs(impact)).toBe(500);
|
||||
expect(risingTideArrivalDelayMs({ ...impact, sourceSpellId: 491147 })).toBe(0);
|
||||
expect(risingTideArrivalDelayMs({ ...impact, delivery: "area" })).toBe(0);
|
||||
expect(projectileTravelDurationMs({ ...release, targetPosition: [100, 0, 0] })).toBe(550);
|
||||
expect(projectileTravelDurationMs({ ...release, targetPosition: [1, 0, 0] })).toBe(180);
|
||||
});
|
||||
|
||||
it("places Rising Tide impact on a giant target's caster-facing body surface", () => {
|
||||
const cast = event(1, { origin: [0, 0, 0], targetPosition: [10, 0, 0] });
|
||||
const verdan = risingTideImpactPlacement(cast, { height: 15.85, radius: 5.3 });
|
||||
expect(verdan.height).toBe(2.4);
|
||||
expect(verdan.offsetX).toBeCloseTo(-5.05);
|
||||
expect(verdan.offsetZ).toBe(0);
|
||||
expect(verdan.scale).toBe(5);
|
||||
const humanoid = risingTideImpactPlacement(cast, { height: 2.65, radius: 0.9 });
|
||||
expect(humanoid.height).toBeCloseTo(1.1925);
|
||||
expect(humanoid.offsetX).toBeCloseTo(-0.65);
|
||||
expect(humanoid.scale).toBe(1);
|
||||
expect(risingTideImpactPlacement(cast, null)).toEqual({
|
||||
offsetX: 0, height: 1.05, offsetZ: 0, scale: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("expires effects by wall time while bounding motion after a hitch", () => {
|
||||
let frame = advanceCombatEffectFrame(0, 1 / 60, 420);
|
||||
frame = advanceCombatEffectFrame(frame.elapsedMs, 1 / 60, 420);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CombatPresentationEvent,
|
||||
CombatVfxQuality,
|
||||
} from "../game/combatPresentation";
|
||||
import type { CombatEffectTargetVisualSize } from "./combatEffectAnchorRegistry";
|
||||
|
||||
export interface ActiveCombatEffect {
|
||||
readonly key: string;
|
||||
@@ -15,6 +16,55 @@ export const COMBAT_EFFECT_SYSTEM_LIMIT: Readonly<Record<CombatVfxQuality, numbe
|
||||
high: 32,
|
||||
};
|
||||
|
||||
/** Keep source projectile travel and its arrival cue on the same presentation clock. */
|
||||
export function projectileTravelDurationMs(
|
||||
event: Pick<CombatPresentationEvent, "origin" | "targetPosition" | "delivery">,
|
||||
): number {
|
||||
if (event.delivery !== "projectile" || !event.targetPosition) return 0;
|
||||
const distance = Math.hypot(
|
||||
event.targetPosition[0] - event.origin[0],
|
||||
event.targetPosition[1] - event.origin[1],
|
||||
event.targetPosition[2] - event.origin[2],
|
||||
);
|
||||
return Number.isFinite(distance) ? Math.max(180, Math.min(550, distance / 28 * 1_000)) : 0;
|
||||
}
|
||||
|
||||
/** RuneWaker's Rising Tide impact model and sound are target arrival events. */
|
||||
export function risingTideArrivalDelayMs(
|
||||
event: Pick<CombatPresentationEvent,
|
||||
"phase" | "source" | "sourceSpellId" | "origin" | "targetPosition" | "delivery">,
|
||||
): number {
|
||||
return event.phase === "impact" && event.source === "runewaker" && event.sourceSpellId === 490256
|
||||
? projectileTravelDurationMs(event) : 0;
|
||||
}
|
||||
|
||||
/** Approximate RuneWaker's target `freeze_hit` attachment from displayed creature bounds. */
|
||||
export function risingTideImpactPlacement(
|
||||
event: Pick<CombatPresentationEvent, "origin" | "targetPosition">,
|
||||
size: CombatEffectTargetVisualSize | null,
|
||||
): { readonly offsetX: number; readonly height: number; readonly offsetZ: number; readonly scale: number } {
|
||||
if (!size || !event.targetPosition || !Number.isFinite(size.height)
|
||||
|| !Number.isFinite(size.radius)) {
|
||||
return { offsetX: 0, height: 1.05, offsetZ: 0, scale: 1 };
|
||||
}
|
||||
const radius = Math.max(0.6, Math.min(6, size.radius));
|
||||
// Giant bosses can extend beyond the camera's top edge. Keep the hit on the
|
||||
// visible lower body even when their model label is many metres above ground.
|
||||
const height = Math.max(1.05, Math.min(2.4, size.height * 0.45));
|
||||
const frontDistance = Math.max(0, radius - 0.25);
|
||||
const towardCasterX = event.origin[0] - event.targetPosition[0];
|
||||
const towardCasterZ = event.origin[2] - event.targetPosition[2];
|
||||
const horizontalLength = Math.hypot(towardCasterX, towardCasterZ);
|
||||
const directionX = horizontalLength > 0.001 ? towardCasterX / horizontalLength : 0;
|
||||
const directionZ = horizontalLength > 0.001 ? towardCasterZ / horizontalLength : 0;
|
||||
return {
|
||||
offsetX: directionX * frontDistance,
|
||||
height,
|
||||
offsetZ: directionZ * frontDistance,
|
||||
scale: Math.max(1, Math.min(5, radius / 0.9)),
|
||||
};
|
||||
}
|
||||
|
||||
export interface CombatEffectFrameState {
|
||||
readonly elapsedMs: number;
|
||||
readonly motionDeltaSeconds: number;
|
||||
|
||||
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseRunewakerParticleGraph, runeDurationMs, runeEmissionsBetween, runeLayers, runeMotion, runeSample,
|
||||
parseRunewakerParticleGraph, runeDurationMs, runeEmissionsBetween, runeGlobalAlpha,
|
||||
runeLayers, runeMotion, runeSample,
|
||||
} from "./runewakerParticleRuntime";
|
||||
|
||||
const archiveRoot = path.join(process.cwd(), "public/assets/game/combat-effects/original/runewaker");
|
||||
@@ -46,6 +47,48 @@ describe("RuneWaker sprite particle runtime", () => {
|
||||
expect(() => parseRunewakerParticleGraph(graphFor("earth/act_casting_c_hand.ros"))).toThrow();
|
||||
});
|
||||
|
||||
it("renders the source Rising Tide cast, projectile, and target hit layers", () => {
|
||||
const cast = parseRunewakerParticleGraph(graphFor("ice/act_icecone01_c_hand.ros"));
|
||||
const release = parseRunewakerParticleGraph(graphFor("ice/act_icecone01_f.ros"));
|
||||
const impact = parseRunewakerParticleGraph(graphFor("ice/act_riptide_t_freeze_hit.ros"));
|
||||
expect(runeLayers(cast, "cast-start")).toHaveLength(4);
|
||||
expect(runeLayers(release, "release")).toHaveLength(3);
|
||||
expect(runeMotion(impact, "impact")?.name).toBe("explode");
|
||||
expect(runeLayers(impact, "impact")).toHaveLength(5);
|
||||
expect(runeGlobalAlpha(cast, "cast-start", 0.3)).toBe(1);
|
||||
expect(runeGlobalAlpha(impact, "impact", 0.1)).toBe(1);
|
||||
const altered = graphFor("ice/act_riptide_t_freeze_hit.ros");
|
||||
altered.motions[0].controllers.find((controller: { eventID: number }) => controller.eventID === 1).eventID = 25;
|
||||
expect(() => parseRunewakerParticleGraph(altered)).toThrow();
|
||||
});
|
||||
|
||||
it("accepts Urgent Heal's original sprites, rune entity, and level-ground decal as partial", () => {
|
||||
const graph = parseRunewakerParticleGraph(graphFor("holy/act_benedictory_heal_t_down.ros"));
|
||||
expect(graph.rendererReady).toBe(false);
|
||||
expect(graph.partialRendererReady).toBe(true);
|
||||
expect(graph.visualFidelity).toBe("partial-original");
|
||||
expect(graph.omittedVisuals).toEqual(["CRuEntity_Decal:terrain-projection"]);
|
||||
expect(runeLayers(graph, "impact")).toHaveLength(2);
|
||||
expect(graph.runeMesh?.positions).toHaveLength(72 * 9);
|
||||
expect(graph.runeMesh?.uvs).toHaveLength(72 * 6);
|
||||
expect(graph.motions[0].controllers.filter((controller) =>
|
||||
controller.type === "entity-visualizer" || controller.type === "decal"
|
||||
|| controller.type === "global-color")).toHaveLength(3);
|
||||
expect(runeDurationMs(graph, "impact")).toBeGreaterThan(1_630);
|
||||
expect(runeGlobalAlpha(graph, "impact", 0)).toBe(1);
|
||||
const altered = graphFor("holy/act_benedictory_heal_t_down.ros");
|
||||
altered.motions[0].controllers.find((controller: { sourceType: string }) =>
|
||||
controller.sourceType === "CRuController_Particle_Visualizer_Entity").entityName = "model/other.ros";
|
||||
expect(() => parseRunewakerParticleGraph(altered)).toThrow();
|
||||
const missingMesh = graphFor("holy/act_benedictory_heal_t_down.ros");
|
||||
delete missingMesh.runeMesh;
|
||||
expect(() => parseRunewakerParticleGraph(missingMesh)).toThrow();
|
||||
const wrongDecal = graphFor("holy/act_benedictory_heal_t_down.ros");
|
||||
wrongDecal.motions[0].controllers.find((controller: { type: string }) =>
|
||||
controller.type === "decal").decalTexture = "texture/other.dds";
|
||||
expect(() => parseRunewakerParticleGraph(wrongDecal)).toThrow();
|
||||
});
|
||||
|
||||
it("keeps a 33 ms source burst when the first browser frame is slower", () => {
|
||||
const graph = parseRunewakerParticleGraph(graphFor("act_shieldstorm_t_freeze_hit_e.ros"));
|
||||
const rate = runeLayers(graph, "impact")[0].emitter.tracks[9] as { time: number; value: number }[];
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface RuneSpriteController {
|
||||
readonly textureAssetUrl: string;
|
||||
readonly shader: "ruSHADER_PARTICLE_SPRITEEX";
|
||||
readonly blendMode: 0 | 1 | 2;
|
||||
readonly billboardType: 1;
|
||||
readonly billboardType: 1 | 2;
|
||||
readonly tileSizePixels: readonly [number, number];
|
||||
readonly frameRate: number;
|
||||
readonly frameCount: number;
|
||||
@@ -36,16 +36,61 @@ export interface RuneEmitterController {
|
||||
readonly tracks: readonly RuneTrack<number | RuneVector3>[];
|
||||
}
|
||||
|
||||
export interface RuneAudioTriggerController {
|
||||
export interface RuneEventTriggerController {
|
||||
readonly type: "unsupported";
|
||||
readonly sourceType: "CRuController_EventTrigger";
|
||||
readonly anchor: string;
|
||||
readonly eventID: 43;
|
||||
readonly eventParameter0: 1;
|
||||
readonly eventParameter1: 0;
|
||||
readonly eventID: number;
|
||||
readonly eventParameter0: number;
|
||||
readonly eventParameter1: number;
|
||||
readonly eventString: string;
|
||||
readonly eventDuration: number;
|
||||
readonly startTime: 0;
|
||||
readonly startTime: number;
|
||||
}
|
||||
|
||||
export interface RuneGlobalAlphaController {
|
||||
readonly type: "global-alpha";
|
||||
readonly sourceType: "CRuController_GlobalAlpha";
|
||||
readonly anchor: string;
|
||||
readonly startTime: number;
|
||||
readonly duration: number;
|
||||
readonly alphaTrack: RuneTrack<number>;
|
||||
}
|
||||
|
||||
export interface RuneEntityVisualizerController {
|
||||
readonly type: "entity-visualizer";
|
||||
readonly sourceType: "CRuController_Particle_Visualizer_Entity";
|
||||
readonly anchor: "frame/rune";
|
||||
readonly entityName: string;
|
||||
readonly orientToTravel: false;
|
||||
readonly tracks: readonly [RuneTrack<string>, RuneTrack<readonly [number, number, number, number]>,
|
||||
RuneTrack<number>, RuneTrack<number>];
|
||||
}
|
||||
|
||||
export interface RuneDecalController {
|
||||
readonly type: "decal";
|
||||
readonly sourceType: "CRuController_Decal";
|
||||
readonly anchor: "frame/decal";
|
||||
readonly decalTexture: string;
|
||||
readonly textureAssetUrl: string;
|
||||
readonly billboardType: 0;
|
||||
readonly tracks: readonly [RuneTrack<string>, RuneTrack<number>, RuneTrack<number>];
|
||||
}
|
||||
|
||||
export interface RuneGlobalColorController {
|
||||
readonly type: "global-color";
|
||||
readonly sourceType: "CRuController_GlobalColor";
|
||||
readonly anchor: "frame/decal";
|
||||
readonly tracks: readonly [RuneTrack<readonly [number, number, number, number]>];
|
||||
}
|
||||
|
||||
export interface RuneMeshGeometry {
|
||||
readonly sourceModel: string;
|
||||
readonly sourceSha256: string;
|
||||
readonly sourceTexture: string;
|
||||
readonly textureAssetUrl: string;
|
||||
readonly positions: readonly number[];
|
||||
readonly uvs: readonly number[];
|
||||
}
|
||||
|
||||
export interface RuneParticleLayer {
|
||||
@@ -61,10 +106,16 @@ export interface RunewakerParticleGraph {
|
||||
readonly sourceModel: string;
|
||||
readonly sourceSha256: string;
|
||||
readonly rendererReady: boolean;
|
||||
readonly partialRendererReady?: boolean;
|
||||
readonly visualFidelity?: "partial-original";
|
||||
readonly omittedVisuals?: readonly string[];
|
||||
readonly runeMesh?: RuneMeshGeometry;
|
||||
readonly textures: readonly { readonly url: string; readonly sourcePath: string }[];
|
||||
readonly motions: readonly {
|
||||
readonly name: string;
|
||||
readonly controllers: readonly (RuneSpriteController | RuneEmitterController | RuneAudioTriggerController)[];
|
||||
readonly controllers: readonly (RuneSpriteController | RuneEmitterController
|
||||
| RuneEventTriggerController | RuneGlobalAlphaController
|
||||
| RuneEntityVisualizerController | RuneDecalController | RuneGlobalColorController)[];
|
||||
}[];
|
||||
readonly sprites: readonly { readonly name: string; readonly position: RuneVector3 }[];
|
||||
readonly unsupported: readonly string[];
|
||||
@@ -105,6 +156,163 @@ function isVisualOnlyHolyCastTrigger(
|
||||
&& finite(controller.eventDuration) && controller.eventDuration > 0;
|
||||
}
|
||||
|
||||
const ICE_CAST_MODEL = "model/fx/skill/ice/act_icecone01_c_hand.ros";
|
||||
const ICE_RELEASE_MODEL = "model/fx/skill/ice/act_icecone01_f.ros";
|
||||
const RIPTIDE_IMPACT_MODEL = "model/fx/skill/ice/act_riptide_t_freeze_hit.ros";
|
||||
const URGENT_IMPACT_MODEL = "model/fx/skill/holy/act_benedictory_heal_t_down.ros";
|
||||
const expectedSpecialControllerCounts = new Map([
|
||||
["model/fx/skill/holy/act_casting_c_hand.ros", [1, 0]],
|
||||
[ICE_CAST_MODEL, [3, 3]],
|
||||
[ICE_RELEASE_MODEL, [4, 0]],
|
||||
[RIPTIDE_IMPACT_MODEL, [2, 1]],
|
||||
[URGENT_IMPACT_MODEL, [1, 1]],
|
||||
]);
|
||||
const urgentOmittedVisuals = [
|
||||
"CRuEntity_Decal:terrain-projection",
|
||||
];
|
||||
const urgentUnsupportedTypes = new Set([
|
||||
"CRuController_EventTrigger",
|
||||
]);
|
||||
|
||||
function isVerifiedUrgentPartial(graph: Record<string, unknown>): boolean {
|
||||
const mesh = graph.runeMesh && typeof graph.runeMesh === "object"
|
||||
? graph.runeMesh as Record<string, unknown> : null;
|
||||
return graph.sourceModel === URGENT_IMPACT_MODEL
|
||||
&& graph.rendererReady === false && graph.partialRendererReady === true
|
||||
&& graph.visualFidelity === "partial-original"
|
||||
&& Array.isArray(graph.omittedVisuals)
|
||||
&& graph.omittedVisuals.length === urgentOmittedVisuals.length
|
||||
&& graph.omittedVisuals.every((visual, index) => visual === urgentOmittedVisuals[index])
|
||||
&& Array.isArray(graph.unsupported)
|
||||
&& graph.unsupported.length === urgentUnsupportedTypes.size
|
||||
&& graph.unsupported.every((type) => urgentUnsupportedTypes.has(type))
|
||||
&& mesh?.sourceModel === "model/fx/skill/holy/_polygon/rune_cylinder.ros"
|
||||
&& mesh.sourceTexture === "texture/fx/skill/rune.dds"
|
||||
&& typeof mesh.sourceSha256 === "string" && /^[a-f0-9]{64}$/.test(mesh.sourceSha256)
|
||||
&& typeof mesh.textureAssetUrl === "string"
|
||||
&& Array.isArray(mesh.positions) && mesh.positions.length === 72 * 9 && mesh.positions.every(finite)
|
||||
&& Array.isArray(mesh.uvs) && mesh.uvs.length === 72 * 6 && mesh.uvs.every(finite);
|
||||
}
|
||||
|
||||
function isUrgentPartialController(
|
||||
graph: Record<string, unknown>, motion: Record<string, unknown>, controller: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (!isVerifiedUrgentPartial(graph) || motion.name !== "explode") return false;
|
||||
return controller.sourceType === "CRuController_EventTrigger" && controller.anchor === "frame"
|
||||
&& controller.eventID === 43 && controller.eventParameter0 === 0
|
||||
&& controller.eventParameter1 === 0 && controller.startTime === 0
|
||||
&& controller.eventDuration === 0
|
||||
&& normalizedEventString(controller) === "sound/fx/skill/holy/act_benedictory_heal_t_down.mp3";
|
||||
}
|
||||
|
||||
function stringKeys(value: unknown): value is RuneTrack<string> {
|
||||
return Array.isArray(value) && value.every((key) => {
|
||||
const sample = record(key);
|
||||
return finite(sample.time) && typeof sample.value === "string";
|
||||
});
|
||||
}
|
||||
|
||||
function isUrgentEntityController(graph: Record<string, unknown>, controller: Record<string, unknown>): boolean {
|
||||
if (!isVerifiedUrgentPartial(graph)) return false;
|
||||
const tracks = controller.tracks;
|
||||
return controller.sourceType === "CRuController_Particle_Visualizer_Entity"
|
||||
&& controller.anchor === "frame/rune" && controller.orientToTravel === false
|
||||
&& typeof controller.entityName === "string"
|
||||
&& controller.entityName.replaceAll("\\", "/").toLowerCase()
|
||||
=== "model/fx/skill/holy/_polygon/rune_cylinder.ros"
|
||||
&& Array.isArray(tracks) && tracks.length === 4
|
||||
&& stringKeys(tracks[0]) && tracks[0].length === 2
|
||||
&& tracks[0][0].value === "idle"
|
||||
&& keys(tracks[1]) && tracks[1].length === 2
|
||||
&& tracks[1].every((key) => Array.isArray(key.value) && key.value.length === 4)
|
||||
&& keys(tracks[2]) && tracks[2].length === 3
|
||||
&& keys(tracks[3]) && tracks[3].length === 3;
|
||||
}
|
||||
|
||||
function isUrgentDecalController(graph: Record<string, unknown>, controller: Record<string, unknown>): boolean {
|
||||
if (!isVerifiedUrgentPartial(graph)) return false;
|
||||
const tracks = controller.tracks;
|
||||
return controller.sourceType === "CRuController_Decal"
|
||||
&& controller.anchor === "frame/decal" && controller.billboardType === 0
|
||||
&& typeof controller.decalTexture === "string"
|
||||
&& controller.decalTexture.replaceAll("\\", "/").toLowerCase()
|
||||
=== "texture/fx/skill/kan_rune_circle.dds"
|
||||
&& typeof controller.textureAssetUrl === "string"
|
||||
&& Array.isArray(tracks) && tracks.length === 3
|
||||
&& stringKeys(tracks[0]) && tracks[0].length === 1
|
||||
&& tracks[0][0].value.replaceAll("\\", "/").toLowerCase()
|
||||
=== "texture/fx/skill/kan_rune_circle.dds"
|
||||
&& keys(tracks[1]) && tracks[1].length === 2
|
||||
&& keys(tracks[2]) && tracks[2].length === 3;
|
||||
}
|
||||
|
||||
function isUrgentGlobalColor(graph: Record<string, unknown>, controller: Record<string, unknown>): boolean {
|
||||
if (!isVerifiedUrgentPartial(graph)) return false;
|
||||
const tracks = controller.tracks;
|
||||
return controller.sourceType === "CRuController_GlobalColor"
|
||||
&& controller.anchor === "frame/decal" && Array.isArray(tracks)
|
||||
&& tracks.length === 1 && keys(tracks[0]) && tracks[0].length === 2
|
||||
&& tracks[0].every((key) => Array.isArray(key.value) && key.value.length === 4);
|
||||
}
|
||||
|
||||
function normalizedEventString(controller: Record<string, unknown>) {
|
||||
return typeof controller.eventString === "string"
|
||||
? controller.eventString.replaceAll("\\", "/").toLowerCase() : null;
|
||||
}
|
||||
|
||||
function isVisualOnlyRisingTideTrigger(
|
||||
graph: Record<string, unknown>, motion: Record<string, unknown>, controller: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (graph.rendererReady !== true || controller.sourceType !== "CRuController_EventTrigger"
|
||||
|| controller.startTime !== 0 || controller.eventParameter1 !== 0) return false;
|
||||
const eventString = normalizedEventString(controller);
|
||||
if (graph.sourceModel === ICE_CAST_MODEL && ["idle", "appear", "disappear"].includes(String(motion.name))) {
|
||||
return controller.eventID === 43 && controller.eventParameter0 === 1
|
||||
&& eventString === "sound/fx/skill/ice/act_cone01_c_hand.mp3"
|
||||
&& finite(controller.eventDuration) && controller.eventDuration > 0;
|
||||
}
|
||||
if (graph.sourceModel === ICE_RELEASE_MODEL && ["idle", "appear", "disappear"].includes(String(motion.name))) {
|
||||
return controller.eventID === 44 && controller.eventParameter0 === 1
|
||||
&& eventString === "missile" && controller.eventDuration === 1
|
||||
|| motion.name === "appear" && controller.eventID === 43
|
||||
&& controller.eventParameter0 === 0 && controller.eventDuration === 0
|
||||
&& eventString === "sound/fx/skill/ice/icenew/act_icecone01_f.ros.mp3";
|
||||
}
|
||||
if (graph.sourceModel === RIPTIDE_IMPACT_MODEL && motion.name === "explode") {
|
||||
return controller.eventID === 1 && controller.eventParameter0 === 0
|
||||
&& controller.eventDuration === 0 && eventString === ""
|
||||
|| controller.eventID === 43 && controller.eventParameter0 === 0
|
||||
&& controller.eventDuration === 0
|
||||
&& eventString === "sound/fx/skill/ice/act_riptide_t_freeze_hit.mp3";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isVerifiedGlobalAlpha(
|
||||
graph: Record<string, unknown>, motion: Record<string, unknown>, controller: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (graph.rendererReady !== true && !isVerifiedUrgentPartial(graph)) return false;
|
||||
if (controller.sourceType !== "CRuController_GlobalAlpha"
|
||||
|| controller.startTime !== 0 || !finite(controller.duration) || !keys(controller.alphaTrack)
|
||||
|| !controller.alphaTrack.every((key) => finite(key.value) && key.value >= 0 && key.value <= 1)) return false;
|
||||
const track = controller.alphaTrack;
|
||||
if (graph.sourceModel === ICE_CAST_MODEL && ["idle", "appear", "disappear"].includes(String(motion.name))) {
|
||||
return motion.name === "appear"
|
||||
? controller.duration > 0 && track.length === 2
|
||||
&& track[0].time === 0 && track[0].value === 0 && track[1].value === 1
|
||||
: controller.duration === 0 && track.length === 1
|
||||
&& track[0].time === 0 && track[0].value === 1;
|
||||
}
|
||||
if (isVerifiedUrgentPartial(graph) && motion.name === "explode") {
|
||||
return controller.anchor === "frame/decal" && controller.duration > 0
|
||||
&& track.length === 4 && track[0].time === 0 && track[0].value === 0
|
||||
&& track[1].value === 1 && track[2].value === 1 && track[3].value === 0;
|
||||
}
|
||||
return graph.sourceModel === RIPTIDE_IMPACT_MODEL && motion.name === "explode"
|
||||
&& controller.duration === 0 && track.length === 1
|
||||
&& track[0].time === 0 && track[0].value === 1;
|
||||
}
|
||||
|
||||
/** Parse only the source controller slice implemented by the browser renderer. */
|
||||
export function parseRunewakerParticleGraph(value: unknown): RunewakerParticleGraph {
|
||||
const graph = record(value);
|
||||
@@ -114,12 +322,20 @@ export function parseRunewakerParticleGraph(value: unknown): RunewakerParticleGr
|
||||
|| !Array.isArray(graph.sprites) || !Array.isArray(graph.unsupported)
|
||||
|| !Array.isArray(graph.missing)) throw new TypeError("Invalid RuneWaker particle graph header.");
|
||||
if (graph.missing.length) throw new TypeError("RuneWaker graph has missing source layers.");
|
||||
if (graph.unsupported.some((type) => type !== "CRuController_EventTrigger")) {
|
||||
const urgentPartial = isVerifiedUrgentPartial(graph);
|
||||
if (!urgentPartial && graph.unsupported.some((type) => type !== "CRuController_EventTrigger")) {
|
||||
throw new TypeError("RuneWaker graph has unsupported source layers.");
|
||||
}
|
||||
const textureUrls = new Set(graph.textures.map((texture) => record(texture).url));
|
||||
if (urgentPartial && !textureUrls.has((graph.runeMesh as RuneMeshGeometry).textureAssetUrl)) {
|
||||
throw new TypeError("Urgent Heal rune mesh texture is not packaged.");
|
||||
}
|
||||
let layerCount = 0;
|
||||
let audioTriggerCount = 0;
|
||||
let visualOnlyTriggerCount = 0;
|
||||
let globalAlphaCount = 0;
|
||||
let entityCount = 0;
|
||||
let decalCount = 0;
|
||||
let colorCount = 0;
|
||||
for (const rawMotion of graph.motions) {
|
||||
const motion = record(rawMotion);
|
||||
if (typeof motion.name !== "string" || !Array.isArray(motion.controllers)) {
|
||||
@@ -133,13 +349,13 @@ export function parseRunewakerParticleGraph(value: unknown): RunewakerParticleGr
|
||||
if (typeof controller.anchor !== "string" || controller.particlePath !== ""
|
||||
|| controller.emitterPath !== "" || !Array.isArray(controller.tracks)
|
||||
|| controller.tracks.length !== 27 || !controller.tracks.every(keys)
|
||||
|| ![0, 1, 4].includes(Number((controller.tracks[1] as RuneTrack<number>)[0]?.value))) {
|
||||
|| ![0, 1, 4, 5].includes(Number((controller.tracks[1] as RuneTrack<number>)[0]?.value))) {
|
||||
throw new TypeError("Unsupported RuneWaker emitter controller.");
|
||||
}
|
||||
} else if (controller.type === "sprite") {
|
||||
if (typeof controller.anchor !== "string" || !emitters.has(controller.anchor)
|
||||
|| controller.shader !== "ruSHADER_PARTICLE_SPRITEEX"
|
||||
|| ![0, 1, 2].includes(Number(controller.blendMode)) || controller.billboardType !== 1
|
||||
|| ![0, 1, 2].includes(Number(controller.blendMode)) || ![1, 2].includes(Number(controller.billboardType))
|
||||
|| typeof controller.textureAssetUrl !== "string" || !textureUrls.has(controller.textureAssetUrl)
|
||||
|| !Array.isArray(controller.tileSizePixels) || !controller.tileSizePixels.every(finite)
|
||||
|| !Array.isArray(controller.tracks) || controller.tracks.length !== 4
|
||||
@@ -149,12 +365,31 @@ export function parseRunewakerParticleGraph(value: unknown): RunewakerParticleGr
|
||||
}
|
||||
layerCount++;
|
||||
} else if (controller.type === "unsupported" && isVisualOnlyHolyCastTrigger(graph, motion, controller)) {
|
||||
audioTriggerCount++;
|
||||
visualOnlyTriggerCount++;
|
||||
} else if (controller.type === "unsupported" && isVisualOnlyRisingTideTrigger(graph, motion, controller)) {
|
||||
visualOnlyTriggerCount++;
|
||||
} else if (controller.type === "unsupported" && isUrgentPartialController(graph, motion, controller)) {
|
||||
visualOnlyTriggerCount++;
|
||||
} else if (controller.type === "entity-visualizer" && isUrgentEntityController(graph, controller)) {
|
||||
if (!emitters.has(controller.anchor)) throw new TypeError("Urgent rune entity has no emitter.");
|
||||
entityCount++;
|
||||
} else if (controller.type === "decal" && isUrgentDecalController(graph, controller)) {
|
||||
if (!textureUrls.has(controller.textureAssetUrl)) throw new TypeError("Urgent decal texture is not packaged.");
|
||||
decalCount++;
|
||||
} else if (controller.type === "global-color" && isUrgentGlobalColor(graph, controller)) {
|
||||
colorCount++;
|
||||
} else if (controller.type === "global-alpha" && isVerifiedGlobalAlpha(graph, motion, controller)) {
|
||||
globalAlphaCount++;
|
||||
} else throw new TypeError(`Unsupported RuneWaker controller ${String(controller.sourceType)}.`);
|
||||
}
|
||||
}
|
||||
if (audioTriggerCount !== graph.unsupported.length || audioTriggerCount > 1) {
|
||||
throw new TypeError("RuneWaker graph has unverified event triggers.");
|
||||
const [expectedTriggers, expectedAlpha] = expectedSpecialControllerCounts.get(graph.sourceModel) ?? [0, 0];
|
||||
if (visualOnlyTriggerCount !== expectedTriggers || globalAlphaCount !== expectedAlpha
|
||||
|| entityCount !== (urgentPartial ? 1 : 0)
|
||||
|| decalCount !== (urgentPartial ? 1 : 0)
|
||||
|| colorCount !== (urgentPartial ? 1 : 0)
|
||||
|| graph.unsupported.length !== (urgentPartial ? urgentUnsupportedTypes.size : expectedTriggers > 0 ? 1 : 0)) {
|
||||
throw new TypeError("RuneWaker graph has unverified event or alpha controllers.");
|
||||
}
|
||||
if (!layerCount) throw new TypeError("RuneWaker graph has no renderable sprites.");
|
||||
return graph as unknown as RunewakerParticleGraph;
|
||||
@@ -216,6 +451,17 @@ export function runeLayers(graph: RunewakerParticleGraph, phase: CombatPresentat
|
||||
});
|
||||
}
|
||||
|
||||
export function runeGlobalAlpha(graph: RunewakerParticleGraph, phase: CombatPresentationPhase, time: number): number {
|
||||
// Urgent Heal's alpha track belongs to its ground decal, not its sprite layers.
|
||||
if (graph.partialRendererReady) return 1;
|
||||
const controller = runeMotion(graph, phase)?.controllers.find(
|
||||
(candidate): candidate is RuneGlobalAlphaController => candidate.type === "global-alpha",
|
||||
);
|
||||
if (!controller) return 1;
|
||||
return Math.max(0, Math.min(1, runeSample(controller.alphaTrack,
|
||||
Math.max(0, time - controller.startTime), 1)));
|
||||
}
|
||||
|
||||
export function runeDurationMs(graph: RunewakerParticleGraph, phase: CombatPresentationPhase): number {
|
||||
const layers = runeLayers(graph, phase);
|
||||
const lastEmission = layers.reduce((maximum, layer) => Math.max(maximum,
|
||||
@@ -223,5 +469,9 @@ export function runeDurationMs(graph: RunewakerParticleGraph, phase: CombatPrese
|
||||
const maxLife = layers.reduce((maximum, layer) => Math.max(maximum,
|
||||
...layer.emitter.tracks[12].map((sample) => Number(sample.value)),
|
||||
...layer.emitter.tracks[13].map((sample) => Number(sample.value))), 0);
|
||||
return Math.max(400, Math.min(6_000, (lastEmission + maxLife + 0.2) * 1_000));
|
||||
const decal = runeMotion(graph, phase)?.controllers.find(
|
||||
(controller): controller is RuneDecalController => controller.type === "decal",
|
||||
);
|
||||
const decalEnd = decal ? Math.max(...decal.tracks[2].map((sample) => sample.time)) : 0;
|
||||
return Math.max(400, Math.min(6_000, Math.max(lastEmission + maxLife + 0.2, decalEnd) * 1_000));
|
||||
}
|
||||
Reference in new issue
Block a user