fix/crossbow-attack-animation #207

Merged
sickday merged 4 commits from fix/crossbow-attack-animation into main 2026-07-22 12:00:11 +00:00
Owner
No description provided.
The ranged path picked its animation from a hardcoded per-class cond in
RangeFormula, which gave every crossbow 427 — a bow shot — instead of
the crossbow's own 4230 (Karil's 2075). Point it at the per-weapon
Game.Item.AttackAnims table the melee path already reads, keeping the
per-class ids as the fallback for anything the table doesn't cover, and
correct that fallback to 4230 as well. Cross-checked against a rev-474
server's equipment definitions: crossbows attack with 4230 and stand
with 4591, which is also what our stance table already had.

That exposed a stale entry in the table: every dart claims seq 582,
which in the 468 cache is a 74-frame sequence looping from frame 12
with no loop cap — an idle animation, not a throw, and the same class
of unbounded loop that soft-locked Rabbit Hop. The extractor only
checked that a seq id existed, so add a one-shot gate (drop anything
that loops without a cap) and regenerate: the 28 dart entries fall out,
nothing else, and darts keep throwing with 806. Obsidian rings pick up
their own 2614 from the table.
Follow-up sweep after the crossbow animation fix, checking every
ranged weapon in the 468 cache against the era-matched rev-474
equipment definitions and the 468 seq/spotanim archives.

Weapon classification was name-based and had three holes. Worn Karil's
crossbow is named "Karils x-bow 100", so it read as a *bow* — which
also meant enchanted bolts never procced off it, since that gate asks
crossbow?/1; lookups now resolve through Degradation first, the same
normalisation the combat panel already did. Seercull has no "bow" in
its name and one javelin is truncated to "Bronze jav'n(p++)", so both
swung as melee and could not shoot at all; they are listed by id.
Matching is now per word rather than substring, which stops a Bowl of
hot water, a Rainbow fish and a Fishbowl helmet from being bows.

A table animation belonging to another weapon class is now discarded:
the reference gives the Phoenix crossbow the bow shot 426, the same
bug as before in a single item. Every one of the 183 ranged weapons
now plays a class-consistent, one-shot 468 sequence — bows 426,
crossbows 4230, Karil's 2075, thrown 806, obsidian 2614.

Poisoned ammo and thrown weapons get their own item ids in 468, and
all 108 of them fell through to the unknown-ammo defaults: a
bronze-arrow projectile, no nock graphic and zero ranged strength, so
a poisoned rune arrow hit like a stick. They now resolve to the item
they were made from. Filled the remaining visual gaps from the same
ammo table: black dart, dragon dart, ogre arrow, and the bone/kebbit
bolts that were flying as arrows. Black knife had no ranged strength
row either; rev-474 gives it 8, between steel and mithril.

Attack speed now reads the per-weapon table the melee path already
uses instead of one speed per class, which was a tick out on every
shortbow (5 -> 4) and longbow (7 -> 6), two ticks on Karil's (6 -> 4),
and three on the ogre bow (5 -> 8). Ranged DPS goes up accordingly.

Known gap, unchanged: broad, brutal, ogre and bone ammo still have no
ranged strength row — the 468 cache carries no ranged-strength stat
and neither reference has a trustworthy number for them.
fix: ranged strength and flight graphics for specialty ammo
Some checks failed
ci / test (pull_request) Failing after 19s
119688b1f1
Closes the gap left by the ranged sweep: the ammo neither reference
carried a ranged strength for, which meant firing it did nothing. The
468 cache has no ranged-strength stat at all — item_bonuses.exs reads
0 for every ammo — so the numbers come from the OSRS Wiki: broad
arrows 28, ogre arrows 22, the brutal ladder 11/13/19/22/34/45/60,
bone bolts 49, kebbit 28 and long kebbit 38, barbed bolts 12, ice
arrows 16, training arrows 7, black dart 6, and fire arrows matching
the plain arrow of their tier.

Wiki values are current-OSRS, which only holds where the stats never
moved. The black dart is the check that the method is sound: the wiki
records its Ranged Attack dropping 7 -> 0 in 2021 and the 468 cache
still carries the 7, so the strength beside it is the old number too.
Broad bolts are the counter-example and stay out — they are a 2014
item and aren't in this cache.

That ammo was flying wrong as well. Bone, kebbit and barbed bolts used
the bronze-arrow projectile instead of the bolt one; broad arrows, ice
arrows, training arrows and every fire arrow tier had neither a
projectile nor a nock graphic. All taken from the same reference ammo
table as the rest, each graphic id verified present in the 468
spotanim archive.

Finally, the cache's same-named twin ids — the unused second Mithril
arrow, the extra Broad arrows ids, and the second id each fire-arrow
tier carries — now fold onto the real item the way poisoned variants
already did, so they can't silently read as unknown ammo. Every pair
was read off the archive once and written down; nothing resolves an id
by name at runtime.

Every named ammo id in the cache now has a strength except fletching
materials and containers (arrow shafts, headless arrows, bolt moulds,
Bolt of cloth), which correctly have none.
sickday force-pushed fix/crossbow-attack-animation from 119688b1f1
Some checks failed
ci / test (pull_request) Failing after 19s
to 635ba7a382
Some checks failed
ci / test (pull_request) Failing after 16s
2026-07-22 11:42:05 +00:00
Compare
fix: drop dead thrown_base_speed, harden death-drop test setup
All checks were successful
ci / test (pull_request) Successful in 2m29s
e935593b38
CI failed at the "Compile (warnings as errors)" step: moving ranged
attack speed onto the per-weapon table left thrown_base_speed/1 with
no callers, and an unused private function is a warning. Local runs
never caught it because mix test doesn't compile with that flag —
reproduce with MIX_ENV=test mix compile --force --warnings-as-errors.

Also hardened the death-drop test's setup, which failed on roughly two
in three local runs with "the table identifier does not refer to an
existing ETS table". Zone.init_tables/0 is all-or-nothing and its
tables belong to whichever process called it, so when a concurrent
async test's process exits, part of the set survives: the next
init_tables/0 raises on the table that is still there without
recreating the ones that aren't, and the unconditional
delete_all_objects that followed then blew up on the missing one. It
now seeds each table individually and publishes the buffer index if
nothing else has. Nine seeds green.
sickday referenced this pull request from a commit 2026-07-22 12:00:12 +00:00
Sign in to join this conversation.
No description provided.