From d45191dc01094317ee74a306754d47c156c1866f Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Mon, 17 Aug 2026 18:50:37 -0700 Subject: [PATCH] Fix muddy skull being unobtainable after dying with it The rocks at the mining spot tracked whether the skull had been taken with the `rocks_restless_ghost` player var, and only reset it from a `playerDeath` handler that checked `ownsItem("muddy_skull")`. `Death.killed` runs synchronously before `dropItems`, so the skull was still in the inventory when that handler ran and the var was never reset. The skull then dropped to the gravestone and, once that expired, searching the rocks only replied "You already have the ghost's skull." - leaving the quest stuck at `found_skull` because the coffin requires the skull to be carried. Make ownership authoritative at search time instead: searching the emptied rocks now hands the skull back whenever the player no longer owns one, which covers death, manually dropping it and an expired gravestone alike. The ineffective death handler is removed; the `destroyed` handler stays so the rocks visually refill straight away. --- .../lumbridge/swamp/LumbridgeSwamp.kt | 73 ++++++++++--------- .../free/restless_ghost/RestlessGhostQuest.kt | 51 +++++++++++++ 2 files changed, 90 insertions(+), 34 deletions(-) diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/LumbridgeSwamp.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/LumbridgeSwamp.kt index f48593f4bf..645c0fbbce 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/LumbridgeSwamp.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/swamp/LumbridgeSwamp.kt @@ -8,6 +8,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.instruction.handle.interactPlayer import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.queue.queue @@ -20,47 +21,21 @@ class LumbridgeSwamp : Script { init { objectOperate("Search", "rocks_skull_restless_ghost_quest") { - if (quest("the_restless_ghost") != "mining_spot" && quest("the_restless_ghost") != "found_skull") { - message("There's nothing there of any use to you.") - return@objectOperate - } - if (inventory.isFull()) { - message("You can see the skull under the rocks, but you don't have enough space to carry it.") - return@objectOperate - } - statement("You take the skull from the pile of rocks.") - inventory.add("muddy_skull") - set("rocks_restless_ghost", "no_skull") - set("the_restless_ghost", "found_skull") - val index: Int? = remove("restless_ghost_warlock") - if (index != null) { - val skeleton = NPCs.indexed(index) - if (skeleton != null) { - NPCs.remove(skeleton) - } - } - message("A skeleton warlock has appeared.") - val warlock = NPCs.add("skeleton_warlock", Tile(3236, 3149), Direction.SOUTH, ticks = TimeUnit.SECONDS.toTicks(60), owner = this) - set("restless_ghost_warlock", warlock.index) - warlock.anim("restless_ghost_warlock_spawn") - val player = this - warlock.queue("delayed_attack", 4) { - warlock.interactPlayer(player, "Attack") - } + takeSkull() } objectOperate("Search", "rocks_no_skull_restless_ghost_quest") { if (quest("the_restless_ghost") == "completed") { message("There's nothing of any interest.") - } else { - message("You already have the ghost's skull.") + return@objectOperate } - } - - playerDeath { - if (!ownsItem("muddy_skull")) { - set("rocks_restless_ghost", "skull") + if (ownsItem("muddy_skull")) { + message("You already have the ghost's skull.") + return@objectOperate } + // The skull is no longer held (died with it, dropped it, or let the gravestone + // expire) so it's back under the rocks rather than being lost for good. + takeSkull() } destroyed("muddy_skull") { @@ -89,4 +64,34 @@ class LumbridgeSwamp : Script { ) } } + + suspend fun Player.takeSkull() { + if (quest("the_restless_ghost") != "mining_spot" && quest("the_restless_ghost") != "found_skull") { + message("There's nothing there of any use to you.") + return + } + if (inventory.isFull()) { + message("You can see the skull under the rocks, but you don't have enough space to carry it.") + return + } + statement("You take the skull from the pile of rocks.") + inventory.add("muddy_skull") + set("rocks_restless_ghost", "no_skull") + set("the_restless_ghost", "found_skull") + val index: Int? = remove("restless_ghost_warlock") + if (index != null) { + val skeleton = NPCs.indexed(index) + if (skeleton != null) { + NPCs.remove(skeleton) + } + } + message("A skeleton warlock has appeared.") + val warlock = NPCs.add("skeleton_warlock", Tile(3236, 3149), Direction.SOUTH, ticks = TimeUnit.SECONDS.toTicks(60), owner = this) + set("restless_ghost_warlock", warlock.index) + warlock.anim("restless_ghost_warlock_spawn") + val player = this + warlock.queue("delayed_attack", 4) { + warlock.interactPlayer(player, "Attack") + } + } } diff --git a/game/src/test/kotlin/content/quest/free/restless_ghost/RestlessGhostQuest.kt b/game/src/test/kotlin/content/quest/free/restless_ghost/RestlessGhostQuest.kt index fa194197fe..28018ff1c9 100644 --- a/game/src/test/kotlin/content/quest/free/restless_ghost/RestlessGhostQuest.kt +++ b/game/src/test/kotlin/content/quest/free/restless_ghost/RestlessGhostQuest.kt @@ -97,4 +97,55 @@ class RestlessGhostQuest : WorldTest() { assertEquals("completed", player.quest("the_restless_ghost")) assertEquals(1125.0, player.experience.get(Skill.Prayer)) } + + @Test + fun `Search the rocks again after losing the skull on death`() { + val player = createPlayer(Tile(3234, 3147)) + player["the_restless_ghost"] = "mining_spot" + + val rock = GameObjects.find(Tile(3234, 3145), "rocks_skull_restless_ghost_quest_base") + player.interactObject(rock, "Search") + tick() + player.continueDialogue() + + assertEquals(1, player.inventory.count("muddy_skull")) + assertEquals("no_skull", player["rocks_restless_ghost", ""]) + + // Die in the wilderness so the skull is dropped rather than kept, and never reclaimed. + player.tele(3100, 3550) + tick(2) + player.levels.set(Skill.Constitution, 0) + tick(10) + assertEquals(0, player.inventory.count("muddy_skull")) + + // The rocks now show as emptied but must hand the skull back rather than dead-end the quest. + player.tele(3234, 3147) + tick() + val emptyRock = GameObjects.find(Tile(3234, 3145), "rocks_skull_restless_ghost_quest_base") + player.interactObject(emptyRock, "Search") + tick() + player.continueDialogue() + + assertEquals(1, player.inventory.count("muddy_skull")) + assertEquals("found_skull", player.quest("the_restless_ghost")) + } + + @Test + fun `Rocks stay empty while still carrying the skull`() { + val player = createPlayer(Tile(3234, 3147)) + player["the_restless_ghost"] = "mining_spot" + + val rock = GameObjects.find(Tile(3234, 3145), "rocks_skull_restless_ghost_quest_base") + player.interactObject(rock, "Search") + tick() + player.continueDialogue() + assertEquals(1, player.inventory.count("muddy_skull")) + + player.interactObject(GameObjects.find(Tile(3234, 3145), "rocks_skull_restless_ghost_quest_base"), "Search") + tick() + + // No "You take the skull" statement, no second skull. + assertNull(player.dialogue) + assertEquals(1, player.inventory.count("muddy_skull")) + } }