Summoning: complete familiar abilities (specials, passives, dialogue)#1052
Summoning: complete familiar abilities (specials, passives, dialogue)#1052HarleyGilpin wants to merge 138 commits into
Conversation
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 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.
…ead 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.
…ractions-and-dialogues # Conflicts: # game/src/main/kotlin/content/entity/combat/Target.kt
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.
…e 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| [expression_wolf_happy] | ||
| id = 8372 | ||
|
|
||
| [expression_wolf_angry] | ||
| id = 8373 | ||
|
|
||
| [expression_wolf_sad] | ||
| id = 8374 | ||
|
|
||
| [expression_wolf_quiz] | ||
| id = 8375 | ||
|
|
||
| [expression_bird_sad] | ||
| id = 6824 | ||
|
|
||
| [expression_bird_happy] | ||
| id = 7 | ||
|
|
||
| [expression_bird_neutral] | ||
| id = 8 | ||
|
|
||
| [expression_bird_quiz] | ||
| id = 9 | ||
|
|
||
| [expression_leech_1] | ||
| id = 8411 | ||
|
|
||
| [expression_leech_sad] | ||
| id = 8412 | ||
|
|
||
| [expression_leech_neutral] | ||
| id = 8413 | ||
|
|
||
| [expression_leech_angry] | ||
| id = 8414 No newline at end of file |
| // A transformed npc (e.g. a familiar's wilderness "<id>_combat" form) falls back to its base | ||
| // id's handlers when the transform has none of its own for the item. | ||
| override fun hasOperate() = Operation.itemOnNpc.containsKey("${item.id}:*") || Operation.itemOnNpc.containsKey("${item.id}:${target.def(player).stringId}") || Operation.itemOnNpc.containsKey("*:${target.def(player).stringId}") || | ||
| Operation.itemOnNpc.containsKey("${item.id}:${target.id}") || Operation.itemOnNpc.containsKey("*:${target.id}") |
There was a problem hiding this comment.
Using transformed ids is intentional and don't really want to start adding combinations, should use prefixes instead
| // The "Attack" option is a player-facing menu concern. A familiar's base (non-wilderness) | ||
| // form deliberately has no "Attack" option so its owner can't click it, but an NPC must | ||
| // still be able to retaliate against a familiar that attacks it, so skip the check there. | ||
| val retaliatingAgainstFamiliar = source is NPC && target["owner_index", -1] != -1 | ||
| if (!retaliatingAgainstFamiliar) { | ||
| if (target.transform != "") { | ||
| if (!NPCDefinitions.get(target.transform).options.contains("Attack")) { | ||
| return false | ||
| } | ||
| } else if (target.def.options[1] != "Attack") { |
There was a problem hiding this comment.
Can probably be applied globally no need for familiar check for this?
| // The "Attack" option is a player-facing menu concern. A familiar's base (non-wilderness) | |
| // form deliberately has no "Attack" option so its owner can't click it, but an NPC must | |
| // still be able to retaliate against a familiar that attacks it, so skip the check there. | |
| val retaliatingAgainstFamiliar = source is NPC && target["owner_index", -1] != -1 | |
| if (!retaliatingAgainstFamiliar) { | |
| if (target.transform != "") { | |
| if (!NPCDefinitions.get(target.transform).options.contains("Attack")) { | |
| return false | |
| } | |
| } else if (target.def.options[1] != "Attack") { | |
| if (source is Player && target.def(source).options.contains("Attack") { | |
| } else if (target.def.options.contains("Attack") { | |
| } |
| val killer = (killer as? NPC)?.get("owner_index", -1)?.takeIf { it != -1 } | ||
| ?.let { Players.indexed(it) } ?: killer |
There was a problem hiding this comment.
Cleaner to read and understand imo
| val killer = (killer as? NPC)?.get("owner_index", -1)?.takeIf { it != -1 } | |
| ?.let { Players.indexed(it) } ?: killer | |
| var killer = killer | |
| if (killer is NPC && killer.contains("owner_index")) { | |
| killer = Players.indexed(killer.getValue("owner_index")) | |
| } |
| private data class Headgear(val base: String, val enchanted: String, val charged: String, val level: Int, val capacity: Int) | ||
|
|
||
| private val HEADGEAR = listOf( | ||
| // Hunter helms - plain form is already scroll-capable. | ||
| Headgear("antlers", "antlers", "antlers_charged", 10, 40), | ||
| Headgear("lizard_skull", "lizard_skull", "lizard_skull_charged", 30, 65), | ||
| Headgear("feather_headdress_blue", "feather_headdress_blue", "feather_headdress_charged_blue", 50, 150), | ||
| Headgear("feather_headdress_yellow", "feather_headdress_yellow", "feather_headdress_charged_yellow", 50, 150), | ||
| Headgear("feather_headdress_red", "feather_headdress_red", "feather_headdress_charged_red", 50, 150), | ||
| Headgear("feather_headdress_stripy", "feather_headdress_stripy", "feather_headdress_charged_stripy", 50, 150), | ||
| Headgear("feather_headdress_orange", "feather_headdress_orange", "feather_headdress_charged_orange", 50, 150), | ||
| // Metal/god helms - Pikkupstix enchants the plain helm first (base -> enchanted). | ||
| Headgear("snakeskin_bandana", "snakeskin_bandana_enchanted", "snakeskin_bandana_charged", 20, 50), | ||
| Headgear("archer_helm", "archer_helm_enchanted", "archer_helm_charged", 30, 70), | ||
| Headgear("berserker_helm", "berserker_helm_enchanted", "berserker_helm_charged", 30, 70), | ||
| Headgear("warrior_helm", "warrior_helm_enchanted", "warrior_helm_charged", 30, 70), | ||
| Headgear("farseer_helm", "farseer_helm_enchanted", "farseer_helm_charged", 30, 70), | ||
| Headgear("rune_full_helm", "rune_full_helm_enchanted", "rune_full_helm_charged", 30, 60), | ||
| Headgear("splitbark_helm", "splitbark_helm_enchanted", "splitbark_helm_charged", 30, 50), | ||
| Headgear("helm_of_neitiznot", "helm_of_neitiznot_enchanted", "helm_of_neitiznot_charged", 45, 90), | ||
| Headgear("dragon_med_helm", "dragon_helm_enchanted", "dragon_helm_charged", 50, 110), | ||
| Headgear("lunar_helm", "lunar_helm_enchanted", "lunar_helm_charged", 55, 110), | ||
| Headgear("armadyl_helmet", "armadyl_helmet_enchanted", "armadyl_helmet_charged", 60, 120), | ||
| ) | ||
|
|
||
| private val BY_ID = HEADGEAR.flatMap { listOf(it.base to it, it.enchanted to it, it.charged to it) }.toMap() |
There was a problem hiding this comment.
You know the drill, put it in tables
| private data class Boost(val flat: Int, val multiplier: Double = 0.0) | ||
|
|
||
| private val FAMILIAR_BOOSTS: Map<String, Map<Skill, Boost>> = mapOf( | ||
| "beaver_familiar" to mapOf(Skill.Woodcutting to Boost(2)), | ||
| "granite_crab_familiar" to mapOf(Skill.Fishing to Boost(1)), | ||
| "ibis_familiar" to mapOf(Skill.Fishing to Boost(3)), | ||
| "granite_lobster_familiar" to mapOf(Skill.Fishing to Boost(4)), | ||
| "arctic_bear_familiar" to mapOf(Skill.Hunter to Boost(7)), | ||
| "wolpertinger_familiar" to mapOf(Skill.Hunter to Boost(5)), | ||
| "spirit_graahk_familiar" to mapOf(Skill.Hunter to Boost(5)), | ||
| "spirit_kyatt_familiar" to mapOf(Skill.Hunter to Boost(5)), | ||
| "spirit_larupia_familiar" to mapOf(Skill.Hunter to Boost(5)), | ||
| "desert_wyrm_familiar" to mapOf(Skill.Mining to Boost(1)), | ||
| "void_ravager_familiar" to mapOf(Skill.Mining to Boost(1)), | ||
| "obsidian_golem_familiar" to mapOf(Skill.Mining to Boost(7)), | ||
| "lava_titan_familiar" to mapOf(Skill.Mining to Boost(10), Skill.Firemaking to Boost(10)), | ||
| "pyrelord_familiar" to mapOf(Skill.Firemaking to Boost(3)), | ||
| "forge_regent_familiar" to mapOf(Skill.Firemaking to Boost(4)), | ||
| "magpie_familiar" to mapOf(Skill.Thieving to Boost(3)), | ||
| "stranger_plant_familiar" to mapOf(Skill.Farming to Boost(1, 0.04)), | ||
| "geyser_titan_familiar" to mapOf(Skill.Ranged to Boost(1, 0.03)), | ||
| ) |
| * Each block returns whether the move actually happened so [castFamiliarSpecial] only spends a | ||
| * scroll + points on a real cast. | ||
| */ | ||
| object FamiliarSpecialMoves { |
There was a problem hiding this comment.
Will also need a clear() which is called by Script.clear
|
GregHib
left a comment
There was a problem hiding this comment.
A few comments and fixes but overall looking really strong
| chathead != null -> interfaces.sendChat(id, head, chathead, title ?: npcDef.name, lines) | ||
| npcDef.contains("dialogue") -> interfaces.sendChat(id, head, "${npcDef["dialogue", ""]}_$expression", title ?: npcDef.name, lines) | ||
| else -> interfaces.sendChat(id, head, expression, title ?: npcDef.name, lines) | ||
| } |
There was a problem hiding this comment.
This is too low level of a change, use npc(npcId: String, expression: String, text: String if custom expression needs to be passed
|
|
||
| object FixGraphics { | ||
|
|
||
| /** The barker toad's Toad Bark cannonball, whose model faces backwards in this revision. */ |
There was a problem hiding this comment.
I don't think this is the correct cannonball anim. See: https://youtu.be/o0zwPoONP8I?&t=17
Maybe it's 1401 for shoot gfx and 2143 for projectile (same as hand cannon)?
| import world.gregs.voidps.engine.Script | ||
| import world.gregs.voidps.type.random | ||
|
|
||
| class RavenousLocust : Script { |
There was a problem hiding this comment.
Using Famine on an npc target causes the player to attack
|
|
||
| // The pyrelord and forge regent act as portable fire sources - the demon breathes fire over | ||
| // the logs, lighting them beneath itself, granting the log's xp plus a bonus for its help. | ||
| for (familiar in listOf("pyrelord_familiar", "forge_regent_familiar")) { |
There was a problem hiding this comment.
forge_regent needs different animations
| } | ||
| // The extra hit re-fires this handler - the flag keeps it from chaining. | ||
| this["ferocious"] = true | ||
| anim("ferocious") |
There was a problem hiding this comment.
This special is meant to stun the enemy?
| import world.gregs.voidps.engine.Script | ||
| import world.gregs.voidps.type.random | ||
|
|
||
| class IronTitan : Script { |
There was a problem hiding this comment.
Iron/steel titans attack target doesn't clear after target death so if a familiar respawns you can call it and it'll re-engage the same target as before.
Summary
Fleshes out summoning familiars: talk dialogue, special moves for every familiar, foraging, skill boosts, passive abilities and overhead chat.
Talk dialogue & chatheads
<id>_combatform - 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:
Passive & other abilities
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.Target.attackable), not a Clan-Wars-only flag.Foraging
Engine fixes surfaced by this work
GraphicEncoder+FixGraphicstool, wired intoCacheBuilder) rotates the backwards-facing Toad Bark cannonball 180 degrees - the protocol itself has no projectile rotation.Overhead dialogue
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):engine:testand:game:testsuites greenReferences
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.java2009scape (animation/config reference): local checkout,
npc_configs.json