Skip to content

Summoning: complete familiar abilities (specials, passives, dialogue)#1052

Merged
GregHib merged 158 commits into
GregHib:mainfrom
HarleyGilpin:feat/summoning/interactions-and-dialogues
Jul 12, 2026
Merged

Summoning: complete familiar abilities (specials, passives, dialogue)#1052
GregHib merged 158 commits into
GregHib:mainfrom
HarleyGilpin:feat/summoning/interactions-and-dialogues

Conversation

@HarleyGilpin

@HarleyGilpin HarleyGilpin commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fleshes out summoning familiars: talk dialogue, special moves for every familiar, foraging, skill boosts, passive abilities and overhead chat.

Talk dialogue & chatheads

  • Per-familiar Interact talk dialogue for ~60 familiars (random + context-aware lines).
  • Familiars use a large, static correct-model chathead.
  • Interact (and every other option) keeps working on the wilderness <id>_combat form - transformed npcs fall back to their base id's interaction handlers.

Special moves

Every familiar's cast-button special is implemented - combat hits, stat drains, stuns/binds, AoEs, boosts, heals, teleports, foraging drops and banking. Highlights:

  • Cast surfaces: plain click (instant, or auto-fired at the familiar's current combat target as in the live game), click-a-target (npc/player/scenery), Cast-on-inventory-item (Swallow Whole, Immense Heat, Ophidian Incubation, Winter Storage) and Cast-on-ground-item (Rise from the Ashes). Item-target specials print a usage hint on a bare click instead of doing nothing.
  • Projectiles fly at the live game's distance-scaled speed (1 tick + 1/3 tick per tile).

Passive & other abilities

  • Hunting cat teleports (graahk/kyatt/larupia; lava titan's Lava Maze option now gated + animated), bunyip raw-fish -> water runes and 20 LP/15s passive heal, geyser titan bowl-boiling and glory recharging, giant ent essence -> earth/nature runes (stacking), pyrelord/forge regent log-burning beneath themselves (with the player-style step-west), spirit dagannoth Ferocious double-strike, ibis/granite lobster harpoon substitution and fishing-gated foraging with a cut of the xp, karamthulhu Drown / smoke devil Flames attack orders, cockatrice Drain recast, unicorn Cure (fixed - it checked a nonexistent variable).
  • Style-gated defence passives: iron/steel titan owners are 10%/15% harder to hit with melee, wolpertinger owners 5% harder with magic.
  • Data-driven Player.familiarBoost(skill) now supports level-scaled formulas (stranger plant Farming +1+4%, geyser Ranged +1+3%) alongside flat boosts; the graahk's missing Hunter +5 restored.
  • Familiars follow the real PvP rules when sent at players (wilderness or PvP area + combat-level bracket via Target.attackable), not a Clan-Wars-only flag.

Foraging

  • Generic forager system: gathers from a forage_ drop table into the familiar inventory every ~30s, retrieved via Withdraw/Take (reuses the beast-of-burden UI). Covers 20 overworld foragers; the fishing foragers only produce while their owner fishes.

Engine fixes surfaced by this work

  • A second same-tick graphic no longer overwrites the first (owner sparkles now render alongside the scroll-cast flourish).
  • Familiar AoEs find npcs by their bodies, not just their anchor tile - large monsters beside the familiar count.
  • NPC followers face an adjacent idle owner instead of shuffling onto the follow tile, and step out to a free tile when stood on (as combat movement does).
  • Projectile model orientation: cache graphic patching (GraphicEncoder + FixGraphics tool, wired into CacheBuilder) rotates the backwards-facing Toad Bark cannonball 180 degrees - the protocol itself has no projectile rotation.

Overhead dialogue

  • Familiars shout a random overhead line ~every 30s; 83 familiars' lines sourced from the RS Wiki, stored as a data-driven familiar_overhead table.

Manual Testing

Tracked per-familiar in the QA sheet (special move, boosts, other abilities, in-game visuals):
https://docs.google.com/spreadsheets/d/14LjVE4VdZ8FEGrl42_MJjrY3iAfAV_6CJ1ZuV31r-eQ/edit?usp=sharing

In-game verification during development caught and fixed: silent cast buttons, backwards projectiles, wrong attack animations, PvP-area refusals, non-stacking runes, forager output, and follow/step-out movement quirks.

Unit Tests

game/src/test/kotlin/content/skill/summoning - all passing, including:

  • FamiliarSpecialMoveTest - the scroll/points cast gate (spend-on-success, refusals)
  • FamiliarSpecialEffectTest, FamiliarCombatSpecialEffectTest, FamiliarUtilitySpecialEffectTest - every special's effect through the registered blocks and, where the surface matters, the real dispatchers (orb cast clicks, item-on-npc, cast-on-item, dialogue options)
  • Beast-of-burden, forager, heal, and definition tests
  • Full :engine:test and :game:test suites green

References

Special Moves (Familiars lvl 1-40): https://www.youtube.com/watch?v=kduBCqBiLB0
Summoning Scrolls Knowledge Base Article: https://2011.rs/kb/summoning_scrolls
darkan (primary mechanics reference): local checkout, Scroll.java / Pouch.java
2009scape (animation/config reference): local checkout, npc_configs.json

Port talk dialogue and special-op interactions for ~62 summoning
familiars from a reference implementation, matching the existing
per-familiar Script pattern (SpiritWolf/AbyssalLurker/AbyssalParasite).

- Per-familiar Interact dialogue scripts (random + context-aware:
  inventory/equipment/stat/BoB-slot/run-energy/staged-state checks),
  grouped via wildcard ids for minotaurs, cockatrices, titan tiers,
  and gorajo/clay dungeoneering familiars.
- Special ops: unicorn Cure (gated; inert until poison exists),
  pyrelord logs->firemaking, lava titan Lava Maze teleport,
  dreadfowl/compost mound Boost Farming.
- Generic expression_familiar_* chathead animation family + unicorn
  cure anim/gfx; dialogue="familiar" + interacts=false on all
  scriptable familiars.
The generic expression_familiar_* family applied the shared cat head
talk animations to every familiar's chathead model, which animates
incorrectly for non-feline familiars. Drop the dialogue="familiar"
override and the generic family so each familiar renders its own
chathead model with no forced (wrong) animation. Correct per-familiar
talk anims can be added later as expression_<familiar>_* families
(via ::expr_scan), like the existing wolf/bird/leech sets.
Familiars are creatures; add large_head = true to every familiar npc
def so their dialogue renders the bigger chathead (as cows, camels and
other creature npcs already do).
Foragers (starting with Magpie) periodically gather loot from a
forage_<familiar> drop table into the familiar's inventory while
summoned, and the player retrieves it via the familiar's Withdraw/Take
option (reusing the beast-of-burden inventory + interface). Players
can't deposit their own items into a forager.

- Forager script: 30s forage timer rolling the drop table into the
  familiar inventory, plus Withdraw/Take open handlers. Foragers are
  detected data-drivenly by the existence of a forage_<id> drop table
  (no new npc-def param, which must map to a real cache param).
- forage_magpie drop table (gems/rings) in summoning.drops.toml.
- Magpie marked as a beast-of-burden (cap 30) to reuse the storage +
  withdraw UI; store() rejects deposits into foragers.
- Start/stop the forage timer on summon/dismiss.

Other forager familiars are now just data: add a forage_<name> table.
Port the forage drop tables for beaver, spirit spider, granite crab,
granite lobster, ibis, macaw (grimy herbs), compost mound, evil turnip,
fruit bat, giant ent, stranger plant, void ravager and the cockatrice
family, and mark each as a beast of burden (cap 30) so they reuse the
forage inventory + Withdraw/Take interaction.

Tables model the reference's "1/outOf chance, then drop one" via a
1/<roll> outer roll deferring to an `_items` sub-table for multi-item
foragers (also corrects magpie, which previously dropped every tick).
Item ids resolved to void string ids.

Dungeoneering hoardstalker foragers (tier t1-t10) are deferred.
The farming boost was wrongly offered as a "Boost Farming" entry in a
Talk-to/Boost choice on Interact. Make Interact talk-only again and
trigger the boost from the familiar's special-move ("Special") option
instead, via the cast_dreadfowl_strike / cast_generate_compost buttons
on the summoning orb and familiar details tab.
The boost is on the familiar's "Special" right-click option, not the
special-move interface button. Use npcOperate("Special", ...) guarded
to the player's own familiar.
Clicking the dreadfowl's "Special" option now plays the asking-to-fight
dialogue, the dreadfowl's special animation (7810) + gfx (1523, from
darkanrs' Dreadfowl Strike sync), then boosts Farming by one.
Player plays gfx 1307 and the dreadfowl plays gfx 1317 alongside its
special-strike animation (5387) when boosting Farming via the Special
option.
The dreadfowl/compost-mound Special Farming boost now refuses to apply
when Farming is already boosted (e.g. by a garden pie), and is stripped
when the familiar is dismissed or dies (both route through
dismissFamiliar) - only if it's still the active, unchanged boost.
Introduce a data-driven registry of the passive skill-level boosts a
summoned familiar grants (beaver +2 Woodcutting, granite lobster +4
Fishing, obsidian golem +7 Mining, etc.) and a Player.familiarBoost
function. The boost is read live from the active follower and added to
the effective level in skill success checks - so it's invisible (not in
the skill tab), never lets the player act above their real level, and
disappears the moment the familiar is dismissed or killed.

Wired into woodcutting, fishing, mining, firemaking and thieving.
Hunter boosts (arctic bear, wolpertinger, kyatt, larupia) are in the
registry but inert until the Hunter skill is implemented.
Summoned familiars now periodically shout a random overhead line every
~30s (driven by the existing familiar timer), matching RS. Lines are a
data-driven map keyed by familiar id, sourced from the RuneScape Wiki
transcript "Overhead dialogue" sections - 83 familiars covered. Phoenix
and the gorajo (bloodrager/stormbringer/etc.) tiers have no overhead on
the wiki and stay silent.
Replace the hardcoded Kotlin FAMILIAR_OVERHEAD map with a data-driven
familiar_overhead table (list<string> "lines" column, keyed by familiar
id), matching how the rest of the project stores config (e.g. the pet
talk tables). The familiar timer reads it via Rows/Tables, so lines can
be edited as data without touching code.
Familiars now play their own "fade into existence" animation (alongside
the existing summon graphic) when summoned and when called. The
per-familiar spawn anims are ported from darkanrs' Pouch spawnAnim
values, mapped by npc id, as named <familiar>_spawn animation defs.
Split the grouped IronSteelTitan stub (a single grunt) into separate
IronTitan and SteelTitan scripts with their full random talk dialogue,
ported from the RuneScape Wiki transcripts (iron titan: 4 conversations;
steel titan: 5).
darkanrs' albino rat spawn anim (16080) is outside void's animation
cache. The albino rat reuses the generic rat skeleton (Base 1152) for
its render anims; that skeleton's summoning-era animation is 8545 -
matching how arctic bear (8522, bear skeleton) and the bats (8279, bat
skeleton) take their spawn anim from their skeleton's list. 8545 is in
cache range, so all 144 familiars now have a spawn animation.
albino rat spawn anim (16080) is outside 634 cache animation boundry. The albino rat reuses the generic rat skeleton (Base 1152) for
its render anims; that skeleton's summoning-era animation is 8545 -
matching how arctic bear (8522, bear skeleton) and the bats (8279, bat
skeleton) take their spawn anim from their skeleton's list. 8545 is in
cache range, so all 144 familiars now have a spawn animation.
…dialogues' into feat/summoning/interactions-and-dialogues
A player and its familiar count as one side, so they may both attack the
same target in single-way combat:
- Target.attackable exempts the owner<->familiar pair from the
  single-combat gate.
- FamiliarCombat lets the familiar be sent at an NPC outside multi-combat
  when it's the owner's own target (offensive/defensive assist).

Combat familiars now also grant their owner combat xp for the damage they
deal - the owner's chosen melee style skill (or the familiar's range/magic
type) plus Constitution - mirroring CombatExperience.
Familiars wouldn't approach a target they were sent at - they stood
frozen and only attacked if it was already in range. Two causes:

- Movement only ran pathfinding for players; NPCs used cheap single-step
  line movement that can't route around obstacles. Owned familiars now
  pathfind like players so they actually walk to the target.
- CombatMovement applied the spawn/aggro leash to familiars, so a target
  beyond their ~9-tile aggro range (e.g. one commanded up to 16 tiles
  away) dropped them straight to EmptyMode. Owned familiars are now
  exempt from the leash - they chase what their owner directs and fall
  back to following when the fight ends.
Locks in the Target.attackable exemption: in a single-way zone (one
attacker per target) the owner and its familiar count as one side, so
the familiar may still join the owner's fight.
Reverts the single-way assist allowance: combat familiars can only
attack in multi-combat zones. In single-way combat the player can still
use the familiar (storage, foraging, special moves) but it won't assist
in the fight. Removes the owner/familiar single-combat exemption from
Target.attackable and the single-way assist path in FamiliarCombat.
Player and familiar are separate attackers under the single-way "one
attacker per target" rule:
- commandFamiliarAttack: drop the multi-combat-only gate so a familiar
  can be ordered to attack in single-way, but pre-check
  Target.attackable so it can't pile onto an NPC already under attack
  (e.g. one the owner is fighting), rejecting with a message instead.
- assistFamiliar: auto-join only in multi-combat; in single-way the
  owner's target is theirs alone, so the familiar must be ordered at a
  separate NPC.

Tests cover: ordered solo attack in single-way, no sharing from the
player's side, no auto-assist in single-way, auto-assist in multi.
Covers the approach path (interact -> combat) for a target beyond the
familiar's approach range, the scenario that wasn't exercised by the
adjacent-target ordered-attack test.
When ordered/assisting, the familiar walked to its target via the
interact-then-combat path. The interact mode gives up the instant a
player or npc fully blocks the route (cantReach -> EmptyMode) and never
re-paths once the obstruction clears, leaving the familiar frozen.

Drive the familiar with CombatMovement directly instead: it re-paths
every tick and (for npcs) never bails on cantReach, so the familiar
routes around or waits out blockers and resumes as soon as they move.
NPCCharacterTargetStrategy.destination() returns Tile.EMPTY for npcs
with an EMPTY walk mode. Familiars have that walk mode (they don't
wander), so destination() always returned EMPTY and recalculate()
short-circuited (EMPTY == cleared destination) - the familiar never
re-pathed, following only its initial path. A stationary target worked;
a target that moved/retreated left the familiar stuck at the old tile.

Exempt owned familiars (owner_index set) from the stationary-npc
short-circuit so they recompute a path to the target's current tile
each tick and chase it.
Familiars persist in CombatMovement and never call cantReach, so an
unreachable target (fled somewhere with no path) left them stuck chasing
forever. Track ticks of no progress while still far from the target; if
the familiar makes none for a short grace period it gives up and reverts
to EmptyMode, which NPCTask turns back into following its owner. Moving,
or being all but in range (just waiting on a free attack tile in a
crowd), resets the grace period so genuine fights aren't abandoned.
When a familiar dealt the killing blow it was the npc's killer, but the
death handler only drops loot (and grants slayer / logs the kill) when
the killer is a Player, so familiar kills dropped nothing. Resolve a
familiar killer to its owner so the loot drops for the player.
The varbit map's zeros are unverified gaps from the original capture,
so familiars like the desert wyrm and arctic bear resolved no enum
entry and bobbed with humanoid expression animations on non-human
head models. Give them a static head (-1) instead, matching what the
details interface shows, until their real values are identified.
The details interface animates value-0 familiars (desert wyrm,
arctic bear) correctly because the client's datamap lookup returns
the enum default - a generic head-bob - for missing keys. Match
that instead of sending a static head; the earlier crash was the
typographic punctuation, not this animation.
talkToPet keyed the chathead enum by npc id - the wrong key space,
the enums key on the varbit value - and passed the resulting
animation name as an expression, which double-prefixed the lookup
and rendered a static head. Mapped pets (sneakerpeeper) now resolve
through familiarChatheadAnimation like familiars; unmapped pets fall
back to the neutral expression.
Summoning's current level doubles as the points pool, so gates using
Level.has - summoning a pouch familiar, dropping a pet, incubating an
egg, enchanting headgear - wrongly refused players whose points had
drained below the requirement. Switch them to hasMax, which reads the
level derived from experience. Boostable-skill gates (bunyip's
Cooking, pyrelord's Firemaking) are left on the current level.
Comment thread data/skill/summoning/summoning.anims.toml
Comment thread game/src/main/kotlin/content/entity/combat/Target.kt Outdated
Comment thread game/src/main/kotlin/content/entity/death/NPCDeath.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/EnchantedHeadgear.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/FamiliarBoosts.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/FamiliarSpecialMoves.kt Outdated
@GregHib

GregHib commented Jul 9, 2026

Copy link
Copy Markdown
Owner
  1. Transforming npcs: Werewolf, kalphiteQueen, Shades use if (source is Player) { interactPlayer(source, "Attack") to retaliate, if the attacker is your familiar they'll need to attack the owner instead (currently they'll get stuck in PauseMode and be indefinitely unattackable)

  2. In single combat areas you can't attack the npc your familiar is attacking and visa-versa not sure if this is correct or not. And you can also get your familiar to attack one npc where you attack another.

  3. Opening and closing world map (and other full screen interfaces) breaks/removes the right click options on the summoning orb - The settings will need to be resent on refresh/open

  4. Special moves have no range limit, e.g. with vampyre bat you can start in lumbridge, tele to goblins and use an attack scroll and it'll do damage even though it's > 30 tiles away. (same with beaver and trees)

  5. No combat sounds when familiars are fighting with npcs

@GregHib GregHib left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments and fixes but overall looking really strong

Comment thread game/src/main/kotlin/content/entity/player/dialogue/type/NPCDialogue.kt Outdated
Comment thread tools/src/main/kotlin/world/gregs/voidps/tools/cache/FixGraphics.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/familiar/Pyrelord.kt
Comment thread game/src/main/kotlin/content/skill/summoning/familiar/IronTitan.kt
HarleyGilpin and others added 20 commits July 12, 2026 08:30
Much neater, good recommendation.

Co-authored-by: Greg <GregHib@users.noreply.github.com>
Opening report abuse in select-player mode (cs2 246) unhooks the chat
input key listener (137:55) and sets report mode varc 11. The client's
close script (cs2 675) never restores either, so chat stays dead until
relog. The restore script (cs2 244) has no client-side callers - the
server is expected to run it. Close the interface server-side on submit
and run cs2 244 whenever report_abuse closes.

Closes GregHib#1061
* Start on pitfall traps

* Update pitfall messages

* Fix bank withdraw full inventory message happening when all items were removed

* Add pitfall level checks

* Add pitfall collapse

* Add pitfall logic and tests

* Remove hunter class

* Fix teasing, add tests for larupia and kyatts

* Remove duplicate mazchna

* Fix barbarian course advanced laps reward collection

* Remove unused hunter data
* Fix run orb status invalid on first login

* Fix logging in requiring display name instead of account name

* Fix being able to create an account using a display name that's already in use

* Allow spaces in message request usernames fixes GregHib#1068

* Some script params

* Fix account unit test

* Fix account merge tests

* Fix account definitions incorrectly using account name instead of display name as the key
* Add random event system and maze event

Implements the random event trigger, kidnap teleport and mysterious old
man framework, plus the Maze random event where players navigate to a
central shrine before time runs out.

Tutorial Island is tagged no_random_events so events don't fire there.

Ref GregHib/void-private#223

* Add in-place random event framework

Extend the random event system beyond the kidnap-to-instance pattern so
in-place nagging events (Certer, Sandwich Lady, Evil Twin) can be built:

- RandomEvents.noteAndTeleport: ignore penalty that notes the player's
  inventory then exiles them, so an event can't be macroed through.
- InPlaceRandomEvent + startInPlaceEvent/endInPlaceEvent: spawn an event
  NPC beside the player that follows and nags on a timer, applying the
  ignore penalty when its lifetime runs out.
- Decouple entry from the old man: the trigger now calls RandomEvents.start
  directly and each launcher owns its intro. Kidnap events call the new
  mysteriousOldMan() helper; in-place events spawn their own NPC. Maze's
  launcher allocates its instance before the intro so a resumed timer
  doesn't fail the event mid-intro.
- rewardCostumeOrCoins: shared collect-the-set reward (camo, lederhosen).

Ref GregHib/void-private#223

* Add Freaky Forester random event

The player is teleported to the forester's clearing and told to kill a
pheasant with a specific number of tails. Only the assigned pheasant
drops the correct raw pheasant; wrong ones drop an incorrect one that the
forester rejects. Handing in the correct pheasant rewards the first
missing lederhosen piece, or 500 coins if the set is complete.

- Conditional pheasant drops keyed to the killer's assigned task
- Blocks attacking further pheasants once one is killed
- Static pheasant spawns in the clearing; forester dialogue drives the task
- Removes the unconditional pheasant drop table (drops are now per-task)

Ref GregHib/void-private#255

* Drop the raw pheasant to the floor instead of the inventory

The assigned pheasant now drops the raw pheasant as a normal killer-owned
floor drop (non-tradeable, private) for the player to pick up and hand to
the forester, matching the live behaviour.

* Add Sandwich Lady random event

First in-place event on the framework: the Sandwich Lady appears beside
the player pushing her tray and nags until spoken to. Talking opens her
refreshment tray (interface 297); choosing the food she offered hands it
over and ends the event in place, while any other choice knocks the
player out via the note-and-teleport ignore penalty. Talking mid-combat
hands over the correct food directly, and other players are turned away.

Adds RandomEvents.completeInPlace for events that finish without a return
teleport, and defines interface 297's seven food buttons (10-22).

Ref GregHib/void-private#261

* Sandwich Lady reacts to the tray choice

She now says "Hope that fills you up!" when handing over the correct
food, and smacks the player for 3 before the knockout when they pick the
wrong one, matching live behaviour.

* Give the Sandwich Lady knockout its animation and dialogue

- Show "Hope that fills you up!" as a chatbox line on a correct pick so
  the player actually sees it, instead of a fleeting overhead force-chat.
- On a wrong pick she swings her baguette (animation), smacks the player,
  who plays the death animation and fades out before waking up elsewhere.

* Add Drill Demon random event

Sergeant Damien whisks the player to his exercise yard and barks an
order. Each of the four mats shows an exercise on its sign (a varbit);
the player uses the mat matching the order, performing the exercise
animation. Four correct exercises earns a piece of the camouflage outfit
(helmet/top/bottoms, first missing) or 500 coins if the set is complete.

- Shuffles the four exercises across the mat signs each round
- Exercise index 0 (jog) is read with a 0 default since the variable
  store clears vars set to their default value
- Reuses the shared kidnap/complete and rewardCostumeOrCoins helpers

Ref GregHib/void-private#254

* Fix Drill Demon signs and mat positioning

- Encode the four exercises as sign varbit values 1-4 (0 = no sign). The
  variable store clears any var set to its default 0, so a sign set to 0
  never reached the client - one mat showed no sign and the client's signs
  drifted out of sync with the server, making the correct mat read as wrong.
- Walk the player onto the mat before performing the exercise.

* Polish Drill Demon exercise and sign-change effects

- On using a mat the player now faces the sergeant, plays the exercise
  sound and waits two ticks before performing the exercise animation.
- When the signs reshuffle, a puff of smoke (gfx 2000) plays on each sign
  tile in turn from sign 1 to sign 4, a tick apart, as it changes.

* Play the sign-change puff on the sign tile (mat y + 2)

* Sweep the Drill Demon sign puffs half a tick apart (Option B)

The four signs now all change on one tick and the smoke puffs sweep
across them client-side, sign 1 to sign 4, half a tick (15 client ticks)
apart, instead of one server tick between each change.

* Use the large chathead for Sergeant Damien's dialogue

* Show the sign item dialogue after Drill Demon's order

After Sergeant Damien names the exercise, an item box shows the matching
sign with "Go to this mat and <action>!" ("this mat" in red), matching
2009scape. Adds the missing push-up sign item (10946). Darkan doesn't
implement Drill Demon.

* Make Drill Demon wait before the first order

On arrival the yard shows "Follow Sergeant Damien's orders!" and waits
about five seconds before the sergeant barks the first exercise. Using a
mat (or talking to him) before the order is given has him snap "I haven't
given you the order yet, worm!" and earns no credit for that attempt.

* Add Certer random event

One of the certificate brothers appears beside the player, bows and nags.
Talking opens the identify interface (184) showing an item and three
descriptions; picking the one that matches the item rolls the shared
gem/coin reward table, a wrong pick gives nothing, and ignoring him
applies the note-and-teleport penalty. Adds the shared random_event_certer
loot table and its roller (reused later by Pillory).

Ref GregHib/void-private#253

* Rework Certer to use the item box and a choice menu

Interface 184 isn't the Certer window in the 634 cache (only five
components, and the runtime item definition can't supply a model to
render on it), so the puzzle didn't display. Show the item with the
standard item dialogue box and pick the description from a choice menu
instead - reliable and still faithful to the identify-the-item mechanic.
Certer isn't implemented in Darkan (only 2009scape).

* Use interface 184 for Certer with the real 634 layout

Decoded interface 184's full component types: it has a text prompt
(comp 1) and three item-model options (comps 4-6, not text), so the 634
version reads out a description and the player picks the matching item
model - the reverse of the older cache. The certer now sends the
description to the prompt and three item models to the options.

* Render Certer option items with sendModel

The type-6 model option components don't render items sent via
INTERFACE_ITEM, so send each item's model id directly. The runtime item
definition doesn't expose model ids, so the eleven pool items' models
(decoded from the cache) are listed alongside their descriptions.

* Fix Certer interface: item model in the box, text on the options

The item to identify is a separate model component (comp 0, the box) and
the three A/B/C Select components hold the description text. Send the
item's model to the box and the three descriptions (one correct, two
false) to the options; the player picks the description matching the
shown item.

* Drive Certer's interface 184 via its varbits

Interface 184 builds itself from a load script (cs2 2136): it reads the
three option descriptions from enum 2224 and the item model from enum
2225, all selected by varbits (5855-5863). So instead of pushing text and
models (which the script overwrites), the event now sets those varbits -
three description keys, the model key for the answer ((desc+2)%9 in enum
2225), and rotation/scale - then opens the interface. The dynamically
created option rows declare their Select action so clicks validate.

Removes the temporary interface-click debug log.

* Point Certer's option clicks at the right components

The load script's clickable option rows are components 4-6, not 0-2, so
map option_1/2/3 to them; clicks now register and the matching answer
rewards.

* Add Mime random event

The player is whisked to a stage where the mime performs emotes to copy.
A per-player mime spawns in an instance and repeats the target emote on a
timer; the event hooks the emote tab (alongside its own handler) to check
the copy. Four correct emotes in a row unlock the mime emotes (glass
wall/box, climb rope, lean) and award a mime costume piece, or coins if
the set is complete. No 2009scape/Darkan reference - built from the wiki.

Ref GregHib/void-private#258

* Teleport the mime event player to 2008, 4762

* Add Evil Bob random event

An evil incarnation of Bob the cat teleports the player to the Scape2009
island and demands fish. A terrified servant pans the camera to the
fishing spot Bob likes; the player nets there (the fish comes out already
cooked), uncooks it at the cold fire, and serves the raw fish to Bob. The
right fish sends him to sleep and opens the exit portal for a 650 Fishing
or Magic XP reward; a wrong one makes Bob more attentive so an extra
correct fish is needed. Ported from 2009scape (no Darkan version).

Ref GregHib/void-private#262

* Add Evil Bob random event

An evil incarnation of Bob the cat teleports the player to the ScapeRune
island and demands fish. A terrified servant pans the camera to the
fishing spot Bob likes; the player nets there (the fish comes out already
cooked), uncooks it at the cold fire, and serves the raw fish to Bob. The
right fish sends him to sleep and opens the exit portal for a 650 Fishing
or Magic XP reward; a wrong one makes Bob more attentive so an extra
correct fish is needed.

Ref GregHib/void-private#262

* Face Evil Bob and pause before the raspberry send-off

* Face Evil Bob and pause before the raspberry send-off

* Fix Evil Bob's spawn posiiton.

* Use the cat chathead expressions for Evil Bob's dialogue

* Add Quiz Master random event

Odd One Out on interface 191: pick the golden model that doesn't belong; four correct wins a choice of 1000 coins or a random item. Ported from 2009scape. Ref GregHib/void-private#260

* Make the Quiz Master answer models clickable

Interface 191's models are non-clickable in the cache, so send the access mask on open; without it the interface has no way to answer.

* Wire the Quiz Master answers to the interface's real buttons

The clickable answer buttons are the invisible components 3-5 (setting=1 in the cache), not the display models on 6-8. Target those instead of trying to unlock the non-clickable model components.

* Handle Quiz Master answers as dialogue continues

Interface 191's setting=1 buttons deliver clicks as dialogue-continue events (like choice() options), not interface options. Suspend each question on pauseInt and resume it from a continueDialogue handler keyed to the button clicked.

* Add Pillory random event

A guard arrests the player into a pillory; unlock it (interface 189) by picking the swinging key matching the spinning lock, N-in-a-row (starts 3, +1 and resets on a wrong pick, cap 6). Escaping rewards a roll of the shared certer table. Ported from 2009scape; object/tile ids remapped for the 634 cache (cage 777, city pillory tiles). Ref GregHib/void-private#268

* Add Pillory random event

A guard arrests the player into a pillory; unlock it (interface 189) by picking the swinging key matching the spinning lock, N-in-a-row (starts 3, +1 and resets on a wrong pick, cap 6). Escaping rewards a roll of the shared certer table. Ported from 2009scape; object/tile ids remapped for the 634 cache (cage 777, city pillory tiles). Ref GregHib/void-private#268

* Add Surprise Exam random event

The Mysterious Old Man teleports the player into Mr Mordaut's classroom; talking to Mordaut opens a 'what comes next?' pattern quiz (interface 103) - pick the item that completes the three shown. Three correct passes the exam and Mordaut names one of four coloured doors; leaving through it returns the player with a Book of Knowledge (an XP lamp). Ported from 2009scape. Ref GregHib/void-private#263

Also relax the flaky Pillory arrest assertion to allow landing beside a cage when the stocks block the tile.

* Handle Surprise Exam answers as dialogue continues

Interface 103 opens in the chatbox, so answer clicks arrive as dialogue-continue events (like Quiz Master's 191), not interface options. Open it as a dialogue_box, suspend each question on pauseInt, and resume from a continueDialogue handler keyed to the option clicked.

* Open the Surprise Exam quiz in the main game window

Interface 103 is a centred main-window modal, not a chatbox dialogue; set type=main_screen and take answers as interface options again.

* Send the access mask for the Surprise Exam answer buttons

Interface 103's answer icons are enabled in the cache but the client won't report a click until the server sends the interface settings; unlock them on open.

* Unlock Surprise Exam answer buttons over their item slot

Send the interface settings for slot 0..0 of each answer component so the client enables the click.

* Dispatch clicks on the Surprise Exam item icons

getInterfaceItem tried to resolve the clicked answer icon against an inventory the display-only interface doesn't have, so the click was silently dropped. Allowlist surprise_exam_pattern like the other display-only item interfaces (farming_equipment_store, common_item_costs).

* Dispatch clicks on the Surprise Exam item icons

getInterfaceItem tried to resolve the clicked answer icon against an inventory the display-only interface doesn't have, so the click was silently dropped. Allowlist surprise_exam_pattern like the other display-only item interfaces (farming_equipment_store, common_item_costs).

* Use the large chathead for Mr Mordaut's dialogue

* Address review: clear random events on reload, reuse addOrDrop

Make RandomEvents AutoCloseable and register it in ContentLoader so a script reload resets the launcher map instead of keeping stale entries. Replace the duplicated give-or-drop helpers with the existing Player.addOrDrop.

* Rework the Mime event around interface 188 and the theatre

The old man drops the player into the theatre (2008,4764), challenges them, then they walk to the watch spot. The Mime performs one of eight emotes under the spotlight, bows to the player, and the player copies it by picking the matching emote on interface 188 (dialogue_macro_mime_emotes). Correct picks say 'Correct!', pause ~8s, and after three the player is teleported home with the mime emotes unlocked and a costume piece. Spotlight objects (3644) are wired but their on/off animation is left as a TODO pending the anim ids.

* Run the Mime event in the real theatre instead of an instance

Teleport to the actual region 8010 coordinates and use the statically-spawned Mime rather than copying the region into an instance.

* Show Mime prompts as chatbox dialogue

Display 'Watch the Mime...' as a no-prompt chatbox line during each performance, and make 'Correct!' a chatbox statement instead of a game message.

* Drop the Mime 'Correct!' text down a line

Show it on the lower line of a two-line message box (empty first line), since the box top-aligns its already-centred text.

* Bump the Mime 'Watch the Mime' prompt down a line too

Generalise the empty-first-line message helper and use it for both the watch prompt and 'Correct!'.

* Animate the Mime spotlights on and off

Wire object 3644's turn-on (1136) and turn-off (1135) animations into the spotlight helper: the Mime's light comes on while it performs, the player's while they copy.

* Let the Mime spotlight finish before the emote

The spotlight on/off animation runs ~3 ticks; wait that out after lighting the Mime (before it performs) and after lighting the player (before they pick and copy).

* Teleport the Mime player out with the modern teleport

Play the modern teleport animation and gfx, wind up, then return the player and land with the modern landing gfx.

* Use the modern teleport helper for the Mime exit

Replace the hand-rolled teleport animation with teleport(origin, "modern") after clearing the event state.

* Add Kiss the Frog random event

The Frog Herald hops up and whisks the player to the Land of the Frogs, where one of the identical frogs wears a crown - the Frog Prince (female player) or Princess (male). Kissing the crowned frog restores it to human form and gives a gift box; talking to a plain frog turns the player into a frog until they apologise to the crowned one. Ported from 2009scape (content/global/random/event/frog_princess); ids/anims mapped to the 634 cache. Ref GregHib/void-private#256

* Fix frog chatheads and face-before-kiss in Kiss the Frog

Give the frog NPCs the 'old' dialogue namespace (2009scape used FaceAnim.OLD_*) so their chatheads animate, and pause after turning to face each other before the kiss.

* Rework Kiss the Frog fail/escape flow and fix royal detection

- Crowned royal is now frog_9_frogland (3300); the rogue decorative 3300
  spawn that looked crowned but gave plain dialogue is now a plain frog.
- Talking to wrong frogs offends the royal; a third offence morphs the
  player into a plain frog and banishes them to a frog cave, where a second
  crowned royal releases them (random exile, no reward).
- Broaden the plain-frog handler to frog_*frogland so the bare frogland
  frogs baked into the region respond instead of "Nothing interesting".
- Restrict the Land of the Frogs to the six social tabs; land arrival moved
  to 2445,4770 with crown-spawn tiles near it.
- Make the kiss and cave-escape sequences walk-off safe: a walkTrigger fades
  the player out and completes/escapes so they can't be stranded mid-dialogue.

* Seat the player during the Quiz Master show

Play the sit animation (2378) when the contestant takes their seat in the
studio, and stand back up on winning before teleporting home.

* Add Pinball random event

* Add Prison Pete random event

* Add Lost and Found random event

* Add Cap'n Arnav's Chest random event

* Add Beekeeper random event

* Add Evil Twin random event

* Add Gravedigger random event

* Reward all random events with a random event gift

* Add Mystery box random event reward

* Add Casket treasure

* Add Iffie's costume store

* Remove dead code and unnecessary comments from random events

* Exile without noting items on sandwich lady knockout

* Allow players to disable random events with ::randomevents

* Skip already-noted items in the random event ignore penalty

* Guard the maze shrine and reset the chest cap between mazes

* Only unlock Prison Pete's locks when keys are handed over

* Exile the player when the beekeeper's hive build fails

* Let the Quiz Master resume an interrupted quiz

* Clean up Kiss the Frog royals on logout and resume on relog

* Resume random event progress on relog and clean up leftover state

* Add Pinball random event tests

* Organise random event NPC definitions into per-event configs

* Fix the unreachable home teleport cooldown handler

* Move random event settings to game.properties

* Reuse String.an for MysteryBox.kt

* Add random event exception areas

* Include morphing rings/transforms in random event eligibility

* Trigger random events on a timer instead of experience drop

Increase cooldown between 1-2 hours

Add config for lost and found event chance

* Disable random events in integration tests to avoid issues

* Fix random event trigger tests

---------

Co-authored-by: GregHib <greghib@users.noreply.github.com>
…rees

The projectile protocol carries no rotation (darkan's packet is
field-identical - its 'angle' byte is our 'curve'), but the cache's
graphic definitions do: opcode 6 is a model rotation in degrees (the
revision's existing values are only ever 90/180/270, gfx 1402's was 0).
Adds a GraphicEncoder mirroring the decoder, and a FixGraphics tool
(fixGraphics gradle task, also wired into CacheBuilder) that patches
gfx 1402 with a 180 degree rotation after round-trip-validating the
re-encode - so Toad Bark keeps its smoke-trailed cannonball, flying the
right way round. The local cache has been patched.
killer.getValue("owner_index") doesn't exist; use the indexed get with a
default like every other owner_index read.
…ne fallbacks

Revert the ItemOnNPCInteract base-id fallback combinations; transformed id
lookups are intentional. Item-on-familiar handlers now register with a
"<id>*" prefix so the wilderness "<id>_combat" form's keys are expanded
at registration instead.
Replace the manual transform branching with def(source) for player
sources, which already resolves the transform_id attribute. NPC sources
keep a minimal owner_index guard: base familiar forms deliberately lack
the Attack option but combat() re-validates attackable() every tick, so
retaliation against familiars would otherwise disengage.
Replace the hardcoded HEADGEAR list with the enchanted_headgear table in
enchanted_headgear.tables.toml; the script reads rows for registration
and lookups.
Replace the hardcoded boost map with the familiar_boosts table in
familiar_boosts.tables.toml (flat amount plus percentage of base level).
FamiliarSpecialMoves now implements AutoCloseable, cleared by
Script.clear like the other content registries via ContentLoader.
…ialogue api

Revert the familiar branch in the private npc renderer; the dialogue
target overload resolves familiarChatheadAnimation and passes it via
npc(npcId, expression, text), as does the pet talk script. sendChat
falls back to raw animation names/ids for such custom expressions.
Drop the cache graphic rotation patch (FixGraphics/GraphicEncoder); the
dedicated toad bark projectile (1402) faces backwards in the cache, so
shoot with gfx 1401 and the hand cannon's cannonball (2143) instead.
…mbat

Sends the real InteractInterfaceNPC instruction for familiar_details
cast_famine at an npc; the locust engages while the player gains no
combat mode or target.
The pyrelord's fire breath (8085) doesn't fit the forge regent's rig;
it breathes with its own animation (7871, also its Inferno breath).
Combat only emits combatStop for fights whose first swing landed, so a
familiar engaged mid-retaliation whose target died pre-swing kept the
stale target - a titan's called special would re-engage the same npc
after it respawned. Death now clears every attacker's matching target.
…m:HarleyGilpin/void into feat/summoning/interactions-and-dialogues
@GregHib GregHib merged commit 65d6290 into GregHib:main Jul 12, 2026
2 checks passed
HarleyGilpin added a commit to HarleyGilpin/void that referenced this pull request Jul 12, 2026
…GregHib#1052)

* Add familiar interactions and dialogues for summoning

Port talk dialogue and special-op interactions for ~62 summoning
familiars from a reference implementation, matching the existing
per-familiar Script pattern (SpiritWolf/AbyssalLurker/AbyssalParasite).

- Per-familiar Interact dialogue scripts (random + context-aware:
  inventory/equipment/stat/BoB-slot/run-energy/staged-state checks),
  grouped via wildcard ids for minotaurs, cockatrices, titan tiers,
  and gorajo/clay dungeoneering familiars.
- Special ops: unicorn Cure (gated; inert until poison exists),
  pyrelord logs->firemaking, lava titan Lava Maze teleport,
  dreadfowl/compost mound Boost Farming.
- Generic expression_familiar_* chathead animation family + unicorn
  cure anim/gfx; dialogue="familiar" + interacts=false on all
  scriptable familiars.

* Use static familiar chatheads instead of generic cat anims

The generic expression_familiar_* family applied the shared cat head
talk animations to every familiar's chathead model, which animates
incorrectly for non-feline familiars. Drop the dialogue="familiar"
override and the generic family so each familiar renders its own
chathead model with no forced (wrong) animation. Correct per-familiar
talk anims can be added later as expression_<familiar>_* families
(via ::expr_scan), like the existing wolf/bird/leech sets.

* Use large chatheads for summoning familiars

Familiars are creatures; add large_head = true to every familiar npc
def so their dialogue renders the bigger chathead (as cows, camels and
other creature npcs already do).

* Add forager familiar system with Magpie Withdraw

Foragers (starting with Magpie) periodically gather loot from a
forage_<familiar> drop table into the familiar's inventory while
summoned, and the player retrieves it via the familiar's Withdraw/Take
option (reusing the beast-of-burden inventory + interface). Players
can't deposit their own items into a forager.

- Forager script: 30s forage timer rolling the drop table into the
  familiar inventory, plus Withdraw/Take open handlers. Foragers are
  detected data-drivenly by the existence of a forage_<id> drop table
  (no new npc-def param, which must map to a real cache param).
- forage_magpie drop table (gems/rings) in summoning.drops.toml.
- Magpie marked as a beast-of-burden (cap 30) to reuse the storage +
  withdraw UI; store() rejects deposits into foragers.
- Start/stop the forage timer on summon/dismiss.

Other forager familiars are now just data: add a forage_<name> table.

* Add foraging to the remaining overworld forager familiars

Port the forage drop tables for beaver, spirit spider, granite crab,
granite lobster, ibis, macaw (grimy herbs), compost mound, evil turnip,
fruit bat, giant ent, stranger plant, void ravager and the cockatrice
family, and mark each as a beast of burden (cap 30) so they reuse the
forage inventory + Withdraw/Take interaction.

Tables model the reference's "1/outOf chance, then drop one" via a
1/<roll> outer roll deferring to an `_items` sub-table for multi-item
foragers (also corrects magpie, which previously dropped every tick).
Item ids resolved to void string ids.

Dungeoneering hoardstalker foragers (tier t1-t10) are deferred.

* Move dreadfowl/compost farming boost to the special-move option

The farming boost was wrongly offered as a "Boost Farming" entry in a
Talk-to/Boost choice on Interact. Make Interact talk-only again and
trigger the boost from the familiar's special-move ("Special") option
instead, via the cast_dreadfowl_strike / cast_generate_compost buttons
on the summoning orb and familiar details tab.

* Trigger dreadfowl/compost farming boost from the Special npc option

The boost is on the familiar's "Special" right-click option, not the
special-move interface button. Use npcOperate("Special", ...) guarded
to the player's own familiar.

* Add dreadfowl Special dialogue and animation

Clicking the dreadfowl's "Special" option now plays the asking-to-fight
dialogue, the dreadfowl's special animation (7810) + gfx (1523, from
darkanrs' Dreadfowl Strike sync), then boosts Farming by one.

* Use dreadfowl special animation (5387)

* Add dreadfowl farming-boost graphics

Player plays gfx 1307 and the dreadfowl plays gfx 1317 alongside its
special-strike animation (5387) when boosting Farming via the Special
option.

* Make familiar Farming boost non-stacking and removed on dismiss

The dreadfowl/compost-mound Special Farming boost now refuses to apply
when Farming is already boosted (e.g. by a garden pie), and is stripped
when the familiar is dismissed or dies (both route through
dismissFamiliar) - only if it's still the active, unchanged boost.

* Add passive invisible familiar skill boosts

Introduce a data-driven registry of the passive skill-level boosts a
summoned familiar grants (beaver +2 Woodcutting, granite lobster +4
Fishing, obsidian golem +7 Mining, etc.) and a Player.familiarBoost
function. The boost is read live from the active follower and added to
the effective level in skill success checks - so it's invisible (not in
the skill tab), never lets the player act above their real level, and
disappears the moment the familiar is dismissed or killed.

Wired into woodcutting, fishing, mining, firemaking and thieving.
Hunter boosts (arctic bear, wolpertinger, kyatt, larupia) are in the
registry but inert until the Hunter skill is implemented.

* Add random familiar overhead dialogue

Summoned familiars now periodically shout a random overhead line every
~30s (driven by the existing familiar timer), matching RS. Lines are a
data-driven map keyed by familiar id, sourced from the RuneScape Wiki
transcript "Overhead dialogue" sections - 83 familiars covered. Phoenix
and the gorajo (bloodrager/stormbringer/etc.) tiers have no overhead on
the wiki and stay silent.

* Move familiar overhead dialogue into a table definition

Replace the hardcoded Kotlin FAMILIAR_OVERHEAD map with a data-driven
familiar_overhead table (list<string> "lines" column, keyed by familiar
id), matching how the rest of the project stores config (e.g. the pet
talk tables). The familiar timer reads it via Rows/Tables, so lines can
be edited as data without touching code.

* Play per-familiar spawn animation on summon and call

Familiars now play their own "fade into existence" animation (alongside
the existing summon graphic) when summoned and when called. The
per-familiar spawn anims are ported from darkanrs' Pouch spawnAnim
values, mapped by npc id, as named <familiar>_spawn animation defs.

* Add iron and steel titan talk dialogue

Split the grouped IronSteelTitan stub (a single grunt) into separate
IronTitan and SteelTitan scripts with their full random talk dialogue,
ported from the RuneScape Wiki transcripts (iron titan: 4 conversations;
steel titan: 5).

* Add albino rat spawn animation (best-guess 8545)

darkanrs' albino rat spawn anim (16080) is outside void's animation
cache. The albino rat reuses the generic rat skeleton (Base 1152) for
its render anims; that skeleton's summoning-era animation is 8545 -
matching how arctic bear (8522, bear skeleton) and the bats (8279, bat
skeleton) take their spawn anim from their skeleton's list. 8545 is in
cache range, so all 144 familiars now have a spawn animation.

* Add albino rat spawn animation (best-guess 8545)

albino rat spawn anim (16080) is outside 634 cache animation boundry. The albino rat reuses the generic rat skeleton (Base 1152) for
its render anims; that skeleton's summoning-era animation is 8545 -
matching how arctic bear (8522, bear skeleton) and the bats (8279, bat
skeleton) take their spawn anim from their skeleton's list. 8545 is in
cache range, so all 144 familiars now have a spawn animation.

* Remove incorrect Albino rat familiar spawn animation.

* Let familiars assist in single-way combat and grant owner xp

A player and its familiar count as one side, so they may both attack the
same target in single-way combat:
- Target.attackable exempts the owner<->familiar pair from the
  single-combat gate.
- FamiliarCombat lets the familiar be sent at an NPC outside multi-combat
  when it's the owner's own target (offensive/defensive assist).

Combat familiars now also grant their owner combat xp for the damage they
deal - the owner's chosen melee style skill (or the familiar's range/magic
type) plus Constitution - mirroring CombatExperience.

* Make familiars path to and chase their combat target

Familiars wouldn't approach a target they were sent at - they stood
frozen and only attacked if it was already in range. Two causes:

- Movement only ran pathfinding for players; NPCs used cheap single-step
  line movement that can't route around obstacles. Owned familiars now
  pathfind like players so they actually walk to the target.
- CombatMovement applied the spawn/aggro leash to familiars, so a target
  beyond their ~9-tile aggro range (e.g. one commanded up to 16 tiles
  away) dropped them straight to EmptyMode. Owned familiars are now
  exempt from the leash - they chase what their owner directs and fall
  back to following when the fight ends.

* Test familiar assists owner in single-way combat

Locks in the Target.attackable exemption: in a single-way zone (one
attacker per target) the owner and its familiar count as one side, so
the familiar may still join the owner's fight.

* Familiars only fight in multi-combat, not single-way

Reverts the single-way assist allowance: combat familiars can only
attack in multi-combat zones. In single-way combat the player can still
use the familiar (storage, foraging, special moves) but it won't assist
in the fight. Removes the owner/familiar single-combat exemption from
Target.attackable and the single-way assist path in FamiliarCombat.

* Familiars fight solo in single-way, can't share a target

Player and familiar are separate attackers under the single-way "one
attacker per target" rule:
- commandFamiliarAttack: drop the multi-combat-only gate so a familiar
  can be ordered to attack in single-way, but pre-check
  Target.attackable so it can't pile onto an NPC already under attack
  (e.g. one the owner is fighting), rejecting with a message instead.
- assistFamiliar: auto-join only in multi-combat; in single-way the
  owner's target is theirs alone, so the familiar must be ordered at a
  separate NPC.

Tests cover: ordered solo attack in single-way, no sharing from the
player's side, no auto-assist in single-way, auto-assist in multi.

* Test familiar walks to a distant ordered target in single-way

Covers the approach path (interact -> combat) for a target beyond the
familiar's approach range, the scenario that wasn't exercised by the
adjacent-target ordered-attack test.

* Keep familiar pursuing its target when its path is blocked

When ordered/assisting, the familiar walked to its target via the
interact-then-combat path. The interact mode gives up the instant a
player or npc fully blocks the route (cantReach -> EmptyMode) and never
re-paths once the obstruction clears, leaving the familiar frozen.

Drive the familiar with CombatMovement directly instead: it re-paths
every tick and (for npcs) never bails on cantReach, so the familiar
routes around or waits out blockers and resumes as soon as they move.

* Let familiars chase a moving combat target

NPCCharacterTargetStrategy.destination() returns Tile.EMPTY for npcs
with an EMPTY walk mode. Familiars have that walk mode (they don't
wander), so destination() always returned EMPTY and recalculate()
short-circuited (EMPTY == cleared destination) - the familiar never
re-pathed, following only its initial path. A stationary target worked;
a target that moved/retreated left the familiar stuck at the old tile.

Exempt owned familiars (owner_index set) from the stationary-npc
short-circuit so they recompute a path to the target's current tile
each tick and chase it.

* Familiar returns to following its owner when it can't reach its target

Familiars persist in CombatMovement and never call cantReach, so an
unreachable target (fled somewhere with no path) left them stuck chasing
forever. Track ticks of no progress while still far from the target; if
the familiar makes none for a short grace period it gives up and reverts
to EmptyMode, which NPCTask turns back into following its owner. Moving,
or being all but in range (just waiting on a free attack tile in a
crowd), resets the grace period so genuine fights aren't abandoned.

* Credit a familiar's kill to its owner

When a familiar dealt the killing blow it was the npc's killer, but the
death handler only drops loot (and grants slayer / logs the kill) when
the killer is a Player, so familiar kills dropped nothing. Resolve a
familiar killer to its owner so the loot drops for the player.

* Fix crash resolving familiar owner on non-familiar npc kills

A plain npc killer has owner_index -1, and Players.indexed(-1) throws
ArrayIndexOutOfBoundsException - so every npc-on-npc kill crashed the
tick. Only resolve an owner when owner_index is set.

* Add familiar combat foundation

Combat data and wiring the familiar combat behaviour builds on:
- summoning.combat.toml / summoning_combat.anims.toml / .gfx.toml: per-
  familiar combat definitions (stats, attack styles, anims, projectiles).
- summoning.npcs.toml: combat stats and combat_def links per familiar.
- gameframe.ifaces.toml: summoning orb / follower-details "Attack" option.
- Summoning.kt: apply the wilderness pvp combat form on summon and call.
- NPCTask.kt: an idle familiar resumes following its owner instead of
  wandering.
- Combat.kt: retaliation anchors its leash via leashAnchor() (owner tile
  for familiars).
- Attack.kt: resolve an npc's swing definition through combat_def.

* Familiar re-acquires the owner's current target when freed

In multi-combat the familiar engaged the owner's target on combatStart
and (correctly) never switched mid-fight, but once its own target died
nothing re-engaged it if the owner had since moved to another enemy - it
just stood idle. Assist on the owner's combatAttack too: each hit an
idle familiar joins the owner's current (last-attacked) target, while
the idle guard still leaves an actively fighting familiar alone.

* Let familiars phase through players, not npcs

With world.players.collision on, a player stamps BLOCK_PLAYERS on its
tile and an npc's blockMove (BLOCK_PLAYERS|BLOCK_NPCS) is the canTravel
extraFlag, so a familiar couldn't step onto any player tile - standing
between it and its target blocked it. Drop BLOCK_PLAYERS from blockMove
for owned followers (familiars/pets) so they phase through players
including their owner, while keeping BLOCK_NPCS; solid npcs already
stamp FLOOR so the pathfinder still routes the familiar around them.

* Set standard familiar attack speed to 8 ticks

Combat defs were left at the engine default of 4 (2.4s). Standard
summoning familiar attack speed is 8 ticks (4.8s) - add attack_speed = 8
to every familiar combat def.

* feat: add familiar attack projectiles and animations.

* Drain summoning points over familiar lifetime

Summoning was free: no summon cost was consumed, points never drained,
and the lifetime timer read summoning_time_minutes which was set on no
familiar, so the countdown never worked.

Mirror the live game:
- Gate summoning on having enough points and drain the pouch's
  summon_points on summon.
- New per-tick SummoningDrain timer drains the player's summoning points
  evenly over the familiar's life (total = levelRequired - summonCost + 1),
  using the same integer-accumulator approach as PrayerDrain, and
  dismisses the familiar when points hit zero.
- Populate per-familiar summoning_time_minutes (ported from 2009scape) so
  the duration timer counts down; convert the stray meerkats familiar_time.
- Add a 0-60 special-move-points pool (varp 1177): initialised to 60 on
  summon and regenerated +15 every 30s by the familiar timer. Special
  moves now spend the pool via useFamiliarSpecial instead of draining
  summoning points; Unicorn Stallion's cure is the first to use it, driven
  by a new summoning_special_cost npc param.

* feat: add familiar special moves with Howl projectile tuning

Add the cast-button familiar special moves ported from 2009scape: combat,
boost and utility specials plus their shared helpers, dispatcher, stubs and
named anim/gfx data. Spirit Wolf Howl fires its tornado from the familiar
with 2009scape projectile timing (delay 50, flight 46 + dist*10, curve 5)
at ground height so it visibly travels instead of snapping from the owner.

* adjust howl special move projectile for spirit wolf familiar.

* fix: familiar special move casting, projectiles, hitsplats

- Name familiar_details cast button component (79) so target specials
  actually dispatch instead of "Nothing interesting happens".
- Tune Dreadfowl Strike projectile (delay/speed/curve) and remove the
  stray magic-bolt projectile from dreadfowl's normal melee attack.
- Sync special-hit damage + impact gfx to projectile flight time so the
  hit lands with the projectile instead of on a fixed delay.
- Attribute a familiar's hitsplats to its owner so the owner sees the
  blue 0/miss (block) splat, previously hidden from non-participants.
- Stop target familiar specials firing twice: use approachRange(update=false)
  in the dispatch handlers and set the cooldown before the effect runs.
  Adds a re-entrancy regression test.

* fix: wire albino rat as a forager with Cheese Feast

- Configure albino rat as a beast-of-burden forager (capacity 30) with a
  forage_albino_rat table producing cheese, so its "Withdraw"/"Take"
  option opens the familiar inventory (was "Nothing"/"can't carry").
- Cheese Feast special now produces 4 cheese into the familiar's own
  inventory and plays the player summoning-cast animation (7660) with the
  cheese gfx (1384) on the rat, matching 2009scape.

* Adjust player animation to correct animation for renewing summoning points at obelisk.

* Adjust player animation to correct animation for renewing summoning points at obelisk.

* Feat: add egg spawn special move for spirit spider familiar

* fix: allow npcs to retaliate against player owned familiars.

* fix formatting - ran spotless apply

* bug: fix allows npcs to retaliate against familiars that use special move against them.

* feat: add Burrow npc option to desert wyrm familiar

The wyrm digs to the nearest ore rock within 7 tiles and drops it. Like the
live game it only mines the tiers below silver (copper, tin, clay, blurite,
iron), preferring the highest tier available; messages when no rocks are near.

* feat: spirit scorpion venom shot fires a projectile at its owner

The scorpion plays its wind-up graphic, then once it finishes shoots a slow
venom bolt at the owner, with the charge graphic landing on the owner when the
bolt arrives. Flight time and start/end heights are passed explicitly because
the projectile shares cache graphic 1355 with the impact gfx and void keys
graphic definitions by numeric id (the shared entry's height/flight fields
would otherwise collide).

* refactor: drop venom shot impact graphic, keep only the projectile

Graphic 1355 has no owner-impact effect, so remove the venom_shot_owner gfx and
the timed impact queue; Venom Shot now just plays the wind-up and fires the bolt.

* Adjust poison damage from venom shot charge

* feat: play cast animation, graphic and sound on every special move

Move the owner's special-cast visuals (anim 7660 + gfx 1316) and sound (4161)
into castFamiliarSpecial so they fire on every successful special, not just
Cheese Feast. The albino rat now plays its own Cheese Feast anim + gfx on top. Also play the
obelisk renew sound (4214) when renewing summoning points.

* feat: add compost mound Generate Compost special move

Cast on an empty compost bin to fill it with a full bin of ready compost, or
supercompost on a 1-in-10 roll. The familiar animates
and fires a projectile at the bin, filling it on impact. Removes the compost
mound scenery stub.

* feat: giant chinchompa Explode special move

Detonates around the familiar hitting up to nine nearby foes
(random 0-12) then dismisses it. Cast from the button, and auto-fires
free on ~1/10 of the familiar's own attacks (no scroll/points spent).

* feat: chinchompa auto-explode triggers when attacked, for free

Move the ~1/10 auto-detonation from the familiar's own attacks to
incoming hits (npcCombatDamage), and fire it directly instead of
through the scroll/points gate so it costs no scroll, points, or
owner scroll-throw flourish. A cooldown guard blocks re-entry.

* fix: show owner the blue 0 splat when a familiar is hit

The prior attribution only covered hits a familiar deals; a 0 hit
landed on the familiar kept the attacker as the splat source, so the
owner (neither victim nor source) never saw the blue block splat.
Attribute splats on a familiar to its owner too.

* fix: giant chinchompa attacks melee instead of ranged

Its combat def used a range attack; switch to a melee crush attack
at range 1 so it bites in melee like the live familiar.

* feat: honey badger Insane Ferocity buff

Wire the previously-inert special: enrage the owner, boosting Attack
and Strength by 5 + 15% each at the cost of 10% Ranged, Magic and
Defence, matching the scroll description. Boosts decay naturally and
recasting refreshes them. Adds the owner enrage graphic.

* feat: beaver Multichop, fletching and forage message

Multichop: cast on a tree, the beaver walks over, chops (facing the
tree, held in PauseMode so the idle-familiar watchdog doesn't drag it
back), and stashes a log - possibly a lower tier than the tree - in its
30-slot pack. Interacting with it mid-chop is refused with a busy
message, except using a log to fletch.

Fletch: use a log on the beaver to fletch bows with no knife. Refactored
the shared fletch flow to take a tool predicate; the beaver counts as a
knife only while cutting, so a batch that outlasts the chop stops with
"You need a knife to do that."

Forage: foragers now message "Your familiar has produced an item."

* feat: Void familiars' Call to Arms teleport special

Ports the Call to Arms special (shared by the Void ravager, shifter,
spinner and torcher familiars) that teleports the owner to the Void
Knights' Outpost courtyard (2659, 2658, 0). The outpost island is on the
map even though Pest Control itself isn't implemented, so the destination
is safe.

Registers the shared instant move in FamiliarUtilitySpecials with the
2009 teleport-out/teleport-in visuals, removes the four familiars from
FamiliarSpecialStubs, and adds the named anim/gfx entries. Covered by
FamiliarSpecialEffectTest.

* feat: Void spinner heals its owner 100 life points every 15s

Adds a passive heal-over-time for the Void spinner familiar via a shared
`familiar_heal` timer, gated by the FAMILIAR_HEAL_LIFEPOINTS id map (same
shape as FAMILIAR_BOOSTS). Started on summon and stopped on dismiss;
restore no-ops at full life points so it only tops up after damage.

* feat: Void shifter teleports owner to safety below 10% life points

Hooks Constitution level changes on the shifter's owner: once their life
points fall under 10% of maximum, it teleports them to the Void Knights'
Outpost courtyard (the same tile Call to Arms uses).

* test: cover Void shifter 10% threshold in life-point units

Locks in that the shifter's below-10% trigger scales with life points
(level 10 Constitution = 100 life points -> 10 life-point threshold),
not the raw skill level. No behaviour change - getMax already returns
life points.

* feat: Bull Rush stuns its target

Minotaur Bull Rush now stuns the target for 5 ticks on a real cast (via
the shared stun primitive), on top of the tier-scaled ranged hit, so it
can't act while the familiar charges in - completing the earlier TODO.

* fix: stunned characters can't swing while charging in

Stun set the target's delay/movement_delay/stunned clock, but the combat
swing path (CombatMovement -> combatReached -> Combat.combat) only gated
on action_delay. Movement was blocked, attacks weren't - so a minotaur's
Bull Rush showed the stun graphic yet the target kept hitting.

Bail out of combat() when the character is stunned; the stun then expires
and the fight resumes on the next tick. Covers players too (e.g. a
pickpocket stun landing mid-fight), not just Bull Rush.

* feat: minotaur familiars swing their axe (gfx 1498) on normal attacks

Every metal tier of minotaur familiar now plays the "minotaur axe swing"
graphic on a normal melee attack, matching the live game. Adds a shared
minotaur_axe_swing gfx def (id 1498) referenced by all six tiers.

* fix: projectiles emit from the source edge facing the target

* feat: Bull Rush stuns on impact, a third of the time, over a 2s charge

The minotaur's Bull Rush projectile now flies for ~2s and its stun waits for
the charge to land rather than firing the instant the special is cast, and
only lands about a third of the time (1-in-3).

familiarSpecialHit gains an onLand callback, queued to the hit's landing tick
so on-impact effects run once the shot arrives. It runs a tick after the
projectile flight - the same tick the damage lands, queued after the hit - so
the stun (which sets the target's "delay" and would otherwise block the still
-pending hit's queue) only fires once the damage has been applied.

* feat: Macaw Herbcall searches out a grimy herb, on a 1-minute cooldown

Reworks the Macaw's Herbcall special to follow 2009scape's MacawNPC: the
familiar plays its search animation (anim 8013 / gfx 1321), then a few ticks
later drops a random grimy herb at its feet. The herb is a uniform pick from
all fifteen herb types (guam through torstol, low- and high-level alike), and
the special has a one-minute cooldown of its own on top of the usual scroll
and point cost.

Previously it dropped a single grimy herb from a smaller pool at the player's
feet instantly, with no animation or cooldown.

* feat: Herbcall flies up, drops its herb mid-graphic, then recalls the macaw

Sequences the Macaw's Herbcall around its animations: the macaw plays its
fly-up/search animation on cast, its drop graphic bursts where it hovers
about three seconds later, and the grimy herb lands a moment after that
before the bird is called back down beside its owner.

The queue delays account for two engine quirks - a queued action fires
initialDelay + 1 ticks later, and a dropped floor item only becomes visible
the following tick - so the herb settles roughly four seconds after the cast.

* feat: cockatrice Drain option recasts Petrifying Gaze; Evil Flames plays its breath gfx and stops self-healing

* fix: familiar specials deal life-point (x10) damage, matching normal attacks

All special max hits and flat heals were written against 2009scape's 1x
hitpoint scale, but familiar auto-attacks (and the rest of combat) use
x10 life points - so every special hit for a tenth of its real value.
Rescaled to the x10 reference values, and while in here: Arctic Blast
now stuns small targets on impact 1 time in 5, and Crushing Claw drains
5% Defence, both as in the live game.

* feat: nine familiar combat specials - Doomsphere, Spike Shot, Swamp Plague, Ebon Thunder, Mantis Strike, Deadly Claw, Acorn Missile, Famine and Inferno

Karamthulhu's Doomsphere washes away 5% of the target's Magic; the swamp
titan's plague poisons; the lava titan's bolt saps a player's special
attack energy on impact; the praying mantis binds small targets in place
(movement only - deliberately not the bull-rush stun); the talon beast
rakes three times in a burst; the giant ent's acorns splash everything
adjacent to the target; the ravenous locust turns a player's food rotten;
and the forge regent burns another player's weapon and shield out of
their hands. Values from darkan's x10 scale.

* feat: Dust Cloud chokes up to seven nearby foes; Iron Within and Steel of Legends charge the titan's next swing into a flurry

The smoke devil's Dust Cloud is a radius-1 burst hitting up to seven
targets for up to 80 each. The iron and steel titans charge their next
attack (melee-only for iron) to land 3 / 4 hits on their target - the
charge is stored on the owner like the spirit scorpion's Venom Shot and
spent by an npcCombatAttack handler, which clears the flag before the
extra hits so re-fired events can't recurse.

* feat: Regrowth revives a felled tree from its stump

The hydra's object-target special fires the stump's pending regrow
timer, bringing the tree back immediately; farming-patch trees are
player-side variables, so only world stumps qualify.

* feat: Swallow Whole, Ophidian Incubation and Immense Heat item-target specials

The bunyip gulps a raw fish the owner could cook, healing the cooked
value with no eat delay; the spirit cobra transmutes god-bird eggs into
their cockatrice-family counterparts; and the pyrelord stands in for a
furnace, opening the jewellery-crafting mould interface from a gold bar.
With the last stubs implemented, FamiliarSpecialStubs is gone.

* feat: familiar passives and item interactions - bunyip heal, forage xp, toad cannonball, ent transmute, regent firelighting, unicorn Cure fix

The bunyip passively heals its owner 20 life points every 15s (with the
water splash graphic); the granite lobster and ibis grant a tenth of the
catch's Fishing xp on their foraged fish; Toad Bark now requires loading
the barker toad with a cannonball and fires it for up to 300; the giant
ent transmutes pure essence into earth/nature runes; the forge regent
burns logs like the pyrelord (both now with the +10 xp helper bonus);
and the unicorn stallion's Cure option actually cures - it checked a
nonexistent 'poisoned' variable before - for 2 special-move points.

* test: cover the combat, boost, item and passive familiar specials

Two new suites invoke every newly-ported special and the rescaled ones:
projectile hits, stat drains, stuns/binds (size-gated, so a 1x1 rat),
the titan charge flurries, Dust Cloud, Toad Bark's cannonball cycle,
the item-target specials through the real item-on-npc interaction, the
Regrowth stump revival, and the passive bunyip heal, unicorn Cure, ent
transmutes and forge-regent firelighting.

Item-target specials moved from itemOnNPCApproach to itemOnNPCOperate:
when the player is adjacent the interaction resolves the operate map,
where the beast-of-burden store's *:familiar wildcard would swallow the
click; operate's exact item:npc key wins instead. The titan flurry rolls
its own flat max per extra hit (230 iron / 244 steel, from darkan) since
NPC damage lives in combat defs, not npc-def max_hit params.

* feat: cast button explains how to trigger item-target specials

Immense Heat, Swallow Whole and Ophidian Incubation are cast by using
the item on the familiar, so the familiar-tab/orb cast button had no
registration and silently did nothing - it now messages the trigger
(e.g. 'use a gold bar on the pyrelord') without spending anything.

* feat: item-target specials cast from the button onto an inventory item

A new FamiliarSpecialMoves.item map dispatches the familiar-tab/orb cast
component used on an inventory item (spell-on-item style), so Immense
Heat, Swallow Whole and Ophidian Incubation now work from the Cast
option as well as by using the item on the familiar - both surfaces
share one block per special.

Winter Storage moves off item-on-npc entirely: the beast-of-burden
store always wins that click when adjacent, so banking was unreachable.
It now lives on Cast-on-item alone (refusing its own scroll), while
using an item directly on the yak keeps storing it in the pack.

* fix: pyrelord and forge regent breathe fire beneath themselves when burning logs

The familiar now plays its fire-breathing animation (8085) and the fire
and ashes land on the familiar's tile, instead of the player miming a
tinderbox and lighting the fire underfoot.

* feat: adjacent familiars face their idle owner; pyrelord steps west off its fire

An npc following a target that is already within a tile now just faces
them instead of shuffling onto the follow tile every step - it only
moves again once the target walks out of reach.

After lighting logs the pyrelord/forge regent steps one tile west when
unblocked, like players do, then stands beside the fire facing its owner
(resuming the follow immediately would walk it straight back onto the
fire) until the owner moves off.

* fix: familiar no longer tucks in behind an owner who steps up beside it

The adjacency guard lived only in Follow.recalculate, but a
freshly-restored Follow mode calculates a full path to the follow tile
on its first tick (Movement.calculate), bypassing the guard - so after
the pyrelord's step west, the owner walking up next to it sent it
circling behind them. The guard now also runs in Follow.tick before
any movement.

* feat: logs land at the familiar's feet before the pyrelord breathes fire

Using logs on the pyrelord/forge regent now drops them as a floor item
on the familiar's tile (owner-visible, as a tinderbox drop would be),
plays the fire-breath, and lights them two ticks later - aborting,
fireless, if the logs are gone by then (e.g. picked back up).

* feat: familiars step out from under a player standing on their tile

Follow mode now mirrors combat movement's step-out: when the followed
target stands on top of the npc (and it isn't allowed_under), it steps
to a random free cardinal tile instead of idling underfoot. The
adjacent-face guard exempts the overlap case so the step-out can run.

* fix: Thieving Fingers actually boosts Thieving by 2

It was implemented as a visual-only flourish on the assumption the
passive +3 covered it, but both references boost the stat on cast too -
darkan raises Thieving by 2 with the owner sparkle (gfx 1300), on top of
the magpie's passive invisible +3.

* fix: a second same-tick graphic no longer overwrites the first

Character.gfx checked the primary-graphic mask but flagged the
secondary, so every graphic landed in the secondary slot and a second
gfx() in the same tick clobbered the first - which is why Thieving
Fingers' owner sparkle never showed: the cast gate's scroll flourish
(1316) played right after it. Checking the secondary mask (the slot
actually used) sends a simultaneous second graphic to the free primary
slot, so both render.

* fix: Blood Drain plays its visuals and restores stats by 2 + 20%

The bloated leech's special ran with no visuals at all; per darkan it
now plays the leech's draw animation (7715) and graphic (1419), sends
the blood bolt (1417) to the owner with their burst graphic (1420), and
the restore gains darkan's flat +2 on top of the 20% (adjustStat(2,
0.20)) - Levels.restore(skill, 2, 0.2) matches exactly. Cure, all-stat
restore and the 25 life-point bite are unchanged.

* fix: Tireless Run plays the right visuals on the right characters

Per darkan: the 1521 graphic belongs on the terrorbird (with its 8229
flap animation, not 1009), the owner gets the 1300 sparkle - previously
1521 played on the player and the familiar showed nothing. The cast now
also refuses (charging nothing) at full run energy, and the energy
restored is half the boosted Agility level rather than the base.

* fix: QA pass over every familiar special in the testing tracker

- Abyssal Drain saps 10% Magic and 10% Prayer (was flat 1-3 Prayer)
- Dissolve/Rending drain 10% of Attack/Strength (were flat 3/1), with
  their missing familiar and impact graphics (1361/1360, 1372)
- Goad gores twice for up to 120 each (was engage-only); Ambush pounces
  the kyatt onto its target for up to 224 (was a recall-to-owner)
- Dust Cloud is a combat special: the picked target plus up to six foes
  within seven tiles (was a radius-1 AoE around the familiar)
- Famine no longer deals damage and rots a random (not first) food
- Inferno gains its up-to-85 fiery bolt, castable on npcs too; the
  player-target disarm stays on top
- Deadly Claw uses the talon beast's attack animation with tighter
  volley timing; Acorn Missile splash caps at nine and shows its
  projectile and impact per extra target
- Iron Within / Steel of Legends are immediate volleys (3x230 melee /
  3x220 magic, 4x244 melee/ranged by distance) with darkan's 7954/8190
  animations - not charge-next-attack buffs
- Boil picks melee up close, ranged or magic at even odds from afar,
  with the titan's own 1373 graphic; Ebon Thunder drains a tenth of
  remaining special energy (was flat 100)
- Testudo boosts Defence by 8 (was 9); Volcanic Strength and Abyssal
  Stealth gained their missing animations/graphics; Magic Focus uses
  anim 8308; Titan's Constitution heals 10% of max (was flat 80) with
  per-titan colours and a full-health refusal; Healing Aura gets the
  same refusal and its owner sparkle (familiar keeps 1356)
- Fish Rain drops one of shrimps/cod/bass/mackerel at the ibis' feet
  with its 8199/1337 visuals; Fruitfall shakes 0-6 fruit (papaya first,
  pineapple joins the pool); Essence Shipment also empties the titan's
  pack and plays 7698/1457; Winter Storage shows the yak's 1358 gfx

* feat: familiar 'other abilities' from the testing tracker

- Hunting cats teleport their owner home from the Interact option
  (graahk to the Karamja hunter area by the nature altar, kyatt to
  Piscatoris, larupia to Feldip Hills), refusing above level-20
  wilderness; the lava titan's existing Lava Maze option now uses the
  same gated jewellery-style teleport instead of a bare tele
- Using a raw fish on the bunyip transmutes it into 1 to heal/10 water
  runes (Swallow Whole moves to the Cast option alone)
- The geyser titan boils a bowl into a bowl of hot water and recharges
  amulets of glory like the Fountain of Heroes
- The ibis and granite lobster stand in for a harpoon when fishing
- Ferocious: one in five of the spirit dagannoth's melee attacks
  strikes a second time (sync 7791/1427)
- Passive boosts: the spirit graahk's missing Hunter +5, the stranger
  plant's Farming +1+4% and the geyser titan's Ranged +1+3% - the boost
  table now supports level-scaled formulas

* feat: fishing foragers only produce while their owner fishes

The granite lobster and ibis forage was already wired and rolling (a
1-in-12 chance per 30-second window, exactly darkan's 50-tick cadence)
but rolled whether or not the owner was doing anything. Per darkan's
canRollForager both now only roll while the owner is actively fishing,
spearing swordfish/sharks (lobster) or tuna/swordfish (ibis) alongside
them with a tenth of the catch xp.

* feat: iron/steel titan and wolpertinger style-gated defence passives

While summoned, the iron titan makes its owner 10% harder to hit with
melee, the steel titan 15%, and the wolpertinger 5% harder to hit with
magic - applied as a multiplier to the owner's defensive rating for the
incoming attack style only (the tracker's 'boost only applies to
melee/magic attacks'). Darkan only implements the steel titan's and
unconditionally; the style gate follows the wiki.

* feat: Drown and Flames order the familiar at its owner's target

The karamthulhu overlord's Drown and smoke devil's Flames right-click
options had no handler, so clicking them did nothing. Both now send the
familiar at its current target (falling back to the owner's), firing
its water/fire auto-attack - neither reference implements these
(2009scape's Flames is a TODO stub), so they follow the wiki's
'hits opponent with a water/fire spell'.

* fix: Dust Cloud fires as an AoE on a plain cast click

Converting it to a target-then-cast combat special (darkan's shape) in
the audit left a bare click on the cast button doing nothing until an
npc was picked. A plain cast now chokes everything around the smoke
devil itself (up to six foes, radius 1, as in 2009scape), while casting
onto a specific foe keeps the darkan behaviour - that target plus up to
six more around it.

* fix: familiar AoE specials find npcs by their bodies, not anchor tiles

Dust Cloud (and Fireball Assault, Sandstorm, Explode, the Acorn Missile
splash) scanned spiral tiles against NPCs.at, which only matches an
npc's south-west anchor tile - any monster larger than 1x1 standing
right beside the familiar was invisible to the special and it reported
nothing nearby to attack. The scan is now zone-based and measures the
distance to the npc's bounds, nearest first.

* fix: transformed npcs fall back to base-id interaction handlers

Npc interactions resolve handlers by the transformed definition id, so
a familiar in its wilderness combat form ('<id>_combat') stopped
answering Interact, Store, Dismiss, Drown and item-on-familiar uses -
every handler is keyed on the base id. Player-on-npc and item-on-npc
interactions now fall back to the base id when the transform has no
handler of its own, with transform-specific handlers keeping priority.

* fix: stranger plant swings its own attack animation

Its combat anims (8292/8294/8295) belonged to npcs 6829/6830 - an
off-by-one-npc slip that darkan's stranger_plant.json also carries.
2009scape's npc config gives the stranger plant melee animation 8208
and death 8209, with no defence animation, so the defend entry is
dropped rather than left pointing at the wrong model.

* fix: Toad Bark's cannonball flies darkan's low rising arc

The shot flew flat at the default height-40 line with no curve, which
the client renders with the model oriented backwards. There is no
projectile rotation in the protocol - the client orients the model
along its trajectory - so the fix is darkan's launch profile: low out
of the toad's mouth, rising slightly (10 -> 16, curve 16).

* fix: familiar special projectiles fly at distance-scaled speed

Special-move projectiles either flew near-instantly regardless of range
(multiplier = 1, ~1 client tick per tile) or fell back to the fast
0 + 5/tile default. All are now delay = 30, time_offset = 10,
multiplier = 10 - a flight of 1 game tick plus a third of a tick per
tile, matching the live game's '1 + floor(distance / 3)' hit delay.
Deliberate exceptions stay put: Bull Rush's constant 2s charge, the
compost lob, and Venom Shot's explicit slow bolt. The standard is
documented in the file header.

* fix: Toad Bark fires the multicannon's round cannonball graphic

The reference's 1402 projectile model faces backwards in this
revision's cache, and the projectile protocol has no rotation - the
client orients models along the trajectory alone - so no arc could
right it. The dwarf multicannon's cannonball (gfx 53) is a plain sphere
with no facing to get wrong; the low rising arc stays.

* feat: cache graphic patching rotates the Toad Bark cannonball 180 degrees

The projectile protocol carries no rotation (darkan's packet is
field-identical - its 'angle' byte is our 'curve'), but the cache's
graphic definitions do: opcode 6 is a model rotation in degrees (the
revision's existing values are only ever 90/180/270, gfx 1402's was 0).
Adds a GraphicEncoder mirroring the decoder, and a FixGraphics tool
(fixGraphics gradle task, also wired into CacheBuilder) that patches
gfx 1402 with a 180 degree rotation after round-trip-validating the
re-encode - so Toad Bark keeps its smoke-trailed cannonball, flying the
right way round. The local cache has been patched.

* feat: Testudo plays its shell tint (gfx 1308) on the owner

The war tortoise's cast showed only the familiar's own animation and
graphic; the owner now gets the 1308 graphic alongside the scroll-cast
flourish. The boost helper accepts an optional owner graphic for any
future single-stat boosts that need one.

* fix: the bunyip transmute projectile flies from the player to the bunyip

The tossed fish flew out of the bunyip at its owner; it now travels the
other way, from the player's throw to the bunyip's mouth.

* fix: Fruitfall scatters fruit onto free tiles with landing splashes

Per darkan, each shaken-loose fruit lands on its own free tile around
the owner - deduped, splash graphic (1331) a tick before it appears -
instead of the whole harvest piling up underfoot with no visuals. The
first fruit is still always a papaya and an unlucky cast still drops
nothing.

* feat: the fruit bat flies up into the trees before dropping its fruit

Fruitfall now plays 2009scape's two-stage sequence: the bat ascends
(anim 8320 with the swirling-leaves graphic 1332), then ~4 ticks later
swoops into its drop animation (8321) as the landing splashes flash and
the fruit falls a tick after - the same staged-queue shape as the
macaw's Herbcall.

* fix: familiars follow the real PvP rules when sent at players

familiarCanSpecial and commandFamiliarAttack required the in_pvp flag
on both sides, but the wilderness only sets in_wilderness - so Famine,
Ebon Thunder, Inferno and the attack command all refused with 'You can
only attack players in a player-vs-player area' inside actual PvP.
Player targets are now validated by Target.attackable with the owner
as source - the same wilderness/PvP-area, combat-level-bracket and
single-way rules as attacking directly.

* fix: the arctic bear swings its own claws, not a spider's legs

Its combat animations (5327/5328/5329) belong to the spider family -
the spirit spider familiar two npc ids along uses the same set, and
darkan's arctic_bear.json carries the identical off-by-one copy-paste.
2009scape's npc config gives the bear attack 4925, defend 4928 and
death 4929, completing its own 4925-4929 cluster alongside the Arctic
Blast special's already-correct 4926.

* feat: Rise from the Ashes - the phoenix's special, built from the wiki

Neither reference implements it: darkan's is a TODO stub ('this special
is a pain in the ass to code') and 2009scape commented the scroll out,
so the phoenix was deliberately left without a special until now. Per
the wiki it has two modes: cast on a foe it blasts for up to the
phoenix's 160 magic max and sears away half their Attack, Magic and
Ranged, returned exactly ~6 seconds later (set back directly - restore
caps at base level and would swallow it); cast on ashes the phoenix is
reborn, the ashes burning away along with all its wounds. A plain cast
click explains both triggers.

* fix: Rise from the Ashes targets ashes on the ground, reborn-scaled

The special is cast on a pile of ashes lying on the ground - not on a
combat target or inventory ashes as first built. A new floor-item
dispatch surface (FamiliarSpecialMoves.floorItem + cast-component
onFloorItemApproach hooks) carries it: the phoenix drops in place and
glows, then is reborn atop the ashes at full life points, scorching up
to five foes beside them with magic hits scaling on the life points it
was missing - a quarter of the deficit as the max hit, so an unhurt
phoenix's rebirth is a pure heal. It stands over the spent ashes a few
ticks (PauseMode) so the follow doesn't drag it back mid-flare. The
live game's more-than-five glitch (rebirth but no attack) is not
reproduced - the flare simply caps at five.

* fix: the giant ent's transmuted runes stack

inventory.replace swaps the essence for a rune in the same slot, so
every transmute produced its own one-rune slot instead of joining the
stack. Remove-then-add lets the rune merge with any existing stack.

* fix: Titan's Constitution over-heals 80 life points past maximum

The heal was a max-capped restore (darkan's 10%) with a full-health
refusal; per the live game it boosts life points by a flat 80 that can
carry the owner above their maximum, alongside the 12.5% Defence boost,
shared by the fire, ice and moss titans. levels.boost on Constitution
does exactly this, capping at max+260 like other constitution boosts.

* fix: Spike Shot stuns its target on impact

Darkan's version is damage-only, but per the wiki the spirit
dagannoth's spike 'attacks the target, stunning them' - the stun now
lands with the projectile (5 ticks, like Bull Rush) rather than the
hit alone.

* fix: combat specials fire at the familiar's foe on a plain cast click

Iron Within (and every other npc-target special) only worked by
clicking the button and then clicking a target - a plain click silently
armed targeting mode, reading as broken. As in darkan and the live
game, a plain click now auto-fires the special at the familiar's
current combat target, falling back to the owner's, with a message when
neither is fighting; clicking a target directly still works through
the approach hooks.

* fix: Steel of Legends' blade-storm graphic plays on the target alone

The 1449 graphic showed on both the titan and its target; it belongs
on the target tile only - the titan keeps just its attack animation.

* fix: pack yak attack and defend animations

The yak used darkan's 853/851 - another wrong-model set from its
familiar combatdefs. 2009scape's npc config gives the pack yak's own
cluster: attack 5782, defend 5783.

* chore: strip narration comments from the special toml files, remove dead code

The per-entry 'X's Y animation' comments in summoning_special.anims/gfx
said nothing the entry names don't; only the load-bearing notes remain
(suffix conventions, the projectile speed standard, Bull Rush's
constant-flight exception, the Toad Bark arc/cache-rotation note, and
Herbcall's tick maths). Dead code: dropForage (Fruitfall and Fish Rain
place their drops per tile themselves now) and familiarBoostSkill
(never called - the boost specials use their own helper), plus unused
imports and commandFamiliarAttack's stale PvP-area-only doc.

* test: wrap InterfaceApi.onItem calls in runTest - it is suspend on main

* chore: adjust npc spawn for pikkupstix.

* feat: Pikkupstix tutor dialogue and enchanted headgear scroll storage

Pikkupstix gains his post-Wolf-Whistle Talk-to dialogue (2009scape
transcript: the summoning overview, supplies shop, skillcape purchase)
and an Enchant option.

Enchanted headgear (from darkan's EnchantedHeadwear) stores combat
scrolls: use combat scrolls on a hunter helm to charge it (antlers 40,
lizard skull 65, feather headdress 150, one scroll type at a time),
empty it back on a summoning obelisk, and a worn charged helm feeds a
scroll to a special move when the pack has none. Only combat scrolls
store (checked via the scroll -> pouch -> familiar -> npcTarget lookup);
non-combat scrolls like Healing Aura are refused. For void's hunter
helms the plain form is already the enchanted one (darkan's
base==enchanted), so charging is the only visible state change.

* feat: enchant metal/god helms for scroll storage; Commune and Uncharge options

Extends enchanted headgear from the three hunter helms to darkan's full
set - snakeskin bandana, archer/berserker/warrior/farseer/rune/splitbark
helms, helm of Neitiznot, dragon med helm, lunar helm and armadyl helmet
- each with its Summoning level and capacity. Unlike the hunter helms
(whose plain form is already scroll-capable) these have the real three
-state flow: Pikkupstix enchants the plain helm into its empty (e) form,
scrolls charge it, and it disenchants back. The (e) middle-state items
already exist in the cache; added their item defs.

The charged helms already carry Commune and Uncharge in their cache
option arrays but had no handlers, so the options did nothing. Commune
now reports the stored scrolls and Uncharge empties them back to the
pack (reverting to the enchanted form) - replacing the obelisk-empty
stopgap.

* feat: mirror stored scroll count onto the charged helm's item charge

The count on the follower-details cast button (662,75) and the item is
rendered client-side from the item's charge value, so the helm's stored
scrolls have to live on the item, not just a server variable. The
charged helms now carry a charges/charges_max cap, and the stored count
is mirrored onto the worn or held helm's charge whenever it changes -
so the number shows on the helm and feeds the cast-button count. Reverts
reset the charge to 1 first so an emptied helm is a clean single item,
not one carrying the old count.

* feat: mirror stored scroll count onto the charged helm's item charge

The count on the follower-details cast button (662,75) and the item is
rendered client-side from the item's charge value, so the helm's stored
scrolls have to live on the item, not just a server variable. The
charged helms now carry a charges/charges_max cap, and the stored count
is mirrored onto the worn or held helm's charge whenever it changes -
so the number shows on the helm and feeds the cast-button count. Reverts
reset the charge to 1 first so an emptied helm is a clean single item,
not one carrying the old count.

* fix: add the albino rat's missing summon animation

The albino_rat_spawn entry was absent, so the familiar appeared with no
animation on summon/call. 2009scape spawns familiars with a graphic and
a player gesture rather than a per-npc animation, so there was no id to
lift directly; identified 14859 from void's cache as the albino rat's
summon animation - a 23-frame materialise matching the spirit wolf's
summon (8298) profile exactly, from the rat's 14856-14863 cluster.

* fix: remove albino rat's incorrect summon animation

No valid rev-634 spawn animation exists for the albino rat: darkan's
value (16080) is a post-634 re-animation, absent from this cache (max
animation id 15260), and 2009scape plays no per-familiar spawn anim at
all. Better to play nothing than a wrong animation.

* feat: abyssal parasite turns Abyss prayer drain into gradual leak

Entry no longer zeroes Prayer with a parasite summoned; instead 10
points leak every 8-16 ticks while inside. Losing the parasite in the
Abyss drains the remainder, so dismissing it grants no free prayer.

* feat: Drown instantly casts water blast for two summoning points

Previously only re-targeted the overlord like a plain attack order.
Now fires an immediate magic-based hit (max 76) with its attack
visuals, charging two summoning points only when the cast succeeds.

* feat: Flames instantly casts fire spell for two summoning points

Previously only re-targeted the smoke devil like a plain attack
order. Now fires an immediate magic-based hit (max 150) with its
attack anim and a dedicated flame projectile (gfx 1473), charging
two summoning points only when the cast succeeds.

* fix: default familiar chatheads to small dialogue heads

Familiars had large_head = true across the board, routing every
dialogue through the large head component; the engine default of
false matches the client. Also drops the per-familiar dialogue
expression keys in favour of the cs2-driven chathead lookup.

* feat: resolve familiar dialogue chatheads like cs2 script 751

Dialogue chatheads previously used the humanoid expression
animations (or hand-maintained per-familiar toml sets, some wrong -
the spirit wolf had the cat's). Familiars now resolve their
animation at runtime exactly as interface 662's client script does:
the follower_details_chathead_animation varbit value keys enum 1276,
or enum 1275 with 50 subtracted when over 50.

Also strips the _familiar suffix before setting the varbit so it
transmits the mapped index instead of -1 (the map keys are base
names), keeping the details interface in sync server-side.

* fix: stop unmapped familiar chatheads playing the enums' default

Familiars with chathead value 0 (arctic bear, albino rat, minotaurs)
or an out-of-range second-bank key (unicorn stallion) have no entry
in enums 1276/1275, so the lookup returned the enums' default - the
cat's animation - and playing it on a mismatched head model crashed
the client. Missing entries now fall back to the standard expression
animations like any unmapped npc.

* fix: replace typographic punctuation that crashes the client

writeString sends each char as a single byte, so smart quotes
(U+2018/9) and ellipses (U+2026) truncate to control bytes the
client's font handling can't render - interacting with the arctic
bear, talon beast or swamp titan crashed the game. Replace them
with plain ASCII in all client-bound strings.

* test: scan content sources for multi-byte client-bound characters

writeString truncates characters over 0xFF to a control byte the
client can't render, so smart quotes or ellipses in dialogue crash
the game. Fails with file:line of any offending string, ignoring
comments and interface option matchers.

* fix: show static chatheads for familiars with unverified map values

The varbit map's zeros are unverified gaps from the original capture,
so familiars like the desert wyrm and arctic bear resolved no enum
entry and bobbed with humanoid expression animations on non-human
head models. Give them a static head (-1) instead, matching what the
details interface shows, until their real values are identified.

* fix: play the enums' default chathead animation like the client does

The details interface animates value-0 familiars (desert wyrm,
arctic bear) correctly because the client's datamap lookup returns
the enum default - a generic head-bob - for missing keys. Match
that instead of sending a static head; the earlier crash was the
typographic punctuation, not this animation.

* feat: resolve pet dialogue chatheads through the cs2 enum lookup

talkToPet keyed the chathead enum by npc id - the wrong key space,
the enums key on the varbit value - and passed the resulting
animation name as an expression, which double-prefixed the lookup
and rendered a static head. Mapped pets (sneakerpeeper) now resolve
through familiarChatheadAnimation like familiars; unmapped pets fall
back to the neutral expression.

* fix: check the real level, not summoning points, in summoning gates

Summoning's current level doubles as the points pool, so gates using
Level.has - summoning a pouch familiar, dropping a pet, incubating an
egg, enchanting headgear - wrongly refused players whose points had
drained below the requirement. Switch them to hasMax, which reads the
level derived from experience. Boostable-skill gates (bunyip's
Cooking, pyrelord's Firemaking) are left on the current level.

* Update game/src/main/kotlin/content/entity/death/NPCDeath.kt

Much neater, good recommendation.

Co-authored-by: Greg <GregHib@users.noreply.github.com>

* Fix report abuse breaking the chat box (#1063)

Opening report abuse in select-player mode (cs2 246) unhooks the chat
input key listener (137:55) and sets report mode varc 11. The client's
close script (cs2 675) never restores either, so chat stays dead until
relog. The restore script (cs2 244) has no client-side callers - the
server is expected to run it. Close the interface server-side on submit
and run cs2 244 whenever report_abuse closes.

Closes #1061

* Add pitfall traps (#1062)

* Start on pitfall traps

* Update pitfall messages

* Fix bank withdraw full inventory message happening when all items were removed

* Add pitfall level checks

* Add pitfall collapse

* Add pitfall logic and tests

* Remove hunter class

* Fix teasing, add tests for larupia and kyatts

* Remove duplicate mazchna

* Fix barbarian course advanced laps reward collection

* Remove unused hunter data

* A few account bug fixes (#1069)

* Fix run orb status invalid on first login

* Fix logging in requiring display name instead of account name

* Fix being able to create an account using a display name that's already in use

* Allow spaces in message request usernames fixes #1068

* Some script params

* Fix account unit test

* Fix account merge tests

* Fix account definitions incorrectly using account name instead of display name as the key

* Add random events (framework + events) (#1064)

* Add random event system and maze event

Implements the random event trigger, kidnap teleport and mysterious old
man framework, plus the Maze random event where players navigate to a
central shrine before time runs out.

Tutorial Island is tagged no_random_events so events don't fire there.

Ref GregHib/void-private#223

* Add in-place random event framework

Extend the random event system beyond the kidnap-to-instance pattern so
in-place nagging events (Certer, Sandwich Lady, Evil Twin) can be built:

- RandomEvents.noteAndTeleport: ignore penalty that notes the player's
  inventory then exiles them, so an event can't be macroed through.
- InPlaceRandomEvent + startInPlaceEvent/endInPlaceEvent: spawn an event
  NPC beside the player that follows and nags on a timer, applying the
  ignore penalty when its lifetime runs out.
- Decouple entry from the old man: the trigger now calls RandomEvents.start
  directly and each launcher owns its intro. Kidnap events call the new
  mysteriousOldMan() helper; in-place events spawn their own NPC. Maze's
  launcher allocates its instance before the intro so a resumed timer
  doesn't fail the event mid-intro.
- rewardCostumeOrCoins: shared collect-the-set reward (camo, lederhosen).

Ref GregHib/void-private#223

* Add Freaky Forester random event

The player is teleported to the forester's clearing and told to kill a
pheasant with a specific number of tails. Only the assigned pheasant
drops the correct raw pheasant; wrong ones drop an incorrect one that the
forester rejects. Handing in the correct pheasant rewards the first
missing lederhosen piece, or 500 coins if the set is complete.

- Conditional pheasant drops keyed to the killer's assigned task
- Blocks attacking further pheasants once one is killed
- Static pheasant spawns in the clearing; forester dialogue drives the task
- Removes the unconditional pheasant drop table (drops are now per-task)

Ref GregHib/void-private#255

* Drop the raw pheasant to the floor instead of the inventory

The assigned pheasant now drops the raw pheasant as a normal killer-owned
floor drop (non-tradeable, private) for the player to pick up and hand to
the forester, matching the live behaviour.

* Add Sandwich Lady random event

First in-place event on the framework: the Sandwich Lady appears beside
the player pushing her tray and nags until spoken to. Talking opens her
refreshment tray (interface 297); choosing the food she offered hands it
over and ends the event in place, while any other choice knocks the
player out via the note-and-teleport ignore penalty. Talking mid-combat
hands over the correct food directly, and other players are turned away.

Adds RandomEvents.completeInPlace for events that finish without a return
teleport, and defines interface 297's seven food buttons (10-22).

Ref GregHib/void-private#261

* Sandwich Lady reacts to the tray choice

She now says "Hope that fills you up!" when handing over the correct
food, and smacks the player for 3 before the knockout when they pick the
wrong one, matching live behaviour.

* Give the Sandwich Lady knockout its animation and dialogue

- Show "Hope that fills you up!" as a chatbox line on a correct pick so
  the player actually sees it, instead of a fleeting overhead force-chat.
- On a wrong pick she swings her baguette (animation), smacks the player,
  who plays the death animation and fades out before waking up elsewhere.

* Add Drill Demon random event

Sergeant Damien whisks the player to his exercise yard and barks an
order. Each of the four mats shows an exercise on its sign (a varbit);
the player uses the mat matching the order, performing the exercise
animation. Four correct exercises earns a piece of the camouflage outfit
(helmet/top/bottoms, first missing) or 500 coins if the set is complete.

- Shuffles the four exercises across the mat signs each round
- Exercise index 0 (jog) is read with a 0 default since the variable
  store clears vars set to their default value
- Reuses the shared kidnap/complete and rewardCostumeOrCoins helpers

Ref GregHib/void-private#254

* Fix Drill Demon signs and mat positioning

- Encode the four exercises as sign varbit values 1-4 (0 = no sign). The
  variable store clears any var set to its default 0, so a sign set to 0
  never reached the client - one mat showed no sign and the client's signs
  drifted out of sync with the server, making the correct mat read as wrong.
- Walk the player onto the mat before performing the exercise.

* Polish Drill Demon exercise and sign-change effects

- On using a mat the player now faces the sergeant, plays the exercise
  sound and waits two ticks before performing the exercise animation.
- When the signs reshuffle, a puff of smoke (gfx 2000) plays on each sign
  tile in turn from sign 1 to sign 4, a tick apart, as it changes.

* Play the sign-change puff on the sign tile (mat y + 2)

* Sweep the Drill Demon sign puffs half a tick apart (Option B)

The four signs now all change on one tick and the smoke puffs sweep
across them client-side, sign 1 to sign 4, half a tick (15 client ticks)
apart, instead of one server tick between each change.

* Use the large chathead for Sergeant Damien's dialogue

* Show the sign item dialogue after Drill Demon's order

After Sergeant Damien names the exercise, an item box shows the matching
sign with "Go to this mat and <action>!" ("this mat" in red), matching
2009scape. Adds the missing push-up sign item (10946). Darkan doesn't
implement Drill Demon.

* Make Drill Demon wait before the first order

On arrival the yard shows "Follow Sergeant Damien's orders!" and waits
about five seconds before the sergeant barks the first exercise. Using a
mat (or talking to him) before the order is given has him snap "I haven't
given you the order yet, worm!" and earns no credit for that attempt.

* Add Certer random event

One of the certificate brothers appears beside the player, bows and nags.
Talking opens the identify interface (184) showing an item and three
descripti…
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.

2 participants