Feat: Add Summoning Pets#983
Open
HarleyGilpin wants to merge 78 commits into
Open
Conversation
GregHib
requested changes
May 15, 2026
- Drop IncubatorDefinitions; walk Tables/Rows for incubator eggs directly. - Rename incubator state varbits to list-format with "empty/incubating/finished" labels; per-region end timer now a tick-based clock via start/remaining. - Collapse pet item/npc handlers to single comma-separated ops; thin PetDefinitions to a lazy Tables wrapper. - Replace the pet_stats blob with per-pet hunger/growth/warn vars; add DoubleValues so 0.0 defaults prune correctly. - Open pet_details (663) instead of familiar_details for pet followers; dismiss handler now globs *_details. - Stop the kitten in place when the Stroke option is used.
Contributor
Author
|
Review feedback addressed in 1b3959f. Per-comment notes:
|
The pet_details (663) call/dismiss buttons emit cache option labels
distinct from familiar_details (662) ("Call Follower" / "Dismiss
Familiar" / "Dismiss Now"). The existing handlers keyed off those
exact strings and silently no-op'd on pets.
Wildcard the option label (`*:*_details:call` / `dismiss`) and branch
on follower-vs-pet inside. Pet dismiss always calls pickupPet so the
item is returned to inventory; familiar dismiss keeps the
confirm-vs-immediate distinction.
Both pet_details:dismiss and the summoning_orb's right-click Dismiss now call dismissPet, which removes the NPC and clears pet state without putting the item back in inventory — the pet runs free. The pet_details path wraps it in an "Are you sure you want to release your pet?" confirmation since the item is gone for good; the orb's right-click path stays a one-click action (matches the familiar "Dismiss Now" semantics).
If the player had a minimap walk queued when they selected Stroke, the player kept moving during the dialogue suspension while the kitten sat in EmptyMode. When Follow was restored at the end of the interaction the kitten had to sprint several tiles to catch up. Clear the player's pending steps and face the kitten at the start of the interaction so the player stays put for the animation.
PR GregHib#980 deleted softQueue; replace the three pet/kitten call sites with weakQueue (closest semantic match: easily-preempted fire-and- forget) and drop the leftover `player.` prefixes inside the lambdas since Player is now the lambda receiver. While in KittenInteract, fix two bugs in chaseVermin: - The cat used to chase pirates because "pirate" contains the substring "rat". Predicate now matches the npc string id with a `_` word boundary (rat / *_rat / rat_* / *_rat_*). - The cat never caught anything because the 5-tick callback always fired the failure message. Roll a 33% catch chance up front; on success despawn the rat and message the win. Adjacency guard covers the case where the rat wanders before the kitten arrives.
Pre-chase dialogue runs before the kitten engages: player asks if it wants to hunt, kitten replies, player tells it to take it easy. On success the wiki "Hey well done puss, you got it!" / kitten "MeeeoooooW!" pair fires, and every 10th catch logs the milestone "Well done puss! N horrible rodents caught!". Caught count persists on the player via pet_rats_caught.
Branches every cat interaction on whether the player is wearing a catspeak amulet and whether the pet is a kitten or adult cat. Without amulet (kitten + adult cat): - Stroke narrates "You softly stroke your cat." + "Purr...purr..." + "The cat turns on its side while you bend down to pet it." - Chase vermin keeps the existing 3-line pre-chase chatter and the wiki success/fail/milestone copy. - Shoo away gains the wiki "Are you sure you want to shoo away the cat?" confirmation + "You choose not to shoo away the cat." cancel message. - Talk-to plays the simple "Hey puss! Any news?" / Purr / Meow lines. - "There aren't any vermin around." message when no rat in scan radius. With catspeak amulet (adult cat only): - Stroke prepends Player "Who's a good cat then?" / Cat "Me, me. Scratch me behind the ears." chathead exchange. - Chase vermin uses the wiki adult opener "Go on get that nasty rodent." / Cat "Yesss, food." - Talk-to opens a 4-option chathead loop (how are you / how old / where to / what to do) with a quit option. - Pick-up plays "Come here furball." / Cat "Can we go adventuring together again, soon?" / Player "Soon, I promise." before the item returns to the inventory. - Drop/Release plays "Hey cat, do you fancy stretching your legs..." / Cat "Miaaow, Are we going adventuring?" / "We'll see puss, we'll see." before summoning. KittenInteract registers Interact-with against every cat-like npc (baby, grown, overgrown) so the menu is reachable past kittenhood. PetScripts in Pet.kt routes the Pick-up / Drop / Release / Talk-to operate handlers to the catspeak variants when the conditions match, falling back to the generic pet handlers otherwise.
Three full-sentence lines were being emitted as overhead chat when they should have been chathead dialogue: - Cat's pre-chase reply "Meoowww. Yeah! Let's go kick some fur!" was cat.say (overhead bubble); now an npc<Happy> chathead line. - Player's success "Hey well done puss, you got it!" and the every- 10th-catch "Well done puss! N horrible rodents caught!" were player overhead; now player<Happy> chathead. Short imperative / sound-effect lines (Go on puss..., Shoo cat!, Meeeoooooowwww!, Eek!, MeeeoooooW!) stay as overhead — those belong above the speaker's head.
- Catspeak "Hey cat, do you fancy stretching your legs..." exchange now plays for every cat life stage (including kittens), not only the adult/overgrown items. - Dropping a cat without the amulet now plays a "Miaow!" overhead one tick after the spawn settles (the new NPC is wired in summonPet's own weakQueue at +2; we fire at +3 so pet?.say targets the freshly summoned cat). Folded the duplicated Drop/Release item-option bodies into a single suspend helper Player.dropPet to keep the branching in one place.
Splits the chase-vermin weakQueue into a +4 pounce tick and a +5 resolve tick. When the cat has reached the rat (adjacency check) it plays the new pet_pounce_kitten anim, then a tick later restores Follow and resolves the catch/miss outcome. The 9168 anim id is a best guess on the kitten anim grouping (stroke is 9173) and may need swapping for one of 9167/9169-9172 after a visual check in-game.
PR GregHib#799 added a Double->Int*10 conversion in PlayerSave's variables load branch to migrate old fractional XP saves into the new int experience format. The check was unconditional, so every Double variable got collapsed on load — including pet_*_hunger and pet_*_growth (declared format = "double"), which were turning into ints after every restart and then defaulting back to 0.0 on the next get<Double>() because Int can't be smart-cast to Double. Gate the conversion on VariableDefinitions.get(key)?.values being IntValues so it only fires for keys the current schema actually expects to be ints. Doubles for genuinely-double-typed vars now round-trip verbatim, which lets pet hunger / growth persist across server restarts.
GregHib
requested changes
May 17, 2026
The chase resolve was wrapped in nested weakQueues, which the new ActionQueue clears every time any Strong action enters the queue (clicking another NPC/object, taking a hit, teleporting). When the resolve dropped, the cat was left in EmptyMode and reverted to its default idle/hunt wander. Swap the two weakQueue calls in chaseVermin for queue (Normal priority). Normal queues sit in the main queue list, only weakQueue gets cleared on Strong, so the Follow restoration always fires — even after a mid-chase interruption.
Merges upstream/main (PR GregHib#984 brings inc(max) and skill drop gating). Engine reverts per Greg: - Delete DoubleValues from VariableValues, plus its factory mapping. - Remove both legacy double migrations from PlayerSave (variables loop and the experience fractional path). Doubles are not used in any player variable in the new schema. Pet stats moved to integers on a 0..10000 scale: - pet_state.vars.toml regenerated with format = "int". - pets.tables.toml swaps growth_rate (double) for growth_per_tick (int = rate * 50 * 100 per 30s tick). - PetState drops the PetStats class and updatePetStats wrapper; the getters return Int, callers use inc(key, amount, max = PET_STAT_MAX) and dec(key, amount) directly (PR GregHib#984 added the max parameter). - PetTimers thresholds become 7500 / 9000 / 10000 on the new scale, HUNGER_BABY/GROWN become 125 / 90 per tick. PetFeeding feeds 1500. - sendPetDetailsStats divides by 100 on the way out so the orb bars stay on the 0..100 client scale. PetDefinitions deleted; data lives in Tables now. The PetDefinition data class is gone too. Pets.kt adds RowDefinition extensions (isCatLike, stageForItem/Npc, npcFor/itemFor, nextStageItem/Npc, isFinalStage, ambientPhrases) plus petRowForItem / petRowForNpc and allPetRows lookup helpers. Every caller (Pet, KittenInteract, PetTimers, PetFeeding) now consumes RowDefinition directly. The Koin singleton in GameModules is gone. Incubator suffix derivation collapses to it.target.id.removePrefix("incubator_"). Renamed the per-region base objects to incubator_taverley (28550) and incubator_yanille (28352); kept the shared incubator_idle (28336) / incubator_active (28359) transform names so the dispatch finds a registered string id for the displayed mesh. target.id always reflects the base, so the suffix extraction never sees idle/active. The regionVarbits map and the tile.region.id helper are deleted. KittenInteract: - isRat simplifies to id.startsWith("rat") so giant_rat / warped_rat drop out of the chase pool. - talkToCatWithAmulet replaces the while(true) + keepGoing flag with Greg's recursive-option pattern: each answer recurses back into talkToCatWithAmulet, the quit option has no body. pet.npcs.toml: every options = { ... } map deleted; npcs only need the id field. dragon.drops.toml: black dragon egg gated on skill = "summoning", equals = 99 (PR GregHib#984 syntax). Tests adapted: PetLogoutTest stats are ints; IncubatorUseEggTest uses incubator_taverley for the test fixtures.
The previous version swapped the cat into EmptyMode for the duration of the dialogue and restored Follow afterwards. Restoring Follow recalculates against player.steps.follow immediately, which queues a step toward the player's last footprint tile and the cat ends up walking a tile out then back in. Use the movement_delay clock to suspend the cat's movement for the length of the dialogue instead, then clear the clock plus any queued steps at the end. Follow mode stays active throughout, so when the clock releases there is nothing for the recalculation to fight and the cat stays where it was being stroked.
Both pet shop owners (npc ids 6892 and 6893) share the same dialogue tree per the wiki, so a single PetShopOwner script handles both with one npcOperate matcher. Main menu offers four options: open the pet shop (pet_shop), buy a puppy, ask about other available pets, and sell spirit shards. Puppy purchase guards against owning a dog already, runs the wiki exchange about the 500 gold price, then either takes the coins and hands over a default colour puppy or backs out gracefully on no inventory space or insufficient coins. Six breeds are wired (Bulldog / Dalmatian / Greyhound / Labrador / Sheepdog / Terrier). The available-pets branch reproduces the wiki tree verbatim covering nuts, birds + incubator, Karamja lizards, geckos / raccoons and the banana-in-the-trap monkey tip. Spirit shard sale uses intEntry for the count, drops the shards and credits 25 coins each, with a graceful early-out when the player has none on them. Chathead expressions picked per line: Quiz for the player asking questions, Neutral when accepting / refusing, Happy and Pleased for the shop owner's friendlier or proud explanations.
dialogue_pick_a_puppy in summoning.ifaces.toml now declares the six breed components (.bulldog 3, .dalmatian 4, .greyhound 5, .terrier 6, .sheepdog 7, .labrador 8). Ordering taken from 2009scape's PuppyInterfacePlugin click map. PetShopOwner opens the iface instead of running a kotlin choice() list for breed selection, then suspends on pauseInt() and resumes against the clicked component's index. continueDialogue handlers per breed component resolve the suspension. Selecting a breed runs the existing buyPuppy flow (price exchange, coin / inventory checks, puppy item handed over). BREEDS order rearranged to match the iface index map so BREEDS.getOrNull(index) lands on the correct breed.
The cache buttons on iface 668 don't ship the continue-dialogue packet, so the existing continueDialogue handlers never received the click and the suspended caller stayed parked. Register each breed component under interfaceOption as well so the InteractInterface packet path also resumes the pauseInt; the continueDialogue registration stays as a fallback for any client variant that does ship the dialogue packet.
The option<Quiz>() inline form echoes the option text as a player chathead before invoking the block. spiritShards itself doesn't say that line, but the choice menu shows the question and then the player chathead repeated it a second time. Use the plain option() form for the shards entry so the player chathead echo is skipped and the conversation flows menu -> npc reply -> intEntry (or refusal).
ActionList.remove nulls the action's next pointer, so the previous sweep loop exited after the first removal. Cache next before remove so all matching actions are dropped in one call.
Previously the hunger/growth timer was started inside a 2-tick weakQueue alongside the interface open. A strong-priority action in those two ticks would wipe the weakQueue, leaving the pet with no timer at all. Start the timer immediately and keep only the interface open on a normal queue, which survives strong actions.
dropPet played the catspeak flavour dialogue before validating the summoning level / existing follower checks, so a player who could never summon the pet had to sit through three chathead lines just to receive the failure message. Run a precondition check first and fall straight through to summonPet's normal failure path when it fails.
petRowForItem and petRowForNpc previously did a linear firstOrNull across the full pets table on every call, and the hot paths (timer ticks, Interact-with, Talk-to, feed, drop) hit them repeatedly. Build a Map<String, RowDefinition> for each axis on first access and reuse it for the rest of the process.
ActionQueue.logout previously force-resumed every active Suspension type, including Custom whose predicate is what gates the await condition (e.g. awaitDialogues waiting on dialogue == null). Force resuming makes the suspended coroutine continue as if the condition were satisfied, executing dialogue-dependent logic on a player who is logging out. Only resume Custom when its predicate reports ready and break out of the fast-forward loop otherwise.
Despawn/Spawn handlers fired inside FloorItems.run() can call FloorItems.remove/add, which append to the same queue being iterated -> ConcurrentModificationException crashes the game loop. Snapshot-and-clear each queue before iterating so re-entrant entries land on the live queue and get processed next tick, preserving the existing clear-after-loop semantics.
The chase resolves five ticks after the verminc scan. If the rat dies or despawns in that window, NPCs.indexed at the same slot might now hold a freshly-spawned NPC that has nothing to do with this chase. Snapshot the rat's index up front and verify identity on both the pounce and resolve callbacks before pouncing on / removing the captured NPC.
Previously summonPet committed pet/active_item state, then the caller (dropPet) did inventory.remove afterwards. Anything between those two commits could leave a player with active_item set but the inventory item still in hand. Move the remove inside summonPet so the spawn aborts if the item cannot be consumed, mirroring the atomic pattern in completePuppySale.
The state varbit only carries the values empty and incubating; finished is computed from remaining time, never persisted. The previous playerSpawn unconditionally forced state back to incubating, justified by a stale comment about an older save format. Gate the set on isFinished so a finished slot keeps the state value it logged out with and the comment matches reality.
Both Math.random() calls in pet code (kitten chase catch and timer ambient chatter) used the global JVM random, which can't be seeded or replaced from tests. Switch to world.gregs.voidps.type.random to match the rest of the codebase and let tests inject a deterministic Random via setRandom.
PetState.clearPetStats already uses clear() for all the per-pet
counters; pickupPet and dismissPet were inconsistently using
set("pet_active_item", "") to represent the same idea. Switch
both to clear() so deactivation flows match a single convention.
completePuppySale and spiritShards both refund the player when the follow-up add fails, but if that refund also fails (inventory state shifted between the original check and the rollback) the player silently loses coins or shards. Log a warning with the account name and surface a "contact staff" message so the loss is recoverable out-of-band.
The (growth shl 1) or (hunger shl 9) layout matches a specific client-side varp; the magic shifts were silently fragile to anyone reading the function in isolation. Name the varp, point to the TOML that documents it and explain why bits 0 and 8 are unused.
Investigation: ItemDestroy registers itemOption(\"Release\") as a catch-all wildcard that opens a destroy-confirm dialog. The cache wires pet items to \"Release\" (not the default \"Drop\"), so the pet-specific registration takes precedence and routes the click straight into dropPet instead of the destroy confirm. Drop is kept for the handful of pet items that still expose it. Document the non-obvious dispatch interplay so a future cleanup doesn't trim either registration as dead code.
talkToCatWithAmulet and talkToHellcatWithAmulet previously re-called themselves at the end of every option body, so a long catspeak conversation grew an unbounded coroutine stack. Replace the self-call with a while-loop that exits when the player picks the quit option. Same player-visible behaviour, flatter control flow.
A pet growing into its next stage kept the hunger value and warn counter from its previous stage. If a kitten reached the starving threshold and immediately metamorphosed to grown, the warn level stayed maxed out, so the new stage's hungry / starving messages could never re-fire when their thresholds were crossed again. Clear both vars on metamorphose so the new stage gets a fresh warning ladder. Growth is already zeroed by the caller; loneliness only applies to kittens so it stops naturally.
Register a direct Shoo away npcOperate handler for pet_clockwork_cat_baby. Opens an "Are you sure?" confirm and runs dismissPet on yes with clockwork-flavoured "Whir..." / "winds down and stops" lines so the toy is released (destroyed) rather than returned to inventory.
The cache exposes the option as "Shoo", not "Shoo away", so the previous registration never matched any menu click. Rename the npcOperate string to match.
Drop the "Shoo!" / "Whir..." overhead lines and simplify the confirm prompt to "Are you sure you want to release your pet?" with Yes / No answers.
Once a pet has reached its final stage, the X% growth label on iface pet_details (663) is meaningless. Register iface component 17 as growth_percentage and override its text with "NA" via interfaces.sendText whenever sendPetDetailsStats runs for a final- stage pet. The packed varp bar is sent at 100% so the underlying visual stays consistent with the textual sentinel.
Component 17 on iface pet_details has a stateChange CS2 handler (script 820) bound to varp 1175 that re-formats the growth text as "X%" whenever the varp updates. Sending interfaces.sendText in the same tick as the varp send gets clobbered by the CS2, so the label kept showing 100%. Defer the "NA" text override by one tick via queue so it lands after the CS2 has finished its refresh.
Reverse-engineered CS2 (script 753 on iface 662, same pattern on script 820 / iface 663) checks growth and hunger varbits for the literal value 101 and renders "NA" instead of "X%" when it sees the sentinel. Send 101 in the growth bits of the packed pet_details_stats varp when the pet is at its final stage and let the client format the label. Drops the deferred-sendText workaround and the growth_percentage component registration; both are now redundant.
2011 RuneScape pets did not shout an overhead phrase when fed —
feeding only emitted a chat message like "Your pet happily eats
the raw shark." Drop the row.stringOrNull("feed_phrase")?.say
call in PetFeeding and clear the now-dead feed_phrase entries
from pets.tables.toml (broav, penguin, giant_crab, vulture rows)
plus the schema declaration. Ambient idle_phrases and hungry_phrase
overhead text remain — those were authentic.
The Yes / No options used typed option<Quiz> / option<Sad>, which makes choice replay the option text as a player chathead line. The clockwork cat shoo should be a silent confirm — switch to untyped option so picking Yes / No goes straight to the action without the player saying the word back.
The owner's "Where are you going to put it, on your head?" line already lived in completePuppySale, but by that point the player had already picked a breed and sat through the price haggle. Mirror the existing check at the top of puppyTree (right after the alreadyHasDog gate) so a player without inventory space gets the brush-off immediately, before the breed picker even opens. The late check in completePuppySale stays as a defensive guard against state shifts during the multi-tick dialogue.
talkToPet passed npc: lines verbatim, so data like "Honk! (Hello!)" rendered on a single chathead line with the translation jammed against the bark. Insert a newline before the opening paren so the npc() splitter routes bark to line1 and the translation to line2. Mirrors what DogTalk's renderDogLine already does for dogs, applied here to every non-dog pet that uses the parenthese translation convention.
The npc / player chathead helpers used to skip the font-width wrap whenever the input contained \n, on the assumption that the caller had already laid out the lines. That was fine until pet talk started inserting \n before the parenthese translation — suddenly long translations overflowed the chathead because they no longer ran through splitLines. Wrap each chunk after splitting on \n so callers get both their hard breaks and per-chunk word wrapping. The chunked-into-4-line-chathead fallback handles any case where wrapping pushes the total over the 4-line limit.
This reverts commit 9125420.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add pets system
Summary
Adds the pets system to Void. Pets are non-combat follower NPCs that follow the player, grow over time, get hungry, can be fed, and can be talked to. A player can have one follower at a time (pet or combat familiar, not both).
Features
pet:render as overhead speech; the rest as chatbox statements.Notable files
game/src/main/kotlin/content/skill/summoning/pet/: new package containing the registry, lifecycle, timers, feeding, incubator, dialogue, and persistence helpers.game/src/main/kotlin/content/skill/summoning/Summoning.kt: pet/familiar mutual exclusion and orb-option routing.game/src/main/kotlin/content/area/misthalin/varrock/Gertrude.kt: Minimal dialogue for Gertrude.data/skill/summoning/pet/: pet registry, incubator egg registry, NPC and item string-id overrides, incubator object string-ids, pet/incubator variable definitions.data/skill/summoning/summoning.ifaces.toml,data/entity/player/modal/toplevel/gameframe.ifaces.toml,data/skill/summoning/summoning.varps.toml: missing interface option and varp declarations.engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerSave.kt: bumps the save reader buffer for the pet stats blob.game/src/test/kotlin/content/skill/summoning/pet/: regression tests covering drop-to-summon, logout persistence, mutual exclusion, ownership, overhead chat, Talk-to dialogue, incubator flow, and orb/Follower Details routing.Test plan