From 3df25cd3a0a8752987643512f9d2058320cc3fa2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 28 Aug 2026 12:49:40 -0400 Subject: [PATCH 1/3] Add Pirate's Treasure quest Covers the full quest: Redbeard Frank's rum bargain, taking a job on Luthas' banana plantation to smuggle a bottle past the customs officer, Wydin's back room, Hector's chest in the Blue Moon Inn, and digging up the casket in Falador park. The shipped script referenced items and npcs that don't exist here (karamja_rum, piratemessage, pirate_casket, chest_key, redbeard_frank, luthas, customs_officer), so every id is resolved against the real definitions. Progress is stored as named varp stages like other quests rather than raw ints, which transmit as -1 through a map format. Wydin, Luthas and the customs officer already had scripts, so the quest extends those instead of duplicating them, reusing the existing banana_plantation_job and banana_crate_bananas variables for the crate. Karamjan rum is destroyed on every route off the island that this server implements - customs search, the charmed dance, magic teleports and the Shilo cart - and the routes that can't be covered yet are listed at the bottom of PirateTreasure.kt. Also fixed along the way: - The Pay-fare gate checked questCompleted("Pirate's Treasure"), a display name rather than the quest id, so with skipMissing enabled it always passed. Pay-Fare also called search(), which opens the fare dialogue and left the option hanging. - The handwriting message scroll opened as a wide screen interface rather than a screen one. - The Port Sarim seamen spawned as ids that transform through the unimplemented A Void Dance varbit, whose default maps to -1, leaving them with no Talk-to option. - Six npc definitions in the dungeoneering file duplicated ids 11700-11705, stealing them from the seamen and dispatching Lorris and Thresnor to the makeover mage. - Npc 1217 was missing its farmer's fork attack and defend animations and its own combat definition. Co-Authored-By: Claude Opus 5 (1M context) --- .../area/asgarnia/falador/falador.combat.toml | 11 + data/area/asgarnia/falador/falador.npcs.toml | 2 +- .../port_sarim/port_sarim.npc-spawns.toml | 6 +- data/entity/player/human.anims.toml | 6 + data/entity/player/modal/scrolls.ifaces.toml | 2 +- .../pirates_treasure.objs.toml | 9 + .../pirates_treasure.varps.toml | 2 +- .../pirates_treasure.vars.toml | 19 + .../area/asgarnia/port_sarim/RedbeardFrank.kt | 157 ++++++ .../area/asgarnia/port_sarim/Seaman.kt | 32 +- .../content/area/asgarnia/port_sarim/Wydin.kt | 145 +++++- .../area/kandarin/ardougne/CaptainBarnaby.kt | 4 + .../area/karamja/musa_point/CustomsOfficer.kt | 119 ++++- .../content/area/karamja/musa_point/Luthas.kt | 52 +- .../area/karamja/shilo_village/Vigroy.kt | 11 + game/src/main/kotlin/content/quest/Quest.kt | 1 + .../free/piratetreasure/PirateTreasure.kt | 474 ++++++++++++++++++ .../area/asgarnia/port_sarim/SeamanTest.kt | 59 +++ .../kandarin/ardougne/CaptainBarnabyTest.kt | 2 + .../karamja/musa_point/CustomsOfficerTest.kt | 4 + .../free/piratetreasure/PirateTreasureTest.kt | 401 +++++++++++++++ 21 files changed, 1480 insertions(+), 38 deletions(-) create mode 100644 data/quest/members/pirates_treasure/pirates_treasure.objs.toml create mode 100644 data/quest/members/pirates_treasure/pirates_treasure.vars.toml create mode 100644 game/src/main/kotlin/content/area/asgarnia/port_sarim/RedbeardFrank.kt create mode 100644 game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt create mode 100644 game/src/test/kotlin/content/area/asgarnia/port_sarim/SeamanTest.kt create mode 100644 game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt diff --git a/data/area/asgarnia/falador/falador.combat.toml b/data/area/asgarnia/falador/falador.combat.toml index f0a23d7609..59abf42cb7 100644 --- a/data/area/asgarnia/falador/falador.combat.toml +++ b/data/area/asgarnia/falador/falador.combat.toml @@ -101,3 +101,14 @@ range = 1 anim = "sword_slash" target_hit = { offense = "slash", max = 30 } +[gardener] +defend_anim = "human_farmersfork_def" +defend_sound = "man_defend" +death_anim = "human_death" +death_sound = "man_death" + +[gardener.melee] +range = 1 +anim = "human_farmersfork_stab" +target_sound = "farmer_attack" +target_hit = { offense = "stab", max = 10 } diff --git a/data/area/asgarnia/falador/falador.npcs.toml b/data/area/asgarnia/falador/falador.npcs.toml index 690cf3558a..be2820cb6f 100644 --- a/data/area/asgarnia/falador/falador.npcs.toml +++ b/data/area/asgarnia/falador/falador.npcs.toml @@ -672,7 +672,7 @@ examine = "An old gardener." id = 1217 hitpoints = 70 def = 7 -combat_def = "man" +combat_def = "gardener" respawn_delay = 50 drop_table = "bones" examine = "An old gardener." diff --git a/data/area/asgarnia/port_sarim/port_sarim.npc-spawns.toml b/data/area/asgarnia/port_sarim/port_sarim.npc-spawns.toml index e6835397d4..afc2fe3002 100644 --- a/data/area/asgarnia/port_sarim/port_sarim.npc-spawns.toml +++ b/data/area/asgarnia/port_sarim/port_sarim.npc-spawns.toml @@ -44,9 +44,9 @@ spawns = [ { id = "felkrash_rat_pits", x = 3016, y = 3234, members = true }, { id = "tool_leprechaun", x = 3064, y = 3255 }, { id = "redbeard_frank_port_sarim", x = 3049, y = 3253 }, - { id = "captain_tobias", x = 3028, y = 3216, members = true }, - { id = "seaman_lorris", x = 3028, y = 3221, members = true }, - { id = "seaman_thresnor", x = 3026, y = 3217, members = true }, + { id = "captain_tobias_1", x = 3028, y = 3216, members = true }, + { id = "seaman_lorris_1", x = 3028, y = 3221, members = true }, + { id = "seaman_thresnor_1", x = 3026, y = 3217, members = true }, { id = "squire_port_sarim", x = 3041, y = 3202 }, { id = "woman", x = 3011, y = 3236 }, { id = "security_guard_port_sarim", x = 3010, y = 3187, level = 1 }, diff --git a/data/entity/player/human.anims.toml b/data/entity/player/human.anims.toml index a853cd4aea..9b309e5bf1 100644 --- a/data/entity/player/human.anims.toml +++ b/data/entity/player/human.anims.toml @@ -37,6 +37,12 @@ id = 422 [human_block] id = 422 +[human_farmersfork_stab] +id = 433 + +[human_farmersfork_def] +id = 434 + [human_defend] id = 424 diff --git a/data/entity/player/modal/scrolls.ifaces.toml b/data/entity/player/modal/scrolls.ifaces.toml index 18d8d4aeb7..74e17f3510 100644 --- a/data/entity/player/modal/scrolls.ifaces.toml +++ b/data/entity/player/modal/scrolls.ifaces.toml @@ -20,7 +20,7 @@ id = 16 [message_scroll_handwriting] id = 222 -type = "wide_screen" +type = "main_screen" [.line0] id = "1-11" diff --git a/data/quest/members/pirates_treasure/pirates_treasure.objs.toml b/data/quest/members/pirates_treasure/pirates_treasure.objs.toml new file mode 100644 index 0000000000..32690f09a2 --- /dev/null +++ b/data/quest/members/pirates_treasure/pirates_treasure.objs.toml @@ -0,0 +1,9 @@ +[bluemoon_chest] +id = 2079 + +[bluemoon_chest_open] +id = 2080 + +[raw_chicken_crate] +id = 15032 +examine = "A wooden crate." diff --git a/data/quest/members/pirates_treasure/pirates_treasure.varps.toml b/data/quest/members/pirates_treasure/pirates_treasure.varps.toml index df69286439..dc7ac4941f 100644 --- a/data/quest/members/pirates_treasure/pirates_treasure.varps.toml +++ b/data/quest/members/pirates_treasure/pirates_treasure.varps.toml @@ -3,4 +3,4 @@ id = 71 persist = true format = "map" default = "unstarted" -values = { unstarted = 0, started = 1 } +values = { unstarted = 0, started = 1, chest_key = 2, read_message = 3, completed = 4 } diff --git a/data/quest/members/pirates_treasure/pirates_treasure.vars.toml b/data/quest/members/pirates_treasure/pirates_treasure.vars.toml new file mode 100644 index 0000000000..a234f4f389 --- /dev/null +++ b/data/quest/members/pirates_treasure/pirates_treasure.vars.toml @@ -0,0 +1,19 @@ +[pirates_treasure_wydin] +persist = true +format = "boolean" + +[pirates_treasure_stashed_rum] +persist = true +format = "boolean" + +[pirates_treasure_delivered_rum] +persist = true +format = "boolean" + +[pirates_treasure_collected_rum] +persist = true +format = "boolean" + +[pirates_treasure_spawned_gardener] +persist = true +format = "boolean" diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/RedbeardFrank.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/RedbeardFrank.kt new file mode 100644 index 0000000000..7db73f3b13 --- /dev/null +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/RedbeardFrank.kt @@ -0,0 +1,157 @@ +package content.area.asgarnia.port_sarim + +import content.entity.player.bank.ownsItem +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Laugh +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty +import content.entity.player.dialogue.Shock +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.item +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.inv.item.addOrDrop +import content.quest.questCompleted +import content.quest.questStage +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.male +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove + +class RedbeardFrank : Script { + + init { + npcOperate("Talk-to", "redbeard_frank_port_sarim") { + npc("Arr, Matey!") + when (questStage("pirates_treasure")) { + 0 -> unstartedMenu() + 1 -> broughtRum() + else -> completedMenu() + } + } + } + + private suspend fun Player.unstartedMenu() { + choice { + option("I'm in search of treasure.") { + treasureOffer() + } + option("Arr!") { + npc("Arr!") + } + option("Do you have anything for trade?") { + nothingToTrade() + } + } + } + + private suspend fun Player.completedMenu() { + if (questStage("pirates_treasure") < 3 && !ownsItem("chest_key_pirates_treasure")) { + player("I seem to have lost my chest key...") + npc("Arr, silly you. Fortunately I took the precaution to have another one made.") + addOrDrop("chest_key_pirates_treasure") + item(item = "chest_key_pirates_treasure", text = "Frank hands you a chest key") + } + choice { + option("Arr!") { + npc("Arr!") + } + option("Do you have anything for trade?") { + nothingToTrade() + } + } + } + + private suspend fun Player.nothingToTrade() { + npc("Nothin' at the moment, but then again the Customs Agents are on the warpath right now.") + } + + private suspend fun Player.treasureOffer() { + npc("Arr, treasure you be after eh? Well I might be able to tell you where to find some... For a price...") + player("What sort of price?") + npc("Well for example if you can get me a bottle of rum... Not just any rum mind...") + npc("I'd like some rum made on Karamja Island. There's no rum like Karamja Rum!") + if (questCompleted("rum_deal")) { // TODO + player("Would some Braindeath 'Rum' do?") + npc("Gadzooks, that swill! Not a chance!") + npc("It's Karamja Rum or no deal.") + } + choice { + option("Ok, I will bring you some rum.") { + set("pirates_treasure", "started") + npc("Yer a saint, although it'll take a miracle to get it off Karamja.") + whyItsHard() + } + option("Not right now.") { + npc("Fair enough. I'll still be here and thirsty whenever you feel like helpin' out.") + } + } + } + + private suspend fun Player.whyItsHard() { + player("What do you mean?") + npc( + "The Customs office has been clampin' down on the export of spirits. You seem like a " + + "resourceful young ${if (male) "lad" else "lass"}, I'm sure ye'll be able to find a way to slip the " + + "stuff past them.", + ) + player("Well I'll give it a shot.") + if (questStage("pirates_treasure") < 1) { + npc("Arr, that's the spirit!") + } + } + + private suspend fun Player.broughtRum() { + npc("Have ye brought some rum for yer ol' mate Frank?") + val plainRum = inventory.contains("karamjan_rum") + val slicedRum = inventory.contains("karamjan_rum_sliced_banana") + val bananaRum = inventory.contains("karamjan_rum_banana") + if (!plainRum && !slicedRum && !bananaRum) { + player("No, not yet.") + npc("Not surprising, tis no easy task to get it off Karamja.") + whyItsHard() + return + } + player("Yes, I've got some.") + when { + slicedRum -> rejectRum("karamjan_rum_sliced_banana", "Arr - I don't likes banana in me rum!") + bananaRum -> rejectRum("karamjan_rum_banana", "Arr - this here rum's got a banana stuck in it!") + else -> acceptRum() + } + } + + private suspend fun Player.rejectRum(item: String, complaint: String) { + if (!inventory.remove(item)) { + return + } + message("Frank happily takes the rum.") + delay(4) + npc(complaint) + message("Frank hands you back the rum.") + addOrDrop(item) + } + + private suspend fun Player.acceptRum() { + npc("Now a deal's a deal, I'll tell ye about the treasure. I used to serve under a pirate captain called One-Eyed Hector.") + npc("Hector were very successful and became very rich. But about a year ago we were boarded by the Customs and Excise Agents.") + npc("Hector were killed along with many of the crew, I were one of the few to escape and I escaped with this.") + if (!inventory.remove("karamjan_rum")) { + return + } + set("pirates_treasure", "chest_key") + addOrDrop("chest_key_pirates_treasure") + item(item = "chest_key_pirates_treasure", text = "Frank happily takes the rum... and hands you a key.") + npc("This be Hector's key. I believe it opens his chest in his old room in the Blue Moon Inn in Varrock.") + npc("With any luck his treasure will be in there.") + choice { + option("Ok thanks, I'll go and get it.") + option("So why didn't you ever get it?") { + npc("I'm not allowed in the Blue Moon Inn. Apparently I'm a drunken trouble maker.") + } + } + } +} diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Seaman.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Seaman.kt index 74a83b3d0b..011b8e47a6 100644 --- a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Seaman.kt +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Seaman.kt @@ -5,15 +5,19 @@ import content.entity.player.dialogue.Happy import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.player import content.entity.player.dialogue.type.statement +import content.quest.questCompleted import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot import world.gregs.voidps.type.Tile class Seaman : Script { @@ -28,24 +32,44 @@ class Seaman : Script { player("Oh dear, I don't seem to have enough money.") return@option } - travel() + travel("You pay 30 coins and board the ship.") + } + if (charmed) { + option("Or I could pay you nothing at all...") { + player("Or I could pay you nothing at all...") + npc("Mmmm, nothing at all, you say...") + npc("Yes, why not - jump aboard then.") + travel("You board the ship.") + } } option("No, thank you.") } } npcOperate("Pay-fare", "seaman_lorris*,captain_tobias*,seaman_thresnor*") { + if (!questCompleted("pirates_treasure")) { + return@npcOperate message("You may only use the Pay-fare option after completing Pirate's Treasure.") + } if (!inventory.remove("coins", 30)) { message("You do not have enough money for that.") return@npcOperate } - travel() + travel("You pay 30 coins and board the ship.") } } - private suspend fun Player.travel() { - message("You pay 30 coins and board the ship.") + private suspend fun Player.travel(boarding: String) { + message(boarding) boatTravel("port_sarim_to_karamja", 7, Tile(2956, 3143, 1)) statement("The ship arrives at Karamja.") } + + private val Player.charmed: Boolean + get() = equipped(EquipSlot.Ring).id == "ring_of_charos_a" } + +// TODO dragon slayer dialog +// I'd rather go to Crandor Isle. +// No I need to stay alive, I have a wife and family to support. + +// TODO diary cost diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Wydin.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Wydin.kt index 138d59ce5a..c84906c227 100644 --- a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Wydin.kt +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Wydin.kt @@ -1,35 +1,152 @@ package content.area.asgarnia.port_sarim import content.entity.npc.shop.openShop +import content.entity.obj.door.Door +import content.entity.obj.door.enterDoor +import content.entity.player.dialogue.Confused import content.entity.player.dialogue.Happy import content.entity.player.dialogue.Idle import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.questStage import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.ui.dialogue.talkWith +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import world.gregs.voidps.type.Tile class Wydin : Script { init { npcOperate("Talk-to", "wydin") { (target) -> + if (get("pirates_treasure_wydin", false)) { + npc("Is it nice and tidy round the back now?") + employedMenu(target) + return@npcOperate + } npc("Welcome to my food store! Would you like to buy anything?") - choice { - option("Yes please.") { - openShop(target.def["shop"]) + shopMenu(target) + } + + objectOperate("Open", "door_37_closed") { (target) -> + if (target.tile != SHOP_DOOR) { + Door.openDoor(this, target) + return@objectOperate + } + val entering = tile.x >= target.tile.x + if (!entering) { + enterDoor(target) + return@objectOperate + } + val wydin = NPCs.findOrNull(tile.regionLevel, "wydin") + when { + questStage("pirates_treasure") != 1 -> { + if (wydin != null) { + talkWith(wydin) + } + npc("Hey, you can't go in there. Only employees of the grocery store can go in.") + player("Sorry, I didn't realise.") + } + !get("pirates_treasure_wydin", false) -> { + if (wydin != null) { + talkWith(wydin) + } + npc("Hey, you can't go in there. Only employees of the grocery store can go in.") + doorMenu(wydin) } - option("No, thank you.") - option("What can you recommend?") { - npc("We have this really exotic fruit all the way from Karamja. It's called a banana.") - choice { - option("Hmm, I think I'll try one.") { - npc("Great. You might as well take a look at the rest of my wares as well.") - openShop(target.def["shop"]) - } - option("I don't like the sound of that.") { - npc("Well, it's your choice, but I do recommend them.") - } + equipped(EquipSlot.Chest).id != "white_apron" -> { + if (wydin != null) { + talkWith(wydin) } + npc("Can you put your white apron on before going in there, please?") } + else -> enterDoor(target) + } + } + } + + private companion object { + val SHOP_DOOR = Tile(3012, 3204) + } + + private suspend fun Player.doorMenu(wydin: NPC?) { + choice { + option("Well, can I get a job here?") { + jobOffer(wydin) + } + option("Sorry, I didn't realise.") + } + } + + private suspend fun Player.shopMenu(target: NPC) { + choice { + option("Yes please.") { + openShop(target.def["shop"]) + } + option("No, thank you.") + option("What can you recommend?") { + recommendation(target) + } + if (questStage("pirates_treasure") == 1) { + option("Can I get a job here?") { + jobOffer(target) + } + } + } + } + + private suspend fun Player.recommendation(target: NPC) { + npc("We have this really exotic fruit all the way from Karamja. It's called a banana.") + choice { + option("Hmm, I think I'll try one.") { + npc("Great. You might as well take a look at the rest of my wares as well.") + openShop(target.def["shop"]) + } + option("I don't like the sound of that.") { + npc("Well, it's your choice, but I do recommend them.") + } + } + } + + private suspend fun Player.jobOffer(target: NPC?) { + npc("Well, you're keen, I'll give you that. Okay, I'll give you a go. Have you got your own white apron?") + if (!inventory.contains("white_apron") && equipped(EquipSlot.Chest).id != "white_apron") { + player("No, I haven't.") + npc("Well, you can't work here unless you have a white apron. Health and safety regulations, you understand.") + player("Where can I get one of those?") + npc("Well, I get all of mine over at the clothing shop in Varrock. They sell them cheap there.") + npc("Oh, and I'm sure that I've seen a spare one over in Gerrant's fish store somewhere. It's the little place just north of here.") + return + } + player("Yes, I have one right here.") + set("pirates_treasure_wydin", true) + npc("Wow - you are well prepared! You're hired. Go through to the back and tidy up for me, please.") + if (equipped(EquipSlot.Chest).id != "white_apron") { + npc("You need to put your white apron on first though.") + } + } + + private suspend fun Player.employedMenu(target: NPC) { + choice { + option("Yes, can I work out front now?") { + npc("No, I'm the one who works here.") + } + option("Yes, are you going to pay me yet?") { + npc("Umm... No, not yet.") + } + option("No, it's a complete mess.") { + npc("Ah well, it'll give you something to do, won't it.") + } + option("Can I buy something please?") { + npc("Yes, of course.") + openShop(target.def["shop"]) } } } diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt index b437dca662..813c2627ca 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/CaptainBarnaby.kt @@ -9,6 +9,7 @@ import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.player import content.entity.player.dialogue.type.statement +import content.quest.questCompleted import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.player.Player @@ -35,6 +36,9 @@ class CaptainBarnaby : Script { } npcOperate("Pay-fare", "captain_barnaby_2") { + if (!questCompleted("pirates_treasure")) { + return@npcOperate message("You may only use the Pay-fare option after completing Pirate's Treasure.") + } if (!inventory.remove("coins", 30)) { message("You do not have enough money for that.") return@npcOperate diff --git a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt index 24206222fb..aec213f5c8 100644 --- a/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt +++ b/game/src/main/kotlin/content/area/karamja/musa_point/CustomsOfficer.kt @@ -1,20 +1,28 @@ package content.area.karamja.musa_point import content.entity.obj.ship.boatTravel +import content.entity.player.dialogue.Bored +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Laugh import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty +import content.entity.player.dialogue.Shock import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.player import content.entity.player.dialogue.type.statement +import content.quest.questCompleted import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot import world.gregs.voidps.type.Tile class CustomsOfficer : Script { @@ -24,18 +32,8 @@ class CustomsOfficer : Script { npc("Can I help you?") choice { option("Can I journey on this ship?") { - npc("Hey, I know you, you work at the plantation.") - npc("I don't think you'll try smuggling anything, you just need to pay a boarding charge of 30 coins.") - choice { - option("Ok.") { - if (!inventory.remove("coins", 30)) { - player("Oh dear, I don't seem to have enough money.") - return@option - } - travel(officer) - } - option("Oh, I'll not bother then.") - } + npc("You need to be searched before you can board.") + searchMenu(officer, askedWhy = false) } option("Does Karamja have unusual customs then?") { player("Does Karamja have any unusual customs then?") @@ -45,6 +43,12 @@ class CustomsOfficer : Script { } npcOperate("Pay-Fare", "customs_officer_brimhaven") { (officer) -> + if (!questCompleted("pirates_treasure")) { + return@npcOperate message("You may only use the Pay-fare option after completing Pirate's Treasure.") + } + if (confiscateRum()) { + return@npcOperate + } if (!inventory.remove("coins", 30)) { message("You do not have enough money for that.") return@npcOperate @@ -53,6 +57,97 @@ class CustomsOfficer : Script { } } + private suspend fun Player.searchMenu(officer: NPC, askedWhy: Boolean) { + choice { + if (!askedWhy) { + option("Why?") { + npc("Because Asgarnia has banned the import of intoxicating spirits.") + searchMenu(officer, askedWhy = true) + } + } + option("Search away, I have nothing to hide.") { + search(officer) + } + option("You're not putting your hands on my things!") { + npc("You're not getting on this ship then.") + } + if (charmed) { + option("[Charm] You don't need to look through my things.") { + player("You don't need to look through my things.") + npc("Oh, I think I do. If you don't want to let me search your possessions, then you're not coming aboard this ship.") + } + } + } + } + + private suspend fun Player.search(officer: NPC) { + if (inventory.contains("karamjan_rum")) { + npc("Aha, trying to smuggle rum are we?") + if (!charmed) { + confiscateRum() + return + } + choice { + option("Umm... it's for personal use?") { + confiscateRum() + } + option("[Charm] This is not the Karamja rum you are looking for.") { + player("This is not the Karamja rum you are looking for.") + npc("This is not the Karamja rum we are looking for.") + player("Well that was easy!") + loseRum() + } + } + return + } + npc("Well you've got some odd stuff, but it's all legal. Now you need to pay a boarding charge of 30 coins.") + choice { + option("Ok.") { + if (!inventory.remove("coins", 30)) { + player("Oh dear, I don't seem to have enough money.") + return@option + } + travel(officer) + } + option("Oh, I'll not bother then.") + if (charmed) { + option("[Charm] Or perhaps you could let me travel for free?") { + player("Or perhaps you could let me travel for free?") + npc("Yes, perhaps I could. Okay, jump aboard then.") + travel(officer) + } + } + } + } + + private suspend fun Player.confiscateRum(): Boolean { + if (!inventory.contains("karamjan_rum")) { + return false + } + player("Umm... it's for personal use?") + if (!inventory.remove("karamjan_rum", inventory.count("karamjan_rum"))) { + return true + } + message("The customs officer confiscates your rum.") + message("You will need to find some way to smuggle it off the island...") + return true + } + + private suspend fun Player.loseRum() { + message("You dance for joy...") + anim("emote_dance") + delay(4) + if (!inventory.remove("karamjan_rum", inventory.count("karamjan_rum"))) { + return + } + message("...and accidentally drop the rum.") + player("Drat...") + message("You will need to try again.") + } + + private val Player.charmed: Boolean + get() = equipped(EquipSlot.Ring).id == "ring_of_charos_a" + private suspend fun Player.travel(officer: NPC) { message("You pay 30 coins and board the ship.") if (officer.tile in Areas["brimhaven"]) { diff --git a/game/src/main/kotlin/content/area/karamja/musa_point/Luthas.kt b/game/src/main/kotlin/content/area/karamja/musa_point/Luthas.kt index 4f9ccd406d..1621003017 100644 --- a/game/src/main/kotlin/content/area/karamja/musa_point/Luthas.kt +++ b/game/src/main/kotlin/content/area/karamja/musa_point/Luthas.kt @@ -1,5 +1,6 @@ package content.area.karamja.musa_point +import content.entity.player.dialogue.Angry import content.entity.player.dialogue.Happy import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz @@ -9,6 +10,7 @@ import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.player import content.entity.player.dialogue.type.statement +import content.quest.questStage import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.player.Player @@ -33,8 +35,7 @@ class Luthas : Script { } if (get("banana_crate_bananas", 0) < CRATE_CAPACITY) { npc("Have you completed your task yet?") - player("No, the crate isn't full yet.") - npc("Well come back when it is.") + inProgress() return@npcOperate } player("I've filled a crate with bananas.") @@ -45,7 +46,13 @@ class Luthas : Script { } set("banana_crate_bananas", 0) set("banana_plantation_job", false) + if (get("pirates_treasure_stashed_rum", false)) { + set("pirates_treasure_stashed_rum", false) + set("pirates_treasure_collected_rum", false) + set("pirates_treasure_delivered_rum", true) + } message("Luthas hands you 30 coins.") + npc("If you go outside you should see the old crate has been loaded on to the ship, and there is another empty crate in its place.") } objectOperate("Search", "crate_14") { (target) -> @@ -57,12 +64,17 @@ class Luthas : Script { return@objectOperate } val bananas = get("banana_crate_bananas", 0) + val stashed = get("pirates_treasure_stashed_rum", false) when { + bananas == 0 && stashed -> message("There is some rum in here, although with no bananas to cover it. It is a little obvious.") bananas == 0 -> message("The crate is completely empty.") bananas >= CRATE_CAPACITY -> message("The crate is full of bananas.") bananas == 1 -> message("The crate has 1 banana inside.") else -> message("The crate has $bananas bananas inside.") } + if (stashed && bananas > 0) { + message("There is also some rum stashed in here too.") + } } objectOperate("Fill", "crate_14") { (target) -> @@ -92,6 +104,25 @@ class Luthas : Script { } } + itemOnObjectOperate("karamjan_rum", "crate_14") { (target) -> + if (target.tile != EXPORT_CRATE) { + return@itemOnObjectOperate + } + when { + questStage("pirates_treasure") == 0 -> message("Why would I want to do that?") + questStage("pirates_treasure") > 1 -> message("I see no reason to do that.") + get("pirates_treasure_stashed_rum", false) -> message("There's already some rum in here...") + !get("banana_plantation_job", false) -> message("I don't know what goes in there.") + else -> { + if (inventory.remove("karamjan_rum")) { + animDelay("take") + set("pirates_treasure_stashed_rum", true) + message("You stash the rum in the crate.") + } + } + } + } + itemOnObjectOperate("banana", "crate_14") { (target) -> if (target.tile != EXPORT_CRATE) { return@itemOnObjectOperate @@ -113,6 +144,23 @@ class Luthas : Script { } } + private suspend fun Player.inProgress() { + choice { + option("What did I have to do again?") { + npc("There's a crate ready to be loaded onto the ship. If you could fill it up with bananas, I'll pay you 30 gold.") + } + option("No, the crate isn't full yet...") { + npc("Well come back when it is.") + } + option("So where are these bananas going to be delivered to?") { + npc("I sell them to Wydin who runs a grocery store in Port Sarim.") + } + option("That customs officer is annoying isn't she?") { + customsOfficer() + } + } + } + private suspend fun Player.introduction() { choice { option("Could you offer me employment on your plantation?") { diff --git a/game/src/main/kotlin/content/area/karamja/shilo_village/Vigroy.kt b/game/src/main/kotlin/content/area/karamja/shilo_village/Vigroy.kt index 70fb649143..34cbdb2b06 100644 --- a/game/src/main/kotlin/content/area/karamja/shilo_village/Vigroy.kt +++ b/game/src/main/kotlin/content/area/karamja/shilo_village/Vigroy.kt @@ -56,6 +56,15 @@ class Vigroy : Script { const val CART_FARE = 10 + private fun Player.stealRum() { + val amount = inventory.count("karamjan_rum") + if (amount <= 0) { + return + } + inventory.remove("karamjan_rum", amount) + message("Oh no! On the trip a monkey swung out from the trees and stole your rum!") + } + suspend fun payFare(player: Player, tile: Tile) { if (!player.inventory.remove("coins", CART_FARE)) { player.npc("Sorry, but it looks as if you don't have enough money. Come and see me when you have enough for the ride.") @@ -63,6 +72,7 @@ class Vigroy : Script { } player.open("fade_out") player.delay(3) + player.stealRum() player.tele(tile) player.open("fade_in") player.message("You feel tired from the journey, but at least you didn't have to walk all that distance.", ChatType.Filter) @@ -76,6 +86,7 @@ class Vigroy : Script { player.statement("You hop into the cart and the driver urges the horses on. You take a taxing journey through the jungle to $location.", clickToContinue = false) player.open("fade_out") player.delay(3) + player.stealRum() player.tele(tile) player.open("fade_in") player.statement("You hop into the cart and the driver urges the horses on. You take a taxing journey through the jungle to $location. You feel tired from the journey, but at least you didn't have to walk all that distance.") diff --git a/game/src/main/kotlin/content/quest/Quest.kt b/game/src/main/kotlin/content/quest/Quest.kt index 55e5af9e40..297abf5d61 100644 --- a/game/src/main/kotlin/content/quest/Quest.kt +++ b/game/src/main/kotlin/content/quest/Quest.kt @@ -20,6 +20,7 @@ val quests = setOf( "the_knights_sword", "prince_ali_rescue", "imp_catcher", + "pirates_treasure", // members "creature_of_fenkenstrain", "druidic_ritual", diff --git a/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt new file mode 100644 index 0000000000..469c10be57 --- /dev/null +++ b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt @@ -0,0 +1,474 @@ +package content.quest.free.piratetreasure + +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.inv.item.addOrDrop +import content.quest.messageScroll +import content.quest.questComplete +import content.quest.questJournal +import content.quest.questStage +import content.quest.refreshQuestJournal +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.command.adminCommand +import world.gregs.voidps.engine.client.instruction.handle.interactPlayer +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.entity.character.jingle +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.replace +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.timer.toTicks +import world.gregs.voidps.type.Tile +import java.util.concurrent.TimeUnit + +class PirateTreasure : Script { + + init { + + adminCommand("reset_pirates_treasure", desc = "Reset Pirate's Treasure back to unstarted") { + resetQuest() + message("Pirate's Treasure reset.") + } + + questJournalOpen("pirates_treasure") { + val lines = when (val stage = questStage("pirates_treasure")) { + 0 -> notStartedJournal() + 4 -> completedJournal() + else -> startedJournal(stage) + } + questJournal("Pirate's Treasure", lines) + } + + itemOnNPCOperate("casket_pirates_treasure", "redbeard_frank_port_sarim") { + player("I have the treasure, would you like a share?") + npc("No lad, you got it fair and square.") + npc("You enjoy it. It's what Hector would have wanted.") + } + + objectOperate("Open", "bluemoon_chest") { + message("The chest is locked.") + sound("locked") + } + + itemOnObjectOperate("chest_key_pirates_treasure", "bluemoon_chest") { (target) -> + anim("open_chest") + sound("chest_open") + target.replace(id = "bluemoon_chest_open", ticks = 4) + message("You unlock the chest.") + message("All that's in the chest is a message...") + + delay(4) + + anim("close_chest") + sound("chest_close") + message("You take the message from the chest.") + inventory.remove("chest_key_pirates_treasure") + inventory.add("pirate_message") + } + + itemOption("Read", "pirate_message") { + messageScroll( + listOf( + "", + "", + "", + "", + "Visit the city of the White Knights. In the park,", + "Saradomin points to the X which marks the spot.", + ), + handwriting = true, + ) + if (questStage("pirates_treasure") == 2) { + set("pirates_treasure", "read_message") + } + } + + itemOption("Open", "casket_pirates_treasure") { + val needsSpace = if (inventory.contains("coins")) 1 else 2 + if (inventory.spaces < needsSpace) { + return@itemOption message( + "From the weight of this you can guess you need more inventory space to get at the contents.", + ) + } + sound("chest_open") + message("You open the casket, and find One-Eyed Hector's treasure.") + inventory.remove("casket_pirates_treasure") + inventory.add("gold_ring") + inventory.add("emerald") + inventory.add("coins", 450) + } + + itemOption("Dig", "spade") { + if (tile != DIG_SITE) { + return@itemOption + } + handleSpadeDig() + } + + objectOperate("Search", "crate_13") { (target) -> + if (target.tile != WYDIN_BANANA_CRATE) { + return@objectOperate + } + message("There are a lot of bananas in the crate.") + if (get("pirates_treasure_delivered_rum", false)) { + delay(4) + message("You find your bottle of rum in amongst the bananas.") + addOrDrop("karamjan_rum") + anim("human_pickuptable") + sound("pick_2") + set("pirates_treasure_collected_rum", true) + set("pirates_treasure_stashed_rum", false) + set("pirates_treasure_delivered_rum", false) + } + + delay(4) + + choice("Do you want to take a banana?") { + option("Yes.") { + if (inventory.spaces > 0) { + message("You take a banana.") + inventory.add("banana") + sound("pick_2") + anim("human_pickuptable") + } else { + message("You do not have enough free space to take a banana.") + } + } + option("No.") {} + } + } + + objectOperate("Search", "crate_116") { (target) -> + if (target.tile != POTATO_CRATE) { + return@objectOperate + } + searchFoodCrate("potatoes", "potato", "raw_potato") + } + + objectOperate("Search", "crate_117") { (target) -> + if (target.tile != CABBAGE_CRATE) { + return@objectOperate + } + searchFoodCrate("cabbages", "cabbage", "cabbage") + } + + objectOperate("Search", "raw_chicken_crate") { (target) -> + if (target.tile != CHICKEN_CRATE) { + return@objectOperate + } + searchFoodCrate("raw chickens", "raw chicken", "raw_chicken") + } + + for (type in MAGIC_TELEPORTS) { + teleportTakeOff(type) { + spillRum() + true + } + } + + itemOnItem("banana", "karamjan_rum") { _, _ -> + inventory.remove("karamjan_rum") + inventory.remove("banana") + inventory.add("karamjan_rum_banana") + message("You stuff the banana into the neck of the bottle. You begin to wonder why.") + } + + itemOnItem("sliced_banana", "karamjan_rum") { _, _ -> + inventory.remove("karamjan_rum") + inventory.remove("sliced_banana") + inventory.add("karamjan_rum_sliced_banana") + message("You add the banana slices to the Karamjan rum.") + } + } + + private suspend fun Player.searchFoodCrate(plural: String, name: String, itemId: String) { + message("There are a lot of $plural in the crate.") + delay(4) + choice("Do you want to take a $name?") { + option("Yes.") { + if (inventory.spaces > 0) { + message("You take a $name.") + inventory.add(itemId) + sound("pick_2") + anim("human_pickuptable") + } else { + message("You do not have enough free space to take a $name.") + } + } + option("No.") {} + } + } + + private suspend fun Player.handleSpadeDig() { + if (questStage("pirates_treasure") != 3) { + return message("It seems a shame to dig up these nice flowers for no reason.") + } + if (gardener() != null) { + return message("I can't dig up anything with him attacking me!") + } + if (!get("pirates_treasure_spawned_gardener", false)) { + set("pirates_treasure_spawned_gardener", true) + val gardener = NPCs.add( + id = "gardener_level_4", + tile = tile.add(x = 1), + ticks = TimeUnit.MINUTES.toTicks(8), + owner = this, + ) + gardener.say("First moles, now this! Take this, vandal!") + gardener.interactPlayer(this, "Attack") + return + } + message("You dig a hole in the ground...") + delay(4) + message("and find a little chest of treasure.") + addOrDrop("casket_pirates_treasure") + completeQuest() + } + + private fun Player.resetQuest() { + val completed = questStage("pirates_treasure") >= 4 + clear("pirates_treasure") + for (flag in FLAGS) { + clear(flag) + } + clear("banana_plantation_job") + clear("banana_crate_bananas") + for (item in QUEST_ITEMS) { + inventory.remove(item, inventory.count(item)) + } + NPCs.remove(gardener()) + if (completed) { + inc("quest_points", -2) + } + refreshQuestJournal() + } + + private fun Player.spillRum() { + val amount = inventory.count("karamjan_rum") + if (amount <= 0) { + return + } + inventory.remove("karamjan_rum", amount) + message("Your Karamjan rum gets broken and spilled.") + } + + private fun Player.gardener(): NPC? = NPCs.at(tile.regionLevel).firstOrNull { + it.id == "gardener_level_4" && it["owner", ""] == accountName && !it["dead", false] + } + + private suspend fun Player.completeQuest() { + set("pirates_treasure", "completed") + jingle("quest_complete_1") + inc("quest_points", 2) + AuditLog.event(this, "quest_completed", "pirates_treasure") + refreshQuestJournal() + questComplete( + "Pirate's Treasure", + "2 Quest Points", + "One-Eyed Hector's Treasure", + item = "casket_pirates_treasure", + ) + } + + private fun Player.notStartedJournal(): List = listOf( + "I can start this quest by speaking to Redbeard Frank who", + "is at Port Sarim, south of the Rusty Anchor.", + "", + "There aren't any requirements for this quest.", + ) + + private fun completedJournal(): List = listOf( + "I have spoken to Redbeard Frank. He has agreed to tell me", + "the location of some treasure for some Karamja Rum.", + "", + "I have smuggled some rum off Karamja, and retrieved it", + "from the back room of Wydin's shop.", + "I have given the rum to Redbeard Frank. He has told me", + "that the treasure is hidden in the chest in the upstairs", + "room of the Blue Moon Inn in Varrock.", + "I have opened the chest in the Blue Moon, and found a", + "note inside. I think it will tell me where to dig.", + "The note reads 'Visit the city of the White Knights. In the", + "park, Saradomin points to the X which marks the spot.'", + "", + "QUEST COMPLETE!", + "", + "I've found the treasure, gained 2 Quest Points and gained", + "access to the Pay-fare option to travel to and from", + "Karamja!", + ) + + private fun Player.startedJournal(stage: Int): List { + val list = mutableListOf() + + list += "I have spoken to Redbeard Frank. He has agreed to tell me" + list += "the location of some treasure for some Karamja Rum." + list += "" + + if (stage == 1) { + val collected = get("pirates_treasure_collected_rum", false) + val luthas = get("banana_plantation_job", false) + val delivered = get("pirates_treasure_delivered_rum", false) + val wydin = get("pirates_treasure_wydin", false) + val stashed = get("pirates_treasure_stashed_rum", false) + val bananas = get("banana_crate_bananas", 0) + val hasRum = inventory.contains("karamjan_rum") + + when { + collected -> { + if (hasRum) { + list += "I have the Karamja Rum. I should take it to Redbeard Frank." + } else { + list += "I had some rum, but I seem to have lost it. I will need to" + list += "smuggle some more off Karamja." + } + } + luthas -> { + if (delivered) { + list += "I have taken employment on the banana plantation, as the" + list += "Customs Officers might not notice the rum if it is covered" + list += "in bananas." + list += "" + list += "I have hidden my rum in the crate. I should fill it with" + list += "bananas and speak to Luthas to have it shipped over." + list += "" + list += "I have spoken to Luthas, and the crate has been shipped" + list += "to Wydin's store in Port Sarim north of the jail. Now all I" + list += "have to do is get to it..." + + if (wydin) { + list += "I have spoken to Luthas, and the crate has been shipped" + list += "to Wydin's store in Port Sarim. Now all I have to do is get to" + list += "it..." + list += "" + list += "I have taken a job at Wydin's store. I now have access to" + list += "the back room of his shop where the rum is hidden..." + } + } else { + list += "I have taken employment on the banana plantation, as the" + list += "Customs Officers might not notice the rum if it is covered" + list += "in bananas." + list += "" + + if (stashed) { + list += if (bananas == 10) { + "I have hidden my rum in the crate and filled it with" + } else { + "I have hidden my rum in the crate. I should fill it with" + } + list += "bananas and speak to Luthas to have it shipped over." + } else { + list += if (hasRum) { + "I'm sure I will be able to hide my rum in the next crate" + } else { + "Now all I need is some rum to hide in the next crate" + } + list += "destined for Wydin's store..." + } + } + } + hasRum -> { + list += "I have the rum, and now I need to find a way to get the rum" + list += "off Karamja. This might be tricky, as the Customs Officers" + list += "are searching people for it. I should look around and see if" + list += "there is a way to smuggle the rum off the island." + } + else -> { + list += "I need to go to Karamja and buy some rum. I hope it is not" + list += "too expensive." + } + } + } + + if (stage >= 2) { + list += "I have smuggled some rum off Karamja, and retrieved it" + list += "from the back room of Wydin's shop." + list += "" + list += "I have given the rum to Redbeard Frank. He has told me" + list += "that the treasure is hidden in the chest in the upstairs" + list += "room of the Blue Moon Inn in Varrock." + list += "" + + val hasMessage = inventory.contains("pirate_message") + val hasKey = inventory.contains("chest_key_pirates_treasure") + + when { + hasMessage || stage >= 3 -> { + list += "I have opened the chest in the Blue Moon, and found a" + list += "note inside. I think it will tell me where to dig." + } + hasKey -> { + list += "I have a key that can be used to unlock the chest that" + list += "holds the treasure. The chest is located in Hector's old" + list += "room, at the Blue Moon Inn in Varrock." + } + else -> { + list += "I have lost the key that Redbeard Frank gave me. I should" + list += "see if he has another." + } + } + } + + if (stage >= 3) { + list += "I have opened the chest in the Blue Moon, and found a" + list += "note inside. I think it will tell me where to dig." + list += "" + list += "The note reads: 'Visit the city of the White Knights. In the" + list += "park, Saradomin points to the X which marks the spot.'" + } + + return list + } + + private companion object { + val MAGIC_TELEPORTS = listOf("modern", "ancient", "lunar") + + val FLAGS = listOf( + "pirates_treasure_wydin", + "pirates_treasure_stashed_rum", + "pirates_treasure_delivered_rum", + "pirates_treasure_collected_rum", + "pirates_treasure_spawned_gardener", + ) + val QUEST_ITEMS = listOf( + "karamjan_rum", + "karamjan_rum_banana", + "karamjan_rum_sliced_banana", + "chest_key_pirates_treasure", + "pirate_message", + "casket_pirates_treasure", + ) + val DIG_SITE = Tile(3000, 3383) + val WYDIN_BANANA_CRATE = Tile(3009, 3207) + val POTATO_CRATE = Tile(3011, 3203) + val CABBAGE_CRATE = Tile(3010, 3203) + val CHICKEN_CRATE = Tile(3009, 3209) + } +} + +/* + * TODO Karamjan rum destruction routes from the original quest that have no equivalent here yet: + * + * - Fairy rings. The original only breaks the rum when departing one of three specific rings + * (2801,3003 / 2900,3111 / 2650,4730); the teleport take-off hook here doesn't expose which + * ring was used, and fairy ring travel isn't implemented on this server. + * - Gnome glider. The original destroys the rum inside the glider pilot's dialogue ("Oh my! What + * is that thing over there?" ... "Hmm, my mistake, it must have been an optical illusion."), + * which distracts you while he takes it. This server's gliders travel from the glider map + * interface instead and there is no pilot conversation to hang it off. + * - Captain Shanks' boat to Port Sarim, which loses the rum to a sailor in a game of dice + * ("During the trip you lose your rum to a sailor in a game of dice. Better luck next time!"). + * Captain Shanks doesn't exist here. + * - Shilo Village gate and Mosol Rei's dialogue, both of which drop the rum on entry + * ("Oh dear, you have dropped your rum!"). Neither is implemented here. + * - The TzHaar city cave entrance, which destroys the rum and burns you for 10 damage per bottle + * ("Your Karamja Rum explodes in the heat!" with a fire wave graphic). The object + * 'cave_entrance_tzhaar_city' has no handler here, so entering the city isn't implemented. + */ diff --git a/game/src/test/kotlin/content/area/asgarnia/port_sarim/SeamanTest.kt b/game/src/test/kotlin/content/area/asgarnia/port_sarim/SeamanTest.kt new file mode 100644 index 0000000000..d8b2c13a78 --- /dev/null +++ b/game/src/test/kotlin/content/area/asgarnia/port_sarim/SeamanTest.kt @@ -0,0 +1,59 @@ +package content.area.asgarnia.port_sarim + +import WorldTest +import containsMessage +import dialogueOption +import equipItem +import npcOption +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.client.ui.dialogue +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.type.Tile +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SeamanTest : WorldTest() { + + @Test + fun `A ring of charos gets you aboard for free`() { + val player = createPlayer(Tile(3028, 3217)) + player.inventory.add("ring_of_charos_a") + player.equipItem("ring_of_charos_a", option = "Wear") + tick(2) + val tobias = createNPC("captain_tobias_2", Tile(3028, 3216)) + tick(2) + + player.npcOption(tobias, "Talk-to") + chat(player) + player.dialogueOption("line2") // Or I could pay you nothing at all... + chat(player) + + assertTrue(player.containsMessage("You board the ship.")) + assertFalse(player.containsMessage("You pay 30 coins and board the ship.")) + } + + @Test + fun `Without the ring the free option isn't offered`() { + val player = createPlayer(Tile(3028, 3217)) + player.inventory.add("coins", 30) + val tobias = createNPC("captain_tobias_2", Tile(3028, 3216)) + tick(2) + + player.npcOption(tobias, "Talk-to") + chat(player) + player.dialogueOption("line2") // No, thank you. + chat(player) + + assertFalse(player.containsMessage("You board the ship.")) + assertEquals(30, player.inventory.count("coins")) + } + + private fun chat(player: Player) { + tickIf(60) { player.dialogue == null } + player.skipDialogues() + } +} diff --git a/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt b/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt index aefc7ff10f..07f6a78fe8 100644 --- a/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt +++ b/game/src/test/kotlin/content/area/kandarin/ardougne/CaptainBarnabyTest.kt @@ -17,6 +17,8 @@ class CaptainBarnabyTest : WorldTest() { @Test fun `Pay fare from Ardougne sails to Brimhaven`() { val player = createPlayer(Tile(2683, 3274)) + // the Pay-fare option is a Pirate's Treasure reward + player["pirates_treasure"] = "completed" player.inventory.add("coins", 30) val barnaby = createNPC("captain_barnaby_2", Tile(2683, 3275)) diff --git a/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt b/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt index 9ef4f7d75c..63eafe7f9c 100644 --- a/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt +++ b/game/src/test/kotlin/content/area/karamja/musa_point/CustomsOfficerTest.kt @@ -15,6 +15,8 @@ class CustomsOfficerTest : WorldTest() { @Test fun `Pay fare from Brimhaven sails to Ardougne`() { val player = createPlayer(Tile(2772, 3226)) + // the Pay-fare option is a Pirate's Treasure reward + player["pirates_treasure"] = "completed" player.inventory.add("coins", 30) val officer = createNPC("customs_officer_brimhaven", Tile(2772, 3225)) @@ -29,6 +31,8 @@ class CustomsOfficerTest : WorldTest() { @Test fun `Pay fare from Musa Point sails to Port Sarim`() { val player = createPlayer(Tile(2953, 3148)) + // the Pay-fare option is a Pirate's Treasure reward + player["pirates_treasure"] = "completed" player.inventory.add("coins", 30) val officer = createNPC("customs_officer_brimhaven", Tile(2953, 3147)) diff --git a/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt b/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt new file mode 100644 index 0000000000..b481c9a5ea --- /dev/null +++ b/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt @@ -0,0 +1,401 @@ +package content.quest.free.piratetreasure + +import WorldTest +import containsMessage +import dialogueOption +import equipItem +import itemOnObject +import itemOption +import npcOption +import objectOption +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.command.Commands +import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.PlayerRights +import world.gregs.voidps.engine.entity.character.player.Teleport +import world.gregs.voidps.engine.entity.character.player.rights +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Walked in two halves because the quest crosses Port Sarim, Karamja, Varrock and Falador; + * the second half starts from the stage the first one ends on. + */ +class PirateTreasureTest : WorldTest() { + + @Test + fun `Start the quest and smuggle the rum off Karamja`() { + val player = createPlayer(Tile(3049, 3254)) + + // unstarted -> started: Frank wants a bottle of Karamja rum + val frank = createNPC("redbeard_frank_port_sarim", Tile(3049, 3253)) + tick(2) + player.npcOption(frank, "Talk-to") + chat(player) + player.dialogueOption("line1") // I'm in search of treasure. + chat(player) + player.dialogueOption("line1") // Ok, I will bring you some rum. + chat(player) + assertEquals("started", player.stage) + + // Take the plantation job so there's a crate heading for Wydin's + player.moveTo(2941, 3151) + val luthas = createNPC("luthas_musa_point", Tile(2940, 3151)) + tick(2) + player.npcOption(luthas, "Talk-to") + chat(player) + player.dialogueOption("line1") // Could you offer me employment on your plantation? + chat(player) + assertTrue(player["banana_plantation_job", false]) + + // Hide the rum in the crate, then bury it under bananas + val crate = GameObjects.find(EXPORT_CRATE, "crate_14") + player.inventory.add("karamjan_rum") + player.itemOnObject(crate, player.inventory.indexOf("karamjan_rum")) + tick(4) + assertTrue(player["pirates_treasure_stashed_rum", false]) + assertFalse(player.inventory.contains("karamjan_rum")) + + player.inventory.add("banana", 10) + player.objectOption(crate, "Fill") + tick(4) + assertEquals(10, player["banana_crate_bananas", 0]) + + // Luthas ships the crate; the rum goes with it + player.npcOption(luthas, "Talk-to") + chat(player) + assertTrue(player["pirates_treasure_delivered_rum", false]) + assertFalse(player["pirates_treasure_stashed_rum", false]) + assertEquals(30, player.inventory.count("coins")) + + // Wydin only lets employees into the back room, and only in a white apron + player.moveTo(3015, 3204) + val wydin = createNPC("wydin", Tile(3014, 3204)) + tick(2) + player.inventory.add("white_apron") + player.npcOption(wydin, "Talk-to") + chat(player) + player.dialogueOption("line4") // Can I get a job here? + chat(player) + assertTrue(player["pirates_treasure_wydin", false]) + + // Retrieve the bottle from the delivered crate + player.equipItem("white_apron", option = "Wear") + tick(2) + player.moveTo(3009, 3208) + val bananaCrate = GameObjects.find(WYDIN_CRATE, "crate_13") + player.objectOption(bananaCrate, "Search") + chat(player) + player.dialogueOption("line2") // No. + tick(2) + assertTrue(player.inventory.contains("karamjan_rum")) + assertTrue(player["pirates_treasure_collected_rum", false]) + assertFalse(player["pirates_treasure_delivered_rum", false]) + } + + @Test + fun `Trade the rum for Hector's treasure`() { + val player = createPlayer(Tile(3049, 3254)) + player["pirates_treasure"] = "started" + player["pirates_treasure_collected_rum"] = true + player.inventory.add("karamjan_rum") + + // started -> chest_key: Frank swaps the rum for Hector's key + val frank = createNPC("redbeard_frank_port_sarim", Tile(3049, 3253)) + tick(2) + player.npcOption(frank, "Talk-to") + chat(player) + player.dialogueOption("line1") // Ok thanks, I'll go and get it. + chat(player) + assertEquals("chest_key", player.stage) + assertFalse(player.inventory.contains("karamjan_rum")) + assertTrue(player.inventory.contains("chest_key_pirates_treasure")) + + // The chest is locked without the key + player.moveTo(3219, 3395, 1) + val chest = GameObjects.find(BLUE_MOON_CHEST, "bluemoon_chest") + player.objectOption(chest, "Open") + tick(6) + assertTrue(player.containsMessage("The chest is locked.")) + + // chest_key -> read_message: the key gets you the note + player.itemOnObject(chest, player.inventory.indexOf("chest_key_pirates_treasure")) + tick(8) + assertFalse(player.inventory.contains("chest_key_pirates_treasure")) + assertTrue(player.inventory.contains("pirate_message")) + + player.itemOption("Read", "pirate_message") + tick(2) + assertEquals("read_message", player.stage) + + // Digging the flowers wakes the gardener before it yields anything + player.moveTo(3000, 3383) + player.inventory.add("spade") + player.itemOption("Dig", "spade") + tick(4) + assertTrue(player["pirates_treasure_spawned_gardener", false]) + assertFalse(player.inventory.contains("casket_pirates_treasure")) + + // He blocks digging while he's around + player.itemOption("Dig", "spade") + tick(4) + assertTrue(player.containsMessage("I can't dig up anything with him attacking me!")) + + // read_message -> completed: dig again once he's gone + val gardener = NPCs.findOrNull(player.tile.regionLevel, "gardener_level_4") + assertNotNull(gardener) + NPCs.remove(gardener) + tick(2) + player.itemOption("Dig", "spade") + tick(8) + assertEquals("completed", player.stage) + assertTrue(player.inventory.contains("casket_pirates_treasure")) + assertEquals(2, player["quest_points", 0]) + + // The casket holds Hector's treasure + player.itemOption("Open", "casket_pirates_treasure") + tick(2) + assertFalse(player.inventory.contains("casket_pirates_treasure")) + assertTrue(player.inventory.contains("gold_ring")) + assertTrue(player.inventory.contains("emerald")) + assertEquals(450, player.inventory.count("coins")) + } + + @Test + fun `Wydin's back room is closed to anyone who isn't working for him`() { + val player = createPlayer(Tile(3013, 3204)) + createNPC("wydin", Tile(3014, 3204)) + tick(2) + val door = GameObjects.find(SHOP_DOOR, "door_37_closed") + + player.objectOption(door, "Open") + chat(player) + tick(4) + + assertTrue(player.tile.x >= SHOP_DOOR.x, "should not have got past the door") + } + + @Test + fun `Wydin's employee still needs the apron on`() { + val player = createPlayer(Tile(3013, 3204)) + player["pirates_treasure"] = "started" + player["pirates_treasure_wydin"] = true + player.inventory.add("white_apron") + createNPC("wydin", Tile(3014, 3204)) + tick(2) + val door = GameObjects.find(SHOP_DOOR, "door_37_closed") + + player.objectOption(door, "Open") + chat(player) + tick(4) + assertTrue(player.tile.x >= SHOP_DOOR.x, "apron off: should not have got past the door") + + player.equipItem("white_apron", option = "Wear") + tick(2) + player.objectOption(GameObjects.find(SHOP_DOOR, "door_37_closed"), "Open") + tick(8) + assertTrue(player.tile.x < SHOP_DOOR.x, "apron on: should be through into the back room") + } + + @Test + fun `Reading the note opens the handwriting scroll`() { + val player = createPlayer(Tile(3219, 3395, 1)) + player["pirates_treasure"] = "chest_key" + player.inventory.add("pirate_message") + + player.itemOption("Read", "pirate_message") + tick(2) + + assertTrue(player.hasOpen("message_scroll_handwriting")) + assertEquals("message_scroll_handwriting", player.interfaces.get("main_screen")) + assertEquals("read_message", player.stage) + } + + @Test + fun `Someone else's gardener doesn't block the dig`() { + val player = createPlayer(Tile(3000, 3383)) + player["pirates_treasure"] = "read_message" + player["pirates_treasure_spawned_gardener"] = true + player.inventory.add("spade") + // an ordinary gardener standing nearby isn't the one chasing this player + createNPC("gardener_level_4", Tile(3001, 3383)) + tick(2) + + player.itemOption("Dig", "spade") + tick(8) + + assertFalse(player.containsMessage("I can't dig up anything with him attacking me!")) + assertEquals("completed", player.stage) + assertTrue(player.inventory.contains("casket_pirates_treasure")) + } + + @Test + fun `The gardener the dig spawns attacks and blocks further digging`() { + val player = createPlayer(Tile(3000, 3383)) + player["pirates_treasure"] = "read_message" + player.inventory.add("spade") + + player.itemOption("Dig", "spade") + tick(4) + + val gardener = NPCs.at(player.tile.regionLevel).firstOrNull { it.id == "gardener_level_4" } + assertNotNull(gardener) + assertEquals(player.accountName, gardener["owner", ""]) + assertTrue(player["pirates_treasure_spawned_gardener", false]) + + player.itemOption("Dig", "spade") + tick(4) + assertTrue(player.containsMessage("I can't dig up anything with him attacking me!")) + assertFalse(player.inventory.contains("casket_pirates_treasure")) + } + + @Test + fun `The reset command puts the quest back to unstarted`() { + val admin = createPlayer(Tile(3000, 3383), name = "ptadmin") + admin.rights = PlayerRights.Admin + admin["pirates_treasure"] = "completed" + admin["pirates_treasure_wydin"] = true + admin["pirates_treasure_collected_rum"] = true + admin["pirates_treasure_spawned_gardener"] = true + admin["banana_plantation_job"] = true + admin["banana_crate_bananas"] = 7 + admin["quest_points"] = 2 + admin.inventory.add("casket_pirates_treasure") + admin.inventory.add("chest_key_pirates_treasure") + admin.inventory.add("karamjan_rum") + + Script.launch { Commands.call(admin, "reset_pirates_treasure") } + tick(2) + + assertEquals("unstarted", admin.stage) + assertFalse(admin["pirates_treasure_wydin", false]) + assertFalse(admin["pirates_treasure_collected_rum", false]) + assertFalse(admin["pirates_treasure_spawned_gardener", false]) + assertFalse(admin["banana_plantation_job", false]) + assertEquals(0, admin["banana_crate_bananas", 0]) + assertEquals(0, admin["quest_points", 0]) + assertFalse(admin.inventory.contains("casket_pirates_treasure")) + assertFalse(admin.inventory.contains("chest_key_pirates_treasure")) + assertFalse(admin.inventory.contains("karamjan_rum")) + } + + @Test + fun `The customs officer confiscates rum at the gangplank`() { + val player = createPlayer(Tile(2956, 3146)) + player["pirates_treasure"] = "started" + val officer = createNPC("customs_officer_brimhaven", Tile(2956, 3145)) + tick(2) + player.inventory.add("karamjan_rum") + player.inventory.add("karamjan_rum") + player.inventory.add("karamjan_rum") + + player.npcOption(officer, "Talk-to") + chat(player) + player.dialogueOption("line1") // Can I journey on this ship? + chat(player) + player.dialogueOption("line2") // Search away, I have nothing to hide. + chat(player) + + assertEquals(0, player.inventory.count("karamjan_rum")) + assertTrue(player.containsMessage("The customs officer confiscates your rum.")) + } + + @Test + fun `Pay-Fare is locked until the quest is finished`() { + val player = createPlayer(Tile(2956, 3146), name = "ptfare") + val officer = createNPC("customs_officer_brimhaven", Tile(2956, 3145)) + tick(2) + + player.npcOption(officer, "Pay-Fare") + tick(4) + + assertTrue(player.containsMessage("You may only use the Pay-fare option after completing Pirate's Treasure.")) + } + + @Test + fun `Teleporting breaks the rum`() { + val player = createPlayer(Tile(2956, 3146)) + player.inventory.add("karamjan_rum") + player.inventory.add("karamjan_rum") + + assertTrue(Teleport.takeOff(player, "modern", "")) + + assertEquals(0, player.inventory.count("karamjan_rum")) + assertTrue(player.containsMessage("Your Karamjan rum gets broken and spilled.")) + } + + @Test + fun `Digging the flowers does nothing before the note is read`() { + val player = createPlayer(Tile(3000, 3383)) + player.inventory.add("spade") + + player.itemOption("Dig", "spade") + tick(4) + + assertTrue(player.containsMessage("It seems a shame to dig up these nice flowers for no reason.")) + assertFalse(player["pirates_treasure_spawned_gardener", false]) + } + + @Test + fun `Frank replaces a lost chest key`() { + val player = createPlayer(Tile(3049, 3254)) + player["pirates_treasure"] = "chest_key" + val frank = createNPC("redbeard_frank_port_sarim", Tile(3049, 3253)) + tick(2) + + player.npcOption(frank, "Talk-to") + chat(player) + + assertTrue(player.inventory.contains("chest_key_pirates_treasure")) + } + + @Test + fun `Frank refuses rum with a banana in it`() { + val player = createPlayer(Tile(3049, 3254)) + player["pirates_treasure"] = "started" + player.inventory.add("karamjan_rum_banana") + val frank = createNPC("redbeard_frank_port_sarim", Tile(3049, 3253)) + tick(2) + + player.npcOption(frank, "Talk-to") + chat(player) // greeting and "Yes, I've got some." + chat(player) // Frank pauses, then complains about the banana + tick(2) + + assertEquals("started", player.stage) + assertTrue(player.inventory.contains("karamjan_rum_banana")) + assertFalse(player.inventory.contains("chest_key_pirates_treasure")) + } + + private fun chat(player: Player) { + tickIf(60) { player.dialogue == null } + player.skipDialogues() + } + + private fun Player.moveTo(x: Int, y: Int, level: Int = 0) { + tele(x, y, level) + tick(3) + } + + private val Player.stage: String + get() = this["pirates_treasure", "unstarted"] + + private companion object { + val EXPORT_CRATE = Tile(2943, 3151) + val WYDIN_CRATE = Tile(3009, 3207) + val BLUE_MOON_CHEST = Tile(3219, 3396, 1) + val SHOP_DOOR = Tile(3012, 3204) + } +} From 1dc63130974f465e8d0ec7411d4b3ca6a7a36c39 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 2 Sep 2026 12:50:46 +0100 Subject: [PATCH 2/3] Update rum smuggling routes --- data/area/karamja/karamja.areas.toml | 4 + .../tree_gnome_stronghold/CaptainErrdo.kt | 16 ++++ .../content/entity/obj/ship/CharterShip.kt | 35 ++++++-- .../free/piratetreasure/PirateTreasure.kt | 83 ++++++++++++------- 4 files changed, 100 insertions(+), 38 deletions(-) diff --git a/data/area/karamja/karamja.areas.toml b/data/area/karamja/karamja.areas.toml index 4c30a0d0b5..1a7548a745 100644 --- a/data/area/karamja/karamja.areas.toml +++ b/data/area/karamja/karamja.areas.toml @@ -8,3 +8,7 @@ x = [2752,2752,2979,2979,3007,3007] y = [2880,3135,3135,3104,3104,2880] tags = ["penguin_area"] hint = "deep in the jungle." + +[karamja_dungeon] +x = [2827, 2867] +y = [9546, 9599] diff --git a/game/src/main/kotlin/content/area/kandarin/tree_gnome_stronghold/CaptainErrdo.kt b/game/src/main/kotlin/content/area/kandarin/tree_gnome_stronghold/CaptainErrdo.kt index 1c596f95a1..f8c5f61042 100644 --- a/game/src/main/kotlin/content/area/kandarin/tree_gnome_stronghold/CaptainErrdo.kt +++ b/game/src/main/kotlin/content/area/kandarin/tree_gnome_stronghold/CaptainErrdo.kt @@ -7,6 +7,8 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove class CaptainErrdo : Script { @@ -47,6 +49,7 @@ class CaptainErrdo : Script { takeMe(target) return@npcOperate } + checkForRum() location(this, target) open("glider_map") } @@ -61,9 +64,22 @@ class CaptainErrdo : Script { } } + private suspend fun Player.checkForRum() { + if (inventory.contains("karamjan_rum")) { + npc("Is that some rum I see in your backpack?") + player("Yes, why do you ask?") + npc("No reason, I just...") + npc("Oh my! What is that thing over there?") + player("Where?") + inventory.remove("karamjan_rum", inventory.count("karamjan_rum")) + npc("Hmm, my mistake, it must have been an optical illusion.") + } + } + fun ChoiceOption.takeMe(target: NPC) { option("Can you take me on the glider?") { npc("Of course!") + checkForRum() location(this, target) open("glider_map") } diff --git a/game/src/main/kotlin/content/entity/obj/ship/CharterShip.kt b/game/src/main/kotlin/content/entity/obj/ship/CharterShip.kt index 0173d85396..75e7a2d273 100644 --- a/game/src/main/kotlin/content/entity/obj/ship/CharterShip.kt +++ b/game/src/main/kotlin/content/entity/obj/ship/CharterShip.kt @@ -11,11 +11,13 @@ import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.chat.toDigitGroupString import world.gregs.voidps.engine.client.ui.closeMenu import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.jingle import world.gregs.voidps.engine.entity.character.move.tele import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.type.Tile @@ -80,17 +82,16 @@ class CharterShip(val ships: CharterShips, val teles: ObjectTeleports) : Script option("No thanks.") } } - option("Yes, I would like to charter a ship.") { - player("Yes, I would like to charter a ship.") - npc("Certainly sir. Where would you like to go?") - } + charter(target) option("No thanks.") } } npcOperate("Charter", "trader_stan,trader_crewmember*") { (target) -> - set("charter_ship", location(target)) - open("charter_ship_map") + if (!checkBans(target)) { + set("charter_ship", location(target)) + open("charter_ship_map") + } } interfaceOption("Ok", "charter_ship_map:*") { @@ -136,6 +137,20 @@ class CharterShip(val ships: CharterShips, val teles: ObjectTeleports) : Script } } + private suspend fun Player.checkBans(target: NPC): Boolean { + if (inventory.contains("karamjan_rum") && (target.tile in Areas["greater_brimhaven"] || target.tile in Areas["karamja"])) { + npc("Sorry, we can't take you anywhere if you are trying to smuggle that rum.") + return true + } else if (carriesItem("bedsheet_ectoplasm")) { + npc("Is that your bedsheet covered in filthy slime?") + return true + } else if (carriesItem("bedsheet")) { + npc("Sorry, we aren't a laundry ship. You'll need to leave those dirty bedsheets behind.") + return true + } + return false + } + fun ChoiceOption.trading() { option("Yes, let's see what you're trading.") { openShop("trader_stans_trading_post") @@ -144,9 +159,11 @@ class CharterShip(val ships: CharterShips, val teles: ObjectTeleports) : Script fun ChoiceOption.charter(target: NPC) { option("Yes, I would like to charter a ship.") { - npc("Certainly sir. Where would you like to go?") - set("charter_ship", location(target)) - open("charter_ship_map") + if (!checkBans(target)) { + npc("Certainly sir. Where would you like to go?") + set("charter_ship", location(target)) + open("charter_ship_map") + } } } diff --git a/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt index 469c10be57..f194de86d3 100644 --- a/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt +++ b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt @@ -1,5 +1,7 @@ package content.quest.free.piratetreasure +import content.entity.combat.dead +import content.entity.combat.hit.directHit import content.entity.player.dialogue.Happy import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc @@ -14,10 +16,12 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.command.adminCommand import world.gregs.voidps.engine.client.instruction.handle.interactPlayer import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.jingle import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.Teleport import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.obj.replace import world.gregs.voidps.engine.event.AuditLog @@ -32,6 +36,14 @@ class PirateTreasure : Script { init { + /* + TODO remove karamjan rum from: + Captain Shanks' boat to Port Sarim, which loses the rum to a sailor in a game of dice + ("During the trip you lose your rum to a sailor in a game of dice. Better luck next time!"). + Shilo village gate ("Oh dear, you have dropped your rum!") + + */ + adminCommand("reset_pirates_treasure", desc = "Reset Pirate's Treasure back to unstarted") { resetQuest() message("Pirate's Treasure reset.") @@ -93,9 +105,8 @@ class PirateTreasure : Script { itemOption("Open", "casket_pirates_treasure") { val needsSpace = if (inventory.contains("coins")) 1 else 2 if (inventory.spaces < needsSpace) { - return@itemOption message( - "From the weight of this you can guess you need more inventory space to get at the contents.", - ) + message("From the weight of this you can guess you need more inventory space to get at the contents.") + return@itemOption } sound("chest_open") message("You open the casket, and find One-Eyed Hector's treasure.") @@ -141,7 +152,7 @@ class PirateTreasure : Script { message("You do not have enough free space to take a banana.") } } - option("No.") {} + option("No.") } } @@ -166,8 +177,31 @@ class PirateTreasure : Script { searchFoodCrate("raw chickens", "raw chicken", "raw_chicken") } + objTeleportTakeOff("Enter", "cave_entrance_tzhaar_city") { _, _ -> + explodeRum() + Teleport.CONTINUE + } + + exited("greater_brimhaven") { + if (tile !in Areas["karamja"] && tile !in Areas["karamja_dungeon"]) { + spillRum() + } + } + + exited("karamja") { + if (tile !in Areas["greater_brimhaven"]) { + spillRum() + } + } + + exited("karamja_dungeon") { + if (tile !in Areas["greater_brimhaven"]) { + spillRum() + } + } + for (type in MAGIC_TELEPORTS) { - teleportTakeOff(type) { + teleportRemoveItems(type) { spillRum() true } @@ -250,6 +284,17 @@ class PirateTreasure : Script { refreshQuestJournal() } + private fun Player.explodeRum() { + val amount = inventory.count("karamjan_rum") + if (amount <= 0) { + return + } + inventory.remove("karamjan_rum", amount) + gfx("fire_wave_impact") + directHit(10 * amount) + message("Your Karamja Rum explodes in the heat!") + } + private fun Player.spillRum() { val amount = inventory.count("karamjan_rum") if (amount <= 0) { @@ -260,10 +305,10 @@ class PirateTreasure : Script { } private fun Player.gardener(): NPC? = NPCs.at(tile.regionLevel).firstOrNull { - it.id == "gardener_level_4" && it["owner", ""] == accountName && !it["dead", false] + it.id == "gardener_level_4" && it["owner", ""] == accountName && !it.dead } - private suspend fun Player.completeQuest() { + private fun Player.completeQuest() { set("pirates_treasure", "completed") jingle("quest_complete_1") inc("quest_points", 2) @@ -277,7 +322,7 @@ class PirateTreasure : Script { ) } - private fun Player.notStartedJournal(): List = listOf( + private fun notStartedJournal(): List = listOf( "I can start this quest by speaking to Redbeard Frank who", "is at Port Sarim, south of the Rusty Anchor.", "", @@ -428,7 +473,7 @@ class PirateTreasure : Script { } private companion object { - val MAGIC_TELEPORTS = listOf("modern", "ancient", "lunar") + val MAGIC_TELEPORTS = listOf("modern", "ancient", "lunar", "jewellery", "scroll", "tablet", "spirit_tree", "fairy_ring") val FLAGS = listOf( "pirates_treasure_wydin", @@ -452,23 +497,3 @@ class PirateTreasure : Script { val CHICKEN_CRATE = Tile(3009, 3209) } } - -/* - * TODO Karamjan rum destruction routes from the original quest that have no equivalent here yet: - * - * - Fairy rings. The original only breaks the rum when departing one of three specific rings - * (2801,3003 / 2900,3111 / 2650,4730); the teleport take-off hook here doesn't expose which - * ring was used, and fairy ring travel isn't implemented on this server. - * - Gnome glider. The original destroys the rum inside the glider pilot's dialogue ("Oh my! What - * is that thing over there?" ... "Hmm, my mistake, it must have been an optical illusion."), - * which distracts you while he takes it. This server's gliders travel from the glider map - * interface instead and there is no pilot conversation to hang it off. - * - Captain Shanks' boat to Port Sarim, which loses the rum to a sailor in a game of dice - * ("During the trip you lose your rum to a sailor in a game of dice. Better luck next time!"). - * Captain Shanks doesn't exist here. - * - Shilo Village gate and Mosol Rei's dialogue, both of which drop the rum on entry - * ("Oh dear, you have dropped your rum!"). Neither is implemented here. - * - The TzHaar city cave entrance, which destroys the rum and burns you for 10 damage per bottle - * ("Your Karamja Rum explodes in the heat!" with a fire wave graphic). The object - * 'cave_entrance_tzhaar_city' has no handler here, so entering the city isn't implemented. - */ From 7154c04c60357604331d14d66d072c4aa7ea28a7 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 2 Sep 2026 13:03:49 +0100 Subject: [PATCH 3/3] Fix teleport rum test --- .../kotlin/content/quest/free/piratetreasure/PirateTreasure.kt | 3 +-- .../content/quest/free/piratetreasure/PirateTreasureTest.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt index f194de86d3..1edd62e377 100644 --- a/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt +++ b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt @@ -201,9 +201,8 @@ class PirateTreasure : Script { } for (type in MAGIC_TELEPORTS) { - teleportRemoveItems(type) { + teleportLand(type) { spillRum() - true } } diff --git a/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt b/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt index b481c9a5ea..c2a9bd99c7 100644 --- a/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt +++ b/game/src/test/kotlin/content/quest/free/piratetreasure/PirateTreasureTest.kt @@ -330,7 +330,7 @@ class PirateTreasureTest : WorldTest() { player.inventory.add("karamjan_rum") player.inventory.add("karamjan_rum") - assertTrue(Teleport.takeOff(player, "modern", "")) + Teleport.land(player, "modern") assertEquals(0, player.inventory.count("karamjan_rum")) assertTrue(player.containsMessage("Your Karamjan rum gets broken and spilled."))