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 026c80bf55..43e6d35430 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..5402d77d87 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/canifis/VanstromKlause.kt @@ -0,0 +1,174 @@ +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..79c296af74 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/CurpileFyod.kt @@ -0,0 +1,250 @@ +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..24c73f9129 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/HaroldEvans.kt @@ -0,0 +1,82 @@ +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..82a520e7b1 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/IvanStrom.kt @@ -0,0 +1,85 @@ +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..9d444db6d1 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/PolmafiFerdygris.kt @@ -0,0 +1,82 @@ +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..093a18e36d --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/RadigadPonfit.kt @@ -0,0 +1,79 @@ +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.") + } +} 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..e3821f3ce0 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/SaniPiliu.kt @@ -0,0 +1,87 @@ +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..30cded02a0 --- /dev/null +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/VeliafHurtz.kt @@ -0,0 +1,385 @@ +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.EvilLaugh +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.quest +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.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 { + if (quest("in_search_of_the_myreque") == "hellhound_summoned") { + tele(HOUND_SPOT) + } else { + tele(VELIAF_SPOT) + } + 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..9ea5b67143 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,297 @@ 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.carriesItem +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) && carriesItem("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 de2a239de8..9a1acb138a 100644 --- a/game/src/main/kotlin/content/quest/Quest.kt +++ b/game/src/main/kotlin/content/quest/Quest.kt @@ -25,6 +25,7 @@ val quests = setOf( "creature_of_fenkenstrain", "druidic_ritual", "hand_in_the_sand", + "in_search_of_the_myreque", "jungle_potion", "nature_spirit", "plague_city", @@ -46,6 +47,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..3ed13fdee7 --- /dev/null +++ b/game/src/main/kotlin/content/quest/member/myreque/SearchMyreque.kt @@ -0,0 +1,445 @@ +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) + } + + objectOperate("Climb", "swamp_bridge_tree") { (target) -> + climbTree(target) + } + + 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")) { + statement("There seems to be a strange combination on the door. It would take ages to work it out.") + return@objectOperate + } + 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", + ) + } +} diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/convert/DialogueLineBreakRemover.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/convert/DialogueLineBreakRemover.kt new file mode 100644 index 0000000000..12d9d9ca33 --- /dev/null +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/convert/DialogueLineBreakRemover.kt @@ -0,0 +1,47 @@ +package world.gregs.voidps.tools.convert + +import java.io.File + +/** + * ``` + * statement( + * "Removes dialogues which are formated" + * + "like this", + * ) + * ``` + */ +object DialogueLineBreakRemover { + @JvmStatic + fun main(args: Array) { + val file = File("./game/src/main/kotlin/content/quest/member/myreque/") + + val files = when { + file.isFile -> listOf(file) + file.isDirectory -> file.walkTopDown().filter { it.isFile }.toList() + else -> return + } + val start = Regex("((?:player|npc)<[a-zA-Z]+>|statement)\\([\\r\\n]\\s+\"") + val middle = Regex("\"\\s\\+[\\r\\n]\\s+\"") + val end = Regex("\",[\\r\\n]\\s+\\)") + for (file in files) { + var text = file.readText() + + text = replaceStart(start, text) + text = text.replace(middle, "") + text = text.replace(end, "\")") + file.writeText(text) + } + } + + private fun replaceStart(start: Regex, text: String): String { + val matches = mutableListOf() + for (result in start.findAll(text)) { + matches.add(result) + } + var text = text + for (match in matches.sortedByDescending { it.range.first }) { + text = text.replaceRange(match.range, "${match.value.substringBefore('(')}(\"") + } + return text + } +} \ No newline at end of file