Skip to content

Fix evil twin, pheasant and random event graphic bugs - #1245

Open
HarleyGilpin wants to merge 11 commits into
GregHib:mainfrom
HarleyGilpin:bug/random-event-fixes
Open

Fix evil twin, pheasant and random event graphic bugs#1245
HarleyGilpin wants to merge 11 commits into
GregHib:mainfrom
HarleyGilpin:bug/random-event-fixes

Conversation

@HarleyGilpin

@HarleyGilpin HarleyGilpin commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #1227. Replaces #1233, whose branch picked up duplicate commits from an upstream rebase.

Evil Twin

With world.npcs.randomWalk on, the npc tick re-applies Wander over EmptyMode every tick, so a grabbed suspect strolled around mid-lift and wandered back out of the jail while the face-north flag was applied. WorldTest forces randomWalk off, which is why no test caught it. The grab, the drop at the jail and the re-entry spawn now use PauseMode, and the twin turns north on the same tick as the jailed animation, a tick after clearTransform, so the face update can't be lost to the transform or the walk's leftover facing.

New test runs the grab with randomWalk enabled and asserts the twin ends on the jail tile facing north.

Freaky Forester

  • Pheasants die to a single hit from any attack style, even when the accuracy roll misses, handled in Target.damageLimitModifiers alongside damage_cap and immune_death.
  • They also can't deal damage. Damage.maximum reads max_hit_melee off the npc definition ahead of the combat definition's range, so the inherited max_hit_melee = 10 had to go for the combat definition's target_hit of 0 to apply.
  • The defend anim was wrong because pheasants used the chicken combat definition, and the chicken anims (5387-5389) belong to the reworked chicken rig the pheasant model never got. Pheasants now have their own combat definition with the old bird rig anims (2371-2373) and pheasant sounds (690-692).

New test forces every accuracy roll to fail and asserts one swing kills the pheasant and drops the raw bird; it fails without the fix.

Random event puff of smoke

Adds a shared eventHerald for the spawn and poof for the removal, and routes every event through them. poof spawns the graphic on the npc's tile rather than flagging it on the npc, the way Imp already does, so removal can't swallow it.

New test asserts the graphic is still flagged on the tick the npc reaches clients, since the spawn is queued a tick ahead of that.

Area exit handlers on the way into an event

Reported after the above: being taken by a random event out of Mort Myre kept the swamp decay running, and out of the barrows crypts kept the barrows overlay on screen for the whole event.

Movement.move subtracted the player's instance_offset from both ends of a move. The six events that build an instance set that offset before the teleport that carries the player in, so the from tile - still real map - was offset into a tile the player was never on, and no area they actually left fired its exit handler. Each end is now mapped on its own, and only when it is inside the block of regions instances are allocated from.

Fight cave was relying on that asymmetry. Its exit tile sits inside tzhaar_fight_cave_multi_area, so leaving on foot is not an area change and never was one; the instance end just used to be compared against a meaningless tile. It now tears the instance down in leave, and the exit handler keeps the logout path it always had.

Swamp decay had no exit handler at all and checked immunity ahead of the area, so a player carrying a blessed sickle out of the swamp kept the timer, the message and the shield graphic for the rest of their session however they left.

New tests kidnap a player out of the barrows crypts and out of Mort Myre and assert the overlay closes and the timer stops; both fail without the fix.

Fight cave progress lost on shutdown

World.shutdown despawned every player without marking them as leaving the world, so every area exit handler took its walked-out branch. Fight cave cleared fight_cave_wave and left the player standing on an instance tile, and the shutdown save persisted both - a restart mid-cave cost the run and logged the player back in inside a dead instance. Godwars killcounts and greegrees were dropped the same way.

logged_out is now set before the despawn, which is the flag all of those handlers already check. Logging out normally was never affected; AccountManager.logout sets it.

New test takes a player into the cave, shuts the world down and asserts the wave survives and the player is back on real map; it fails without the fix.

Flippa and Tilt chatheads

Both pinball trolls had no dialogue key, so NPCDialogue fell through to the default expression names. Those are the reworked human chathead set (expression_neutral1 = 9807 onwards, skeleton 2165); Flippa and Tilt share chathead model 3790, a 2005 troll head rigged to the pre-rework skeleton 82, so the frames tore it apart mid-dialogue.

They now use the old family (554-617, skeleton 82), which is the same set void already gives other pre-rework models - frogs, clay golems, etc.

Closes #1113

Pinball posts

The next post was drawn from all five, so it could land on the post that had just been tagged - which reads as the event having ignored the tag rather than as a new target. The post that was just lit is now excluded from the draw.

Tagging was also silent. The 634 cache carries this event's audio: 2278 is the pillar tag and 2279-2283 are one tone per pillar.

Tests now tag whichever post is flashing rather than assuming it never moves, plus a new case asserting the target never repeats across eight tags.

Evil Bob fishing spot pan

Sped up the camera pane and fixed a camera lock bug caused by moving after the last tick of the camera pane cutscene.

With world.npcs.randomWalk enabled the npc tick re-applies Wander over
EmptyMode, so a grabbed suspect strolled around mid-lift and wandered
back out of the jail before turning north. Use PauseMode for the grab,
after the jail walk and for the re-entry spawn, and turn the twin north
on the same tick as the jailed animation so the face update can't be
lost to the transform or the walk's leftover facing.
)

Pheasants now die to a single hit from any attack style, even when the
accuracy roll misses, via a new instant_death npc definition flag.
Replace the chicken combat definition with pheasant-specific anims and
sounds - the chicken anims belong to the reworked chicken rig, which
the pheasant model never got.
The instant_death npc param only ever applied to pheasants, so match on
the id directly instead. Pheasants also can't deal damage: Damage.maximum
reads max_hit_melee off the npc definition ahead of the combat
definition's range, so the inherited value had to go for the combat
definition's target hit of 0 to apply.
Event heralds spawned in with no graphic at all, and the two places that
did play one flagged it on the npc immediately before removing it.
NPCs.remove hides an npc for the rest of the tick, so the update task
drops every visual set on it that tick and the puff was never sent.

Adds shared eventHerald/poof helpers - the vanish graphic is spawned on
the npc's tile so removal can't swallow it - and routes every event's
herald spawn and despawn through them.
…n instance

Movement subtracted the player's instance offset from both ends of a
move. On the teleport into an instance the from tile is still real map,
so it was offset into a tile the player was never on and nothing they
actually left fired its exit handler: the barrows overlay stayed up and
the mort myre swamp decay timer kept running for the whole event.

Each end is now mapped on its own, only when it is inside the block of
regions instances are allocated from.

Fight cave relied on the old asymmetry to clean up: its exit tile is
inside tzhaar_fight_cave_multi_area, so leaving on foot is not an area
change and never was one - the instance end used to be compared against
a meaningless tile. It now tears the instance down in leave, and the
exit handler covers the logout path it always did.

Swamp decay also had no exit handler at all and checked immunity before
the area, so a player carrying a blessed sickle out of the swamp kept
the timer for the rest of their session.
World.shutdown despawned every player without marking them as leaving
the world, so area exit handlers ran their walked-out branch instead.
Fight cave cleared the wave and left the player on an instance tile, and
the shutdown save persisted both, so a restart mid-cave cost the run and
logged the player back in inside a dead instance. Godwars killcounts and
greegrees went the same way.

Sets logged_out before the despawn, which is what every one of those
handlers already checks for a logout.
Both trolls fell through to the default chathead expressions, which are
the reworked human head set (9807+, skeleton 2165). Their chathead model
3790 is a 2005 troll head rigged to the pre-rework skeleton, so those
frames tore it apart mid-dialogue.

Trolls belong to the old expression set (554-617, skeleton 82), which
void already exposes as the "old" dialogue family and already uses for
other pre-rework models such as frogs, clay golems and Thurgo.

Same rig mismatch as the pheasant defend animation earlier in this
branch, from the other direction.
The next post was drawn from all five, so it could land on the one just
tagged and read as the event having ignored the tag. The post that was
just lit is now excluded from the draw.

Tagging was also silent. The 634 cache has the event's audio at 2278
(pillar tag) and 2279-2283 (one tone per pillar); tagging plays the
first and lighting a post plays that post's tone, so the flashing post
can be found by ear.
The pan ran at the slowest camera rate the codebase uses and was held
for ten ticks, so it was still creeping when control came back. Doubles
the constant speed and halves the hold, which is what the hold was
before the pan was slowed down.

The lock itself was already there - the delay attribute makes every
input handler drop the player's clicks - but nothing asserted it, so
the cutscene test only proved that discarded input never resurfaces.
It now checks the lock is held mid-pan and released on the tick the
camera resets, which fails if the hold changes in either direction.

A queue can't be used for this: ActionQueue.noInterrupt() refuses to
run anything while a dialogue is open and the servant's hint line
stays up for the whole pan. Noted in showSpot so it isn't retried.
The lock was correct for its duration - a minimap click decodes to the
same Walk instruction as a map click and was already dropped for every
tick of the hold - but it ended on the same tick as clearCamera, so a
click made during the pan could land on the frame the camera snapped
back. The pan now stays locked one tick past the reset.

The pan's ease-in term was also left behind when the hold was halved.
Both camera terms are per-tick rates, so acceleration scales with the
hold the same way speed does; at 10 the camera was still travelling
when the reset arrived, which is what made the cutscene look like it
had handed control back early.

New test sends a minimap walk on every tick of the pan and asserts the
player hasn't moved, which is what none of the existing cutscene tests
covered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant