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/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/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/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/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/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/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..1edd62e377 --- /dev/null +++ b/game/src/main/kotlin/content/quest/free/piratetreasure/PirateTreasure.kt @@ -0,0 +1,498 @@ +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 +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.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 +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 { + + /* + 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.") + } + + 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) { + 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.") + 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") + } + + 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) { + teleportLand(type) { + spillRum() + } + } + + 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.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) { + 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 + } + + private 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 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", "jewellery", "scroll", "tablet", "spirit_tree", "fairy_ring") + + 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) + } +} 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..c2a9bd99c7 --- /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") + + Teleport.land(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) + } +}