Skip to content

Feat: Add Summoning Pets#983

Open
HarleyGilpin wants to merge 103 commits into
GregHib:mainfrom
HarleyGilpin:feat/pets
Open

Feat: Add Summoning Pets#983
HarleyGilpin wants to merge 103 commits into
GregHib:mainfrom
HarleyGilpin:feat/pets

Conversation

@HarleyGilpin
Copy link
Copy Markdown
Contributor

@HarleyGilpin HarleyGilpin commented May 15, 2026

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).

image

Features

  • 85 pet variants registered through a single TOML registry: kittens, cats, hellcat, six dog breeds (three colours each), four baby dragons, penguin, raven, ten monkey colours, squirrel, vulture, chameleon, giant crab, gecko, platypus, raccoon, godbirds, broav, parrot, clockwork cat, and phoenix egglings.
  • Spawning & interaction: Dropping a pet item spawns it next to the player and starts it following. Right-click exposes Pick-up (returns the item to inventory) and Talk-to (plays dialogue). Ownership is enforced. Drop, pick-up, and feed play the climb-down animation.
  • Hunger & growth: Tick every 30 seconds per pet. Feeding the correct food lowers hunger by 15. Max hunger causes the pet to run away; max growth triggers metamorphosis to the next life stage. Phoenix egglings, clockwork cat, broav, and ex-ex-parrot intentionally skip ticking.
  • Chat: Occasional overhead idle chatter, hungry-threshold phrases, and Talk-to dialogue, all configured per pet. Lines prefixed pet: render as overhead speech; the rest as chatbox statements.
  • Summoning orb: Activates for pets the same way as familiars. Left-click options (Follower Details, Call Follower, Dismiss) route to whichever slot is occupied. Follower Details shows name, chathead, and packed growth/hunger bars.
  • Persistence: Active pet item and a compact per-pet stats blob persist on the player. On login the pet respawns at the player's tile and resumes ticking.
  • Incubators: Taverley and Yanille incubators accept eggs, transform visually while incubating, and hatch into the matching baby pet after the configured time. Incubator state persists across logout.

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

  • Drop a pet item; confirm it spawns and follows.
  • Right-click Pick-up; confirm the item returns to inventory.
  • Right-click Talk-to; confirm dialogue plays for several pet types.
  • Second player tries to Pick-up your pet; confirm refusal.
  • Try to summon a familiar while a pet is active (and vice versa); confirm only one follower at a time.
  • Feed a pet its correct food; confirm hunger drops and the climb-down animation plays.
  • Wait for hunger and growth ticks; confirm hungry-phrase overhead chat, idle chatter, run-away at max hunger, and metamorphosis at max growth.
  • Open the summoning orb with a pet active; confirm Follower Details, Call Follower, and Dismiss all work.
  • Log out with an active pet and an egg incubating; log back in; confirm both resume correctly.
  • Use each egg type on Taverley and Yanille incubators; confirm hatching into the matching baby pet.

Comment thread game/src/main/kotlin/content/skill/summoning/pet/Incubator.kt
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Incubator.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Incubator.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Incubator.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/IncubatorDefinitions.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Pet.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Pet.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/PetDefinitions.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/PetState.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/Summoning.kt
- 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.
@HarleyGilpin
Copy link
Copy Markdown
Contributor Author

HarleyGilpin commented May 17, 2026

Review feedback addressed in 1b3959f. Per-comment notes:

  1. regionVarbits removal: collapsed to a one-line tile.region.id -> suffix helper; cache transforms (28336/28359) are shared between sites so right-click target.id can't carry the region by itself.
  2. contains + restart: applied verbatim.
  3. Dead varbit-reset branch: dropped; playerSpawn now just variables.sends the state varbits.
  4. list-format varbit + clocks: incubator_state_* is format = "list" (empty/incubating/finished); incubator_end_* is a tick clock via start/remaining. Magic ints + System.currentTimeMillis() are gone.
  5. IncubatorDefinitions deletion: gone; reads via Tables.get("incubator_eggs").rows() / Rows.getOrNull("incubator_eggs.$id").
  6. Kitten stops during Stroke: steps.clear() + mode = EmptyMode for the animation, restored to Follow afterwards.
  7. variables.send only on playerSpawn: moved out of updatePetInterface.
  8. Comma-csv ops: one npcOperate per option, one itemOption per option; same for PetFeeding.
  9. PetDefinitions thinned: now a Koin singleton with a by lazy row list and three linear-search lookups. Full removal via naming convention needs a data rename pass first (pet_kitten/pet_cat/overgrown_cat are inconsistent, plus hellcat/clockwork-cat/eggling); happy to follow up.
  10. pet_stats blob replaced: per-pet pet_${id}_hunger/_growth/_warn vars declared in a generated pet_state.vars.toml; added engine-side DoubleValues so 0.0 defaults prune like int/bool.
  11. Interface 663: updatePetInterface opens pet_details; the dismiss handler in Summoning.kt now globs *_details:dismiss so the pet path inherits it.

HarleyGilpin and others added 13 commits May 16, 2026 22:21
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.
Comment thread data/entity/npc/monster/draconic/dragon.drops.toml Outdated
Comment thread data/skill/summoning/pet/pet.npcs.toml Outdated
Comment thread engine/src/main/kotlin/world/gregs/voidps/engine/data/PlayerSave.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/Incubator.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/KittenInteract.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/PetDefinitions.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/KittenInteract.kt Outdated
Comment thread game/src/main/kotlin/content/skill/summoning/pet/PetState.kt Outdated
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).
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.
summonPet now optionally runs a second has(skill, level) check
from row "secondary_skill" / "secondary_level" so sneakerpeeper
can require Dungeoneering 80 AND Summoning 80 — its authentic
double gate. The secondary check stays dormant for every other
pet because they leave both columns unset.

matchesPetCondition gains a "skill_below:<skill>:<level>" branch
that fires when the player's level in the named skill is strictly
under the threshold. Wires up the adult sneakerpeeper "Summoning
< 91" gibberish line in upcoming pet_talks data.

Schema gains the two optional columns and sneakerpeeper's row
sets them to summoning / 80.
Adds row skeletons for the six pets shipped in db30ed60e so Talk-to
fires real conversations instead of the ambient idle_phrases
fallback. Each pet has at least one chathead row and one overhead
row; abyssal_minion has an unconditional whip-request row plus two
conditional rows that fire only when an abyssal whip is carried;
adult sneakerpeeper has a low-Summoning conditional row using the
new "skill_below:summoning:91" condition syntax.

Conversation skeletons are short by design and can be expanded with
fuller dialogue trees from the RuneScape Wiki transcripts at
https://runescape.wiki/w/Transcript:<Pet_name>.
NPCChatTest and PlayerChatTest's "Long line wraps" cases were
written against the 380-px font-split width. The dialogue helpers
now use 400 px, so an extra word ("wrapped") fits on the first
line. Update the expected line1 / line2 split point to match.
ImportPetTranscript fetches a Transcript:<Page_name> page from
runescape.wiki, parses the wikitext into pet_talks-style rows and
prints them to stdout. The user runs it and appends the output to
data/skill/summoning/pet/pet_talks.tables.toml themselves.

Heuristics:
- Top-level == Heading == sections become row groups.
- Sub-headings containing "overhead" or "random" mark overhead rows.
- "removed" / "hunger" / "starv" / "fed" sub-sections are skipped.
- "spawn"/"baby" or "adult"/"grown" in heading sets the stage field.
- "If ..." or comparison-bearing headings emit a # TODO condition hint
  for hand-editing.
- Wiki markup (links, bold, italic, html) is stripped from line bodies.
The five overhead-only pet_talks rows (creeping_hand_2, minitrice_2,
baby_basilisk_2, baby_kurask_2, abyssal_minion_overhead) were being
picked as Talk-to results, which is the wrong channel for overhead
bubble text. Dogs and cats let those phrases surface periodically
via the 30-second ambient roll instead, so do the same here: fold
each pet's overhead lines into its idle_phrases list in
pets.tables.toml and delete the now-redundant pet_talks rows. The
ambientChatter helper at PetTimers.kt picks one at random per tick
with the existing 12% probability.

Talk-to now picks only conversational chathead rows, matching the
behaviour of every other pet.
Replaces the single baby_basilisk_1 placeholder row with three
conversations the user supplied:

- baby_basilisk_eyes — "Why do my eyes sting?" / kill-with-eyes reveal.
- baby_basilisk_origin — serpent egg + chicken-or-egg exchange.
- baby_basilisk_mum — "Mum!" mistaken-identity loop.

Replaces the placeholder hungry_phrase with the tiered eye-burning
hungry_phrases list (hungry / starving / runaway), so the
tickHunger threshold crossings now use the proper escalating
overhead bark instead of the generic "Hisss...".
Replaces the single creeping_hand_1 placeholder row with three
rows reflecting the conversations the user supplied:

- creeping_hand_valiant — "hand it to you" / "hand-some" / "no wrist
  for the wicked" closer.
- creeping_hand_shake — handshake / "Gimme five" / out-of-hand.
- creeping_hand_truth — "hand-le the truth" puns.

Overhead lines were folded into idle_phrases in 61f36d0 already.
Replaces the single minitrice_1 placeholder row with three
conversations:

- minitrice_feathered_frog — "I minitrice" / "feathered frog" reveal.
- minitrice_deaded — the long "you deaded me" stare-prank arc.
- minitrice_dinner — "Me kill you" / hooman dinner closer.

Replaces the generic "Squawk!" hungry_phrase with the tiered
eye-burning hungry_phrases list (hungry / starving / runaway),
matching the escalation we use for baby_basilisk.
Replaces the single baby_kurask_1 placeholder with three rows for
the conversations the user supplied:

- baby_kurask_grunts — grunt-decoding game (pain / hungry / sleepy).
- baby_kurask_economy — economy + Ice Mountain "fascinating response".
- baby_kurask_ispy — I-spy with knave / knucklehead / kumquat guesses.

Overhead lines already live in idle_phrases from 61f36d0.
Replaces the four sneakerpeeper placeholder rows with the eight
conversations the user supplied across the two stages:

Baby (sneakerpeeper_spawn):
- sneakerpeeper_baby_collection — Player-skin-hat horror collection.
- sneakerpeeper_baby_floating — staring-contest-to-the-death message.
- sneakerpeeper_baby_origin — "Player-lips" creepy crush.
- sneakerpeeper_baby_songs — folk-rhyme + earmuffs threat. One song
  line paraphrased in-character rather than quoted verbatim.

Grown (sneakerpeeper):
- sneakerpeeper_grown_spawning — eyeball-squeezing spawning request.
- sneakerpeeper_grown_hairball — Thok's-eyelashes confession.
- sneakerpeeper_grown_hivemind — Lakhrahnaz / Haasghenahk ice puns.
- sneakerpeeper_grown_eyemax — one-eye depth-perception / monocles.

The low-Summoning conditional row from be5a945 stays — adult
sneakerpeeper still emits the garbled line when Summoning < 91.
Pets whose own NPC id has no entry in the client CS2 enum
pet_details_chathead_animations_normal (1276) fall back to the
generic defaultInt (8373), which is the wrong expression for
sneakerpeeper. The enum DOES contain entries for the in-dungeon
Sneakerpeeper NPC (cache id 22 -> anim 8463 normal / 8464 sad),
but our pet variants (13089 baby, 13090 grown) aren't keyed.

Adds two optional row columns:

- chathead_npc -> NPC string id whose cache enum entry the client
  CS2 should look up for the pet details panel chathead anim. The
  varbit follower_details_chathead_animation now writes this when
  set, otherwise the pet's own id as before.
- chathead_anim -> animation string id resolved server-side for
  chat dialogue chathead. Falls through to the enum lookup keyed
  by pet NPC id, then to enum default.

Sneakerpeeper's row sets chathead_npc = "sneakerpeeper_chathead"
(alias for cache NPC 22) and chathead_anim =
"expression_sneakerpeeper_normal" (alias for anim 8463). The two
anim aliases and the npc alias are added to pet.anims.toml /
pet.npcs.toml. Every other pet keeps its existing fallback path.
pet.anims.toml is for pet interaction animations (stroke, pounce
etc.). NPC chathead expressions live in dialogue_expressions.anims.toml
alongside expression_dog_*, expression_cat_*, expression_tree_*.
Move expression_sneakerpeeper_normal / _sad to where they belong;
no code changes needed since the lookup is by string id.
…nown

The chathead_npc / chathead_anim guesses from c623be2 picked anim
8463/8464 based on enum 1276 key 22, which turns out not to be the
correct animation for the pet sneakerpeeper variants. Until the
right value is identified, opt out of the override entirely.

Adds a chathead_disabled boolean row column. When true:

- updatePetInterface skips the follower_details_chathead_animation
  varbit set, leaving it at whatever the previous summon wrote (or
  default).
- talkToPet sends -1 to the chathead anim component, which the
  client treats as "clear / no animation".

Sneakerpeeper's row sets chathead_disabled = true and drops the
two override columns; the alias entries in pet.npcs.toml and
dialogue_expressions.anims.toml stay for future re-use once the
correct anim is identified.
…head anim

Two follow-ups for sneakerpeeper:

1. Wiki-sourced rows use the literal stand-in "Player" wherever
   the in-game line should show the player's display name (e.g.
   "Player find Sneak's collection, yes?"). Add a whole-word
   regex substitution in talkToPet that replaces "Player" with
   player.name on every line. Whole-word match so "Player-skin"
   / "Player-lips" / "Player," still substitute correctly without
   touching unrelated substrings.

2. Make chathead_disabled write -1 to the
   follower_details_chathead_animation varbit instead of skipping
   the set. Clear intent: explicitly request "no anim" on the
   panel until the correct value is identified, rather than
   inheriting whatever the previous summon wrote.
Writing -1 to follower_details_chathead_animation (varbit 4282)
didn't take effect: the underlying bit storage clamps the value
to a positive range, so the CS2 enum lookup still landed on the
generic defaultInt instead of "no anim".

Switch to a direct sendAnimation override on the chathead model
component, which exists on iface 662 (familiar_details, id 1) and
iface 663 (pet_details, id 3). Both are now named "chathead" in
summoning.ifaces.toml. When chathead_disabled is set we skip the
varbit path entirely (so CS2 doesn't fire a refresh) and push -1
straight onto the component instead.
The existing ::expr command sets a single chathead animation on
dialogue_npc_chat1 — useful when you already know the anim id you
want to view. For pets / NPCs without a named expression_* alias
we need to walk through a numeric anim range and identify the
right one visually.

::expr_scan <npc-id> <start-anim> <end-anim>

Opens the dialogue chathead, sets the given NPC's head model, and
cycles every anim id in [start..end]. Each step shows the current
anim id in the chathead title and waits on click-to-continue, so
the player can advance at their own pace and read off the id when
they see the right animation.

Mirrors ::expr's iface / packet shape so the chathead UX is
identical.
…open

The previous loop called open("dialogue_npc_chat1") on every
iteration, which (via Player.open's same-type close behaviour)
closed-and-reopened the dialogue iface between every anim step.
That packet churn appears to be what was crashing clients during
::expr_scan runs.

Open the iface once before entering the loop, then per-iteration
just push the head / anim / text / lines and pauseButton. Guard
the loop with an interfaces.contains check so a manual close of
the dialogue ends the scan cleanly with an informational message,
and wrap the body in try/finally so the iface always closes on
exit (range complete, early return, or exception).
Manual click-to-continue cycling kept crashing the client. The
continue button click triggers client-side iface teardown on the
same tick our coroutine resumes; the next loop iteration was then
firing npcDialogueHead + sendAnimation packets into an iface the
client was already winding down, which appears to have killed the
connection.

Replace pauseButton with a delay(tickDelay) (default 3 ticks,
~1.8s) so the dialogue stays open continuously and we just push
fresh anim + title every cycle. Send the head model once outside
the loop since it's stable across iterations. The 4th arg
(tick-delay) is optional so callers can slow the scan down for
finer inspection.
Identified via ::expr_scan: anim 8008 is the sneakerpeeper normal
expression, 8009 is the excited variant.

- Update the expression_sneakerpeeper_normal alias to 8008.
- Rename expression_sneakerpeeper_sad to _excited and set to 8009.
- Sneakerpeeper row drops chathead_disabled and sets chathead_anim
  = "expression_sneakerpeeper_normal".
- updatePetInterface gains a chathead_anim branch: when set, push
  the resolved anim id directly onto the chathead component via
  sendAnimation, bypassing the varbit / CS2 lookup entirely. Same
  approach the talkToPet path already uses.
- Drop the dead sneakerpeeper_chathead NPC alias — chathead_npc is
  no longer needed for sneakerpeeper now that chathead_anim covers
  it.
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