Skip to content

Add skill cast visuals - #414

Open
zxcjqk wants to merge 47 commits into
vE5li:mainfrom
zxcjqk:add-skill-cast-visuals
Open

Add skill cast visuals#414
zxcjqk wants to merge 47 commits into
vE5li:mainfrom
zxcjqk:add-skill-cast-visuals

Conversation

@zxcjqk

@zxcjqk zxcjqk commented Jul 21, 2026

Copy link
Copy Markdown

merge after
#413 Add skill feedback engine

JA 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.
@zxcjqk zxcjqk mentioned this pull request Jul 21, 2026
JA and others added 13 commits July 21, 2026 23:17
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.
JA 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
zxcjqk force-pushed the add-skill-cast-visuals branch from 64d3381 to e39ee60 Compare July 21, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant