Add skill cast visuals - #414
Open
zxcjqk wants to merge 47 commits into
Open
Conversation
added 16 commits
July 21, 2026 22:05
Map 1-9 and 0 to the ten shortcut-bar slots and keep channel stop events paired with an actual unmodified cast press. Add regression coverage for slot mapping and modified key combinations.
Let selectable hotbar skills cycle their cast level with right click. Store the selection in the existing hotkey quantity, send it to the map server, and derive the true ceiling from the learned skill so reconnects, swaps, and clears remain consistent.
Arm targeted skills until a valid world destination is confirmed, validate typed entity and ground targets, and cancel cleanly across input and connection lifecycles. Track continuous casts by their resolved skill and owning input so slot changes, modifiers, focus loss, and toggle transitions cannot strand a channel.
Keep single-click drag behavior, allow maxed skills to remain movable, and add double-click casting from both the hotbar and skill tree. UI activations toggle continuous skills and respect the skill tree's selected level without interfering with skill-point distribution mode.
Tint only the newly emitted render instructions for a live entity accepted by the armed skill. Keep shadows untouched, suppress the tint over interface windows, and use post-click targeting state so confirmed or cancelled targets do not linger for an extra frame.
Use ZC_USESKILL_ACK header 0x0B1A for the supported 20220406 packet version and include its trailing attack-motion field. Add an exact 29-byte encode/decode regression test for the packed wire layout.
Turn the modern cast acknowledgement into a typed start event and add the fixed-layout ZC_DISPEL cancellation packet. Keep both packets available to the in-client packet history and the PCAP inspector, and cover their back-to-back wire framing with regression tests.
Play the casting action and draw themed progress bars for active casts. Track cast timing independently from sprite actions so missing assets and hit reactions cannot leave stale or hidden progress, and clean it up on cancellation, expiry, death, disappearance, and map changes.
Document and test the 14-byte ZC_ACK_TOUSESKILL layout, keep its signed detail and item context, and represent failure causes with an unknown-tolerant newtype so future server codes remain decodable in packet tooling.
Convert rejected skill acknowledgements into context-preserving events and show useful English or German chat errors. Resolve required item names, format resource and prerequisite details, keep unknown causes safe, and leave successful acknowledgements silent while retaining them in packet history.
Model ZC_NOTIFY_SKILL with the exact signed wire fields so damage sentinels, motions, hit counts, and action values decode without truncation. Add a byte-exact round-trip test to protect the 33-byte layout.
Translate ZC_NOTIFY_SKILL into a dedicated event and show damage or miss particles without triggering auto-attack continuation or normal attack animations. Suppress signed server sentinels and keep critical styling limited to the known critical action values.
Separate immediate playback probing from deferred loading so multi-hit sequences can retry without enqueueing duplicate sounds. Bound queued sound lifetime, expose cleanup, and cover expiry behavior with tests.
Propagate texture loading failures, provide deterministic first-frame blend defaults, guard texture indices, and support effects without point lights. Replace duplicate skill-unit effects by entity id and let lightless effects expire immediately.
Classify entity, ground, and skill-unit attachment for verified Fire, Lightning, Sight, Blessing, Pneuma, and Heal assets. Add bounded procedural Cold Bolt and Frost Diver particles for skills whose GRF lacks a complete named world animation, with explicit direct-effect routing and timing tests.
Extend particle and effect holders with target-following procedural particles and ACT/SPR attachments, including the separately stored local player. Export skill recipes, support keyed attachment replacement and cleanup, and split multi-hit damage without losing the total.
Preserve authoritative cast, damage, no-damage, ground, unit, status, and option-state fields through networking, including unknown UnitId values. Route those events into target-, ground-, and unit-attached animations, paced damage and audio, Sight lifecycle, and world-transition cleanup while retaining debug packet inspection.
The unit id table had a naming off-by-one around 264-268, verified against rAthena's e_skill_unit_id by two independent methods (parsing the enum, and counting entries from the UNT_SAFETYWALL = 0x7e anchor): - Creatingstar was 264; UNT_CREATINGSTAR is 265. - 264 is UNT_GROUNDDRIFT_NEUTRAL, which was missing and had its slot occupied by an unreferenced Dummy0 placeholder at 265. - UNT_SEEDTRAP (268) was absent entirely. No current effect recipe referenced any of these, and UnitId is a struct so unknown values already decoded safely, so this fixes a latent mislabel rather than active behavior. All 180 unit ids now match rAthena exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The official client models Fire Bolt in two stages: a projectile that falls onto the target, then a separate impact animation. Only the impact was implemented, so bolts appeared out of nothing on the target. Add the projectile stage and make hit feedback wait for it. The projectile is a descent-only particle cycling the six 128x64 "fire arrow" frames the reference clients use, approaching from a fixed upper corner with per-bolt jitter so a volley reads as one stream rather than a ring. It converges completely onto the target, because that is where the impact animation plays. Hit feedback is delayed by the projectile's flight time, taken from the packet's own source motion. ZC_NOTIFY_SKILL carries rAthena's sdelay, which is the caster's attack motion and therefore the interval the server itself puts between the skill resolving and the hit landing. The particle's lifetime is that same value, so the impact takes over at the exact moment the projectile arrives instead of the two being tuned against each other. Attack motion is ASPD dependent and unbounded as a flight duration, so it is clamped to a visible range. Skills without a leading projectile keep a zero lead and are unchanged. Fire Bolt's multi-hit cadence moves from 120ms to 200ms. That is the official PLUSATTACKED_MOTIONTIME, which both reference implementations use; the projectile and the impact fan out on it together so the pairing holds across the whole volley. Cold Bolt still paces at 120ms and is left for a separate change. All referenced assets were verified present in data.grf before use: the six projectile frames, the three launch sounds, and the impact animations. GRF filenames decode as EUC-KR, so the Korean texture paths resolve directly.
The projectile rendered as an interface sprite, and that path draws screen-aligned quads with no rotation. The source textures are horizontal streaks, so every bolt flew sideways regardless of where it was heading. Render it through the effect pipeline instead. That is the path the impact animations already use, and its quads carry both a rotation and per-quad blend factors, so no renderer or shader change is needed. The effect trait's update signature matches the particle one, so the projectile still follows a target that moves mid-flight. The rotation aims the texture's local +X axis at the landing point. Which axis is forward was measured rather than assumed: the alpha mass of the frames sits right of centre and thins towards the left edge, so the dense head is at +X. Screen space is y-down and so is the effect renderer's corner space, which lets the heading be used directly. The angle is taken in pixels so a non-square window does not skew it, and a degenerate path leaves the quad unrotated rather than spinning. Rotation pivots about the quad centre by passing the renderer's own effect origin as the offset, which cancels the shift it applies to STR frame coordinates. That origin is now a documented constant instead of being buried in the renderer, so callers building their own quads do not have to repeat the magic numbers. Blending is now additive, matching the reference client's fire blend mode. The interface path could only do premultiplied alpha. The angle convention is split into a pure function so it can be tested without a camera: a sign error there would still look plausible while pointing every bolt backwards.
The projectile was drawn at 32x16 while its source frames are 128x64, so it was both downsampled fourfold and visibly small. Size it to the native 128x64 instead. That keeps texel mapping 1:1 and puts the bolt in the same scale band as the impact it precedes: measured in the same corner space, firehit2.str renders quads of median 81x137. The projectile was about a quarter of that linear size, which is why it read as small next to its own impact. The width and height are now named constants with the halves derived, so resizing is one edit rather than two that can drift apart, and a test pins the 2:1 aspect. A non-2:1 quad stretches the streak, which is most visible once the bolt is rotated onto its flight path.
Cold Bolt was drawn with procedural particles under the belief that the archive had no usable asset. The archive does have one: the official client renders Cold Bolt exactly like Fire Bolt, as a single texture falling onto the target, and ships icearrow.tga for it. The earlier comment was right that there is no STR for it, but the reference client does not use a STR either. Point Cold Bolt at the real asset and generalise the Fire Bolt projectile to carry its own art. The two bolts are the same effect shape with different assets, and sizing them from one shared constant is not possible: the fire arrow is 128x64 and the ice shard is 128x128, so a shared quad would stretch one of them. Size, frames, frame cadence and launch sounds now live on the art, and each is rendered at its own native resolution. Cold Bolt gains the leading behaviour and the 200ms cadence that Fire Bolt already had, so its hit resolves when the projectile lands rather than as the packet arrives. Its sounds are also rebalanced to match the reference: the ice arrow sounds belong to the projectile rather than to the hit, so they now play on launch, randomised per bolt, and the hit plays a generic elemental impact instead. Playing them on the hit meant a launch sound with no projectile to launch. Repeated impacts now vary. The official client picks among firehit1-3 per hit rather than replaying one animation, and Fire Wall's hit shares that effect, so both inherit it. Variant selection takes the roll as a parameter so the choice is testable rather than only observable in motion, and every selectable path is asserted to be preloaded. All referenced assets were confirmed present in data.grf before use, with dimensions read from the files rather than assumed.
Sizing the ice shard at its native 128x128 made it overpower the scene. A square reads considerably heavier than a streak of the same width, so matching the fire arrow's 128 was too much even though both textures share that dimension. Render it at half Fire Bolt's width and the same height, which is the ratio the reference client uses: it sizes the ice shard at 50 against the fire arrow's 100x50. This downsamples the source texture, as the reference does too. The size is now derived from the fire arrow's height rather than written out, so the two stay in proportion if either is retuned. The test that asserted a minimum width against the fire impact was calibrated for the streak and did not apply to the shard, which resolves its hit differently; it is replaced by the reference ratio, which is the invariant actually worth holding.
Both projectiles were drawn 1.28x too large, the streak most visibly so. They were sized at the source textures' native resolution on the reasoning that this kept texel mapping 1:1. That constraint was invented: the official client deliberately draws the fire arrow's 128x64 texture at 100x50, and the ice shard's 128x128 at 50 square. Native resolution is not the intended render size. The reference values transfer directly. Its projectile sizes and STR frame coordinates share one unit space, both divided by 35 to reach world units, and the sprite quad spans -0.5 to 0.5 so the declared size is a full extent rather than a half one. That holds up against a yardstick both clients share. Measured against firehit2.str, which each loads from the same archive, the reference draws the streak at 1.23x the impact's median width. Native resolution had it at 1.58x. Comparing through a shared asset this way cancels out the difference between the two clients' coordinate spaces, so it agrees with the direct reading rather than merely resembling it. Only the fire arrow's numbers change; the ice shard is derived from them and follows automatically. A test now pins both sizes and the ratio to the impact, since this is the third correction to the same constants.
Lightning Bolt played lightning.str with its thunder clap once per damage packet and nothing per hit. The official client stages it as two effects: the big strike once when the cast resolves (EF_LIGHTBOLT), and a wind elemental hit with a fist impact sound for every damaging hit (EF_WINDHIT), varied across windhit1-3 and _hit_fist1-3. Add the missing per-hit stage and a dispatch slot for once-per- resolution visuals, which the handler queues with a hit count of one so it never repeats across a volley. The wind hit paces at the shared bolt cadence and carries no light of its own: the strike already lights the scene, and a per-hit light would stack across a volley. The old 120ms damage-number pacing is dropped rather than moved: number pacing falls back to the per-hit visual's cadence, so the numbers now follow the wind hits. All six assets were verified present in data.grf before use.
The projectile machinery could only drop a bolt onto its target. The reference clients have a second motion family for arrows, thrown weapons, and flying spheres: launch at the caster, fly to the target, fade in over the first quarter of the flight and out over the last. Projectile art now declares its motion. A travelling projectile stores the caster's position at spawn as a fixed launch point while continuing to follow the target, which both references agree on: a projectile in flight must chase a moving target but must not teleport when its caster moves. A caster that despawned before its projectile resolved falls back to the falling entry rather than the world origin. Frames may now also come from an SPR/ACT pair instead of standalone textures. The sprite provides the frames in order and the action provides the cadence, using the same 50ms delay unit as the fork's own action renderer. Quad sizes can be declared outright, as the two bolts are, or resolved from the first frame's native pixels for SPR sources whose frame sizes are not knowable statically. Flight duration is unchanged and remains derived from the packet's attack motion. One reference flies projectiles for a fixed 140ms regardless of distance and the other at fixed speed with the duration falling out; deriving speed from the server's own timing is both more faithful than the first and keeps the impact-after-arrival guarantee the second drifts on. Falling bolts are bit-identical to before: same motion, no fade, same declared sizes, and the tests that pinned them still pass unchanged.
Fire Ball played its sphere sprite as a stationary attachment on the victim. In the official client that sprite is the projectile: the sphere flies from the caster to the target, and the impact is the same fire hit that Fire Bolt uses. The old rendering was wrong, not incomplete, so this deletes it rather than adding around it. The sphere is a travelling projectile animated from its own SPR/ACT pair, fading over the outer quarters of its flight, with the fireball whoosh at launch. Its quad derives from the frames' native pixel sizes at twice scale; the reference table's size for it only makes sense read as a percentage, so the scale is tunable rather than measured. Only the primary victim's packet launches a sphere. Splash victims receive their own damage packets, distinguishable by rAthena's splash damage types, and would otherwise each throw one. Their impacts still wait for the shared flight, so the surrounding burns land with the sphere rather than before it. The splash distinction moves from the deleted sprite dispatch onto the procedural recipe, where any future splash projectile can reuse it. The stationary sphere survives in one place: the direct-effect debug path, whose packet carries a single entity and so has nowhere to throw from. The reference also throws a short trail of ghost copies behind the sphere; that is deliberately left out until the single sphere has been seen in motion.
Eight bow skills resolved with nothing but damage numbers: Double Strafe, Charge Arrow, Musical Strike, Slinging Arrow, Arrow Vulcan, Phantasmic Arrow, Aimed Bolt and Triangle Shot. The reference client gives all of them one shared projectile, the skeleton archer's ammunition sprite, flying from the caster to the target. Wire them to a shared arrow art: SPR/ACT sourced, travelling, fading over the outer quarters of its flight, rendered at the frames' native size. One arrow per hit at the shared bolt cadence, so Arrow Vulcan streams its nine. Silent by design: the reference declares no launch sound for arrows, and the bow's own attack sound already plays through the attack motion. Skill ids were verified against rAthena's skill_db.yml rather than copied from the reference's constants. Also simplify the two dispatch functions the earlier restagings left as single-arm matches, which clippy rightly flagged.
Lightning Bolt's damage numbers and wind hits appeared the moment the strike animation began, while the bolt was still falling: the strike had no lead, so every hit was scheduled at the packet's own time. Model the strike as a wind-up that leads its hits, exactly as a projectile's flight leads its impact and from the same authority: the packet's attack motion is the server's own interval between the skill resolving and the hit landing. The strike starts immediately; the numbers, wind puffs and fist sounds wait for it. Skills with neither a projectile nor a leading cast visual keep a zero lead and are unaffected. The per-hit wind puff deliberately does not lead, or it would compound the delay onto its own hits.
added 18 commits
July 21, 2026 23:17
The flight rotation assumed every projectile's art points along +X, the convention measured from the fire arrow streaks. The fireball and the ammunition arrow are drawn differently, so both flew visibly misaligned with their movement. Art now declares a base angle added to the flight heading. The values are measured, not assumed, by the same alpha-mass method that established the +X convention: the fireball frames' mass sits above centre in all six frames, meaning the flame tail rises and the ball faces the image bottom, a quarter turn past +X. The ammunition sprite is 8x61 and therefore vertical, making a quarter-turn magnitude certain; its mass is symmetric, so which end is the head could not be measured, the image top is assumed, and the constant's sign is the one thing to flip if arrows fly tail-first. The streaks keep a zero correction and render identically.
The sphere flew tail-first. Its frames' alpha mass sits above centre, which the orientation fix read as the flame rising above a downward- facing ball; in motion the opposite is true, the dense upper mass is the ball itself with the flame trailing below, so the forward axis is the image top and the quarter-turn correction takes the other sign. A lesson recorded for the next sprite: alpha mass locates the art's dense end, but which end leads is a judgement about the drawing, not a measurement, and wants confirming in motion.
The hits still preceded the visible strike. The lead derived from the caster's attack motion, clamped to at most 0.6 seconds, but the strike animation is on its own clock: measured from lightning.str, nothing renders for the first 0.58 seconds, the rings swirl from key 35, the first bolt column appears at key 45, and the ground bang flash begins at key 56 of 60fps. The bang is the moment the damage belongs to, 0.93 seconds in, which the attack-motion clamp could never reach. A leading cast visual now declares its wind-up as a measured duration from its own animation instead of borrowing the projectile's clock. Projectiles keep the attack-motion lead, since their flight is paced to arrive when the server says the hit lands; a skill with both waits for the later of the two. With this the order the eye expects holds by construction: rings, bolt, bang, and only then the damage numbers, wind puffs and fist sounds.
The arrow rendered at its frame's native size and flew on the attack- motion clock, and in motion it read as a big slow bolt rather than an arrow. The native-resolution trap once again: a size the artist meant for one renderer is not a declaration of intended world size. Arrows get a declared size in the streak's scale band, keeping the sprite's own slender aspect, and projectile art gains a fixed flight override matching the reference client, which flies arrows in 140 milliseconds and the fireball in 250 regardless of the caster's attack motion. The hit keeps the server's own timing either way: a faster projectile lands earlier than its impact rather than desynchronizing it, so the impact-after-arrival guarantee is untouched. The arrow's orientation note is also settled by measurement now: the row profile shows a short head blob at the image top, a constant shaft and longer fletching tapering to a one-pixel nock at the bottom, so the quarter-turn correction stands confirmed rather than assumed.
Arming a skill left the plain attack cursor. The reference client switches to the spinning aim circle, cursor action 10, with the skill's cast level beside it, which is also what the fork's per-slot cast levels deserve: the number shows which level a click will cast. The cursor state enum already carried action 10 as an unknown; name it and use it whenever a skill is armed outside the interface. The level renders white over a dark offset copy so it stays readable on any ground, at the reference client's own placement beside the circle. The AlignHorizontal re-export was gated behind the debug feature for no reason a text-drawing caller can see; it is ungated rather than gating the cursor's text on debug builds too.
Casting showed only the cast bar; the classic magic circle around the caster was absent. The skill-use acknowledgement already carries the element for it, decoded and discarded until now. The aura is a pulsing elemental ring at the caster's feet with the begin-spell hum, spawned when a cast starts, following the caster, expiring with the server's cast duration and torn down by key when the cast is cancelled. A new cast replaces any aura the entity still has. The archive ships yellow, blue and red rings, so water and fire casts get their own colours and everything else falls back to the classic yellow. The classic aura is a rising textured cylinder; the particle path draws camera-facing quads only, so the ring is flattened to fake the ground perspective, an approximation to revisit if a ground-plane quad ever becomes available.
The classic client announces every skill use with its name in green over the caster's head. Names come from the archive's own skill info table, which the fork already loads for the skill tree, so any caster's skills are covered rather than only the local player's, and a skill the table does not know simply raises no bubble. The bubble follows the caster, fades over its final quarter, and rides the existing entity-particle machinery, expiring on its own. It fires once per cast: for damaging skills on the primary victim's packet only, since splash victims carry packets of their own, and for supportive skills when the server reports success.
Arming a skill showed an arrow instead of the aim circle. The cursor act is not directional: it has 14 actions, one per cursor type, but the shared action indexing computes base * 8 + direction and reduces modulo the action count. Reaching action N therefore requires a direction of 7N modulo 14: zero for even actions, seven for odd ones. The old hardcoded direction list satisfied this by accident for every state it was written against, all of which happened to have the right parity, and silently showed the wrong cursor for the even-numbered ones: Target rendered action 3, NoAction rendered the dialog cursor and WarpFast rendered the attack cursor. Derive the direction from the parity instead, which fixes all three while keeping every previously working state bit-identical, and pin the whole mapping with a test so the next even-numbered state cannot regress it.
A cast showed only the caster's side: the aura at their feet and the cast bar. Both reference clients also mark the victim for the length of the cast with a lock-on circle shrinking onto them, drawn from the same lockon128 texture the archive ships, so the cast reads as aimed at someone rather than merely happening. The aura generalises into a pair of cast rings keyed by the caster: the elemental aura following the caster and the warm-tinted lock-on following the target, shrinking onto it and then breathing. Keying both by the caster means a cancelled cast tears down its aura and its lock-on together, which per-entity keying could not express once the two rings follow different entities. Ground casts and self-casts carry no separate victim to mark and raise no lock-on.
The lock-on circle only scaled and the aura only pulsed: both rendered through the interface sprite path, which cannot rotate a quad or blend additively. They now render through the effect pipeline, which does both, in a dedicated pass keyed off the same particle-holder lifecycle as before. Measuring the textures corrected a wrong assumption on the way. The ring images are opaque and bright to their very edges: they are not top-down ring pictures but cylinder wall bands, horizontally tileable, which is why the reference clients wrap them around a rising cone and why the repeat sampler suits them. The aura is therefore drawn as a trapezoid cone silhouette with the band's U coordinate scrolling for the swirl, two layers at different speeds as the reference spins its arcs at 180 to 300 degrees per second, blended additively as both references do. The lock-on texture's bright corners are likewise part of the art, a reticle with corner brackets. It spins as a whole quad at the reference's 270 degrees per second, flattened after rotation, which is the correct foreshortening of a ground spinner viewed obliquely, and shrinks onto the target as before. The band's vertical orientation could not be determined from the file; if the aura reads brighter at the top than the bottom, the V coordinates want swapping.
The aura and the lock-on drew as screen-space effects over everything, floating on top of the caster and the target. The classic look needs the opposite: the part of a ground ring behind a character hides behind their sprite and the part in front overlaps their feet. The forward pass gains a ground-marker drawer for emissive, depth- tested quads: world-space corners, additive blending, tested against the depth the entities and models have already written but never writing any itself, so markers are occluded correctly from any angle without ever occluding anything. It draws at the end of the forward pass, instanced from a storage buffer with one bind group per marker's texture, and is unlit, since the markers represent glowing magic. The rings use it for their ground components. The aura's circle is the top-down magic-target texture, verified genuinely circular unlike the cylinder bands, tinted by the cast's element and swirling gently; its rising cone stays in the effect pass, compositing over the scene as glow. The lock-on reticle becomes entirely a ground marker, spinning at the reference's rate in world space, which also renders its rotation correctly foreshortened at any camera angle rather than approximated in screen space. The test build tripped the compiler's default trait recursion limit with the added drawer and the limit is raised as the compiler suggests.
The cast circle sat correctly under the caster, but the cone swirling above it still rendered through the screen-space effect pass, painting itself over the caster instead of around them. Keeping it there as glow was a judgement call, and in motion it was the wrong one. The cone becomes camera-facing vertical trapezoids in world space, drawn by the same depth-tested ground-marker drawer as the circles, so the caster's sprite now covers the part of the cone behind them. The swirl survives the move: ground markers gain a texture-coordinate transform, and scrolling the band's U under the repeat sampler is unchanged from the effect-pass version. Billboarding needs the camera's horizontal right vector, which is computed while the camera borrow is active and carried as a plain value into instruction building. With this nothing of the cast rings renders through the effect pass any more, and the last of that path's screen-space cast code is gone.
The circles and cone were sized in world units without a tile anchor and came out about two thirds of the classic look. One tile is five world units, which converts the references directly: the reticle is three tiles across in the reference client, the classic cast circle spans about the same, and the swirling arcs orbit wider than the body and rise past its height. The ground circles grow to three tiles, the lock-on shrinks from three tiles to about one and a third for a deeper contraction onto the target, and the cone opens to roughly three and a half tiles at a character's height.
The cast cone showed a hard diagonal line: it is a flat quad faking a cylinder, and the band textures are opaque to their very edges, so the trapezoid's slanted silhouette rendered as visible lines. The reference clients never show this because the classic cylinder wraps fully around and the arc ribbons are alpha-soft; a single-quad approximation needs the softness added back. Markers gain an edge fade that sinks alpha toward the horizontal edges, which also reads as the curvature itself, a glowing cylinder seen face-on being brightest at its centre. The marker coordinate table is also corrected. It was copied from the walk indicator, whose table is transposed relative to the corner order, which a rotationally symmetric texture hides completely; the scrolling band does not, and swirled vertically along the cone instead of around it. The circles are unaffected either way.
The lock-on read small because it shrank below its official size: the reference reticle snaps from fifteen cells down to three within a fifth of a second and rests at three for the whole cast, only its colour pulsing. Ours contracted gently past that to under a tile and a half and sat there. The reference is unambiguous about the numbers: its unit quad spans half a cell per unit of its size parameter, and the size animates from fifteen down to three cells. The snap replaces the slow shrink, and the resting size is the official three tiles.
A casting monster stood idle: the skill action mapped to a dedicated casting action that monster acts do not have, falling through to idle. The classic client plays their attack motion for the wind-up, which is also what the reference implementations do.
Bow skills resolved with no motion from the caster: arrows simply appeared mid-flight. Skill packets deliberately bypass the auto-attack damage path and its animation, a correct decision that also discarded the attack motion both reference clients play on every skill use. The caster now turns to the victim and plays their attack motion, paced by the packet's own attack motion time, triggered directly from the skill arm so none of the auto-attack buffering is touched. Only the primary victim's packet fires it, since splash victims carry packets of their own. The full answer to bow animation runs deeper than this: the classic client picks among three attack actions per weapon class and overlays an equipped-weapon sprite, driven by its per-job weapon tables, and none of that machinery exists in the client yet. The attack motion is the visible half; weapon sprites and weapon-class action selection remain open as their own feature.
The begin-spell hum played at full volume, drowning the mix for the caster standing at distance zero. Spatial sounds gain a base volume applied on top of the distance attenuation, set at track construction where the mixer already owns a volume parameter, and the hum plays at half, which is the reference client's own level for it. The distance falloff itself is unchanged: linear from five world units to the sound's range, which already reaches near-silence at typical skill-target distances. The problem was the starting level, not the slope.
zxcjqk
force-pushed
the
add-skill-cast-visuals
branch
from
July 21, 2026 14:27
64d3381 to
e39ee60
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
merge after
#413 Add skill feedback engine