diff --git a/data/area/morytania/canifis/canifis.teles.toml b/data/area/morytania/canifis/canifis.teles.toml index 62f0600b1b..e5dfc25d02 100644 --- a/data/area/morytania/canifis/canifis.teles.toml +++ b/data/area/morytania/canifis/canifis.teles.toml @@ -23,12 +23,3 @@ option = "Climb-down" tile = { x = 3422, y = 3484 } to = { x = 3440, y = 9888 } -[canifis_tavern_trapdoor] -option = "Open" -tile = { x = 3495, y = 3465 } -to = { x = 3477, y = 9845 } - -[canifis_tavern_ladder] -option = "Climb-up" -tile = { x = 3477, y = 9846 } -to = { x = 3495, y = 3466 } diff --git a/data/area/morytania/meiyerditch/tunnels/meiyerditch_tunnels.npcs.toml b/data/area/morytania/meiyerditch/tunnels/meiyerditch_tunnels.npcs.toml index f77a436c22..19d16f2afd 100644 --- a/data/area/morytania/meiyerditch/tunnels/meiyerditch_tunnels.npcs.toml +++ b/data/area/morytania/meiyerditch/tunnels/meiyerditch_tunnels.npcs.toml @@ -4,10 +4,12 @@ examine = "Looks like the leader of the Myreque." [sani_piliu_meiyerditch_tunnels] id = 1570 +hitpoints = 300 examine = "A pretty young rogue." [harold_evans_meiyerditch_tunnels] id = 1571 +hitpoints = 300 examine = "Hot headed, big built youth, who trained in the militia." [ivan_strom_meiyerditch_tunnels] diff --git a/data/area/morytania/mort_myre_swamp/mort_myre_swamp.areas.toml b/data/area/morytania/mort_myre_swamp/mort_myre_swamp.areas.toml index 8a6ad20879..3843efe87c 100644 --- a/data/area/morytania/mort_myre_swamp/mort_myre_swamp.areas.toml +++ b/data/area/morytania/mort_myre_swamp/mort_myre_swamp.areas.toml @@ -23,3 +23,8 @@ tags = ["no_cannon", "no_cannon_damp"] [filliman_grotto] x = [3433, 3447] y = [3331, 3343] + +# The rope bridge over the Hollows, from the tile the southern tree lands on up to the top rung +[mort_myre_rope_bridge] +x = [3502, 3502] +y = [3427, 3430] diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.anims.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.anims.toml new file mode 100644 index 0000000000..010a9db128 --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.anims.toml @@ -0,0 +1,33 @@ +[squeeze_past_stalagmite] +id = 3278 + +# Curpile's warning jab, and the punch that puts the player out cold +[curpile_punch] +id = 422 + +[curpile_knockout_punch] +id = 390 + +# Also used for Sani and Harold going down in Vanstrom's ambush +[myreque_knockout] +id = 837 + +# Vanstrom's ambush in the hideout +[vanstrom_transform] +id = 8594 + +[vanstrom_hover] #9855 in 667 +id = 1500 + +[vanstrom_summon] +id = 4707 + +# Vanstrom's pet shares the hellhound skeleton's attack, block and death +[skeleton_hellhound_attack] +id = 6562 + +[skeleton_hellhound_defend] +id = 6563 + +[skeleton_hellhound_death] +id = 6564 diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.combat.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.combat.toml new file mode 100644 index 0000000000..eab162cd62 --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.combat.toml @@ -0,0 +1,11 @@ +[skeleton_hellhound] +defend_anim = "skeleton_hellhound_defend" +death_anim = "skeleton_hellhound_death" +death_sound = "skeleton_hellhound_death" + +[skeleton_hellhound.melee] +range = 1 +anim = "skeleton_hellhound_attack" +sound = "skeleton_hellhound_attack" +target_sound = "skeleton_hellhound_hit" +target_hit = { offense = "crush", max = 120 } diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.drops.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.drops.toml new file mode 100644 index 0000000000..a7d0cb5ebe --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.drops.toml @@ -0,0 +1,6 @@ +[skeleton_hellhound_drop_table] +type = "all" +drops = [ + { id = "big_bones", amount = 4 }, + { id = "uncut_ruby", amount = 2 }, +] diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.gfx.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.gfx.toml new file mode 100644 index 0000000000..b2f81a166a --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.gfx.toml @@ -0,0 +1,11 @@ +[vanstrom_transform_graphic] +id = 785 + +[vanstrom_summon_graphic] +id = 731 + +[curpile_knockout_stars] +id = 245 + +[skeleton_hellhound_summon] +id = 86 diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.npcs.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.npcs.toml index 82ecf7a37a..609ecbcd50 100644 --- a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.npcs.toml +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.npcs.toml @@ -33,7 +33,26 @@ str = 110 def = 100 style = "crush" max_hit_melee = 120 +combat_def = "skeleton_hellhound" +attack_speed = 4 hunt_mode = "cowardly" slayer_xp = 55.0 categories = ["hellhounds"] +drop_table = "skeleton_hellhound" examine = "A creature summoned by Vanstrom to kill the remaining Myreque." + +# Vanstrom's forms during the Myreque hideout cutscene +[route_vanstrom_vampire_misty] +id = 1578 +dialogue = "old" + +[route_vanstrom] +id = 1579 + +[route_vanstrom_vampire] +id = 1580 +dialogue = "old" + +[route_vanstrom_vampire_flying] +id = 1581 +dialogue = "old" diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.objs.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.objs.toml new file mode 100644 index 0000000000..558f505638 --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.objs.toml @@ -0,0 +1,17 @@ +[swamp_bridge1] +id = 5002 + +[spooky_tree_base_forbridge] +id = 5003 + +[route_cavewalltunnel] +id = 5046 + +[route_stalagmite_cave_entrace] +id = 5050 + +[freedomfighterundergrounddooropenl] +id = 30263 + +[freedomfighterundergrounddooropenr] +id = 30264 diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.sounds.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.sounds.toml new file mode 100644 index 0000000000..afc880f1b8 --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.sounds.toml @@ -0,0 +1,17 @@ +[vanstrom_mist] +id = 1186 + +[vanstrom_appear] +id = 1190 + +[vanstrom_vampire] +id = 1194 + +[skeleton_hellhound_attack] +id = 1187 + +[skeleton_hellhound_hit] +id = 1185 + +[skeleton_hellhound_death] +id = 1188 diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varbits.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varbits.toml new file mode 100644 index 0000000000..1aa70a2dae --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varbits.toml @@ -0,0 +1,46 @@ +[routequestionscorrect] +id = 173 +persist = true +format = "int" + +[routequestionsanswered] +id = 174 +persist = true +format = "int" + +[roomnum] +id = 175 +persist = true +format = "boolean" + +# The three rungs of the rope bridge over the Hollows, south to north +[bridgerung1] +id = 176 +persist = true +format = "boolean" + +[bridgerung2] +id = 177 +persist = true +format = "boolean" + +[bridgerung3] +id = 178 +persist = true +format = "boolean" + +# Swaps the stranger in the 'Hair of the Dog' between Vanstrom Klause and a plain stranger +[thsfm_vanstrom_hide] +id = 396 +persist = true +format = "boolean" + +[thsfm_spoken_allmyreque] +id = 2496 +persist = true +format = "boolean" + +[route_bridgecomplete] +id = 3928 +persist = true +format = "boolean" diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varps.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varps.toml index 941693bdce..623d216ad4 100644 --- a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varps.toml +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.varps.toml @@ -3,4 +3,4 @@ id = 387 persist = true format = "map" default = "unstarted" -values = { unstarted = 0, started = 1 } +values = { unstarted = 0, agreed_to_help = 5, refused_delivery = 10, persuaded_boatman = 15, gave_planks = 20, reached_hollows = 25, questioned_by_curpile = 52, answered_questions = 55, entered_hideout = 60, met_veliaf = 65, weapons_accepted = 67, delivered_weapons = 70, hellhound_summoned = 80, killed_hellhound = 85, shown_way_out = 90, in_inn_basement = 95, escaped_to_canifis = 97, completed = 105 } diff --git a/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.vars.toml b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.vars.toml new file mode 100644 index 0000000000..3a77c48ff4 --- /dev/null +++ b/data/quest/members/in_search_of_the_myreque/in_search_of_the_myreque.vars.toml @@ -0,0 +1,20 @@ +# Which members of the Myreque the player has introduced themselves to +[met_sani] +persist = true +format = "boolean" + +[met_harold] +persist = true +format = "boolean" + +[met_ivan] +persist = true +format = "boolean" + +[met_radigad] +persist = true +format = "boolean" + +[met_polmafi] +persist = true +format = "boolean" diff --git a/data/quest/quests.toml b/data/quest/quests.toml index c802a88f6c..a54eab77af 100644 --- a/data/quest/quests.toml +++ b/data/quest/quests.toml @@ -1958,7 +1958,7 @@ req_items = "Steel longsword, 2 steel short swords, steel mace, steel warhammer, req_combat = "You will need to defeat a level 44 enemy." points = 2 reward = "Access to the Hair of the Dog's cellar
Swamp boat shortcut from Mort'ton to The Hollows
Access to 2 uncut rubies and 4 big bones" -xp = "600 Attack XP lamp
600 Strength XP lamp
600 Crafting XP lamp" +xp = "600 Attack XP
600 Defence XP
600 Strength XP
600 Constitution XP
600 Crafting XP" [jungle_potion] id = 66 diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/map/zone/DynamicZones.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/map/zone/DynamicZones.kt index c29dabaf16..57f3c7f4db 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/map/zone/DynamicZones.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/map/zone/DynamicZones.kt @@ -51,6 +51,25 @@ class DynamicZones( update = true } + /** + * Copies a block of zones, for content that only needs part of a region - a room to stage a + * cutscene in, say - rather than all sixty four zones of one. + * @param from The south west zone of the block to copy + * @param to The south west zone the block is copied to + * @param width How many zones wide the block is + * @param height How many zones high the block is + * @param levels How many levels of it to copy, counting up from the level of [from] and [to] + */ + fun copy(from: Zone, to: Zone, width: Int, height: Int, levels: Int = 1) { + for (x in 0 until width) { + for (y in 0 until height) { + for (level in 0 until levels) { + copy(Zone(from.x + x, from.y + y, from.level + level), Zone(to.x + x, to.y + y, to.level + level)) + } + } + } + } + /** * @param from The region to be copied * @param to The region to be replaced diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/DynamicZonesTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/DynamicZonesTest.kt index 7b8196ca5a..3a7abfb5df 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/DynamicZonesTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/DynamicZonesTest.kt @@ -37,6 +37,27 @@ internal class DynamicZonesTest { assertEquals(131140, zones.dynamicZone(zone)) } + @Test + fun `Copy a block of zones to another`() { + val from = Zone(4, 4) + val to = Zone(8, 8) + zones.copy(from, to, width = 2, height = 3, levels = 2) + + assertTrue(zones.dynamic(to.region)) + for (x in 0 until 2) { + for (y in 0 until 3) { + for (level in 0 until 2) { + assertNotNull( + zones.dynamicZone(Zone(to.x + x, to.y + y, level)), + "zone $x, $y, $level of the block", + ) + } + } + } + assertNull(zones.dynamicZone(Zone(to.x + 2, to.y, 0)), "nothing outside the block is copied") + assertNull(zones.dynamicZone(Zone(to.x, to.y, 2)), "and no levels above it") + } + @Test fun `Copy one region to another`() { val from = Region(8, 8) diff --git a/game/src/main/kotlin/content/area/morytania/canifis/CanifisTavern.kt b/game/src/main/kotlin/content/area/morytania/canifis/CanifisTavern.kt index fe0fa85b72..ae4e89c794 100644 --- a/game/src/main/kotlin/content/area/morytania/canifis/CanifisTavern.kt +++ b/game/src/main/kotlin/content/area/morytania/canifis/CanifisTavern.kt @@ -1,7 +1,6 @@ package content.area.morytania.canifis import content.entity.obj.door.DoubleDoor -import content.entity.obj.door.enterDoor import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.move.tele import world.gregs.voidps.engine.entity.character.sound @@ -9,10 +8,6 @@ import world.gregs.voidps.engine.entity.obj.replace class CanifisTavern : Script { init { - objectOperate("Search", "canifis_fake_wall_closed") { (target) -> - enterDoor(target) - } - objectOperate("Open", "canifis_wooden_door_left_closed,canifis_wooden_door_right_closed") { (target) -> anim("take") sound("locked") diff --git a/game/src/main/kotlin/content/area/morytania/canifis/VanstromKlause.kt b/game/src/main/kotlin/content/area/morytania/canifis/VanstromKlause.kt new file mode 100644 index 0000000000..7063f22c9e --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/canifis/VanstromKlause.kt @@ -0,0 +1,238 @@ +package content.area.morytania.canifis + +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.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +import content.quest.questStage +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.player.Player + +class VanstromKlause : Script { + init { + npcOperate("Talk-to", "stranger_2") { + if (questStage("in_search_of_the_myreque") == myrequeStage("unstarted")) { + npc("Hello there, how goes it stranger?") + firstMeetingChoices() + } else { + npc("Hello again, how's the quest going?") + returningChoices() + } + } + } + + private suspend fun Player.firstMeetingChoices() { + player("Quite well thanks for asking, how about you?") + npc( + "Hmm, well, I am a little concerned about some friends of mine, they're in dire need of " + + "some assistance, but I'm at a loss of how I can help them.", + ) + initialQuestionChoice() + } + + suspend fun Player.initialQuestionChoice() { + choice { + whatFriends() + whyHelp() + tooBusy() + option("Ok, thanks.") + } + } + + suspend fun Player.extendedQuestionChoice() { + choice { + whatFriends(extended = true) + whyHelp() + tooBusy() + offerToHelp() + option("Ok, thanks.") + } + } + + fun ChoiceOption.whatFriends(extended: Boolean = false): Unit = option("What friends are these?") { + npc( + "It's a personal tragedy that I have yet to meet them in the flesh. But their exploits " + + "make mouth watering hero stories...the real meat and drink of high adventure and " + + "daring...so they say.", + ) + player( + "What does that mean exactly? I mean, I have some stories. I'm quite a hero myself, " + + "you may actually be talking about me?", + ) + npc( + "They're regarded as heroes in Morytania, though some people see them as vigilantes. " + + "The local villagers call them the 'Myreque'. Some people call them terrorists while " + + "others call them freedom fighters!", + ) + + if (extended) { + extendedQuestionChoice() + } else { + initialQuestionChoice() + } + } + + fun ChoiceOption.whyHelp(): Unit = option("Why do they need help? Are they in trouble?") { + npc( + "I should imagine that heroes of such high calibre are almost always in some sort of " + + "trouble, wouldn't you? There's always some evil heel ready to grind the face of " + + "humanity into the dirt?", + ) + npc( + "However, the Myreque are almost certainly able to handle themselves...given the tools! " + + "I hear they're short of weapons, I was hoping to do it myself but I find that I'm " + + "rather short of time and ability.", + ) + player("What help do you hope to give them?") + npc("I'd have taken some weapons to them!") + player("What kind of weapons do they need?") + npc( + "Steel I believe. All six of them require steel weapons. I would have suggested a " + + "longsword, two shortswords, a dagger, a mace and a warhammer.", + ) + extendedQuestionChoice() + } + + fun ChoiceOption.tooBusy(): Unit = option("I wish I could help, but I'm busy at the moment.") { + npc( + "Hmm, the same as me I fear! Oh well, I'm sure they'll get on well by themselves, " + + "the little dears. Thanks for even considering it though.", + ) + } + + fun ChoiceOption.offerToHelp() { + option("Perhaps I could help you out here.") { + player("Perhaps I could help you out here. Maybe I could take these weapons to the Myreque.") + npc("Oh yes, well that would be very nice of you! Are you sure you want to help out?") + helpDetailsChoice() + } + } + + suspend fun Player.helpDetailsChoice() { + choice { + whatToDo() + whatWeapons() + whereToTake() + agreeToHelp() + declineHelp() + } + } + + fun ChoiceOption.whatToDo(): Unit = option("What would I have to do?") { + npc( + "Well, some freedom fighter, called the Myreque, require some weapons. You'd need to " + + "get the weapons they require and give them to the freedom fighters.", + ) + helpDetailsChoice() + } + + fun ChoiceOption.whatWeapons(): Unit = option("What weapons do I need to get?") { + npc( + "Well, I'm sure the following will be fine. A longsword, two shortswords, a dagger, " + + "a mace and a warhammer, all the items should be made of steel.", + ) + npc( + "You would also have to fund the costs of this yourself and once you've found them, " + + "I'll be sure to give you a nice surprise in return for your help.", + ) + helpDetailsChoice() + } + + fun ChoiceOption.whereToTake(): Unit = option("Where do I need to take the weapons?") { + npc( + "I believe there's a boatman in Mort'ton who can show you the way, I'm sure if you use " + + "your powers of persuasion you can get some information out of him! The Myreque " + + "have a lot of sympathisers.", + ) + helpDetailsChoice() + } + + fun ChoiceOption.agreeToHelp(): Unit = option("Yes, I'll do it!") { + set("in_search_of_the_myreque", "agreed_to_help") + npc( + "That's great news my friend, really great news! Perhaps the many peoples of Morytania " + + "now have an additional hero that they can come to rely upon?", + ) + } + + fun ChoiceOption.declineHelp(): Unit = option("Sorry, I can't do it!") { + npc( + "Oh well, that is a shame, I'm sorry to hear it. But if you change your mind, please " + + "visit me again.", + ) + } + + private suspend fun Player.returningChoices() { + choice { + needHelp() + farewell() + forgotTask() + } + } + + fun ChoiceOption.needHelp(): Unit = option("Not very well, I need some help.") { + npc("Oh dear... what do you need help with?") + helpQuestionsChoice() + } + + suspend fun Player.helpQuestionsChoice() { + choice { + whereMyreque() + whatWeaponsAgain() + howKnowMyreque() + anyoneElse() + farewell() + } + } + + fun ChoiceOption.whereMyreque(): Unit = option("Where are the Myreque?") { + npc( + "You know what? I'm not exactly sure. The only thing I've heard, and it is a rumour, " + + "but the boatman in Mort'ton might be able to help. But again, it is only a rumour.", + ) + helpQuestionsChoice() + } + + fun ChoiceOption.whatWeaponsAgain(): Unit = option("What weapons am I supposed to get again?") { + npc( + "Well, I'm sure the following will be fine. A longsword, two shortswords, a dagger, " + + "a mace and a warhammer, all the items should be made of steel.", + ) + helpQuestionsChoice() + } + + fun ChoiceOption.howKnowMyreque(): Unit = option("How do you know 'The Myreque'?") { + npc( + "Actually you know, I don't know them at all! I've never actually met them, but would " + + "love to at some point! However, I'm sure that you'll be much more able than I at " + + "tracking them down.", + ) + helpQuestionsChoice() + } + + fun ChoiceOption.anyoneElse(): Unit = option("Is there anyone else who can help me?") { + npc("Hmm. Apart from the boatman at Mor'ton, I have no clue my friend.") + helpQuestionsChoice() + } + + fun ChoiceOption.forgotTask(): Unit = option("What am I supposed to do again?") { + npc( + "You've forgotten already? Ha! That's funny! But I do understand, my memory isn't " + + "what it was!", + ) + npc( + "It would be great if you could get some steel weapons and take them to the Myreque, " + + "the boatman in Mort'ton should be able to help you find them. But you may need " + + "to be a bit persuasive!", + ) + } + + fun ChoiceOption.farewell(): Unit = option("Ok, thanks.") { + npc("Well, if you have any problems please feel free to come and chat with me.") + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/CurpileFyod.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/CurpileFyod.kt new file mode 100644 index 0000000000..7f3cfac13a --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/CurpileFyod.kt @@ -0,0 +1,271 @@ +package content.area.morytania.mort_myre_swamp + +import content.entity.combat.hit.damage +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.ChoiceOption +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.member.myreque.hasAllWeapons +import content.quest.member.myreque.myrequeStage +import content.quest.questStage +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.close +import world.gregs.voidps.engine.client.ui.open +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.type.random + +class CurpileFyod : Script { + init { + npcOperate("Talk-to", "curpile_fyod_mort_myre_swamp") { (curpile) -> + val progress = questStage("in_search_of_the_myreque") + when { + progress < myrequeStage("reached_hollows") -> { + npc("Hey...you's tresspassin' be off wiv ya.") + } + progress == myrequeStage("reached_hollows") || progress == myrequeStage("questioned_by_curpile") -> { + npc("Hey...what're you doin' here?") + initialChoice(curpile) + } + progress > myrequeStage("questioned_by_curpile") -> { + npc("Hey...leave me be...I'm guardin' this place.") + } + } + } + } + + suspend fun Player.initialChoice(curpile: NPC) { + choice { + comeToHelp(curpile) + dontHaveToAnswer(curpile) + noReason(curpile) + askForWayOut(curpile) + haveToGo() + } + } + + fun ChoiceOption.comeToHelp(curpile: NPC): Unit = option("I've come to help the Myreque, I've brought weapons.") { + if (!hasAllWeapons()) { + npc( + "It doesn't look as if you have the right weapons to me. Get out of here you fake!", + ) + return@option + } + npc( + "Ok, I see ya got da weapons...but how'd I know you're not gonna use em against my " + + "friends?", + ) + player("But I just want to help deliver these weapons.") + npc( + "Well, dat's as maybe, and I'm not doubting your sincerity here, you seems all sincered " + + "up to me...it's choking me up right here, you're making me cry...but hey, I's godda " + + "do my job or da kids don't get fed! Ok,", + ) + if (questStage("in_search_of_the_myreque") == myrequeStage("reached_hollows")) { + set("in_search_of_the_myreque", "questioned_by_curpile") + } + npc( + "so say I asks you a few questions and you were to answer them all correct and so on, " + + "well that'd make me believe you's...I'd get to feeling that you was the real deal " + + "an all. How's dat sound?", + ) + player("Sounds fine to me...go ahead, shoot!") + npc("Hey...don't tempt me! You's dicing wiv death here my friend!") + runQuiz(curpile) + } + + fun ChoiceOption.dontHaveToAnswer(curpile: NPC): Unit = option("I don't have to answer to you!") { + npc("You do if you know what's good for you!") + sendPunch(curpile) + } + + fun ChoiceOption.noReason(curpile: NPC): Unit = option("No reason!") { + npc( + "Come on...speak up! I may be able to help you. However, yous gotta know that your " + + "tresspassing and that by rights I should slap your around a bit, you know, just " + + "like as a warning.", + ) + initialChoice(curpile) + } + + fun ChoiceOption.askForWayOut(curpile: NPC): Unit = option("I'm lost, can you show me the way out?") { + npc( + "You got yourself in here... now get yourself out.. and quickly if I were you.", + ) + initialChoice(curpile) + } + + fun ChoiceOption.haveToGo(): Unit = option("I have to go.") { + npc("Sure...be on your way stranger.") + } + + private suspend fun Player.runQuiz(curpile: NPC) { + val asked = (0..QUESTIONS).shuffled(random).take(3) + var correct = 0 + + for ((index, q) in asked.withIndex()) { + val prefix = when (index) { + 0 -> "Ok, first question." + 1 -> "Ok, second question." + else -> "Ok, third and final question." + } + if (askQuestion(q, prefix)) { + correct++ + } + val ack = when (index) { + 0 -> "Ok, interesting answer. First question answered." + 1 -> "An interesting response. Second question answered." + else -> "Hmm, a calculated retort. Third question answered." + } + npc(ack) + } + + if (correct == 3) { + set("in_search_of_the_myreque", "answered_questions") + npc("Ok, I believes ya...you can go on.") + player("What's the combination to the door?") + npc("Oh, there isn't one. I'll unlock it for you.") + } else { + val word = if (correct == 1) "question" else "questions" + npc("I'm sorry but you got $correct $word correct! Go on get outta here...") + knockOutPlayer(curpile, correct) + } + } + + private suspend fun Player.askQuestion(questionIndex: Int, prefix: String): Boolean = when (questionIndex) { + 0 -> { + npc("$prefix What is the boatman's name?") + askBoatmanName() + } + 1 -> { + npc("$prefix Who is the leader of the Myreque?") + askMyrequeLeader() + } + 2 -> { + npc("$prefix Name the only female member of the Myreque.") + askFemaleMember() + } + 3 -> { + npc("$prefix Who is the youngest member of the Myreque?") + askYoungestMember() + } + 4 -> { + npc("$prefix What family is rumoured to rule Morytania?") + askRulingFamily() + } + else -> { + npc( + "$prefix Which member of the 'Myreque' originally had a profession as a scholar?", + ) + askScholar() + } + } + + private suspend fun Player.askBoatmanName(): Boolean { + var correct = false + choice("What is the boatman's name?") { + option("Geof Paddleman") {} + option("Cyreg Paddlebone") {} + option("Gyrec Paddlehorn") {} + option("Cyreg Paddlehorn") { correct = true } + option("I don't know!") {} + } + return correct + } + + private suspend fun Player.askMyrequeLeader(): Boolean { + var correct = false + choice("Who is the leader of the Myreque?") { + option("Sani Piliu") {} + option("Harold Evans") {} + option("Veliaf Hurtz") { correct = true } + option("Radigad Ponfit") {} + option("Don't know!") {} + } + return correct + } + + private suspend fun Player.askFemaleMember(): Boolean { + var correct = false + choice("Name the only female member of the Myreque?") { + option("Sani Piliu") { correct = true } + option("Santi Peliou") {} + option("Sani Peridou") {} + option("Sandi Pherimou") {} + option("Don't know!") {} + } + return correct + } + + private suspend fun Player.askYoungestMember(): Boolean { + var correct = false + choice("Who is the youngest member of the Myreque?") { + option("Sani Piliu") {} + option("Ivan Strom") { correct = true } + option("Veliaf Hurtz") {} + option("Radigad Ponfit") {} + option("Don't know!") {} + } + return correct + } + + private suspend fun Player.askRulingFamily(): Boolean { + var correct = false + choice("What family is rumoured to rule Morytania?") { + option("Drunken") {} + option("Drakan") { correct = true } + option("Draynor") {} + option("Praymore") {} + option("I don't know!") {} + } + return correct + } + + private suspend fun Player.askScholar(): Boolean { + var correct = false + choice("Who was previously a scholar?") { + option("Sani Piliu") {} + option("Harold Evans") {} + option("Radogad Ponfit") {} + option("Polmafi Ferdygris") { correct = true } + option("Don't know!") {} + } + return correct + } + + private suspend fun Player.sendPunch(curpile: NPC) { + curpile.anim("curpile_punch") + delay(1) + damage(30, source = curpile) + } + + private suspend fun Player.knockOutPlayer(curpile: NPC, correct: Int) { + val word = if (correct == 1) "question" else "questions" + message("Curpile Fyod: I'm sorry but you got $correct $word correct! Go on get outta here...") + curpile.anim("curpile_knockout_punch") + gfx("curpile_knockout_stars") + delay(2) + anim("myreque_knockout") + delay(1) + open("fade_out") + delay(3) + clearAnim() + tele(3522, 3285) + delay(2) + close("fade_out") + open("fade_in") + delay(3) + statement("Curpile knocked you out...You wake up back in Mort'ton...") + } + + private companion object { + private const val QUESTIONS = 5 + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/HaroldEvans.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/HaroldEvans.kt new file mode 100644 index 0000000000..7c166f02ea --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/HaroldEvans.kt @@ -0,0 +1,112 @@ +package content.area.morytania.mort_myre_swamp + +import content.entity.player.dialogue.Confused +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +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.name + +class HaroldEvans : Script { + init { + npcOperate("Talk-to", "harold_evans_meiyerditch_tunnels") { + when (questStage("in_search_of_the_myreque")) { + in myrequeStage("delivered_weapons")..myrequeStage("hellhound_summoned") -> { + message("This person isn't able to talk to you at this time.") + } + myrequeStage("entered_hideout") -> { + npc( + "Hail and well met friend! You'd better go and introduce yourself to Veliaf. " + + "But after come and have a chat if you've a mind to.", + ) + } + myrequeStage("met_veliaf"), myrequeStage("weapons_accepted") -> { + if (!get("met_harold", false)) { + set("met_harold", true) + npc( + "Well met friend! My name's Harold...nice to meet a fellow soldier. " + + "Just joined us have you?", + ) + player("Not exactly! I'd like to ask a few questions if that's Ok?") + soldierResponse() + } else { + npc("Well met $name! Here's to your strong sword arm!") + player("Do you mind if I ask a few questions?") + soldierResponse() + } + } + } + } + } + + private suspend fun Player.soldierResponse() { + npc("Sure go ahead, I'll answer as best I can.") + questionsChoice() + } + + suspend fun Player.questionsChoice() { + choice { + whatsYourJob() + tellMeAboutSquad() + graspOfSituation() + heardAnyRumors() + option("Ok, thanks.") + } + } + + fun ChoiceOption.whatsYourJob(): Unit = option("What's your job here?") { + npc( + "I generally do what Veliaf orders...taking a bridge defending a tactical retreat..." + + "you name it, I'm usually in the thick of it. It's funny, I seem to cheat death " + + "most of the time.", + ) + questionsChoice() + } + + fun ChoiceOption.tellMeAboutSquad(): Unit = option("What can you tell me about your squad?") { + npc( + "They're all good soldiers, though they're pretty much a bunch of irregulars. I " + + "consider them a good squad, ready to leap into the fray if needed, but we're " + + "seriously outnumbered by the Drakans and their blood.", + ) + npc( + "Most fights we have to improvise but this bunch are very good at that. That young " + + "lad over there, Ivan Strom, he saved our bacon in the last encounter by his " + + "faith in Saradomin, he weakened the Juves and we ran for it!", + ) + player("Juves? What are they?") + npc( + "It's short for 'juvenile', that's the stage that those vampires were at in the " + + "forest. They've only recently become vampiric and they're pretty inexperienced.", + ) + npc( + "However, they're stronger and faster than many well trained militia men, and twice " + + "as blood thirsty.", + ) + questionsChoice() + } + + fun ChoiceOption.graspOfSituation(): Unit = option("What's your grasp of the situation soldier?") { + npc( + "The situation is grave, it would be dangerous to think differently. We're really up " + + "against it here, but we have no choice, this is a battle for our right to exist " + + "and we need to employ any tactics to achieve our goal.", + ) + questionsChoice() + } + + fun ChoiceOption.heardAnyRumors(): Unit = option("Have you heard any rumors?") { + npc( + "Yes, I hear them all the time, thankfully I don't pay any attention to them, though.", + ) + questionsChoice() + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/IvanStrom.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/IvanStrom.kt new file mode 100644 index 0000000000..a7be5ad861 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/IvanStrom.kt @@ -0,0 +1,114 @@ +package content.area.morytania.mort_myre_swamp + +import content.entity.player.dialogue.Angry +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.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +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 + +class IvanStrom : Script { + init { + npcOperate("Talk-to", "ivan_strom_meiyerditch_tunnels") { + when (questStage("in_search_of_the_myreque")) { + in myrequeStage("delivered_weapons")..myrequeStage("hellhound_summoned") -> { + message("This person isn't able to talk to you at this time.") + } + myrequeStage("entered_hideout") -> { + npc( + "Oh...er... hello...greetings that is...sorry... you need to speak to Veliaf " + + "first. I'm not sure if I'm supposed to be talking to you.", + ) + } + myrequeStage("met_veliaf"), myrequeStage("weapons_accepted") -> { + if (get("met_ivan", false)) { + npc("Oh, hello again. How are you?") + questionsChoice() + } else { + firstMeeting() + } + } + else -> aftermath() + } + } + } + + private suspend fun Player.firstMeeting() { + player( + "Hi, Veliaf said that I should introduce myself to all members of the Myreque.", + ) + set("met_ivan", true) + npc( + "Ok, Veliaf says it's ok for me to talk to you. My name is Ivan, nice to meet you.", + ) + player("Would you mind if I ask you a few questions?") + npc("Yeah, I suppose so, but I don't know how much I can help.") + questionsChoice() + } + + suspend fun Player.questionsChoice() { + choice { + whatAreYouDoing() + tellMeAboutMyreque() + aboutMorytania() + heardRumors() + option("Ok, thanks.") + } + } + + fun ChoiceOption.whatAreYouDoing(): Unit = option("What are you doing here?") { + npc( + "I'm not sure really, I should really be at the temple to Saradomin, but Veliaf says " + + "that the trip's too dangerous. I am a trainee cleric of Saradomin. I hope one day " + + "to serve him as faithfully as all his priests.", + ) + questionsChoice() + } + + fun ChoiceOption.tellMeAboutMyreque(): Unit = option("What can you tell me about the Myreque?") { + npc( + "Well, they're certainly brave and they're prepared to put up a fight against the " + + "evil of the Drakan clan. I'm proud to be associated with them, but with my poor " + + "skills I worry that I will let them down.", + ) + questionsChoice() + } + + fun ChoiceOption.aboutMorytania(): Unit = option("What do you know about Morytania?") { + npc( + "It's a lost land ruled by the evil Drakans. People cling to each other in fear every " + + "week when the blood tithes are to be paid. With Saradomin's help we'll bring " + + "light back to this dark land.", + ) + questionsChoice() + } + + fun ChoiceOption.heardRumors(): Unit = option("Have you heard any rumors?") { + npc( + "I'm sorry my friend, but I spend most of my free time in prayer to Saradomin. " + + "I prefer to cleanse my soul in case he should wish that I serve him closer.", + ) + questionsChoice() + } + + private suspend fun Player.aftermath() { + npc( + "Oh, thank Saradomin you managed to kill that awful beast! Oh my, I'm in so much " + + "shock after seeing Sani and Harold killed right in front of my own eyes.", + ) + player( + "I'm so sorry for your loss. If there's anything I can do, please let me know.", + ) + npc( + "I think you've done enough already, bringing that monster Vanstrom here!", + ) + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MortMyreSwamp.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MortMyreSwamp.kt deleted file mode 100644 index 94c7fc17b1..0000000000 --- a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MortMyreSwamp.kt +++ /dev/null @@ -1,37 +0,0 @@ -package content.area.morytania.mort_myre_swamp - -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.message -import world.gregs.voidps.engine.entity.character.move.tele -import world.gregs.voidps.engine.entity.character.sound -import world.gregs.voidps.engine.entity.obj.replace -import world.gregs.voidps.type.Direction -import world.gregs.voidps.type.Tile - -class MortMyreSwamp : Script { - init { - objectOperate("Open", "swamp_wooden_doors_closed") { (target) -> - anim("take") - sound("locked") - delay(1) - target.replace("swamp_wooden_doors_opened", ticks = 3) - delay(2) - tele(3500, 9811) - } - - objectOperate("Climb", "swamp_bridge_tree") { (target) -> - val direction = if (target.tile.y == 3431) Direction.SOUTH else Direction.NORTH - anim("climb_up") - delay(2) - tele(target.tile.add(direction)) - message("You climb up the tree.") - face(direction) - delay(1) - walkOverDelay(target.tile.add(direction).add(direction).add(direction).add(direction)) - message("You climb down the tree.") - anim("climb_down") - delay(1) - tele(if (direction == Direction.SOUTH) Tile(3502, 3425) else Tile(3503, 3431)) - } - } -} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MyrequeStranger.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MyrequeStranger.kt new file mode 100644 index 0000000000..25a393bfe1 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/MyrequeStranger.kt @@ -0,0 +1,64 @@ +package content.area.morytania.mort_myre_swamp + +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Confused +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Shock +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +import content.quest.questComplete +import content.quest.questStage +import content.quest.refreshQuestJournal +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.jingle +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.event.AuditLog + +class MyrequeStranger : Script { + init { + npcOperate("Talk-to", "stranger_3") { + val progress = questStage("in_search_of_the_myreque") + if (progress !in myrequeStage("delivered_weapons")..myrequeStage("escaped_to_canifis")) { + return@npcOperate npc("I think we've been through this before haven't we?") + } + npc("Hello friend? What can I do for you?") + player("Oh sorry, I thought you were someone else.") + if (progress != myrequeStage("escaped_to_canifis")) { + return@npcOperate + } + npc("Who did you think I was?") + player("I thought you were that dirty murderer Vanstrom.") + npc("Woah, it sounds like you've got a score to settle!") + player("I definitely have!") + completeQuest() + } + } + + private fun Player.completeQuest() { + jingle("quest_complete_1") + exp(Skill.Attack, 600.0) + exp(Skill.Defence, 600.0) + exp(Skill.Strength, 600.0) + exp(Skill.Constitution, 600.0) + exp(Skill.Crafting, 600.0) + inc("quest_points", 2) + AuditLog.event(this, "quest_completed", "in_search_of_the_myreque") + set("in_search_of_the_myreque", "completed") + refreshQuestJournal() + questComplete( + "In Search of the Myreque", + "2 Quest Points", + "600 Attack XP", + "600 Defence XP", + "600 Strength XP", + "600 Constitution XP", + "600 Crafting XP", + "A quick route to Mort'ton", + item = "steel_sword", + ) + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/PolmafiFerdygris.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/PolmafiFerdygris.kt new file mode 100644 index 0000000000..6a183256df --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/PolmafiFerdygris.kt @@ -0,0 +1,123 @@ +package content.area.morytania.mort_myre_swamp + +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.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +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 + +class PolmafiFerdygris : Script { + init { + npcOperate("Talk-to", "polmafi_ferdygris") { + when (questStage("in_search_of_the_myreque")) { + in myrequeStage("delivered_weapons")..myrequeStage("hellhound_summoned") -> { + message("This person isn't able to talk to you at this time.") + } + myrequeStage("entered_hideout") -> { + npc( + "Good day. You should quickly introduce yourself to Veliaf and after that " + + "I'll be happy to have a chat with you.", + ) + } + myrequeStage("met_veliaf"), myrequeStage("weapons_accepted") -> { + if (!get("met_polmafi", false)) { + set("met_polmafi", true) + npc( + "Good! Now that you've introduced yourself to Veliaf and observed proper " + + "protocol, I'll be happy to chat with you. Now, please tell me, what " + + "can I do for you?", + ) + player("Do you mind if I ask a few questions?") + } else { + npc("Warmest salutations of the day my friend! How are you?") + player("Fine thanks. I'd like to ask a few questions if that's Ok?") + } + npc("Not at all, I'm sure I'll be able to help and if I can't I'll tell you so.") + questionsChoice() + } + else -> aftermath() + } + } + } + + suspend fun Player.questionsChoice() { + choice { + whatAreYouDoing() + tellMeAboutMyreque() + aboutMorytania() + heardRumors() + option("Ok, thanks.") + } + } + + fun ChoiceOption.whatAreYouDoing(): Unit = option("What are you doing here?") { + npc( + "I am the key advisor to Veliaf, he seeks my council on many issues before we engage " + + "the enemy. Not that we would be doing that any time soon, the situation is " + + "somewhat difficult and terrifying at the moment.", + ) + npc( + "However, we must always remember, 'All that is required for evil to survive is for " + + "good people to do nothing.'", + ) + questionsChoice() + } + + fun ChoiceOption.tellMeAboutMyreque(): Unit = option("What can you tell me about the Myreque?") { + npc( + "Well, we have a hierarchical chain of command. Veliaf reports to Calsidiu who is " + + "considered the head of the armed resistance against the Drakans.", + ) + npc( + "I've also heard people talking about a spiritual man, who inspires many people to " + + "join the resistance. He's said to be very influential with supposed contacts " + + "within the Drakan clan.", + ) + questionsChoice() + } + + fun ChoiceOption.aboutMorytania(): Unit = option("What do you know about Morytania?") { + npc( + "It has a rugged landscape,, situated to the east of the River Salve. Primary " + + "occupants seem to be human in apperance, but are in fact large vampiric predators.", + ) + npc( + "They maintain a stock of human villages in the same way that humans keep cattle. " + + "Weekly blood tithes are paid to the Vampyric overlord Drakan in return for " + + "moderate safety.", + ) + questionsChoice() + } + + fun ChoiceOption.heardRumors(): Unit = option("Have you heard any rumors?") { + npc( + "Yes, I hear them all the time, they're neither correct or even all that interesting, " + + "I suspect that we will be looking to make a strike against the human " + + "sympathisers some time soon.", + ) + questionsChoice() + } + + private suspend fun Player.aftermath() { + npc( + "I'm so sad since Sani and Harold were killed. The Myreque will be lonelier without " + + "them. Still, they didn't suffer at least.", + ) + player( + "My commiserations for your loss. I wish that there was something I could do to help.", + ) + npc( + "Don't worry. No one blames you. It could have been anyone that was tricked by him. " + + "Whilst I'm sad at the loss of my comrades, it serves no useful purpose to blame " + + "anyone for it.", + ) + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/RadigadPonfit.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/RadigadPonfit.kt new file mode 100644 index 0000000000..21e40f45e7 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/RadigadPonfit.kt @@ -0,0 +1,115 @@ +package content.area.morytania.mort_myre_swamp + +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.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +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.name + +class RadigadPonfit : Script { + init { + npcOperate("Talk-to", "radigad_ponfit") { + when (questStage("in_search_of_the_myreque")) { + in myrequeStage("delivered_weapons")..myrequeStage("hellhound_summoned") -> { + message("This person isn't able to talk to you at this time.") + } + myrequeStage("entered_hideout") -> { + npc( + "Greetings friend! Once you've introduced yourself to Veliaf please feel " + + "free to chat with me...", + ) + } + myrequeStage("met_veliaf"), myrequeStage("weapons_accepted") -> { + if (!get("met_radigad", false)) { + set("met_radigad", true) + npc( + "Ah it's $name. I see you've introduced yourself to Veliaf, " + + "good! Now, how can I be of service to you?", + ) + player("Do you mind if I ask a few questions?") + } else { + npc( + "Hello again! Radigad at your service. Are you planning to join the fray " + + "against the Morytania monster and the rest of the Drakans?", + ) + player("Maybe! I'd like to ask a few questions if that's Ok?") + } + npc("Sure, go ahead, I'll answer as best I can.") + questionsChoice() + } + else -> aftermath() + } + } + } + + suspend fun Player.questionsChoice() { + choice { + whatsYourJob() + tellMeAboutMyreque() + futurePlans() + heardRumors() + option("Ok, thanks.") + } + } + + fun ChoiceOption.whatsYourJob(): Unit = option("What's your job here?") { + npc( + "I help where I can. I'm a bit of an all rounder. I try to help Veliaf as much as " + + "I can. I've relied on his sword arm many a time, I like to think that he can " + + "rely on mine.", + ) + questionsChoice() + } + + fun ChoiceOption.tellMeAboutMyreque(): Unit = option("What can you tell me about the Myreque?") { + npc( + "We're just developing the resistance against the Drakans and the rest of the " + + "Vampiric entourage. We're seriously out numbered, Veliaf doesn't like to hear " + + "us saying 'hopeless' but that's our situation at the minute.", + ) + questionsChoice() + } + + fun ChoiceOption.futurePlans(): Unit = option("What are your plans for the future?") { + npc( + "I'm not really sure, I guess Veliaf might know that, however I hope that more " + + "people will join the ranks and take the fight to the dead hearts of our " + + "Vampiric foe so that we might one day reclaim our right to exist.", + ) + questionsChoice() + } + + fun ChoiceOption.heardRumors(): Unit = option("Have you heard any rumors?") { + npc( + "I have. Something you might find interesting is that our enigmatic leader 'Safalaan' " + + "has connections inside the Drakan Clan. I'm not sure how true that is but it " + + "would be great!", + ) + questionsChoice() + } + + private suspend fun Player.aftermath() { + player("Hello, are you ok?") + npc( + "Yeah, I'm Ok thanks. You certainly proved yourself against that unearthly monster " + + "that Vanstrom conjured. I'm sorry I can't be more upbeat, but I'm very sad " + + "about Harold and Sani. It's terrible to see your comrades cut down in combat, " + + "especially those two. They were still so very young.", + ) + } + + private fun getMyrequeMember(player: Player, index: Int): Boolean = player["myreque_member_$index", false] + + private fun setMyrequeMember(player: Player, index: Int, value: Boolean) { + player["myreque_member_$index"] = value + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SaniPiliu.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SaniPiliu.kt new file mode 100644 index 0000000000..394d005777 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SaniPiliu.kt @@ -0,0 +1,133 @@ +package content.area.morytania.mort_myre_swamp + +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.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.member.myreque.myrequeStage +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.name + +class SaniPiliu : Script { + init { + npcOperate("Talk-to", "sani_piliu_meiyerditch_tunnels") { + when (questStage("in_search_of_the_myreque")) { + in myrequeStage("delivered_weapons")..myrequeStage("hellhound_summoned") -> { + message("This person isn't able to talk to you at this time.") + } + myrequeStage("entered_hideout") -> { + npc( + "Hi there...you're new here aren't you? You'd best go and talk with Veliaf. " + + "But afterwards, I'd love to introduce myself properly.", + ) + } + myrequeStage("met_veliaf"), myrequeStage("weapons_accepted") -> { + if (!get("met_sani", false)) { + firstMeeting() + } else { + npc( + "Oh, hello $name nice to see you again! How's things?", + ) + player("Fine thanks, do you mind if I ask a few questions?") + npc("Sure, go ahead, I'll answer as best I can.") + questionsChoice() + } + } + } + } + } + + private suspend fun Player.firstMeeting() { + npc( + "Hello...very nice to meet you! My name is Sani Piliu, what's yours?", + ) + player("I am known as $name nice to meet you Sani.") + set("met_sani", true) + npc( + "The pleasure's all mine! Excuse me for being forward, but you somehow seem different " + + "to other people, somehow more determined. I sense a powerful aura. Sorry, that " + + "must seem really strange?", + ) + player( + "Well, perhaps a little but it's nice of you to say so in any case. I'd like to ask " + + "a few questions if that's ok?", + ) + npc("Sure, go ahead, I'll answer as best I can.") + questionsChoice() + } + + suspend fun Player.questionsChoice() { + choice { + tellAboutYourself() + tellMeAboutMyreque() + whatsGoingOn() + mainCamp() + option("Ok, thanks.") + } + } + + fun ChoiceOption.tellAboutYourself(): Unit = option("Tell me a bit about yourself.") { + npc( + "There's not much to tell really. I was about nine years old when my family was " + + "ripped to pieces by a vampire attack, one day a happy member of a loving " + + "family, the next day an orphan living a hand to mouth existence.", + ) + npc( + "Somehow I managed to get by, eking out a living where ever I could. People took " + + "pity on me, I earnt food by helping people out, before too long I was quite " + + "adept at helping myself, other people appreciated my skills.", + ) + npc( + "I bumped into Veliaf in Canifis. He saw that I would be an asset to the resistance " + + "so I joined. I want to fight back against those killers who took my family. " + + "One day I may escape to the west across the Salve! Who knows?", + ) + questionsChoice() + } + + fun ChoiceOption.tellMeAboutMyreque(): Unit = option("What can you tell me about the Myreque?") { + npc( + "As far as I know Veliaf is one of two Lieutenants who report to another soldier " + + "called Calsidiu. While we don't advertise the fact, there are quite a lot of " + + "people in Morytania who want to put up a fight.", + ) + npc( + "Our current operations are merely trying to find a weakness in the Drakans' " + + "hierarchy amd perhaps get a spy in there. We need to find out how to kill " + + "them before we then take the war to them.", + ) + player("You make it sound easy!") + npc( + "With determination you can achieve great things. However, Veliaf always tells us " + + "not to underestimate the enemy. After those juvenile vampires discovered us " + + "in the forest, it's a mistake I won't be making again.", + ) + questionsChoice() + } + + fun ChoiceOption.whatsGoingOn(): Unit = option("What's going on in Morytania?") { + npc( + "Lowernial Drakan and his siblings collect blood tithes from the locals as a way of " + + "controlling the population and keeping his brood happy. Sanguinesti and Castle " + + "Drakan hold sway over all the inhabitants of Morytania.", + ) + npc( + "We're nothing more than an ill ogranised rabble, but we're determined to fight " + + "against these evil monsters. We won't just offer our family members to the " + + "Drakans for a weekly snack, we're going to fight back!", + ) + questionsChoice() + } + + fun ChoiceOption.mainCamp(): Unit = option("Is this your main camp?") { + npc("It's our only camp! At least, I don't know of any others.") + questionsChoice() + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SwampyBoat.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SwampyBoat.kt index 47eec3f063..c613d246e2 100644 --- a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SwampyBoat.kt +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SwampyBoat.kt @@ -1,5 +1,7 @@ package content.area.morytania.mort_myre_swamp +import content.area.morytania.mort_ton.earlyCyregOptions +import content.entity.player.dialogue.Angry import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz import content.entity.player.dialogue.type.choice @@ -7,13 +9,17 @@ import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.statement import content.entity.player.modal.Tab import content.quest.closeTabs +import content.quest.member.myreque.myrequeStage import content.quest.openTabs -import content.quest.questCompleted +import content.quest.questStage import net.pearx.kasechange.toSnakeCase +import world.gregs.voidps.cache.definition.data.InterfaceDefinition import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.Minimap import world.gregs.voidps.engine.client.clearMinimap import world.gregs.voidps.engine.client.minimap +import world.gregs.voidps.engine.client.sendScript +import world.gregs.voidps.engine.client.ui.close import world.gregs.voidps.engine.client.ui.dialogue.talkWith import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.entity.character.jingle @@ -36,11 +42,12 @@ class SwampyBoat : Script { objectOperate("Board", "swamp_boat_mort_ton") { talkWith(cyreg()) - if (!questCompleted("in_search_of_the_myreque")) { - npc("Hey, hands off my boat!") + if (questStage("in_search_of_the_myreque") <= myrequeStage("persuaded_boatman")) { + npc("Hey, hands off my boat!") + earlyCyregOptions() return@objectOperate } - npc("I'll have to charge you 10 gold to cover the loan of the boat. Is that okay?") + npc("It costs 10 gold to cover the loan of the boat. Will you pay?") choice { option("Yes. I'll pay the ten gold.") { if (!inventory.remove("coins", 10)) { @@ -61,8 +68,9 @@ class SwampyBoat : Script { objectOperate("Board ( Pay 10 )", "swamp_boat_mort_ton") { talkWith(cyreg()) - if (!questCompleted("in_search_of_the_myreque")) { - npc("Hey, hands off my boat!") + if (questStage("in_search_of_the_myreque") <= myrequeStage("persuaded_boatman")) { + npc("Hey, hands off my boat!") + earlyCyregOptions() return@objectOperate } if (!inventory.remove("coins", 10)) { @@ -76,22 +84,30 @@ class SwampyBoat : Script { private fun cyreg(): NPC = NPCs.findOrNull(Region(14131).toLevel(0), "cyreg_paddlehorn") ?: NPCs.find(Region(13875).toLevel(0), "cyreg_paddlehorn") private suspend fun Player.hollows() { - travel("the Hollows", Tile(3498, 3380)) + travel("the Hollows", Tile(3498, 3380)) { + if (questStage("in_search_of_the_myreque") == myrequeStage("gave_planks")) { + set("in_search_of_the_myreque", "reached_hollows") + } + } } - private suspend fun Player.travel(name: String, tile: Tile) { + private suspend fun Player.travel(name: String, tile: Tile, onArrival: Player.() -> Unit = {}) { open("fade_out") delay(1) closeTabs(Tab.Options, Tab.MusicPlayer) jingle("morytania_boatride") open("swamp_boat_journey") + open("total_blackness") + sendScript("text_colour_swapper", InterfaceDefinition.pack(333, 0), 0x433621) minimap(Minimap.HideMap) interfaces.sendAnimation("swamp_boat_journey", "boat", "boat_journey_${name.replace("'", "_").toSnakeCase()}") statement("You board the boat and journey to $name.", clickToContinue = false) delay(14) tele(tile) + onArrival() delay(1) clearMinimap() + close("total_blackness") openTabs(Tab.Options, Tab.MusicPlayer) open("fade_in") statement("You arrive in $name.") diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/VeliafHurtz.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/VeliafHurtz.kt new file mode 100644 index 0000000000..5cc8c7fcf3 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/VeliafHurtz.kt @@ -0,0 +1,447 @@ +package content.area.morytania.mort_myre_swamp + +import content.entity.combat.hit.damage +import content.entity.effect.transform +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Evil +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.type.ChoiceOption +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.entity.proj.shoot +import content.quest.member.myreque.checkMembers +import content.quest.member.myreque.hasAllWeapons +import content.quest.member.myreque.myrequeStage +import content.quest.member.myreque.spawnHellHound +import content.quest.questStage +import content.quest.setInstanceLogout +import content.quest.startCutscene +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.clearCamera +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.moveCamera +import world.gregs.voidps.engine.client.turnCamera +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.entity.character.mode.PauseMode +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.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.get +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Tile + +class VeliafHurtz : Script { + init { + npcOperate("Talk-to", "veliaf_hurtz_meiyerditch_tunnels") { + when (questStage("in_search_of_the_myreque")) { + myrequeStage("delivered_weapons") -> playMyrequeCutscene() + myrequeStage("entered_hideout") -> firstMeeting() + myrequeStage("met_veliaf") -> { + if (!checkMembers()) { + npc("Have you introduced yourself to the rest of the gang?") + introducedYourselfChoice() + } else { + npc( + "Hello again...so, you've introduced yourself to the team have you? Good. " + + "Now, let's have a look at those weapons you brought us.", + ) + if (hasAllWeapons()) { + acceptWeapons() + } else { + wrongWeapons() + } + } + } + myrequeStage("weapons_accepted") -> { + if (hasAllWeapons()) { + deliverWeaponsAndStartCutscene() + } else { + wrongWeapons() + } + } + myrequeStage("hellhound_summoned") -> { + message("Veliaf can't talk right now, he seems to be formulating a plan.") + } + else -> postCutsceneIntro() + } + } + } + + private suspend fun Player.firstMeeting() { + player("Hello there...") + npc("Hello there...how did you get in here? Who are you?") + player( + "I was asked to bring you some weapons...apparently you need them? Don't worry, " + + "I'm a friend.", + ) + set("in_search_of_the_myreque", "met_veliaf") + npc( + "Hmm, well, it's true we could do with some supplies. It's very good of you to bring " + + "them to us. I'm sorry but I'm a bit busy at the moment, please introduce yourself " + + "to the others, then we can talk about those weapons.", + ) + firstMeetingChoice() + } + + suspend fun Player.firstMeetingChoice() { + choice { + areYouLeader() + tellMeAboutOrganization() + tellMeAboutMorytania() + futurePlans() + option("Ok, thanks.") + } + } + + fun ChoiceOption.areYouLeader(): Unit = option("So are you the leader?") { + npc( + "Well, as much as anyone is, it's not official or anything, I guess the others just " + + "look up to me or something. I feel a certain responsibility for them so I guess " + + "that means I'm a leader of sorts.", + ) + firstMeetingChoice() + } + + fun ChoiceOption.tellMeAboutOrganization(): Unit = option("Can you tell me about your organization?") { + npc( + "Huh, organization? That's rich! We're really not that organized, however, the " + + "Myreque are dedicated to mounting a resistance against the Drakans and the evil " + + "that they've brough to Morytania.", + ) + firstMeetingChoice() + } + + fun ChoiceOption.tellMeAboutMorytania(): Unit = option("What can you tell me about Morytania?") { + npc( + "I can tell you that it used to be a beautiful country before Lowerniel and his " + + "quarelling siblings arrived.", + ) + player("Lowerniel? Who's that?") + npc( + "Lowerniel Vergidiyad Drakan - he's the head of the Drakan family and along with his " + + "brother Ranis and his sister Vanescula they play out their petty backstabbing " + + "games using the peoples of Morytania as their pawns.", + ) + player("You sound very bitter.") + npc( + "So would you if you lost everyone dear to you at the hands of such monsters.", + ) + firstMeetingChoice() + } + + fun ChoiceOption.futurePlans(): Unit = option("What are your plans for the future?") { + npc( + "At the moment we're maintaining a low profile after we were spotted scouting out " + + "the Drakans' castle. The forest where we camped is infested with vampire, and " + + "even though they're only juveniles in terms of experience, we only just managed " + + "to survive.", + ) + npc( + "After we've regrouped, we will try to recruit more people to our cause, at the " + + "minute we're hopelessly outnumberd.", + ) + firstMeetingChoice() + } + + suspend fun Player.introducedYourselfChoice() { + choice { + askQuestionsFirst() + goIntroduceNow() + whoElseToMeet() + } + } + + fun ChoiceOption.askQuestionsFirst(): Unit = option("I wanted to ask you some questions first.") { + npc("Well, ok, but only briefly, I have things to do.") + firstMeetingChoice() + } + + fun ChoiceOption.goIntroduceNow(): Unit = option("Not yet, I'll go do that now.") { + npc("Ok, great.") + } + + fun ChoiceOption.whoElseToMeet(): Unit = option("Who else do I have to introduce myself to?") { + val sani = if (get("met_sani", false)) "Sani" else "Sani" + val harold = if (get("met_harold", false)) "Harold" else "Harold" + val radigad = if (get("met_radigad", false)) "Radigad" else "Radigad" + val polmafi = if (get("met_polmafi", false)) "Polmafi" else "Polmafi" + val ivan = if (get("met_ivan", false)) "Ivan" else "Ivan" + statement("$sani
$harold
$radigad
$polmafi
$ivan") + } + + private suspend fun Player.wrongWeapons() { + npc( + "Well, those weapons aren't that useful. We really need specific ones, we have our " + + "own specialism.", + ) + weaponChoice() + } + + suspend fun Player.weaponChoice() { + choice { + goGetWeapons() + whichWeapons() + } + } + + fun ChoiceOption.goGetWeapons(): Unit = option("Ok, I'll go get them.") { + npc("Ok, I'd appreciate that.") + } + + fun ChoiceOption.whichWeapons(): Unit = option("Which weapons do you need?") { + npc("Here, I've compiled a list.") + showWeaponList() + } + + private suspend fun Player.acceptWeapons() { + set("in_search_of_the_myreque", "weapons_accepted") + npc("These weapons look great! Many thanks.") + deliverWeaponsAndStartCutscene() + } + + private suspend fun Player.deliverWeaponsAndStartCutscene() { + inventory.remove("steel_longsword", 1) + inventory.remove("steel_dagger", 1) + inventory.remove("steel_mace", 1) + inventory.remove("steel_warhammer", 1) + inventory.remove("steel_sword", 2) + set("thsfm_vanstrom_hide", true) + set("in_search_of_the_myreque", "delivered_weapons") + playMyrequeCutscene() + } + + private suspend fun Player.showWeaponList() { + val longsword = weapon(inventory.contains("steel_longsword"), "1 Steel ", "Longsword") + val shortSword = weapon(inventory.contains("steel_sword", 2), "2 Steel ", "Short Swords") + val dagger = weapon(inventory.contains("steel_dagger"), "1 Steel ", "Dagger") + val mace = weapon(inventory.contains("steel_mace"), "1 Steel ", "Mace") + val warhammer = weapon(inventory.contains("steel_warhammer"), "1 Steel ", "Warhammer") + statement("$longsword $shortSword $dagger $mace $warhammer") + } + + private fun weapon(has: Boolean, prefix: String, name: String): String = if (has) { + "$prefix$name" + } else { + "$prefix$name" + } + + private suspend fun Player.playMyrequeCutscene() { + open("fade_out") + val cutscene = startCutscene("myreque_ambush", AMBUSH_BASE, width = 6, height = 3) + setInstanceLogout(VELIAF_SPOT) + cutscene.onEnd { + clearCamera() + } + delay(4) + + val veliaf = NPCs.add("veliaf_hurtz_meiyerditch_tunnels", cutscene.convert(VELIAF_SPOT), Direction.SOUTH) + val harold = NPCs.add("harold_evans_meiyerditch_tunnels", cutscene.convert(HAROLD_SPOT), Direction.SOUTH) + val sani = NPCs.add("sani_piliu_meiyerditch_tunnels", cutscene.convert(SANI_SPOT), Direction.SOUTH) + val radigad = NPCs.add("radigad_ponfit", cutscene.convert(RADIGAD_SPOT), Direction.EAST) + for (member in listOf(veliaf, harold, sani, radigad)) { + member.mode = PauseMode + } + tele(cutscene.convert(PLAYER_SPOT), clearInterfaces = false) + delay(1) + veliaf.face(cutscene.tile(3506, 9837, 2)) + harold.face(cutscene.tile(3504, 9836, 2)) + sani.face(cutscene.tile(3508, 9836, 2)) + radigad.face(cutscene.tile(3511, 9831, 2)) + + moveCamera(cutscene.tile(3506, 9846, 2), 750) + turnCamera(cutscene.tile(3506, 9834, 2), 125) + open("fade_in") + delay(3) + face(cutscene.convert(VANSTROM_SPOT)) + moveCamera(cutscene.tile(3508, 9836, 2), 325) + turnCamera(cutscene.tile(3502, 9842, 2), 125) + npc( + "veliaf_hurtz_meiyerditch_tunnels", + """ + Hey, what's that mist coming through the door! + WE'VE A VAMPIRE IN THE ROOM! + """, + ) + + moveCamera(cutscene.tile(3506, 9846, 2), 750) + turnCamera(cutscene.tile(3506, 9834, 2), 125) + message("You see a shifting mist enter the room!") + sound("vanstrom_mist") + val mist = NPCs.add("route_vanstrom_vampire_misty", cutscene.tile(3504, 9832, 2), Direction.SOUTH) + delay(1) + mist.walkTo(cutscene.tile(3505, 9835, 2)) + delay(3) + NPCs.remove(mist) + sound("vanstrom_appear") + + val vanstrom = NPCs.add("route_vanstrom", cutscene.convert(VANSTROM_SPOT), Direction.SOUTH) + vanstrom.mode = PauseMode + vanstrom.face(cutscene.convert(PLAYER_SPOT)) + face(cutscene.convert(VANSTROM_SPOT)) + vanstrom.say("Ha...ha...ha... you took me straight to them!") + message("Vanstrom Klause: Ha...ha...ha...you took me straight to them!") + npc("stranger_2", "Ha...ha...ha... you took me straight to them!") + vanstrom.say("The little dears are going to wonder which side you're on!") + message("Vanstrom Klause: The little dears are going to wonder which side you're on!") + npc("stranger_2", "The little dears are going to wonder which side you're on!") + + moveCamera(cutscene.tile(3511, 9839, 2), 325) + turnCamera(cutscene.tile(3506, 9837, 2), 125) + face(cutscene.convert(SANI_SPOT)) + vanstrom.face(cutscene.convert(SANI_SPOT)) + sani.face(cutscene.convert(VANSTROM_SPOT)) + sani.say("It's Vanstrom...we're dead...") + message("Sani Piliu: It's Vanstrom...we're dead...") + npc("sani_piliu_meiyerditch_tunnels", "It's Vanstrom...we're dead...") + + face(cutscene.convert(VANSTROM_SPOT)) + vanstrom.strike(sani, cutscene.convert(VANSTROM_SPOT)) + sani.say("Ahhhhhh!") + vanstrom.say("Quite right...") + message("Vanstrom Klause: Quite right....") + npc("stranger_2", "Quite right...") + + turnCamera(cutscene.tile(3506, 9838, 2), 125, speed = 3, acceleration = 3) + delay(1) + turnCamera(cutscene.tile(3506, 9837, 2), 125, speed = 3, acceleration = 3) + vanstrom.face(cutscene.convert(HAROLD_SPOT)) + vanstrom.strike(harold, cutscene.convert(VANSTROM_SPOT).add(-1, 0)) + harold.say("Auugh!") + vanstrom.anim("vanstrom_transform") + vanstrom.transform("route_vanstrom_vampire") + vanstrom.say("Sorry, Harold, you too!") + message("Vanstrom Klause: Sorry Harold, you too!") + npc("stranger_2", "Sorry, Harold, you too!") + + delay(2) + vanstrom.transform("route_vanstrom_vampire_flying") + vanstrom.anim("vanstrom_hover") + sound("vanstrom_vampire") + vanstrom.say("And now I'm going to finish the rest of them off.") + message("Vanstrom Klause: And... now I'm going to finish the rest of them off...") + npc("route_vanstrom_vampire", "And now I'm going to finish the rest of them off.") + + vanstrom.face(cutscene.convert(PLAYER_SPOT)) + vanstrom.say("With my little pet!") + message("Vanstrom Klause: With my little pet!") + npc("route_vanstrom_vampire", "With my little pet!") + vanstrom.transform("route_vanstrom_vampire_misty") + vanstrom.say("Ha ha ha ha haaaaaa!") + sound("vanstrom_mist") + delay(2) + vanstrom.walkOverDelay(cutscene.tile(3504, 9832, 2)) + NPCs.remove(vanstrom) + message("Vanstrom Klause: Ha ha ha ha haaaaaa!") + npc("route_vanstrom_vampire", "Ha ha ha ha haaaaaa!") + + open("fade_out") + delay(2) + set("in_search_of_the_myreque", "hellhound_summoned") + tele(HOUND_SPOT) + cutscene.end() + spawnHellHound(this) + } + + private fun NPC.strike(target: NPC, from: Tile) { + anim("thrown_accurate") + gfx("iron_knife_throw") + from.shoot("iron_knife", target) + target.anim("myreque_knockout") + target["absorb_damage"] = 0 + target.damage(300, source = this) + } + + private suspend fun Player.postCutsceneIntro() { + npc( + "That murderer Vanstrom has killed Sani and Harold! He came in and killed them right " + + "in front of our eyes. And that beast he summoned probably would have killed the " + + "rest of us if it wasn't for you!", + ) + player( + "It was the least I could do! After all, I was the one he followed here, I'm sort of " + + "responsible.", + ) + npc( + "That is for your conscience to bear, but now perhaps you can understand why we " + + "fight for our freedom?", + ) + player("Yes, I think I understand now.") + aftermathChoice() + } + + suspend fun Player.aftermathChoice() { + choice { + joinOrganisation() + whatNext() + howToGetOut() + getRevenge() + option("Ok, thanks.") + } + } + + fun ChoiceOption.joinOrganisation(): Unit = option("I want to join your organisation.") { + npc( + "Hmm, well thanks for your support. But I'll need to talk to my superior first.", + ) + aftermathChoice() + } + + fun ChoiceOption.whatNext(): Unit = option("What do you plan to do now?") { + npc( + "Well, I guess Vanstrom will expect that we're dead. He's so arrogant that he doesn't " + + "really consider us a threat. I can understand that, if I was that powerful I " + + "would probably feel the same.", + ) + npc( + "I guess we'll regroup and then try to recruit more people. We first need to know " + + "how we can fight against these monsters, then we'll take the war to them!", + ) + aftermathChoice() + } + + fun ChoiceOption.howToGetOut(): Unit = option("How do I get out of here?") { + npc( + "If you go back into the main corridor which leads into this room, it goes down " + + "towards the basement of an Inn.", + ) + if (questStage("in_search_of_the_myreque") == myrequeStage("killed_hellhound")) { + set("in_search_of_the_myreque", "shown_way_out") + } + npc( + "If you search the wall, you'll find that it is in fact moveable. You should be able " + + "to get into the room, take the ladder to come out to the south of the " + + "'Hair of the Dog Tavern' in Canifis.", + ) + aftermathChoice() + } + + fun ChoiceOption.getRevenge(): Unit = option("Let's get revenge on Vanstrom!") { + npc( + "Yes, of course, we'll certainly get our revenge on him and his superiors! Then " + + "they'll taste the bitter sorrow that we've had to endure so long!", + ) + aftermathChoice() + } + + private companion object { + private val AMBUSH_BASE = Tile(3496, 9824) + + private val PLAYER_SPOT = Tile(3507, 9838, 2) + private val VELIAF_SPOT = Tile(3506, 9838, 2) + private val SANI_SPOT = Tile(3508, 9837, 2) + private val HAROLD_SPOT = Tile(3504, 9837, 2) + private val RADIGAD_SPOT = Tile(3510, 9831, 2) + private val VANSTROM_SPOT = Tile(3506, 9836, 2) + + private val HOUND_SPOT = Tile(3507, 9838, 0) + } +} diff --git a/game/src/main/kotlin/content/area/morytania/mort_ton/CyregPaddlehorn.kt b/game/src/main/kotlin/content/area/morytania/mort_ton/CyregPaddlehorn.kt index 6c30c1ee36..35c9659baf 100644 --- a/game/src/main/kotlin/content/area/morytania/mort_ton/CyregPaddlehorn.kt +++ b/game/src/main/kotlin/content/area/morytania/mort_ton/CyregPaddlehorn.kt @@ -1,22 +1,404 @@ package content.area.morytania.mort_ton +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Confused +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.Scared +import content.entity.player.dialogue.type.ChoiceOption +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.quest.member.myreque.hasAllWeapons +import content.quest.member.myreque.myrequeStage import content.quest.questCompleted +import content.quest.questStage import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove class CyregPaddlehorn : Script { init { npcOperate("Talk-to", "cyreg_paddlehorn") { - if (questCompleted("in_search_of_the_myreque")) { - player("Thanks for your help in finding the Myreque.") - npc("Well, I'd like to say that you're welcome. But I heard that you led Vanstrom straight to their hideout and he killed Sani and Harold. I feel so guilty.") - player("Hmm, yeah, I know how you feel.") - } else { - npc("Hello there friend. I'm afraid all boat rides are out of service due to the high ghast activity in Mort Myre.") + when { + questCompleted("in_search_of_the_myreque") -> { + player("Thanks for your help in finding the Myreque.") + npc( + "Well, I'd like to say that you're welcome. But I heard that you led Vanstrom " + + "straight to their hideout and he killed Sani and Harold. I feel so guilty.", + ) + player("Hmm, yeah, I know how you feel.") + generalQuestionsChoice() + } + questStage("in_search_of_the_myreque") <= myrequeStage("persuaded_boatman") -> earlyCyregOptions() + else -> { + player("Can I ask some more questions?") + npc("Sure you can.") + generalQuestionsChoice() + } } } } } + +suspend fun Player.earlyCyregOptions() { + when (questStage("in_search_of_the_myreque")) { + myrequeStage("unstarted") -> npc( + "Sorry my friend but the boat rides are out of service due to the high ghast " + + "activity in Mort Myre.", + ) + myrequeStage("agreed_to_help") -> { + npc("Hello there friend.") + startWeaponDelivery() + } + myrequeStage("refused_delivery") -> askToFindMyreque() + myrequeStage("persuaded_boatman") -> askForBoatRide() + } +} + +private suspend fun Player.startWeaponDelivery() { + player("Hello there, I have some weapons for you to give to the 'Myreque'.") + if (!hasAllWeapons()) { + npc("Hmm, well, it doesn't look as if you've got the right sort of weapons to me!") + return + } + npc("Hmm, I'm sure I don't know what you're talking about.") + player("Come on, I know you're in cahoots with them, just take these weapons to them.") + npc( + "Ok, seriously, I did some work for them before, but now it's just too dangerous. " + + "I won't take the weapons to them, I'm sorry, it's just too dangerous.", + ) + + if (questStage("in_search_of_the_myreque") == myrequeStage("agreed_to_help")) { + set("in_search_of_the_myreque", "refused_delivery") + } + + askToFindMyreque() +} + +private suspend fun Player.askToFindMyreque() { + player("Can you tell me how to find the Myreque?") + npc( + "Their base is well hidden and I'm sorry but I can't reveal the directions. " + + "Sorry but I guess you're all out of luck.", + ) + persuasionChoice() +} + +private suspend fun Player.persuasionChoice() { + choice { + comeOnTellMe() + guessTheyllDie() + offerCash() + justWantToHelp() + option("Ok, thanks.") + } +} + +private fun ChoiceOption.comeOnTellMe(): Unit = option("Oh come on, you can tell me!") { + npc("I'm sorry, I can't I just can't... people are watching... ...eyes everywhere!") + persuasionChoice() +} + +private fun ChoiceOption.offerCash(): Unit = option("I'll give you some cash if you tell me.") { + npc("You think you can buy me!") + player("Er, no, I just want to compensate you for your troubles.") + npc("You keep your money and I'll keep my secrets.") + persuasionChoice() +} + +private fun ChoiceOption.justWantToHelp(): Unit = option("I just want to help them, I think they need help.") { + npc( + "Aye, well, they may do...but it's just not safe and it's not likely to get safer any " + + "time soon. Though I do feel sorry for Ivan, the baby of the group. He's seen too " + + "few winters to be involved in such toil.", + ) + persuasionChoice() +} + +private fun ChoiceOption.guessTheyllDie(): Unit = option("Well. I guess they'll just die without weapons.") { + npc( + "Hmm, you don't seem too concerned about their welfare... I'm glad I didn't tell you " + + "where they were...in any case they're resourceful, they can look after themselves.", + ) + player("What's that supposed to mean?") + npc( + "They're resourceful folks, that's all I'm saying. Their leader, Veliaf, looks after " + + "them well.", + ) + resourcefulChoice() +} + +private suspend fun Player.resourcefulChoice() { + choice { + comeOnTellMe() + resourcefulEnough() + offerCash() + justWantToHelp() + option("Ok, thanks.") + } +} + +private fun ChoiceOption.resourcefulEnough(): Unit = option("Resourceful enough to get their own steel weapons?") { + npc( + "Maybe they are...what do you care anyway? They've been up against it ever since they " + + "got started. All of 'em have suffered more loss and heartache than you'll ever know. " + + "Now, leave me be!", + ) + upAgainstChoice() +} + +private suspend fun Player.upAgainstChoice() { + choice { + whatHaveTheyBeenUpAgainst() + whatLossAndHeartache() + deathsOnYourHead() + justWantToHelp() + option("Ok, thanks.") + } +} + +private fun ChoiceOption.whatHaveTheyBeenUpAgainst(): Unit = option("What have they been up against?") { + npc( + "You're not from around here or you wouldn't be asking such foolish questions. " + + "Morytania is ruled by a cruel dark overlord by the name of Drakan. His reign over " + + "Morytania means we all live in fear.", + ) + upAgainstChoice() +} + +private fun ChoiceOption.whatLossAndHeartache(): Unit = option("What kind of loss and heartache?") { + npc( + "The worst kind, most have lost members of their family. Little Sani Piliu, she was " + + "orphaned overnight when a vampire went on the rampage... imagine that, losing your " + + "entire family in one night? Terrible!", + ) + player("It sounds awful... Who is Sani Pillu?") + npc( + "She's the only female member of the Myreque. She's already proven herself with her " + + "agility and light fingers, if you know what I mean!", + ) + upAgainstChoice() +} + +private fun ChoiceOption.deathsOnYourHead(): Unit = option("If you don't tell me, their deaths are on your head!") { + npc( + "There's death a plenty in this forsaken place...what do I care that some fool hardy " + + "vigilantes decided to go it alone against the drakans? Stupidity of youth is to " + + "blame, I shan't carry it on my shoulders.", + ) + forsakenChoice() +} + +private suspend fun Player.forsakenChoice() { + choice { + vigilanteFreedomFighter() + whoAreDrakans() + whatKindOfMan() + whyForsaken() + option("Ok, thanks.") + } +} + +private fun ChoiceOption.vigilanteFreedomFighter(): Unit = option( + "One man's vigilante is another man's freedom fighter!", +) { + npc( + "Aye, you can see it from both sides I suppose. But many of us consider it fool hardy " + + "to fight for something we'll never get, even Polmafi, a scholar, such as he was, " + + "agrees that the chances are slim.", + ) + player("Polmafi? Who's he?") + npc( + "Polmafi Ferdygris is one of the Myreque, he's a technical sort and advises on all " + + "sorts of things to Veliaf. He was a scholar before he became a renegade.", + ) + forsakenChoice() +} + +private fun ChoiceOption.whoAreDrakans(): Unit = option("Who are the Drakans?") { + npc( + "The Drakans are the family of overlords that rule Morytania. They're the ones to " + + "whom the blood tithes are paid. Too much I have told you already!", + ) + npc( + "Ignorance is better than these truths I tell you! I can pretend once more that I am " + + "a free man and some relief from this gloom can I feel again. Be gone with you now " + + "and leave me with my dreams.", + ) + forsakenChoice() +} + +private fun ChoiceOption.whyForsaken(): Unit = option("Why do you say that this place is 'forsaken'?") { + npc( + "All of these lands are forsaken of Saradomin's kindness, only cold death from the " + + "evil gods do we now feels. Those lucky ones to the west of the Salve little realise " + + "their fate if the river should one day become tained.", + ) + forsakenChoice() +} + +private fun ChoiceOption.whatKindOfMan(): Unit = option( + "What kind of man are you to say that you don't care?", +) { + npc( + "Don't dare to judge me young fool...what do you know of the heartache I carry? " + + "Can you not see the anchor of woe that holds me fast?", + ) + npc("Very well, if you would take your chance to help these strangers, who am I to stop you?") + player("But will you help me? Will you take me to them?") + offerBoat() +} + +private suspend fun Player.askForBoatRide() { + player("Will you still take me to the Myreque?") + offerBoat() +} + +private suspend fun Player.offerBoat() { + npc( + "No, I won't take you but you can use my boat. You'll be going through Mort Myre " + + "though so I won't be letting you go unless you've some defence against the Ghasts.", + ) + if (questStage("in_search_of_the_myreque") == myrequeStage("refused_delivery")) { + set("in_search_of_the_myreque", "persuaded_boatman") + } + if (inventory.contains("druid_pouch_2", 5) && inventory.contains("silver_sickle_b")) { + showDruidPouch() + } else { + player("I don't have anything which I can use against them at this time.") + } +} + +private suspend fun Player.showDruidPouch() { + item("druid_pouch_2", "You show the boatman your druid pouch.") + player( + "I have this druid pouch! This turns the Ghasts visible and I can kill them once I " + + "can see them.", + ) + npc( + "Very well, You can go! But you'll need to bring me some wood planks first, I need " + + "three and you need three.", + ) + if (!inventory.contains("plank", 3)) { + npc( + "The bridge you cross later is rotten and may need to be mended, so bring tools " + + "and steel metal fixers as you may find them useful.", + ) + return + } + npc( + "The bridge you cross later is rotten and may need to be mended, so bring tools " + + "and steel metal fixers as well, you may find them useful. I see that you have " + + "some with you now, do you want to give them to me?", + ) + plankChoice() +} + +private suspend fun Player.plankChoice() { + choice("Cyreg wants wooden planks!") { + givePlanks() + keepPlanks() + } +} + +private fun ChoiceOption.givePlanks(): Unit = option("Give wooden planks to Cyreg.") { + if (!inventory.remove("plank", 3)) { + return@option + } + set("in_search_of_the_myreque", "gave_planks") + item("plank", "The boatman takes 3 wooden planks from you.") + npc( + "Very well, you can take the boat. Just jump in when you're ready to leave. When you " + + "get to the hollows, just keep going North and look for an unusual tree.", + ) +} + +private fun ChoiceOption.keepPlanks(): Unit = option("Don't give any wooden planks to Cyreg.") { + player("Not just yet sorry.") +} + +private suspend fun Player.generalQuestionsChoice() { + choice { + whereAfterHollows() + tellMeAboutMyreque() + tellMeAboutDrakans() + tellMeAboutYourself() + option("Ok, thanks.") + } +} + +private fun ChoiceOption.whereAfterHollows(): Unit = option("Where do I go after I get to the hollows?") { + npc( + "You should head north and look for an unusual tree. You look like you know a thing " + + "or two about exploring so I guess I don't need to tell you to keep your eyes peeled!", + ) + generalQuestionsChoice() +} + +private fun ChoiceOption.tellMeAboutMyreque(): Unit = option("Tell me about the 'Myreque.'") { + npc("What do you want to know?") + myrequeQuestionsChoice() +} + +private fun ChoiceOption.tellMeAboutDrakans(): Unit = option("Tell me about the Drakans.") { + npc( + "Well it's rumoured that they're controlling the whole of Morytania. They live in " + + "the Sanguinesti reigon, bu I've never been, like most villagers in Morytania, " + + "I'm afraid to leave the village and dread what lies beyond.", + ) + generalQuestionsChoice() +} + +private fun ChoiceOption.tellMeAboutYourself(): Unit = option("Tell me about yourself.") { + npc( + "I'm just a humble boatman, Cyreg Paddlehorn is my name, like most of the Paddlehorns " + + "before me, I make my living by tackling the swamps of Mort Myre.", + ) + generalQuestionsChoice() +} + +private suspend fun Player.myrequeQuestionsChoice() { + choice { + whyCalledMyreque() + tellMeAboutMembers() + askDifferentQuestion() + } +} + +private fun ChoiceOption.whyCalledMyreque(): Unit = option("Why are they called, 'The Myreque'?") { + npc( + "The locals just called them after the place where they hide out, in Mort Myre. " + + "They are Myreque, 'hidden in the myre'.", + ) + myrequeQuestionsChoice() +} + +private fun ChoiceOption.tellMeAboutMembers(): Unit = option("Tell me about the members of the 'Myreque'.") { + npc( + "Well, you have Radigad Ponfit...he's your average mercenary from Asgarnia...ready to " + + "slice anyone's head off for a price. He's got a personal score to settle with the " + + "Drakans, though no one knows what it is.", + ) + npc( + "Then there's Veliaf, he's the leader. And then there's Ivan, he's the baby of the " + + "group. There's Sani Piliu she's a lovely girl, though a bit shady if you know what " + + "I mean.", + ) + npc( + "You've also got Harold Evans, he's a bit hot headed, always straight into the fray. " + + "And the brains of the operation reporting directly to Veliaf is Polmafi Ferdygris, " + + "he used to be quite a clever scholar!", + ) + myrequeQuestionsChoice() +} + +private fun ChoiceOption.askDifferentQuestion() { + option("~~~ Ask a different question. ~~~") { + generalQuestionsChoice() + } +} diff --git a/game/src/main/kotlin/content/quest/Cutscene.kt b/game/src/main/kotlin/content/quest/Cutscene.kt index b65b09a356..8f997e3a27 100644 --- a/game/src/main/kotlin/content/quest/Cutscene.kt +++ b/game/src/main/kotlin/content/quest/Cutscene.kt @@ -202,4 +202,25 @@ fun Player.closeTabs(vararg others: Tab) { } fun Player.startCutscene(name: String, region: Region = Region.EMPTY): Cutscene = Cutscene(this, name, region) + +/** + * Starts a cutscene in a private copy of a [width] by [height] block of chunks, the south west + * corner of which is the chunk [base] sits in. For scenes that only need a room or two rather than + * a whole region. + */ +fun Player.startCutscene(name: String, base: Tile, width: Int, height: Int, levels: Int = 4): Cutscene { + val instance = smallInstance() + return Cutscene(this, name, instance, copyChunks(instance, base, width, height, levels)) +} + +/** + * Copies a [width] by [height] block of chunks, starting at the chunk [base] sits in, into + * [instance], and returns the offset from the original chunks to the copy. + */ +fun Player.copyChunks(instance: Region, base: Tile, width: Int, height: Int, levels: Int = 4): Delta { + val offset = instance.tile.delta(base.zone.tile) + get().copy(base.zone, instance.tile.zone, width, height, levels) + set("instance_offset", offset.id) + return offset +} fun Player.startCutscene(name: String, region: Region, offset: Delta): Cutscene = Cutscene(this, name, region, offset) diff --git a/game/src/main/kotlin/content/quest/Quest.kt b/game/src/main/kotlin/content/quest/Quest.kt index 20766adcf3..4a7f2a8961 100644 --- a/game/src/main/kotlin/content/quest/Quest.kt +++ b/game/src/main/kotlin/content/quest/Quest.kt @@ -23,6 +23,7 @@ val quests = setOf( // members "creature_of_fenkenstrain", "druidic_ritual", + "in_search_of_the_myreque", "jungle_potion", "nature_spirit", "plague_city", @@ -43,6 +44,11 @@ fun Player.questStage(name: String): Int { return def.values.toInt(value) } +fun questStage(name: String, stage: String): Int { + val def = VariableDefinitions.get(name) ?: return 0 + return def.values.toInt(stage) +} + fun Player.questCompleted(name: String): Boolean { if (!quests.contains(name) && Settings["quests.requirements.skipMissing", false]) { return true diff --git a/game/src/main/kotlin/content/quest/member/myreque/SearchMyreque.kt b/game/src/main/kotlin/content/quest/member/myreque/SearchMyreque.kt new file mode 100644 index 0000000000..ba2bf6e178 --- /dev/null +++ b/game/src/main/kotlin/content/quest/member/myreque/SearchMyreque.kt @@ -0,0 +1,448 @@ +package content.quest.member.myreque + +import content.entity.combat.killer +import content.entity.gfx.areaGfx +import content.entity.obj.door.enterDoor +import content.entity.player.dialogue.type.statement +import content.quest.quest +import content.quest.questCompleted +import content.quest.questJournal +import content.quest.questStage +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.instruction.handle.interactPlayer +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.definition.Areas +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.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.Players +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.entity.obj.remove +import world.gregs.voidps.engine.entity.obj.replace +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.queue.strongQueue +import world.gregs.voidps.engine.timer.Timer +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.area.Cuboid + +class SearchMyreque : Script { + + init { + questJournalOpen("in_search_of_the_myreque") { + val lines = when (quest("in_search_of_the_myreque")) { + "unstarted" -> notStartedJournal() + "completed" -> completedJournal() + else -> startedJournal() + } + questJournal("In Search of the Myreque", lines) + } + + objectApproach("Climb", "swamp_bridge_tree") { (target) -> + approachRange(1) + if (!tile.within(target.tile, 1)) { + return@objectApproach message("You can't reach that.") + } + climbTree(target) + } + + objectApproach("Walk-here", "swamp_bridge1") { (target) -> + if (tile == target.tile) { + return@objectApproach + } + if (!tile.within(target.tile, 1)) { + return@objectApproach message("You can't reach that.") + } + val givesWay = target.tile in Areas["mort_myre_rope_bridge"] && + target.tile.y != BRIDGE_LANDING && + !get(rung(target.tile.y), false) + if (!givesWay) { + walkTo(target.tile, noCollision = true) + return@objectApproach + } + target.replace("spooky_tree_base_forbridge", ticks = 200) + statement("This bridge looks pretty old. The wooden boards break apart beneath your feet.") + } + + objectApproach("Repair", "swamp_bridge1") { + message("This bridge rung already looks fixed, that's why you're able to stand on it.") + } + + objectApproach("Repair", "spooky_tree_base_forbridge") { (target) -> + if (!tile.within(target.tile, 1)) { + return@objectApproach message("You can't reach that.") + } + if (!inventory.contains("plank") || !inventory.contains("steel_nails", 75) || !inventory.contains("hammer")) { + return@objectApproach statement("You don't have the required materials to fix this part of the bridge.") + } + anim("smith_item") + inventory.remove("plank") + inventory.remove("steel_nails", 75) + delay(2) + target.remove() + set(rung(target.tile.y), true) + if (bridgeRepaired()) { + set("route_bridgecomplete", true) + } + walkTo(target.tile, noCollision = true) + } + + objectOperate("Open", "swamp_wooden_doors_closed") { (target) -> + arriveDelay() + val progress = questStage("in_search_of_the_myreque") + if (progress < myrequeStage("answered_questions")) { + return@objectOperate statement( + "There seems to be a strange combination on the door. It would take ages to work it out.", + ) + } + if (progress == myrequeStage("answered_questions")) { + set("in_search_of_the_myreque", "entered_hideout") + } + anim("human_lockedchest") + sound("locked") + delay(1) + target.replace("swamp_wooden_doors_opened", ticks = 3) + delay(2) + sound("iron_door_open") + tele(3500, 9812) + } + + objectOperate("Enter", "route_cavewalltunnel") { + tele(3491, 9824) + } + + objectOperate("Look", "route_stalagmite_cave_entrace") { + statement( + "It looks like an ordinary stalagmite, however, beyond it you can see that there is a " + + "small cave entrance.", + ) + } + + objectOperate("Squeeze-past", "route_stalagmite_cave_entrace") { + statement("You try to squeeze past this stalagmite into a small cavern entrance.") + anim("squeeze_past_stalagmite") + delay(3) + val progress = questStage("in_search_of_the_myreque") + tele(3505, 9832, if (progress < myrequeStage("hellhound_summoned")) 2 else 0) + if (progress == myrequeStage("hellhound_summoned")) { + delay(3) + spawnHellHound(this) + } + } + + objectOperate("Search", "canifis_fake_wall_closed") { (target) -> + if (questStage("in_search_of_the_myreque") < myrequeStage("shown_way_out")) { + return@objectOperate message("You see nothing interesting with this wall.") + } + message("You search the wall and find the door which Veliaf told you about.") + if (questStage("in_search_of_the_myreque") == myrequeStage("shown_way_out")) { + set("in_search_of_the_myreque", "in_inn_basement") + } + enterDoor(target) + message("You walk through.") + } + + objectOperate("Climb-up", "canifis_tavern_ladder") { + message("You climb the ladder out into the outside.") + if (questStage("in_search_of_the_myreque") == myrequeStage("in_inn_basement")) { + set("in_search_of_the_myreque", "escaped_to_canifis") + } + anim("climb_up") + delay(1) + tele(3495, 3466) + message("You emerge to the south of the 'Hair of the Dog' tavern in Canifis.") + } + + objectOperate("Open", "canifis_tavern_trapdoor") { + if (!questCompleted("in_search_of_the_myreque")) { + return@objectOperate message("This trap door seems locked, you're not really sure where it would lead to.") + } + delay(1) + tele(3477, 9845) + message("You open the trapdoor and find yourself in the inn basement.") + } + + npcDeath("skeleton_hellhound") { + val killer = killer as? Player ?: return@npcDeath + if (killer.questStage("in_search_of_the_myreque") == myrequeStage("hellhound_summoned")) { + killer["in_search_of_the_myreque"] = "killed_hellhound" + } + } + + npcTimerStart("hellhound_leash") { 1 } + + npcTimerTick("hellhound_leash") { + val owner = Players.findByAccount(get("owner", "")) + if (owner == null || !HIDEOUT.contains(owner.tile)) { + despawn() + return@npcTimerTick Timer.CANCEL + } + Timer.CONTINUE + } + } + + private suspend fun Player.climbTree(target: GameObject) { + val north = target.tile.y == NORTH_TREE + if (north && !get(rung(BRIDGE_TOP), false)) { + statement( + "This bridge run looks broken; you would have nowhere to stand. The southern " + + "side of the rope bridge looks much less damaged.", + ) + return + } + message("You climb the tree.") + val up = if (north) tile.y >= target.tile.y else tile.y <= target.tile.y + val landing = when { + north && up -> Tile(target.tile.x, BRIDGE_TOP) + north -> NORTH_BANK + up -> Tile(target.tile.x, BRIDGE_LANDING) + else -> SOUTH_BANK + } + anim(if (up) "climb_up" else "climb_down") + delay(1) + strongQueue("climb_bridge_tree", 1) { + tele(landing) + } + } + + private fun Player.notStartedJournal(): List = listOf( + "I can start this quest by talking to a stranger in the 'Hair", + "of the Dog Inn' located in the town of Canifis in the land of", + "Morytania.", + "", + "I need to complete the following quest:", + if (questCompleted("nature_spirit")) "Nature Spirit" else "Nature Spirit", + "", + "I also need to be able to defeat a level 97 foe.", + ) + + private fun completedJournal(): List = listOf( + "I was set a quest by Vanstrom Klause to deliver weapons", + "to a group called the Myreque. After negotiating with a", + "boatman, fixing a broken rope bridge and answering a", + "guard's questions I eventually found the group called the", + "Myreque.", + "", + "However, it seems that I was tricked by Vanstrom who", + "turned out to be a vampire following me to the Myreque's", + "hideout. He killed Sani and Harold, both young members of", + "the Myreque.", + "", + "During the quest I found a quicker route between Mort'ton", + "and Canifis, and I feel that I better understand the reason", + "for the darkness of Morytania.", + "", + "Perhaps in the future, I can offer help to the Myreque?", + "", + "QUEST COMPLETE!", + ) + + private fun Player.startedJournal(): List { + val progress = questStage("in_search_of_the_myreque") + val list = mutableListOf() + + list += "I talked to a stranger called Vanstrom Klause in the 'Hair" + list += "of the Dog Inn' located in the town of Canifis." + + if (progress < myrequeStage("delivered_weapons")) { + list += "Vanstrom Klause asked me to take some weapons to a" + list += "group called 'The Myreque'." + list += "The weapons I need to collect are:" + list += weaponLine("steel_longsword", 1, "Steel Longsword") + list += weaponLine("steel_sword", 2, "Steel Sword") + list += weaponLine("steel_dagger", 1, "Steel Dagger") + list += weaponLine("steel_mace", 1, "Steel Mace") + list += weaponLine("steel_warhammer", 1, "Steel Warhammer") + } else { + list += "Vanstrom Klause asked me to take some weapons to a" + list += "group called 'The Myreque'." + } + + if ((hasAllWeapons() || progress >= myrequeStage("delivered_weapons")) && progress < myrequeStage("refused_delivery")) { + list += "I have all the weapons Vanstrom asked me to get." + list += "Vanstrom said that the boatman in Mort'ton should be" + list += "able to help." + } + + if (progress in myrequeStage("refused_delivery") until myrequeStage("persuaded_boatman")) { + list += "The boatman won't take me to the Myreque's hideout." + list += "Vanstrom did say that I have to be persuasive." + } + + if (progress >= myrequeStage("persuaded_boatman")) { + list += "The boatman says he won't take me to the Myreque" + list += "Vanstrom did say that I have to be persuasive." + + if (progress == myrequeStage("persuaded_boatman")) { + val hasPouch = inventory.contains("druid_pouch_2", 5) && inventory.contains("silver_sickle_b") + if (hasPouch) { + list += "I've persuaded the boatman to help me find the Myreque." + list += "I have the druid pouch as a defence against the ghasts." + list += "The boatman says I need to collect 6 wooden planks" + list += "before he will let me use his boat." + + if (inventory.contains("plank", 6)) { + list += "I've got six planks, I should give them to the boatman." + } else { + val needed = 6 - inventory.count("plank") + list += "I need to collect $needed more wooden planks" + } + } else { + list += "I've persuaded the boatman to help me find the Myreque." + list += "He won't lend the boat unless I can defend against" + list += "Ghasts." + } + } + } + + if (progress in myrequeStage("gave_planks") until myrequeStage("reached_hollows")) { + list += "I've persuaded the boatman to help me find the Myreque." + list += "The boatman won't take me to the Myreque but I can use" + list += "his boat." + } + + if (progress in myrequeStage("reached_hollows") until myrequeStage("questioned_by_curpile")) { + list += "The boatman loaned me the use of his boat." + list += "I borrowed the boat and travelled to the 'Hollows' in the" + list += "middle of Mort Myre in search of the Myreque." + } + + if (progress in myrequeStage("questioned_by_curpile") until myrequeStage("answered_questions")) { + list += "I borrowed the boat and travelled to the 'Hollows' in the" + list += "middle of Mort Myre in search of the Myreque." + list += "While looking for the hideout, I came across a guard called" + list += "Curpile, he may know where the Myreque are hidden." + list += "A guard named Curpile asked me to answer some" + list += "questions before he'll let me continue my search for the" + list += "Myreque." + } + + if (progress in myrequeStage("answered_questions") until myrequeStage("entered_hideout")) { + list += "A guard named Curpile asked me some questions about the" + list += "Myreque which I answered, he'll unlock the door for me" + list += "now" + list += "I need to find the entrance to the Myreque's hideout." + } + + if (progress in myrequeStage("entered_hideout") until myrequeStage("met_veliaf")) { + list += "I entered an underground area, it might be the Myreque" + list += "hideout. I'd better look around fully to try and find them." + list += "I need to look for the members of the Myreque now." + } + + if (progress in myrequeStage("met_veliaf") until myrequeStage("hellhound_summoned")) { + list += "I entered an underground area, it might be the Myreque" + list += "hideout. I'd better look around fully to try and find them." + list += "I met Veliaf, he seems to be the leader of the Myreque." + list += "Veliaf is busy, he's asked me to introduce myself to the" + list += "other members of the Myreque." + list += "I need to talk to:" + list += if (get("met_sani", false)) "Sani Piliu" else "Sani Piliu" + list += if (get("met_harold", false)) "Harold Evans" else "Harold Evans" + list += if (get("met_radigad", false)) "Radigad Ponfit" else "Radigad Ponfit" + list += if (get("met_polmafi", false)) "Polmafi Ferdygris" else "Polmafi Ferdygris" + list += if (get("met_ivan", false)) "Ivan Strom" else "Ivan Strom" + + if (checkMembers()) { + list += "Veliaf was busy, so I introduced myself to the other" + list += "members of the Myreque." + list += "I've introduced myself to all the members now" + list += "perhaps I should talk to Veliaf again" + } + + if (progress in myrequeStage("weapons_accepted") until myrequeStage("hellhound_summoned")) { + list += "I tried talking to Veliaf but he was still busy." + list += "I should try talking to Veliaf in a few minutes when he's" + list += "less busy." + } + } + + if (progress in myrequeStage("hellhound_summoned") until myrequeStage("killed_hellhound")) { + list += "Veliaf was busy, so I introduced myself to the other" + list += "members of the Myreque." + list += "While talking to Veliaf a mist cloud appeared in the room." + list += "The mist transformed into Vanstrom, he's a vampire!" + list += "Vanstrom killed Sani and Harold then summoned a hell" + list += "hound!" + list += "I have to try and kill the hell hound to save the Myreque." + } + + if (progress in myrequeStage("killed_hellhound") until myrequeStage("shown_way_out")) { + list += "I killed the hell hound and saved the rest of the Myreque." + list += "I was tricked by Vanstrom, he was a vampire after all!" + list += "Maybe someone can show me the way out of here?" + } + + if (progress in myrequeStage("shown_way_out") until myrequeStage("in_inn_basement")) { + list += "I was tricked by Vanstrom, he was a vampire after all!" + list += "Maybe someone can show me the way out of here?" + list += "I talked to Veliaf, he says that I can get into the basement" + list += "of the Inn through a secret wall." + } + + if (progress in myrequeStage("in_inn_basement") until myrequeStage("escaped_to_canifis")) { + list += "I went through the secret wall Veliaf told me about into the" + list += "basement of the inn." + list += "I should try to find a way up to the surface from the" + list += "basement of the Inn." + } + + if (progress >= myrequeStage("escaped_to_canifis")) { + list += "I climbed up the ladder in the Inn basement to find my way" + list += "out." + list += "I climbed out of the basement of the Inn to find myself in" + list += "the town of Canifis where I first met that double crossing" + list += "Vanstrom. I wonder if I can still find him there?" + } + + return list + } + + private fun Player.weaponLine(item: String, amount: Int, displayName: String): String { + val collected = inventory.contains(item, amount) || questStage("in_search_of_the_myreque") >= myrequeStage("refused_delivery") + return if (collected) { + "$amount x $displayName" + } else { + "$amount x $displayName" + } + } + + private companion object { + private const val NORTH_TREE = 3431 + + private const val BRIDGE_LANDING = 3427 + + private const val BRIDGE_TOP = 3430 + + private val NORTH_BANK = Tile(3503, 3431) + private val SOUTH_BANK = Tile(3502, 3425) + } +} + +fun myrequeStage(name: String): Int = questStage("in_search_of_the_myreque", name) + +fun rung(y: Int): String = "bridgerung${y - 3427}" + +fun Player.bridgeRepaired(): Boolean = (3428..3430).all { get(rung(it), false) } + +fun Player.hasAllWeapons(): Boolean = inventory.contains("steel_longsword", "steel_dagger", "steel_mace", "steel_warhammer") && + inventory.contains("steel_sword", 2) + +fun Player.checkMembers(): Boolean = listOf("sani", "harold", "ivan", "radigad", "polmafi").all { get("met_$it", false) } + +fun spawnHellHound(player: Player): NPC { + val hound = NPCs.add("skeleton_hellhound", HELL_HOUND_SPOT, ticks = HELL_HOUND_TICKS, owner = player) + areaGfx("skeleton_hellhound_summon", HELL_HOUND_SPOT) + hound.softTimers.start("hellhound_leash") + hound.interactPlayer(player, "Attack") + player.message("The skeletal hell hound attacks.") + return hound +} + +private val HELL_HOUND_SPOT = Tile(3505, 9834, 0) + +private const val HELL_HOUND_TICKS = 800 + +private val HIDEOUT = Cuboid(Tile(3503, 9829, 0), width = 12, height = 18, levels = 1) diff --git a/game/src/test/kotlin/content/quest/member/myreque/SearchMyrequeTest.kt b/game/src/test/kotlin/content/quest/member/myreque/SearchMyrequeTest.kt new file mode 100644 index 0000000000..ff86dc2e36 --- /dev/null +++ b/game/src/test/kotlin/content/quest/member/myreque/SearchMyrequeTest.kt @@ -0,0 +1,576 @@ +package content.quest.member.myreque + +import WorldTest +import containsMessage +import content.entity.combat.hit.damage +import dialogueContinue +import dialogueOption +import npcOption +import objectOption +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.client.ui.InterfaceApi +import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.data.definition.CombatDefinitions +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.skill.Skill +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.get +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.setRandom +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * In Search of the Myreque object locations: + * rope bridge rungs (3502, 3427-3430) bridge trees (3502, 3426) and (3502, 3431) + * hillside doors (3508, 3446) stalagmite (3492, 9824) + * secret wall (3480, 9837) inn ladder (3477, 9846) + * tavern trapdoor (3495, 3465) + */ +class SearchMyrequeTest : WorldTest() { + + private fun Player.giveWeapons() { + inventory.add("steel_longsword") + inventory.add("steel_sword", 2) + inventory.add("steel_dagger") + inventory.add("steel_mace") + inventory.add("steel_warhammer") + } + + @Test + fun `Vanstrom talks the player into arming the Myreque`() { + val player = createPlayer(Tile(3503, 3476)) + val vanstrom = createNPC("stranger", Tile(3503, 3477)) + + player.npcOption(vanstrom, "Talk-to") + tick(2) + player.skipDialogues() // greeting through "I'm at a loss of how I can help them." + player.dialogueOption("line2") // Why do they need help? Are they in trouble? + player.skipDialogues() // the weapons he has in mind + player.dialogueOption("line4") // Perhaps I could help you out here. + player.skipDialogues() + player.dialogueOption("line4") // Yes, I'll do it! + player.dialogueContinue() + + assertEquals("agreed_to_help", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `Cyreg refuses to take the weapons himself`() { + val player = createPlayer(Tile(3522, 3285)) + val cyreg = createNPC("cyreg_paddlehorn", Tile(3522, 3284)) + player["in_search_of_the_myreque"] = "agreed_to_help" + player.inventory.add("steel_longsword") + player.inventory.add("steel_sword", 2) + player.inventory.add("steel_dagger") + player.inventory.add("steel_mace") + player.inventory.add("steel_warhammer") + + player.npcOption(cyreg, "Talk-to") + tick(2) + player.skipDialogues() // he admits to having worked for them, then refuses + + assertEquals("refused_delivery", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `Cyreg won't lend the boat out before he has been talked round`() { + val player = createPlayer(Tile(3522, 3285)) + createNPC("cyreg_paddlehorn", Tile(3522, 3284)) + val boat = GameObjects.find(Tile(3523, 3284), "swamp_boat_mort_ton") + + player.objectOption(boat, "Board") + tick(4) + + assertEquals(Tile(3522, 3285), player.tile) + } + + @Test + fun `A rotten bridge rung breaks underfoot and can be nailed back together`() { + val player = createPlayer(Tile(3502, 3427)) + player.inventory.add("plank", 6) + player.inventory.add("steel_nails", 225) + player.inventory.add("hammer") + + val rung = GameObjects.find(Tile(3502, 3428), "swamp_bridge1") + player.objectOption(rung, "Walk-here") + tick(2) + player.dialogueContinue() + + val broken = GameObjects.find(Tile(3502, 3428), "spooky_tree_base_forbridge") + player.objectOption(broken, "Repair") + tick(6) + + assertTrue(player["bridgerung1", false], "the southern rung should be fixed") + assertEquals(5, player.inventory.count("plank")) + assertEquals(150, player.inventory.count("steel_nails")) + } + + @Test + fun `Repairing a rung without the materials tells the player so`() { + val player = createPlayer(Tile(3502, 3427)) + val rung = GameObjects.find(Tile(3502, 3428), "swamp_bridge1") + + player.objectOption(rung, "Walk-here") + tick(2) + player.dialogueContinue() + val broken = GameObjects.find(Tile(3502, 3428), "spooky_tree_base_forbridge") + player.objectOption(broken, "Repair") + tick(2) + + assertFalse(player["bridgerung1", false], "an empty handed player can't fix the bridge") + } + + @Test + fun `Rungs out of arm's reach can't be stepped onto or repaired`() { + val player = createPlayer(Tile(3502, 3427)) + player.inventory.add("plank", 6) + player.inventory.add("steel_nails", 225) + player.inventory.add("hammer") + + val far = GameObjects.find(Tile(3502, 3430), "swamp_bridge1") + player.objectOption(far, "Walk-here") + tick(2) + + assertTrue(player.containsMessage("You can't reach that.")) + assertEquals(Tile(3502, 3427), player.tile, "the player stays put") + assertNull(GameObjects.findOrNull(Tile(3502, 3430), "spooky_tree_base_forbridge"), "and the far rung holds") + + // Break the rung in front of them, then try to fix it from the other end of the bridge + val near = GameObjects.find(Tile(3502, 3428), "swamp_bridge1") + player.objectOption(near, "Walk-here") + tick(2) + player.dialogueContinue() + val broken = GameObjects.find(Tile(3502, 3428), "spooky_tree_base_forbridge") + player.tele(3502, 3425, 0) + tick() + player.objectOption(broken, "Repair") + tick(4) + + assertFalse(player["bridgerung1", false], "a rung out of reach can't be repaired") + } + + @Test + fun `The trees are only in reach from the tile beside them`() { + val player = createPlayer(Tile(3502, 3427)) + for (rung in 1..3) { + player["bridgerung$rung"] = true + } + val tree = GameObjects.find(Tile(3502, 3431), "swamp_bridge_tree") + + // Step out onto the first rung, then try to climb the tree at the far end + val rung = GameObjects.find(Tile(3502, 3428), "swamp_bridge1") + player.objectOption(rung, "Walk-here") + tick(3) + assertEquals(Tile(3502, 3428), player.tile) + + player.objectOption(tree, "Climb") + tick(3) + assertTrue(player.containsMessage("You can't reach that.")) + assertEquals(Tile(3502, 3428), player.tile, "no shuffling along the bridge to it") + + // From the rung it stands on though, the tree drops the player off the bridge + player.tele(3502, 3430, 0) + tick() + player.objectOption(tree, "Climb") + tick(3) + + assertEquals(Tile(3503, 3431), player.tile, "the far bank") + } + + @Test + fun `The northern tree can't be climbed until the top rung is fixed`() { + val player = createPlayer(Tile(3502, 3432)) + val tree = GameObjects.find(Tile(3502, 3431), "swamp_bridge_tree") + + player.objectOption(tree, "Climb") + tick(3) + + assertNotEquals(Tile(3502, 3430), player.tile, "the broken top rung is nowhere to stand") + player.dialogueContinue() + + player["bridgerung3"] = true + player.objectOption(tree, "Climb") + tick(4) + + assertEquals(Tile(3502, 3430), player.tile) + } + + @Test + fun `Curpile only asks his questions of someone carrying the weapons`() { + val player = createPlayer(Tile(3508, 3441)) + val curpile = createNPC("curpile_fyod_mort_myre_swamp", Tile(3508, 3440)) + player["in_search_of_the_myreque"] = "reached_hollows" + player.inventory.add("steel_longsword") + player.inventory.add("steel_sword", 2) + player.inventory.add("steel_dagger") + player.inventory.add("steel_mace") + player.inventory.add("steel_warhammer") + + player.npcOption(curpile, "Talk-to") + tick(2) + player.dialogueContinue() + player.dialogueOption("line1") // I've come to help the Myreque, I've brought weapons. + player.skipDialogues() + + assertEquals("questioned_by_curpile", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `Answering all three of Curpile's questions unlocks the door`() { + val player = createPlayer(Tile(3508, 3441)) + val curpile = createNPC("curpile_fyod_mort_myre_swamp", Tile(3508, 3440)) + player["in_search_of_the_myreque"] = "questioned_by_curpile" + player.giveWeapons() + + player.npcOption(curpile, "Talk-to") + tick(2) + player.dialogueContinue() + // The three questions are drawn at random, so seed the draw and answer what he asks + setRandom(Random(SEED)) + val asked = (0..5).shuffled(Random(SEED)).take(3) + player.dialogueOption("line1") // I've come to help the Myreque, I've brought weapons. + player.skipDialogues() + for (question in asked) { + player.dialogueOption("line${CORRECT_ANSWERS.getValue(question)}") + player.skipDialogues() + } + setRandom(Random) + + assertEquals("answered_questions", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `Getting a question wrong has Curpile knock the player back to Mort'ton`() { + val player = createPlayer(Tile(3508, 3441)) + val curpile = createNPC("curpile_fyod_mort_myre_swamp", Tile(3508, 3440)) + player["in_search_of_the_myreque"] = "questioned_by_curpile" + player.giveWeapons() + + player.npcOption(curpile, "Talk-to") + tick(2) + player.dialogueContinue() + player.dialogueOption("line1") // I've come to help the Myreque, I've brought weapons. + player.skipDialogues() + repeat(3) { + player.dialogueOption("line5") // I don't know! + player.skipDialogues() + } + tick(12) + + assertEquals("questioned_by_curpile", player["in_search_of_the_myreque", "unstarted"], "he doesn't let them past") + assertEquals(Tile(3522, 3285), player.tile, "and they wake up back in Mort'ton") + } + + @Test + fun `Cyreg lends the boat out for three planks`() { + val player = createPlayer(Tile(3522, 3285)) + val cyreg = createNPC("cyreg_paddlehorn", Tile(3522, 3284)) + player["in_search_of_the_myreque"] = "persuaded_boatman" + player.inventory.add("druid_pouch_2", 5) + player.inventory.add("silver_sickle_b") + player.inventory.add("plank", 3) + + player.npcOption(cyreg, "Talk-to") + tick(2) + player.skipDialogues() // the ghast warning and the druid pouch + player.dialogueOption("line1") // Give wooden planks to Cyreg. + player.skipDialogues() + + assertEquals("gave_planks", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(0, player.inventory.count("plank"), "he takes all three") + } + + @Test + fun `The boat ride to the Hollows costs ten gold`() { + val player = createPlayer(Tile(3522, 3285)) + createNPC("cyreg_paddlehorn", Tile(3522, 3284)) + player["in_search_of_the_myreque"] = "gave_planks" + player.inventory.add("coins", 10) + val boat = GameObjects.find(Tile(3523, 3284), "swamp_boat_mort_ton") + + player.objectOption(boat, "Board") + tick(4) + player.dialogueContinue() // It costs 10 gold to cover the loan of the boat. Will you pay? + player.dialogueOption("line1") // Yes. I'll pay the ten gold. + player.dialogueContinue() + tick(20) + + // Arriving is what counts, not clicking through the message that follows it + assertEquals(Tile(3498, 3380), player.tile) + assertEquals("reached_hollows", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(0, player.inventory.count("coins")) + player.dialogueContinue() // You arrive in the Hollows. + } + + @Test + fun `Veliaf sends the player off to meet the rest of the Myreque`() { + val player = createPlayer(Tile(3507, 9838, 2)) + player["in_search_of_the_myreque"] = "met_veliaf" + val veliaf = createNPC("veliaf_hurtz_meiyerditch_tunnels", Tile(3506, 9838, 2)) + val sani = createNPC("sani_piliu_meiyerditch_tunnels", Tile(3508, 9837, 2)) + + player.npcOption(veliaf, "Talk-to") + tick(2) + player.dialogueContinue() // Have you introduced yourself to the rest of the gang? + player.dialogueOption("line2") // Not yet, I'll go do that now. + player.skipDialogues() + assertEquals("met_veliaf", player["in_search_of_the_myreque", "unstarted"], "the weapons wait until he's met everyone") + + player.npcOption(sani, "Talk-to") + tick(2) + player.skipDialogues() + + assertTrue(player["met_sani", false], "introducing yourself is remembered") + } + + @Test + fun `The journal reads through every stage of the quest`() { + val player = createPlayer(Tile(3503, 3476)) + + for (stage in JOURNAL_STAGES) { + player["in_search_of_the_myreque"] = stage + InterfaceApi.openQuestJournal(player, "in_search_of_the_myreque") + assertTrue(player.interfaces.contains("quest_scroll"), "the journal should open at $stage") + player.interfaces.close("quest_scroll") + } + } + + @Test + fun `The hillside doors stay shut until Curpile has been answered`() { + val player = createPlayer(Tile(3508, 3447)) + val doors = GameObjects.find(Tile(3508, 3446), "swamp_wooden_doors_closed") + + player.objectOption(doors, "Open") + tick(2) + assertEquals(Tile(3508, 3447), player.tile) + + player["in_search_of_the_myreque"] = "answered_questions" + player.objectOption(doors, "Open") + tick(6) + + assertEquals("entered_hideout", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(Tile(3500, 9812), player.tile) + } + + @Test + fun `The stalagmite leads to the hideout, and to the emptier one after the ambush`() { + val player = createPlayer(Tile(3491, 9824)) + player["in_search_of_the_myreque"] = "entered_hideout" + val stalagmite = GameObjects.find(Tile(3492, 9824), "route_stalagmite_cave_entrace") + + player.objectOption(stalagmite, "Squeeze-past") + tick(2) + player.dialogueContinue() + tick(6) + assertEquals(Tile(3505, 9832, 2), player.tile) + + player["in_search_of_the_myreque"] = "killed_hellhound" + player.tele(3491, 9824, 0) + tick() + player.objectOption(stalagmite, "Squeeze-past") + tick(2) + player.dialogueContinue() + tick(6) + assertEquals(Tile(3505, 9832, 0), player.tile) + } + + @Test + fun `Vanstrom ambushes the hideout once the weapons are handed over`() { + val player = createPlayer(Tile(3507, 9838, 2)) + player["in_search_of_the_myreque"] = "weapons_accepted" + player.inventory.add("steel_longsword") + player.inventory.add("steel_sword", 2) + player.inventory.add("steel_dagger") + player.inventory.add("steel_mace") + player.inventory.add("steel_warhammer") + val veliaf = createNPC("veliaf_hurtz_meiyerditch_tunnels", Tile(3506, 9838, 2)) + val sani = createNPC("sani_piliu_meiyerditch_tunnels", Tile(3508, 9837, 2)) + val harold = createNPC("harold_evans_meiyerditch_tunnels", Tile(3504, 9837, 2)) + + player.npcOption(veliaf, "Talk-to") + repeat(80) { + tick() + if (player.dialogue != null) { + player.skipDialogues() + } + } + + assertEquals("hellhound_summoned", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(Tile(3507, 9838, 0), player.tile, "the ambush leaves the player in the emptier room") + assertEquals(0, player.inventory.count("steel_longsword"), "the weapons are handed over") + assertTrue(player["thsfm_vanstrom_hide", false], "Vanstrom stops sitting in the tavern") + val hound = NPCs.at(player.tile.regionLevel).firstOrNull { it.id == "skeleton_hellhound" } + assertNotNull(hound, "a hell hound should be left behind") + // The ambush runs in a copy of the room, so the real Sani and Harold are never touched + assertNull(player.get("instance"), "the cutscene instance should be cleaned up") + assertEquals(Tile(3508, 9837, 2), sani.tile) + assertEquals(sani.levels.getMax(Skill.Constitution), sani.levels.get(Skill.Constitution)) + assertEquals(harold.levels.getMax(Skill.Constitution), harold.levels.get(Skill.Constitution)) + } + + @Test + fun `Killing the hell hound saves the rest of the Myreque`() { + val player = createPlayer(Tile(3506, 9838)) + player["in_search_of_the_myreque"] = "hellhound_summoned" + val hound = spawnHellHound(player) + tick(2) + + hound.damage(5000, source = player) + tick(6) + + assertEquals("killed_hellhound", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `The hell hound fights with its own animations and sounds`() { + val definition = get().getOrNull("skeleton_hellhound") + assertNotNull(definition, "the hell hound needs a combat definition to fight with") + + assertEquals("skeleton_hellhound_defend", definition.defendAnim) + assertEquals("skeleton_hellhound_death", definition.deathAnim) + assertEquals("skeleton_hellhound_death", definition.deathSound?.id) + + val melee = definition.attacks["melee"] + assertNotNull(melee, "the hell hound needs a melee attack") + assertEquals("skeleton_hellhound_attack", melee.anim) + assertEquals("skeleton_hellhound_attack", melee.sounds.first().id) + assertEquals("skeleton_hellhound_hit", melee.targetSounds.first().id) + val hit = melee.targetHits.first() + assertEquals("crush", hit.offense) + assertEquals(120, hit.max) + } + + @Test + fun `The hell hound gives up once the player leaves the hideout`() { + val player = createPlayer(Tile(3506, 9838)) + player["in_search_of_the_myreque"] = "hellhound_summoned" + val hound = spawnHellHound(player) + tick(2) + + player.tele(3491, 9824, 0) + tick(3) + + assertEquals(-1, hound.index, "the hound should be gone") + } + + @Test + fun `The hell hound leaves bones and rubies behind`() { + val player = createPlayer(Tile(3506, 9838)) + player["in_search_of_the_myreque"] = "hellhound_summoned" + val hound = spawnHellHound(player) + tick(2) + + val deathTile = hound.tile + hound.damage(5000, source = player) + tick(8) + + // Bones and rubies don't stack, so each one is its own pile + val drops = FloorItems.at(deathTile.zone).flatten() + assertEquals(4, drops.count { it.id == "big_bones" }) + assertEquals(2, drops.count { it.id == "uncut_ruby" }) + } + + @Test + fun `Veliaf's secret wall only opens once he has pointed it out`() { + val player = createPlayer(Tile(3480, 9838)) + val wall = GameObjects.find(Tile(3480, 9837), "canifis_fake_wall_closed") + + player.objectOption(wall, "Search") + tick(2) + assertTrue(player.containsMessage("You see nothing interesting with this wall.")) + + player["in_search_of_the_myreque"] = "shown_way_out" + player.objectOption(wall, "Search") + tick(4) + + assertEquals("in_inn_basement", player["in_search_of_the_myreque", "unstarted"]) + } + + @Test + fun `The inn ladder comes out beside the tavern`() { + val player = createPlayer(Tile(3477, 9845)) + player["in_search_of_the_myreque"] = "in_inn_basement" + val ladder = GameObjects.find(Tile(3477, 9846), "canifis_tavern_ladder") + + player.objectOption(ladder, "Climb-up") + tick(4) + + assertEquals("escaped_to_canifis", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(Tile(3495, 3466), player.tile) + } + + @Test + fun `The tavern trapdoor is a shortcut only for those who finished the quest`() { + val player = createPlayer(Tile(3495, 3466)) + val trapdoor = GameObjects.find(Tile(3495, 3465), "canifis_tavern_trapdoor") + + player.objectOption(trapdoor, "Open") + tick(4) + assertTrue(player.containsMessage("This trap door seems locked")) + assertEquals(Tile(3495, 3466), player.tile, "and it stays shut") + + player["in_search_of_the_myreque"] = "completed" + player.objectOption(trapdoor, "Open") + tick(4) + + assertEquals(Tile(3477, 9845), player.tile) + } + + @Test + fun `Telling the stranger about Vanstrom finishes the quest`() { + val player = createPlayer(Tile(3503, 3476)) + player["in_search_of_the_myreque"] = "escaped_to_canifis" + player["thsfm_vanstrom_hide"] = true + val stranger = createNPC("stranger", Tile(3503, 3477)) + val points = player["quest_points", 0] + + player.npcOption(stranger, "Talk-to") + tick(2) + player.skipDialogues() + + assertEquals("completed", player["in_search_of_the_myreque", "unstarted"]) + assertEquals(points + 2, player["quest_points", 0]) + } + + private companion object { + /** Any seed will do, as long as the test draws the same three questions Curpile does. */ + private const val SEED = 42 + + /** Which option each of Curpile's questions has the right answer on. */ + private val CORRECT_ANSWERS = mapOf(0 to 4, 1 to 3, 2 to 1, 3 to 2, 4 to 2, 5 to 4) + + /** One stage from every branch of the journal. */ + private val JOURNAL_STAGES = listOf( + "unstarted", + "agreed_to_help", + "refused_delivery", + "persuaded_boatman", + "gave_planks", + "reached_hollows", + "questioned_by_curpile", + "answered_questions", + "entered_hideout", + "met_veliaf", + "weapons_accepted", + "delivered_weapons", + "hellhound_summoned", + "killed_hellhound", + "shown_way_out", + "in_inn_basement", + "escaped_to_canifis", + "completed", + ) + } +}