diff --git a/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml b/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml index 2d6145d6cc..c61c73e54b 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npc-spawns.toml @@ -228,5 +228,4 @@ spawns = [ { id = "mound_feldip_hills", x = 2465, y = 2911, members = true }, { id = "mound_feldip_hills", x = 2466, y = 2921, members = true }, { id = "wolf", x = 2470, y = 2865 }, - { id = "rantz", x = 2630, y = 2981 }, ] \ No newline at end of file diff --git a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml index cb1a0a4ad2..f4704e5e15 100644 --- a/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml +++ b/data/area/kandarin/feldip_hills/feldip_hills.npcs.toml @@ -80,6 +80,7 @@ examine = "A large and contentious lady ogre." [rantz_feldip_hills_2] id = 8659 +dialogue = "ogre" [mound_feldip_hills] id = 9466 @@ -115,11 +116,6 @@ id = 13174 id = 5073 examine = "This bird obviously doesn't believe in subtlety." -[rantz] -id = 3587 -wander_range = 4 -examine = "A large dim looking humanoid." - [ogre_boat_feldip_hills_2] id = 3472 @@ -128,6 +124,7 @@ id = 3467 [rantz_feldip_hills_2_2] id = 1010 +dialogue = "ogre" [fishing_spot_big_net_harpoon_feldip_hills] id = 7044 diff --git a/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.areas.toml b/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.areas.toml new file mode 100644 index 0000000000..658f27ce99 --- /dev/null +++ b/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.areas.toml @@ -0,0 +1,5 @@ +[zogre_blackened_area] +x = [2447, 2448] +y = [9459, 9467] +level = 2 +tags = ["dark"] \ No newline at end of file diff --git a/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.combat.toml b/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.combat.toml new file mode 100644 index 0000000000..fab80f06c7 --- /dev/null +++ b/data/area/kandarin/feldip_hills/jiggig/dungeon/jiggig_dungeon.combat.toml @@ -0,0 +1,31 @@ +[zogre_jiggig_dungeon] +attack_speed = 6 +defend_anim = "ogre_defend" +defend_sound = "zogre_hit" +death_anim = "ogre_death" +death_sound = "zogre_death" + +[zogre_jiggig_dungeon.melee] +range = 1 +anim = "ogre_attack" +target_sound = "zogre_attack" +target_hit = { offense = "crush", max = 50 } +impact_disease = 10 + +[zogre_jiggig_dungeon_2] +clone = "zogre_jiggig_dungeon" + +[zogre_jiggig_dungeon_2.melee] +clone = "zogre_jiggig_dungeon.melee" + +[zogre_jiggig_dungeon_3] +clone = "zogre_jiggig_dungeon" + +[zogre_jiggig_dungeon_3.melee] +clone = "zogre_jiggig_dungeon.melee" + +[zogre_jiggig_dungeon_4] +clone = "zogre_jiggig_dungeon" + +[zogre_jiggig_dungeon_4.melee] +clone = "zogre_jiggig_dungeon.melee" \ No newline at end of file diff --git a/data/area/kandarin/feldip_hills/jiggig/jiggig.combat.toml b/data/area/kandarin/feldip_hills/jiggig/jiggig.combat.toml new file mode 100644 index 0000000000..259d3140dc --- /dev/null +++ b/data/area/kandarin/feldip_hills/jiggig/jiggig.combat.toml @@ -0,0 +1,75 @@ +[zogre_jiggig] +attack_speed = 6 +defend_anim = "ogre_defend" +defend_sound = "zogre_hit" +death_anim = "ogre_death" +death_sound = "zogre_death" + +[zogre_jiggig.melee] +range = 1 +anim = "ogre_attack" +target_sound = "zogre_attack" +target_hit = { offense = "crush", max = 50 } +impact_disease = 10 + +[zogre_jiggig_2] +clone = "zogre_jiggig" + +[zogre_jiggig_2.melee] +clone = "zogre_jiggig.melee" + +[zogre_jiggig_3] +clone = "zogre_jiggig" + +[zogre_jiggig_3.melee] +clone = "zogre_jiggig.melee" + +[zogre_jiggig_4] +clone = "zogre_jiggig" + +[zogre_jiggig_4.melee] +clone = "zogre_jiggig.melee" + +[zogre_jiggig_5] +clone = "zogre_jiggig" + +[zogre_jiggig_5.melee] +clone = "zogre_jiggig.melee" + +[zogre_jiggig_6] +clone = "zogre_jiggig" + +[zogre_jiggig_6.melee] +clone = "zogre_jiggig.melee" + +[zogre_jiggig_7] +clone = "zogre_jiggig" + +[zogre_jiggig_7.melee] +clone = "zogre_jiggig.melee" + +[skogre_jiggig] +attack_speed = 6 +defend_anim = "ogre_defend" +defend_sound = "skelly_hit" +death_anim = "ogre_death" +death_sound = "zogre_death" + +[skogre_jiggig.melee] +range = 1 +anim = "ogre_attack" +target_sound = "giant_attack" +target_hit = { offense = "crush", max = 50 } +impact_disease = 10 + +[skogre_jiggig_2] +clone = "skogre_jiggig" + +[skogre_jiggig_2.melee] +clone = "skogre_jiggig.melee" + +[skogre_jiggig_3] +clone = "skogre_jiggig" + +[skogre_jiggig_3.melee] +clone = "skogre_jiggig.melee" \ No newline at end of file diff --git a/data/area/kandarin/feldip_hills/jiggig/jiggig.npc-spawns.toml b/data/area/kandarin/feldip_hills/jiggig/jiggig.npc-spawns.toml index 60cf2469d9..2c799f055f 100644 --- a/data/area/kandarin/feldip_hills/jiggig/jiggig.npc-spawns.toml +++ b/data/area/kandarin/feldip_hills/jiggig/jiggig.npc-spawns.toml @@ -3,7 +3,7 @@ spawns = [ { id = "uglug_nar", x = 2442, y = 3049, members = true }, { id = "pilg", x = 2443, y = 3046, members = true }, { id = "grug", x = 2446, y = 3049, members = true }, - { id = "ogre_guard_jiggig", x = 2454, y = 3047, members = true }, + { id = "zogre_ogre_guard", x = 2454, y = 3047, members = true }, { id = "ogre_guard_jiggig_2", x = 2443, y = 3038, members = true }, { id = "ogre_guard_jiggig_2", x = 2452, y = 3030, members = true }, { id = "zogre_jiggig", x = 2486, y = 3048, members = true }, diff --git a/data/area/kandarin/feldip_hills/jiggig/jiggig.npcs.toml b/data/area/kandarin/feldip_hills/jiggig/jiggig.npcs.toml index 6cf4186246..4833dd0efd 100644 --- a/data/area/kandarin/feldip_hills/jiggig/jiggig.npcs.toml +++ b/data/area/kandarin/feldip_hills/jiggig/jiggig.npcs.toml @@ -1,24 +1,30 @@ [grish] id = 2038 +dialogue = "ogre" examine = "An ogre shaman" [uglug_nar] id = 2039 +dialogue = "ogre" examine = "An ogre shaman" [pilg] id = 2040 +dialogue = "ogre" examine = "They're done for!" [grug] id = 2041 +dialogue = "ogre" examine = "They're done for!" -[ogre_guard_jiggig] +[zogre_ogre_guard] id = 2042 +dialogue = "ogre" [ogre_guard_jiggig_2] id = 2043 +dialogue = "ogre" [zogre_jiggig] id = 2044 @@ -58,6 +64,7 @@ examine = "An undead skeletal ogre." [skogre_jiggig_2] id = 2056 +retaliates = false clone = "skogre_jiggig" [skogre_jiggig_3] @@ -66,20 +73,24 @@ clone = "skogre_jiggig" [zogre_jiggig_3] id = 2051 +clone = "zogre_jiggig" [zogre_jiggig_4] id = 2052 +clone = "zogre_jiggig" [zogre_jiggig_5] id = 2053 +clone = "zogre_jiggig" [zogre_jiggig_6] id = 2054 +clone = "zogre_jiggig" [zogre_jiggig_7] id = 2055 -categories = ["ogres"] -examine = "A partially decomposing zombie ogre." +retaliates = false +clone = "zogre_jiggig" [ogre_cook_jiggig] id = 791 diff --git a/data/area/kandarin/yanille/yanille.objs.toml b/data/area/kandarin/yanille/yanille.objs.toml index 4beec11fcc..81ac7bf730 100644 --- a/data/area/kandarin/yanille/yanille.objs.toml +++ b/data/area/kandarin/yanille/yanille.objs.toml @@ -2,6 +2,10 @@ id = 9302 examine = "A rather strategically placed hole, which appears to be in the ground." +[zogre_outdoor_bell] +id = 6847 +examine = "A bell to attract the attention of the secretary." + [yanille_underwall_tunnel_castle_wall] id = 9301 examine = "A well constructed castle wall." diff --git a/data/area/morytania/braindeath_island/braindeath_island.combat.toml b/data/area/morytania/braindeath_island/braindeath_island.combat.toml index 0d4afb1a3b..000df10208 100644 --- a/data/area/morytania/braindeath_island/braindeath_island.combat.toml +++ b/data/area/morytania/braindeath_island/braindeath_island.combat.toml @@ -19,7 +19,7 @@ range = 1 anim = "spider_large_attack" target_sound = "insect_attack" impact_regardless = true -impact_disease = 80 +impact_disease = 8 [zombie_pirate] attack_speed = 4 diff --git a/data/entity/npc/humanoid/jogre/jogre.combat.toml b/data/entity/npc/humanoid/jogre/jogre.combat.toml index d8614a0b55..fde38f19b7 100644 --- a/data/entity/npc/humanoid/jogre/jogre.combat.toml +++ b/data/entity/npc/humanoid/jogre/jogre.combat.toml @@ -10,5 +10,5 @@ death_sound = "giant_death" chance = 75 range = 1 anim = "ogre_attack" -target_sound = "giant_attack" +target_sound = "moss_giant_attack" target_hit = { offense = "crush", max = 80 } diff --git a/data/entity/player/human.anims.toml b/data/entity/player/human.anims.toml index d77e236f9e..abf3bbcf2b 100644 --- a/data/entity/player/human.anims.toml +++ b/data/entity/player/human.anims.toml @@ -22,6 +22,9 @@ id = 422 [unarmed_kick] id = 423 +[human_dancing] +id = 818 + [unarmed_block] id = 422 diff --git a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml index e8911fba42..310ce63418 100644 --- a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml +++ b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml @@ -5,4 +5,41 @@ id = 6800 id = 6802 [chompy_bird_death] -id = 6823 \ No newline at end of file +id = 6823 + +# Cache-canonical names from D:/Names. Some collide on id with existing aliases; that's allowed. +[chompy_update_attack] +id = 6761 + +[chompy_update_death] +id = 6762 + +[chompy_update_fly_down] +id = 6766 + +[jubbly_update_attack] +id = 6800 + +[jubbly_update_death] +id = 6801 + +[jubbly_update_fly_down] +id = 6805 + +[human_cooking] +id = 896 + +[chompy_toad_inflate] +id = 1019 + +[human_chompybird_ogrebellows] +id = 1026 + +[human_ogre_fletching] +id = 4433 + +[human_castcurse_walkmerge] +id = 11428 + +[human_openchest] +id = 536 \ No newline at end of file diff --git a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.gfx.toml b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.gfx.toml index 380ecd427d..4657cbcfbb 100644 --- a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.gfx.toml +++ b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.gfx.toml @@ -1,2 +1,8 @@ [ogre_bellows] id = 241 + +[chompy_toad_exploding] +id = 240 + +[ogre_arrow_travel] +id = 242 \ No newline at end of file diff --git a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.npcs.toml b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.npcs.toml index 5918753b27..aa5e387717 100644 --- a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.npcs.toml +++ b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.npcs.toml @@ -8,5 +8,29 @@ combat_def = "chompy_bird" slayer_xp = 10.0 categories = ["birds"] immune_poison = true -drop_table = "chompy_bird" +# No drop_table — Java intercepts death and transforms into a pluckable carcass instead. examine = "A large boisterous bird, a delicacy for ogres." + +[jubbly_bird_chompy] +id = 3476 +hitpoints = 100 +att = 5 +str = 5 +def = 3 +combat_def = "chompy_bird" +slayer_xp = 10.0 +categories = ["birds"] +immune_poison = true +examine = "A large boisterous bird, a delicacy for ogres." + +[plucked_chompy] +id = 1016 +examine = "A large boisterous bird, a delicacy for ogres." + +[plucked_jubbly] +id = 3477 +examine = "A large boisterous bird, a delicacy for ogres." + +[bloated_toad_placed] +id = 1014 +examine = "A bloated toad." diff --git a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.sounds.toml b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.sounds.toml index c660b71454..f5ab4a1dcd 100644 --- a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.sounds.toml +++ b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.sounds.toml @@ -12,3 +12,18 @@ id = 1451 [chompy_bird_squak] id = 1453 + +[ogre_bow] +id = 1452 + +[ogre_bellows_suck] +id = 1454 + +[ogre_bellows] +id = 1455 + +[spit_roast] +id = 1456 + +[toad_croak] +id = 1458 \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.anims.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.anims.toml new file mode 100644 index 0000000000..6c355aa9e7 --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.anims.toml @@ -0,0 +1,59 @@ +[ogre_kick] +id = 2102 + +[slash_bash_melee] +id = 11458 + +[ogre_breath] +id = 2101 + +[human_reachforladder] +id = 828 + +[human_mapping] +id = 909 + +[regicide_stepover] +id = 1236 + +[zombie_update_defend_normal] +id = 5567 + +[zombie_update_attack_normal] +id = 5568 + +[zombie_update_death_normal] +id = 5569 + +[zogre_bell_ring] +id = 2103 + +[expression_ogre_neutral] +id = 8579 + +[expression_ogre_quiz] +id = 8580 + +[expression_ogre_angry] +id = 8583 + +[expression_ogre_mad] +id = 8657 + +[expression_ogre_confused] +id = 8584 + +[expression_ogre_happy] +id = 8585 + +[expression_ogre_scared] +id = 8598 + +[expression_ogre_sad] +id = 8659 + +[expression_ogre_shifty] +id = 8661 + +[expression_ogre_shock] +id = 8662 diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.combat.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.combat.toml new file mode 100644 index 0000000000..a947dca2b8 --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.combat.toml @@ -0,0 +1,27 @@ +[slash_bash] +attack_speed = 6 +retreat_range = 12 +defend_anim = "ogre_defend" +defend_sound = "zogre_hit" +death_anim = "ogre_death" +death_sound = "zogre_death" + +[slash_bash.melee] +chance = 1 +range = 1 +anim = "slash_bash_melee" +target_sound = "zogre_attack" +target_hit = { offense = "crush", max = 130 } +impact_disease = 15 +approach = false + +[slash_bash.range] +chance = 1 +range = 10 +anim = "ogre_breath" +target_sound = "ogre_bow" +projectile = "slash_bash_projectile" +projectile_origin_x = 1 +projectile_origin_y = 1 +target_hit = { offense = "range", max = 130 } +impact_disease = 15 \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.drops.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.drops.toml index fe84e93d68..d423cd06da 100644 --- a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.drops.toml +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.drops.toml @@ -12,3 +12,15 @@ roll = 5000 drops = [ { id = "zombie_champions_scroll", members = true, lacks = "zombie_champions_scroll" }, ] + +[zogre_human_brentle_vahn_drop_table] +type = "all" +drops = [ + { id = "ruined_backpack" }, +] + +[zogre_human_brentle_vahn_tertiary] +roll = 5000 +drops = [ + { id = "zombie_champions_scroll", members = true, lacks = "zombie_champions_scroll" }, +] \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.gfx.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.gfx.toml new file mode 100644 index 0000000000..a4f18d72a2 --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.gfx.toml @@ -0,0 +1,12 @@ +[smokepuff_large] +id = 188 + +[slash_bash_projectile] +id = 397 +height = 40 +end_height = 36 +delay = 41 +curve = 15 +size_offset = 11 +time_offset = 15 +multiplier = 5 \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.items.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.items.toml index d6f291c689..399dc5338b 100644 --- a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.items.toml +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.items.toml @@ -61,7 +61,7 @@ weight = 0.005 examine = "A half torn necromantic page." kept = "Wilderness" -[ruined_backpack_zogre_flesh_eaters] +[ruined_backpack] id = 4810 tradeable = false weight = 0.02 @@ -86,21 +86,21 @@ examine = "A pile of Zombie Ogre bones." [zogre_bones_noted] id = 4813 -[sithik_portrait] +[zogre_sithik_portrait_good] id = 4814 tradeable = false weight = 0.02 examine = "A classic realist charcoal portrait of Sithik." kept = "Wilderness" -[sithik_portrait_bad] +[zogre_sithik_portrait_bad] id = 4815 tradeable = false weight = 0.02 examine = "A semi-nihilistic, pseudo-impressionistic, half-squarist charcoal sketch of Sithik." kept = "Wilderness" -[signed_portrait] +[zogre_sithik_portrait_signed] id = 4816 tradeable = false weight = 0.02 @@ -222,7 +222,7 @@ examine = "Ancient ogre bones from the ogre burial tomb." [ourg_bones_noted] id = 4835 -[strange_potion] +[zogre_ogre_trans_potion] id = 4836 tradeable = false weight = 0.001 diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.npcs.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.npcs.toml index b5262456f7..4cf37a2483 100644 --- a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.npcs.toml +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.npcs.toml @@ -14,3 +14,25 @@ slayer_xp = 100.0 categories = ["zombies"] drop_table = "slash_bash" examine = "A powerful looking Zogre." + +[zogre_sithik_man] +id = 2061 + +[zogre_sithik_ogre] +id = 2062 +dialogue = "ogre" + +[zogre_human_brentle_vahn] +id = 1826 +hitpoints = 500 +att = 30 +str = 30 +def = 30 +attack_speed = 6 +style = "crush" +max_hit_melee = 40 +hunt_mode = "cowardly" +slayer_xp = 50.0 +categories = ["zombies"] +drop_table = "zogre_human_brentle_vahn" +examine = "A human zombie." \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.objs.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.objs.toml new file mode 100644 index 0000000000..f4ce32011e --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.objs.toml @@ -0,0 +1,62 @@ +[ogre_stairs_down] +id = 6841 + +[ogre_stairs] +id = 6842 + +[zogre_lecturn] +id = 6846 + +[zogre_coffin_base] +id = 6843 + +[zogre_coffin_special] +id = 6844 + +[zogre_coffin_special_searched] +id = 6845 + +[zogre_brentle_skeleton] +id = 6893 + +[zogre_stand] +id = 6897 + +[ogre_cavedoorr_closed] +id = 6871 + +[ogre_cavedoorl_closed] +id = 6872 + +[ogre_cavedoorr_opened] +id = 6873 + +[ogre_cavedoorl_opened] +id = 6874 + +[zogre_sithik_bed] +id = 6889 + +[zogre_sithik_bed_entity] +id = 6887 + +[ogre_bedman_loc] +id = 6888 + +[sithiks_drawers] +id = 6875 + +[sithiks_cupboard] +id = 6876 + +[sithiks_wardrobe] +id = 55412 + +[ogre_barricade_collapsed] +id = 6879 + +[ogre_barricade_collapsedr] +id = 6881 + +[ogre_barricade_collapsedl] +id = 6882 \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.sounds.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.sounds.toml new file mode 100644 index 0000000000..9ece1937aa --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.sounds.toml @@ -0,0 +1,47 @@ +[ogre_destroy_barricade] +id = 1954 + +[drip_poison] +id = 1955 + +[down_stone_stairs] +id = 1952 + +[up_stone_stairs] +id = 1956 + +[disease_hitsplat] +id = 2388 + +[strangedoor_open] +id = 2410 + +[smokepuff] +id = 1930 + +[zogre_writing] +id = 1958 + +[bonewalk] +id = 1953 + +[zogre_death] +id = 916 + +[zogre_hit] +id = 917 + +[zogre_attack] +id = 914 + +[zogre_shield_hit] +id = 915 + +[zogre_axe_attack] +id = 913 + +[skelly_hit] +id = 779 + +[zogre_bell] +id = 1959 \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.varbits.toml b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.varbits.toml index b01d9fa8a1..622e7d0352 100644 --- a/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.varbits.toml +++ b/data/quest/members/zogre_flesh_eaters/zogre_flesh_eaters.varbits.toml @@ -3,4 +3,94 @@ id = 487 persist = true format = "map" default = "unstarted" -values = { unstarted = 0, started = 1 } +values = { + unstarted = 0, + investigate = 2, + barricade = 3, + sithik = 4, + potion = 6, + permanent_spell = 8, + given_key = 10, + killed_slash_bash = 12, + completed = 14, +} + +[thzfe_prismsearch] +id = 488 +persist = true +format = "int" + +[thzfe_innkeepermugshown] +id = 489 +persist = true +format = "boolean" + +[thzfe_innkeeperportraitshown] +id = 490 +persist = true +format = "boolean" + +[thzfe_shownnecrobook] +id = 491 +persist = true +format = "boolean" + +[thzfe_shownhambook] +id = 492 +persist = true +format = "boolean" + +[thzfe_showntankard] +id = 493 +persist = true +format = "boolean" + +[thzfe_shownsignedportrait] +id = 494 +persist = true +format = "boolean" + +[thzfe_sithik_transformed] +id = 495 +persist = true +format = "int" + +[thzfe_blocking_barricade] +id = 496 +persist = true +format = "boolean" + +[thzfe_makecuredisease] +id = 498 +persist = true +format = "boolean" + +[thzfe_makebrutalarrow] +id = 499 +persist = true +format = "boolean" + +[thzfe_makecompozogrebow] +id = 500 +persist = true +format = "boolean" + +[thzfe_sold_balm] +id = 502 +persist = true +format = "boolean" + +[thzfe_brentle_skele] +id = 503 +persist = true +format = "int" + +[thzfe_cut_scene] +id = 505 +persist = true +format = "boolean" + +[thzfe_grish_warning_yes] +id = 507 +persist = true +format = "boolean" \ No newline at end of file diff --git a/data/quest/members/zogre_flesh_eaters/zogre_human_brentle_vahn.combat.toml b/data/quest/members/zogre_flesh_eaters/zogre_human_brentle_vahn.combat.toml new file mode 100644 index 0000000000..986151d480 --- /dev/null +++ b/data/quest/members/zogre_flesh_eaters/zogre_human_brentle_vahn.combat.toml @@ -0,0 +1,13 @@ +[zogre_human_brentle_vahn] +attack_speed = 6 +retreat_range = 16 +defend_anim = "zombie_update_defend_normal" +defend_sound = "zombie_defend" +death_anim = "zombie_update_death_normal" +death_sound = "zombie_death" + +[zogre_human_brentle_vahn.melee] +range = 1 +anim = "zombie_update_attack_normal" +target_sound = "zombie_attack" +target_hit = { offense = "crush", max = 40 } \ No newline at end of file diff --git a/data/quest/quests.toml b/data/quest/quests.toml index 6a012d9bf8..e67a46e28f 100644 --- a/data/quest/quests.toml +++ b/data/quest/quests.toml @@ -3859,3 +3859,22 @@ req_combat = "You will need to defeat a level 58 enemy and level 36 enemy." points = 1 reward = "3 ourg bones
2 zogre bones
Access to Jiggig
Ability to equip inoculation brace
Ability to fletch composite ogre bow and brutal arrows
Access to post-quest rewards from Zavistic Rarve or Yanni Salika, and Uglug Nar" xp = "2,000 Fletching XP
2,000 Ranged XP
2,000 Herblore XP" +variables = [ + "zogre_flesh_eaters", + "thzfe_prismsearch", + "thzfe_innkeepermugshown", + "thzfe_innkeeperportraitshown", + "thzfe_shownnecrobook", + "thzfe_shownhambook", + "thzfe_showntankard", + "thzfe_shownsignedportrait", + "thzfe_sithik_transformed", + "thzfe_blocking_barricade", + "thzfe_makecuredisease", + "thzfe_makebrutalarrow", + "thzfe_makecompozogrebow", + "thzfe_sold_balm", + "thzfe_brentle_skele", + "thzfe_cut_scene", + "thzfe_grish_warning_yes", +] \ No newline at end of file diff --git a/data/skill/fletching/arrowtip.recipes.toml b/data/skill/fletching/arrowtip.recipes.toml index 258b061241..4dc688c413 100644 --- a/data/skill/fletching/arrowtip.recipes.toml +++ b/data/skill/fletching/arrowtip.recipes.toml @@ -92,6 +92,8 @@ skill = "fletching" level = 7 xp = 8.4 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "bronze_nails", amount = 6 }] add = [{ id = "bronze_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -103,6 +105,8 @@ skill = "fletching" level = 18 xp = 15.6 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "iron_nails", amount = 6 }] add = [{ id = "iron_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -114,6 +118,8 @@ skill = "fletching" level = 33 xp = 30.6 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "steel_nails", amount = 6 }] add = [{ id = "steel_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -125,6 +131,8 @@ skill = "fletching" level = 38 xp = 39.0 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "black_nails", amount = 6 }] add = [{ id = "black_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -136,6 +144,8 @@ skill = "fletching" level = 49 xp = 45.0 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "mithril_nails", amount = 6 }] add = [{ id = "mithril_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -147,6 +157,8 @@ skill = "fletching" level = 62 xp = 61.2 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "adamant_nails", amount = 6 }] add = [{ id = "adamant_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -158,6 +170,8 @@ skill = "fletching" level = 77 xp = 75.0 ticks = 2 +requires = ["hammer"] +requires_message = "You need a hammer to force these nails into the arrow shafts." remove = [{ id = "flighted_ogre_arrow", amount = 6 }, { id = "rune_nails", amount = 6 }] add = [{ id = "rune_brutal", amount = 6 }] message = "You attach the nails to 6 arrow shafts." @@ -183,6 +197,6 @@ ticks = 3 requires = ["knife"] remove = ["achey_tree_logs", "wolf_bones"] add = ["unstrung_comp_bow"] -message = "You carefully cut the wood and add the bones." +message = "You carefully cut the wood into a composite ogre bow." question = "What would you like to fletch?" animation = "fletching_log" \ No newline at end of file diff --git a/data/skill/magic/spells.tables.toml b/data/skill/magic/spells.tables.toml index 9e3174efc7..d394a5199e 100644 --- a/data/skill/magic/spells.tables.toml +++ b/data/skill/magic/spells.tables.toml @@ -15,7 +15,6 @@ poison_damage = "int" projectiles = "list" drain_skill = "skill" drain_percent = "int" -npc_message = "string" player_message = "string" clone = "clone" @@ -516,7 +515,6 @@ xp = 600 [.monster_examine] xp = 660 -npc_message = "" [.npc_contact] xp = 630 diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/ItemOnItemDefinition.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/ItemOnItemDefinition.kt index fbf0d9d6cc..691529a56d 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/ItemOnItemDefinition.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/ItemOnItemDefinition.kt @@ -44,6 +44,7 @@ data class ItemOnItemDefinition( val sound: String = "", val message: String = "", val failure: String = "", + val requiresMessage: String = "", val question: String = "How many would you like to $type?", val maximum: Int = -1, val members: Boolean = false, diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/ItemOnItemDefinitions.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/ItemOnItemDefinitions.kt index 3183032256..3c81d40d41 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/ItemOnItemDefinitions.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/ItemOnItemDefinitions.kt @@ -48,6 +48,7 @@ class ItemOnItemDefinitions { var sound = "" var message = "" var failure = "" + var requiresMessage = "" var question: String? = null var maximum: Int = -1 var members: Boolean = false @@ -72,6 +73,7 @@ class ItemOnItemDefinitions { "sound" -> sound = string() "message" -> message = string() "failure" -> failure = string() + "requires_message" -> requiresMessage = string() "question" -> question = string() "maximum" -> maximum = int() "members" -> members = boolean() @@ -96,6 +98,7 @@ class ItemOnItemDefinitions { sound = sound, message = message, failure = failure, + requiresMessage = requiresMessage, question = question ?: "How many would you like to $type?", maximum = maximum, members = members, diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/npc/NPCs.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/npc/NPCs.kt index a66db9e592..9a3449e010 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/npc/NPCs.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/npc/NPCs.kt @@ -79,7 +79,9 @@ object NPCs : Runnable, fun add(id: String, tile: Tile, direction: Direction = Direction.SOUTH, ticks: Int, owner: Player? = null): NPC { val npc = add(id, tile, direction) - npc.despawn(ticks) + if (ticks > 0) { + npc.despawn(ticks) + } if (owner != null) { npc["owner"] = owner.accountName } @@ -94,11 +96,11 @@ object NPCs : Runnable, * NPC's full size. Returns `null` if the NPC id is unknown or no valid tile can be found * (the underlying [Area.random] retries up to 100 times before giving up). */ - fun addRandom(id: String, area: Area, direction: Direction = Direction.SOUTH): NPC? { + fun addRandom(id: String, area: Area, direction: Direction = Direction.SOUTH, ticks: Int = -1, owner: Player? = null): NPC? { val def = NPCDefinitions.getOrNull(id) ?: return null val collision = CollisionStrategyProvider.get(def) val tile = area.random(collision, def.size) ?: return null - return add(id, tile, direction) + return add(id, tile, direction, ticks, owner) } fun remove(npc: NPC?): Boolean { diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt index b74823bf27..da8d448c1c 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt @@ -124,3 +124,7 @@ class Player( private val logger = InlineLogger("Player") } } + +var Player.wearingGhostspeak: Boolean + get() = get("wearing_ghost_speak_amulet", false) + set(value) = set("wearing_ghost_speak_amulet", value) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt index 76cc081b9a..9ebf97a172 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspend.kt @@ -7,11 +7,16 @@ import world.gregs.voidps.engine.entity.character.player.Player fun Character.resumeSuspension(): Boolean { val suspend = suspension ?: return false - if (suspend is Suspension.Delay && suspend.ready()) { - suspend.resume() - } - if (suspend is Suspension.Custom && suspend.ready()) { - suspend.resume() + try { + if (suspend is Suspension.Delay && suspend.ready()) { + suspend.resume() + } + if (suspend is Suspension.Custom && suspend.ready()) { + suspend.resume() + } + } catch (e: Exception) { + e.printStackTrace() + suspension = null } return true } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspension.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspension.kt index e1fc0dace4..0274077646 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspension.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/suspend/Suspension.kt @@ -1,9 +1,7 @@ package world.gregs.voidps.engine.suspend import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine import world.gregs.voidps.engine.GameLoop -import world.gregs.voidps.engine.entity.character.player.Player import kotlin.coroutines.resume sealed class Suspension { @@ -13,21 +11,36 @@ sealed class Suspension { * p_countdialog */ class IntEntry(private val continuation: CancellableContinuation) : Suspension() { - fun resume(int: Int) = continuation.resume(int) + fun resume(int: Int) { + if (continuation.isCancelled) { + return + } + continuation.resume(int) + } } /** * Wait for string entry dialogue */ class StringEntry(private val continuation: CancellableContinuation) : Suspension() { - fun resume(string: String) = continuation.resume(string) + fun resume(string: String) { + if (continuation.isCancelled) { + return + } + continuation.resume(string) + } } /** * Wait for name entry dialogue */ class NameEntry(private val continuation: CancellableContinuation) : Suspension() { - fun resume(string: String) = continuation.resume(string) + fun resume(string: String) { + if (continuation.isCancelled) { + return + } + continuation.resume(string) + } } /** @@ -35,7 +48,12 @@ sealed class Suspension { * p_pausebutton */ class Continue(private val continuation: CancellableContinuation) : Suspension() { - fun resume() = continuation.resume(Unit) + fun resume() { + if (continuation.isCancelled) { + return + } + continuation.resume(Unit) + } } /** @@ -47,13 +65,23 @@ sealed class Suspension { fun ready(): Boolean = GameLoop.tick >= tick - fun resume() = continuation.resume(Unit) + fun resume() { + if (continuation.isCancelled) { + return + } + continuation.resume(Unit) + } } class Custom(private val continuation: CancellableContinuation, val block: () -> Boolean) : Suspension() { fun ready(): Boolean = block.invoke() - fun resume() = continuation.resume(Unit) + fun resume() { + if (continuation.isCancelled) { + return + } + continuation.resume(Unit) + } } } diff --git a/game/src/main/kotlin/content/area/kandarin/feldip_hills/Grish.kt b/game/src/main/kotlin/content/area/kandarin/feldip_hills/Grish.kt new file mode 100644 index 0000000000..4234dd7208 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/feldip_hills/Grish.kt @@ -0,0 +1,485 @@ +package content.area.kandarin.feldip_hills + +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Mad +import content.entity.player.dialogue.Neutral +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.items +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.quest.quest +import content.quest.questComplete +import content.quest.questCompleted +import content.quest.refreshQuestJournal +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.entity.character.jingle +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.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.entity.character.player.skill.level.Level.hasMax +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove + +class Grish : Script { + + init { + npcOperate("Talk-to", "grish") { (target) -> + when (val progress = quest("zogre_flesh_eaters")) { + "unstarted" -> { + if (get("thzfe_grish_warning_yes", false)) { + confirmQuestStart() + } else { + intro(target) + } + } + "given_key" -> { + if (inventory.contains("ogre_gate_key")) { + artefactReminderMenu() + } else { + lostKey() + } + } + "killed_slash_bash" -> { + if (inventory.contains("ogre_gate_key")) { + questFinishHandover() + } else { + lostKey() + } + } + "completed" -> postQuest() + else -> { + npc("Yous creature dun da fing yet? Da zogries going in da dirt full home?") + if (progress == "permanent_spell") { + foundMenu() + } else { + player("Nope, I haven't figured out why the zogres are here yet.") + } + } + } + } + + itemOnNPCOperate("black_prism", "grish") { (target) -> + item(item = "black_prism", text = "You show the black prism to Grish.") + player("Hey Grish, I found this in the tomb, do you know what it is?") + npc("Whas you's shuvvin wizzy stuff in Grish face...is a pretty one but dat's more stuff for da wizzy's dan Grish.") + } + + itemOnNPCOperate("torn_page", "grish") { (target) -> + item(item = "torn_page", text = "You show the necromantic page to Grish.") + player("This torn page was on a lectern in the tomb, do you know why?") + npc("Dat's der wizzy stuff, not Ogery stuffsies like what Grish got. Das not even big enough for empty da big blower on! No use for Grish dat creatures...you's keeps it.") + } + + itemOnNPCOperate("dragon_inn_tankard", "grish") { (target) -> + item(item = "dragon_inn_tankard", text = "You show the tankard to Grish.") + player("I found this tankard in the tomb, have you got any suggestions?") + npc("Das a good drinker for da drinkies dat un is...is a small-un for Grish so yous creature keeps it yes. Yous creature keeps da fimble drinkers for da smaller drinkies.") + } + } + + // ===== Progress 0: Initial intro ===== + + private suspend fun Player.intro(target: NPC) { + player("Hello there, what's going on here?") + npc("Hey yous creature...wha's you's doing here? Yous be cleverer to be running so da sickies from da zogres don't dead ya.") + introMenu(target) + } + + suspend fun Player.introMenu(target: NPC) { + choice { + justLookingAround(target) + whatDoYouMeanSickies(target) + whatAreZogres(target) + sorryHaveToGo() + } + } + + suspend fun Player.introMenuExpanded(target: NPC) { + choice { + justLookingAround(target) + whatDoYouMeanSickies(target) + whatAreZogres(target) + canIHelp() + sorryHaveToGo() + } + } + + fun ChoiceOption.justLookingAround(target: NPC): Unit = option("I'm just looking around thanks.") { + npc("Yous creature won'ts see muchly in dis place...just da zogries coming wiv da sickies.") + introMenuExpanded(target) + } + + fun ChoiceOption.whatDoYouMeanSickies(target: NPC): Unit = option("What do you mean sickies?") { + npc("Da zogries comin wiv da sickies...yous get bashed by da zogries and get da sickies...den you gonna be like da zogries.") + player("Sorry, I just don't understand...") + target.anim("ogre_fake_death") + npc("Da sickies is when yous creature goes like orange till green and then goes 'Urggghhhh!' ~ Grish imitates falling down with only the white of his eyes visible. ~") + introMenuExpanded(target) + } + + fun ChoiceOption.whatAreZogres(target: NPC): Unit = option("What are Zogres?") { + npc("Da Zogres are da bigun nasties wiv da sickies, deys old pals of Grish but deys jig in Jiggig when dey's full home is deep in da dirt, dey's is not da same dead'uns like was before.") + npc("Dem zogries commin from da under dirt and us is lost for da Jiggie jig place.") + introMenuExpanded(target) + } + + fun ChoiceOption.sorryHaveToGo(): Unit = option("Sorry, I have to go.") + + fun ChoiceOption.canIHelp(): Unit = option("Can I help in any way?") { + npc("Yes creatures...yous does good fings for Grish and learn why Zogries at Jiggig and den get da Zogries back in da ground.") + player("Oh, so you want me to find out why the Zogres have appeared and then find a way of burying them?") + npc("Is what Grish says! But dis is da biggy danger fing yous creatures...yous be geddin' sickies most surely...yous needs be ready..wiv da foodies un da glug-glugs.") + player("Right, so you think there's a good chance that I can get ill from this, so I need to get some food and something to drink?") + npc("Yea creatures, yous just say what Grish says...not know own wordies creature?") + startOrDeclineMenu() + } + + suspend fun Player.startOrDeclineMenu() { + choice { + tooDangerousOption() + okayCheckThings() + } + } + + fun ChoiceOption.tooDangerousOption(): Unit = option("Hmm, sorry, it sounds a bit too dangerous.") { + npc("Yous creature is not a stoopid one...stays out of dere, like clever Grish. Yous can paint circles on chest and be da Shaman too!") + player("Hmm, is it too late to reconsider?") + } + + fun ChoiceOption.okayCheckThings(): Unit = option("Ok, I'll check things out then and report back.") { + confirmQuestStart() + } + + private suspend fun Player.confirmQuestStart() { + npc("Is yous creatures really, really sure yous wanna do dis creatures..we's got no glug-glugs for da sickies? We's knows nuffin for da going of da sickies?") + set("thzfe_grish_warning_yes", true) + choice { + reallySure() + tooDangerousOption() + } + } + + fun ChoiceOption.reallySure(): Unit = option("Yes, I'm really sure!") { + if (!meetsZogreRequirements()) { + npc("Sorry, yous creatures, but yous is too green behind da ears for dis job Grish finks.") + player("No, I'm not!") + npc("Yes you are!") + player("No, I'm not!") + npc("Yes you are and that's final!") + statement("You do not meet all of the requirements to start the Zogre Flesh Eaters quest.") + return@option + } + npc("Dats da good fing yous creature...yous does Grish a good fing. But yous know dat yous get sickies and mebe get dead!") + player("If that's your idea of a pep talk, I have to say that it leaves a lot to be desired.") + npc("Yous creatures is alus says funny stuff...speaks proper like Grish!") + set("zogre_flesh_eaters", "investigate") + addOrDrop("cooked_chompy", 3) + addOrDrop("super_restore_3", 2) + items("cooked_chompy", "super_restore_3", "Grish hands you some food and two potions.") + npc("Der's yous go creatures...da best me's do for yous...and be back wivout da sickies.") + } + + val Player.chompybird: Int + get() = get("chompy_birds", 0) + + private fun Player.meetsZogreRequirements(): Boolean = hasMax(Skill.Ranged, 30) && questCompleted("jungle_potion") && chompybird == 65 // TODO + + suspend fun Player.foundMenu() { + choice { + foundResponsibleOption() + if (get("thzfe_makebrutalarrow", false)) { + if (!get("thzfe_makecompozogrebow", false)) { + killFromDistanceOption() + } else { + easierWay() + } + } + if (get("thzfe_makecuredisease", false)) { + if (!get("thzfe_sold_balm", false)) { + cureDiseaseOption() + } else { + cureDisease() + } + } + otherQuestionsOption() + sorryHaveToGo() + } + } + + fun ChoiceOption.foundResponsibleOption(): Unit = option("I found who's responsible for the Zogres being here.") { + npc("Where is da creature? Me's wants to squeeze him till he's a deadun...") + player("The person responsible is a wizard named 'Sithik Ints' and he's going to be in serious trouble. He told me that the spell which raised the zogres from the ground will last forever.") + player("I'm sorry to say, but you'll have to move the site of your ceremonial dancing somewhere else.") + npc("Dat is da bad fing creature...we's needs new Jiggig for da fallin' down jig.") + player("Yes, that's right, you'll need to create a new ceremonial dance area.") + npc("Urghhh...not good fing creature, yous gotta get da ogrish old fings for da making new jiggig special. You's creature needs da key for getting in da low bury place.") + set("zogre_flesh_eaters", "given_key") + set("thzfe_sithik_transformed", 2) + addOrDrop("ogre_gate_key") + message("Grish gives you a crudely crafted key.") + item(item = "ogre_gate_key", text = "Grish gives you a crudely crafted key.") + player("Oh, so you want me to go back in there and look for something for you?") + npc("Yeah creature, yous gotta get da ogrish old fings for da making new jiggig and proper in da special way.") + } + + fun ChoiceOption.killFromDistanceOption(): Unit = option("I've got some information on how to kill the zogres from a distance.") { + player("Sithik told me how to make Brutal arrows which means I can kill these zogres from a distance!") + teachCompositeBow() + } + + fun ChoiceOption.cureDiseaseOption(): Unit = option("I've found out how to cure the disease.") { + player("I also found out that the disease can be cured.") + npc("Dat's da good fing creature, yous do good fing to give un to Uglug...he gives bright pretties for da sickies glug glug.") + returnToProgressMenu() + } + + /** + * Routes back to whichever menu fits the current quest stage — progress 8 stays + * in the post-Sithik review menu; progress 10/12 (after Grish has handed out the + * tomb key) drops into the post-no menu instead. + */ + private suspend fun Player.returnToProgressMenu() { + if (quest("zogre_flesh_eaters") == "permanent_spell") { + foundMenu() + } else { + postNoMenu() + } + } + + fun ChoiceOption.otherQuestionsOption(): Unit = option("I have some other questions for you.") { + otherQuestionsBranch() + } + + private suspend fun Player.teachCompositeBow() { + npc("Uhggh, whas you's sayin' creature? Yous speakies too stupid for Grish...") + player("I know how to make large arrows...you know, 'big stabbers', to kill the zogres...they're bigger and apparently do a lot of damage, only thing is, the normal ogre bow I need to fire it is quite slow.") + npc("Why you's not say so creature...me's shows you how to make da bigger stabber chucker... ~ Grish gets a couple of items out of his back pack.~") // TODO makes too much of the line blue + set("thzfe_makecompozogrebow", true) + items( + "achey_tree_logs", + "wolf_bones", + "Grish shows you he has Achey tree logs and wolf bones, he starts to whittle away at them both with a knife.", + ) + item(item = "unstrung_comp_bow", text = "Grish shows you his achievement, a rather powerful looking composite bow frame...") + items( + "unstrung_comp_bow", + "bowstring", + "He shows you the bow frame and the string and after some time and a great deal of effort, he strings the composite ogre bow.", + ) + item(item = "comp_ogre_bow", text = "Grish shows you his proud achievement...") + npc("De're creature...now yous is makin' da bigga stabber chucker...") + player("Thanks! I think....") + returnToProgressMenu() + } + + // ===== Other questions branch (lore questions) ===== + + suspend fun Player.otherQuestionsBranch() { + npc("Oh yes creatures...what's other fings yous wanna know?") + if (quest("zogre_flesh_eaters") == "permanent_spell") { + otherQuestionsLimited() + } else { + otherQuestionsFull() + } + } + + suspend fun Player.otherQuestionsFull() { + choice { + shamansOption() + doYouKnowRantz() + whyDoesntRantzLive() + whyJiggig() + talkAboutQuestOption() + } + } + + suspend fun Player.otherQuestionsLimited() { + choice { + doYouKnowRantz() + whyDoesntRantzLive() + whyJiggig() + talkAboutQuestOption() + } + } + + fun ChoiceOption.shamansOption(): Unit = option("Why are you much nicer than the Shaman in Gu'Tanoth?") { + npc("Dey's is da big crazy one's! Dey's biggest angries wiv fings and wanna dead all fings...dey's gotten da biggies wizzy stuff...and dey's wanna eat yous creatures...Grish, not do dat...") + player("Oh, well that's a relief! It's good to know you don't eat humans...") + npc("Grish not say dat! Me's want's tasty looking creatures for yums...you's looks like da sickies chompy...not good for da gutsies...") + player("Gulp!") + // TODO: switch chathead to Uglug Nar + npc("Grish, you's is fright da creatures! Leave it alone!") + // TODO: switch chathead back to Grish + npc("But it's da big laffsies when it's facey goes to whiteness....ha ha ha!") + // TODO: switch chathead to Uglug Nar + npc("But it's not da big yumsies when it's gone to all frighty...") + npc("ha ha ha ha!") + player("Yeah...very funny, I'm sure.") + otherQuestionsFull() + } + + fun ChoiceOption.doYouKnowRantz(): Unit = option("Do you know Rantz?") { + npc("Me's know's about Rantz, he's da biggun chompy hunter..he finks...ha ha ha!") + player("How do you mean?") + npc("He's da bad shot chompy sticker, no good at sneaky, sneaky part, he's more gooder at da 'noisy, noisy miss da chompy', ha ha ha! ") + otherQuestions() + } + + private suspend fun Player.otherQuestions() { + if (quest("zogre_flesh_eaters") == "permanent") { + otherQuestionsLimited() + } else { + otherQuestionsFull() + } + } + + fun ChoiceOption.whyDoesntRantzLive(): Unit = option("Why doesn't Rantz live with the rest of the Ogres?") { + npc("He's been leaving Gu 'Noth 'cos dey's peoples is da big stressy dere? All da ogries is busying all da time...not doin' no good for da healfy fing. Rantz is da brave-un tho! He's got da big secret fing for leaving Gu' Noth but me's not knowin it. But maybe's he's just want's to be da better chompy sticker?") + otherQuestions() + } + + fun ChoiceOption.whyJiggig(): Unit = option("Why do you call this place Jiggig?") { + npc("It's da place where da Jiggig is done...we's jig at Jiggig...") + otherQuestions() + } + + fun ChoiceOption.talkAboutQuestOption(): Unit = option("I want to talk about the quest.") { + foundMenu() + } + + // ===== Progress 10/12: Lost key handling ===== + + private suspend fun Player.lostKey() { + npc("Yous creature got da old fings yet?") + player("I've lost the key you gave me!") + npc("Yous stupid creatures....luckily Grish has 'nother one..") + addOrDrop("ogre_gate_key") + npc("Yous creatures doesn't loosing this ones.") + } + + // ===== Progress 10/12: Have key, ask about artefacts ===== + + private suspend fun Player.artefactReminderMenu() { + npc("Yous creature got da old fings yet?") + choice("Grish asks if you have the items yet.") { + notYet() + easierWay() + cureDisease() + sorryHaveToGo() + } + } + + fun ChoiceOption.noSorry(): Unit = option("No sorry, I don't have them yet.") { + npc("Yous creatures get dem for me soon doh, yes?") + postNoMenu() + } + + fun ChoiceOption.notYet(): Unit = option("Nope, not yet.") { + npc("Yous gets 'em quick tho, cos we'ze wonna do da new Jiggig place...") + postNoMenu() + } + + fun ChoiceOption.easierWay(): Unit = option("There must be an easier way to kill these zogres!") { + npc("Yous creature jus makin da bigga stabber chucker like Grish shows you...") + postNoMenu() + } + + fun ChoiceOption.cureDisease(): Unit = option("There must be a way to cure this disease!") { + npc("Did yous creature makes da sickies glug glug and putin some wiv Uglug for bright pretties? He's goodun for makin' da glug glugs...yous maken da glug-glug, den sellin' one for Uglug, he's makin' more of da sickies glug") + npc("glug and sellin' for bright pretties to yous creature...") + postNoMenu() + } + + suspend fun Player.postNoMenu() { + choice { + if (get("thzfe_makebrutalarrow", false)) { + if (!get("thzfe_makecompozogrebow", false)) { + killFromDistanceOption() + } else { + easierWay() + } + } + if (get("thzfe_makecuredisease", false)) { + if (!get("thzfe_sold_balm", false)) { + cureDiseaseOption() + } else { + cureDisease() + } + } + otherQuestionsOption() + sorryHaveToGo() + } + } + + private suspend fun Player.questFinishHandover() { + npc("Hey, you's creature got da old fings?") + choice { + if (inventory.contains("ogre_artefact")) { + haveThemHere() + } else { + noSorry() + } + howIsItGoing() + otherQuestionsOption() + sorryHaveToGoNow() + } + } + + private suspend fun Player.postQuest() { + npc("Hey yous creatures da good un...") + postFinishMenu() + } + + fun ChoiceOption.howIsItGoing(): Unit = option("How's everything going now?") { + npc("All da zogries stayin' in da oldie Jiggig, we's gonna do da new Jiggig someways else. Yous creature da good-un for geddin' da oldie fings...") + postFinishMenu() + } + + fun ChoiceOption.sorryHaveToGoNow(): Unit = option("Sorry, I have to go now.") {} + + suspend fun Player.postFinishMenu() { + choice { + howIsItGoing() + otherQuestionsOption() + sorryHaveToGo() + } + } + + fun ChoiceOption.haveThemHere(): Unit = option("Yeah, I have them here!") { + npc("Dat is da goodly fing yous creature, now's we's can make da new Jiggig place away from zogries! Yous been da big helpy fing yous creature, Grish wishin' yous good stuff for da next fings for creature.") + npc("~ Grish seems very pleased about the return of the artefacts. ~") + player("Thanks, that's very nice of you!") + sendZogreFleshEatersReward() + } +} + +fun Player.sendZogreFleshEatersReward() { + jingle("quest_complete_1") + inventory.remove("ogre_artefact") + inventory.remove("ogre_gate_key") + exp(Skill.Ranged, 2000.0) + exp(Skill.Fletching, 2000.0) + exp(Skill.Herblore, 2000.0) + inc("quest_points", 1) + AuditLog.event(this, "quest_completed", "zogre_flesh_eaters") + set("zogre_flesh_eaters", "completed") + refreshQuestJournal() + questComplete( + "Zogre Flesh Eaters", + "1 Quest Point", + "Can now make Brutal Arrows", + "and cure disease potions.", + "2000 Ranged, Fletching and", + "Herblore XP.", + item = "ogre_artefact", + ) +} diff --git a/game/src/main/kotlin/content/area/kandarin/feldip_hills/OgreGuard.kt b/game/src/main/kotlin/content/area/kandarin/feldip_hills/OgreGuard.kt new file mode 100644 index 0000000000..4c6d1cfdac --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/feldip_hills/OgreGuard.kt @@ -0,0 +1,64 @@ +package content.area.kandarin.feldip_hills + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.quest +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.type.Tile + +class OgreGuard : Script { + + init { + npcOperate("Talk-to", "zogre_ogre_guard") { (target) -> + when (quest("zogre_flesh_eaters")) { + "unstarted" -> warnAway() + "investigate" -> openBarricade(target) + else -> postBarricadeWarning() + } + } + } + + // ===== Progress 0: Generic warning, player hasn't accepted quest ===== + + private suspend fun Player.warnAway() { + npc("Yous needs ta stay away from dis place...yous get da sickies and mebe yous goes to dead if yous da unlucky fing.") + } + + // ===== Progress 2: Player has accepted quest, ready to break barricade ===== + + private suspend fun Player.openBarricade(guard: NPC) { + npc("Yous needs ta stay away from dis place...yous get da sickies and mebe yous goes to dead if yous da unlucky fing.") + player("But Grish has asked me to look into this place and find out why all the undead ogres are here.") + npc("Ok, dat is da big, big scary, danger fing!
You's sure you's wants to go in?") + player("Yes, I'm sure.") + npc("Ok, I opens da stoppa's for yous creature.") + breakBarricadeCutscene(guard) + npc("Ok der' yous goes!") + } + + // ===== Progress 3+: Past the barricade, just a flavor warning ===== + + private suspend fun Player.postBarricadeWarning() { + npc("Hey yous tryin' not to get da sickies else yous be da sick-un and mebe get to be a dead-un if yous be da unlucky fing.") + player("Don't worry, I know how to take care of myself.") + } + + // ===== Helpers - replace with project-specific implementations ===== + + private suspend fun Player.breakBarricadeCutscene(guard: NPC) { + guard.clearWatch() + guard.face(Tile(2458, 3049, 0)) + delay(2) + guard.anim("ogre_kick") + sound("unarmed_kick") + delay(1) + set("zogre_flesh_eaters", "barricade") + set("thzfe_blocking_barricade", true) + sound("ogre_destroy_barricade") + delay(2) + } +} diff --git a/game/src/main/kotlin/content/area/kandarin/feldip_hills/UglugNar.kt b/game/src/main/kotlin/content/area/kandarin/feldip_hills/UglugNar.kt new file mode 100644 index 0000000000..01abdeda30 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/feldip_hills/UglugNar.kt @@ -0,0 +1,106 @@ +package content.area.kandarin.feldip_hills + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +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.items +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.inv.item.addOrDrop +import content.quest.quest +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove + +class UglugNar : Script { + init { + npcOperate("Talk-to", "uglug_nar") { (target) -> + when (quest("zogre_flesh_eaters")) { + "unstarted" -> firstMeetingMenu() + "investigate", "barricade", "sithik" -> repeatMeetingMenu() + else -> repeatMeetingMenu() + } + } + + npcOperate("Trade", "uglug_nar") { (target) -> + if (get("thzfe_sold_balm", false)) { + openShop("uglugs_stuffsies") + } else { + npc("Me's not got no glug-glugs to sell, yous bring me da sickies glug-glug den me's open da stufsies for ya.") + } + } + + registerSale("relicyms_balm_4", price = 1000) + registerSale("relicyms_balm_3", price = 650) + registerSale("relicyms_balm_2", price = 300) + registerSale("relicyms_balm_1", price = 100) + } + + // ===== Talk-to: First-time meeting (progress 0) ===== + + suspend fun Player.firstMeetingMenu() { + choice { + whatsGoingOn() + whatAreYouSelling() + okayThanks() + } + } + + fun ChoiceOption.whatsGoingOn(): Unit = option("Hey, what's going on here?") { + npc("Dem's dead ogre's come out of da ground...dey's makin' da rest of us into sick-uns ...and dead-uns.") + player("That doesn't sound good!") + npc("Grish want's da person go down der - see what's what!") + } + + fun ChoiceOption.whatAreYouSelling(): Unit = option("What are you selling?") { + if (get("thzfe_sold_balm", false)) { + npc("Me's showin' you da stufsies for yous creatures!") + openShop("uglugs_stuffsies") + } else { + npc("Me's not got no glug-glugs to sell, yous bring me da sickies glug-glug den me's open da stufsies for ya.") + } + } + + fun ChoiceOption.okayThanks(): Unit = option("Ok, thanks.") + + // ===== Talk-to: Repeat meeting (progress 2+) ===== + + suspend fun Player.repeatMeetingMenu() { + choice { + helloAgain() + whatAreYouSelling() + okayThanks() + } + } + + fun ChoiceOption.helloAgain(): Unit = option("Hello again.") { + if (get("thzfe_sold_balm", false)) { + npc("Hey yous creature...yous did good fings gedin that glug-glugs for da sickies! All is ogries pepels are not gettin dead cos of you.") + } else { + npc("Hey yous creature...yous still here?") + player("Yeah, I'm going to help Grish by figuring out what went on here.") + npc("If yous finds somefin for da sickies, yous brings to me...and I's gives you bright pretties, den me make more for alls pepels.") + player("Hmm, ok, I'll try to bear that in mind.") + } + } + + private fun registerSale(potion: String, price: Int) { + itemOnNPCOperate(potion, "uglug_nar") { + if (get("thzfe_sold_balm", false)) { + npc("Yous creatures is da funny ones...yous already solds me's ones now..and us can now sell un to yous!") + return@itemOnNPCOperate + } + item(item = potion, text = "You show the potion to Uglug Nar.") + player("Hey, here you go! I brought you some of the potion which should cure the disease. You said that you would buy some from me.") + npc("Yous creatures done da good fing...yous get many bright pretties for dis...!") + set("thzfe_sold_balm", true) + inventory.remove(potion) + addOrDrop("coins", price) + items(potion, "coins", "You sell the potion and get $price coins in return.") + } + } +} diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/BartenderDragonInn.kt b/game/src/main/kotlin/content/area/kandarin/yanille/BartenderDragonInn.kt index e2aa81f870..46180523a1 100644 --- a/game/src/main/kotlin/content/area/kandarin/yanille/BartenderDragonInn.kt +++ b/game/src/main/kotlin/content/area/kandarin/yanille/BartenderDragonInn.kt @@ -1,12 +1,17 @@ package content.area.kandarin.yanille import content.entity.npc.shop.buy +import content.entity.player.dialogue.Happy import content.entity.player.dialogue.Neutral import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty +import content.entity.player.dialogue.Shock import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.item import content.entity.player.dialogue.type.npc import content.entity.player.dialogue.type.player +import content.entity.player.inv.item.addOrDrop import content.quest.miniquest.alfred_grimhands_barcrawl.barCrawlDrink import content.quest.miniquest.alfred_grimhands_barcrawl.onBarCrawl import world.gregs.voidps.engine.Script @@ -14,6 +19,9 @@ import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove class BartenderDragonInn : Script { @@ -60,6 +68,35 @@ class BartenderDragonInn : Script { } barCrawl(target) } + + // ===== Tankard ===== + itemOnNPCOperate("dragon_inn_tankard", "bartender_dragon_inn") { + item(item = "dragon_inn_tankard", text = "You show the tankard to the Inn Keeper.") + if (get("thzfe_showntankard", false)) { + tankardRepeat() + } else { + tankardFirstTime() + } + } + + // ===== Bad portrait ===== + itemOnNPCOperate("zogre_sithik_portrait_bad", "bartender_dragon_inn") { + item(item = "zogre_sithik_portrait_bad", text = "You show the sketch to the Inn keeper.") + npc("Who's that? I mean, I guess it's a picture of a person isn't it? Sorry...you've got me? And before you ask, you're not putting it up on my wall!") + player("It's a portrait of Sithik Ints...don't you recognise him?") + npc("I'm sorry, I really am, but I just don't see it...can you make a better picture?") + player("I'll try...") + } + + // ===== Good portrait ===== + itemOnNPCOperate("zogre_sithik_portrait_good", "bartender_dragon_inn") { (target) -> + item(item = "zogre_sithik_portrait_good", text = "You show the portrait to the Inn keeper.") + if (get("thzfe_innkeepermugshown", false)) { + npc("Yeah, I recognise that Geezer, he was talking to one of my customers the other day.") + } else { + signPortrait(target) + } + } } suspend fun Player.barCrawl(target: NPC) = barCrawlDrink( @@ -69,4 +106,44 @@ class BartenderDragonInn : Script { levels.drain(Skill.Defence, 6) }, ) + + // ===== First-time tankard reveal ===== + + private suspend fun Player.tankardFirstTime() { + player("Hello there, I found this tankard in an ogre tomb cavern. It has the emblem of this Inn on it and I wondered if you knew anything about it?") + set("thzfe_showntankard", true) + npc("Oh yes, this is Brentle's mug...I'm surprised he left it just lying around down some cave. He's quite protective of it.") + player("Brentle you say? So you knew him then?") + npc("Yeah, this belongs to 'Brentle Vahn', he's quite a common customer, though I've not seen him in a while.") + npc("He was talking to some shifty looking wizard the other day. I don't know his name, but I'd recognise him if I saw him.") + player("Hmm, I'm sorry to tell you this, but Brentle Vahn is dead - I believe he was murdered.") + npc("Noooo! I'm shocked...") + npc("...but not surprised. He was a good customer...but I knew he would sell his sword arm and do many a dark deed if paid enough.") + npc("If you need help bringing the culprit to justice, you let me know.") + } + + // ===== Repeat tankard showing ===== + + private suspend fun Player.tankardRepeat() { + player("Hello again. Can you tell me what you know about this tankard again please?") + npc("Oh yes, Brentle's tankard. Yeah, you've shown me this already. It belonged to Brentle Vahn, he was quite a common customer, though I've not seen him in a while.") + npc("He was talking to some shifty looking wizard the other day. I don't know his name, but I'd recognise him if I saw him.") + } + + // ===== Good portrait + sign-it sequence ===== + + private suspend fun Player.signPortrait(npc: NPC) { + npc("Yeah, that's the guy who was talking to Brentle Vahn the other day! Look at those eyes, never a more shifty looking pair will you ever see!") + player("Hmm, you've just identified the man who I think sent Brentle Vahn to his death.") + player("I'm trying to bring him to justice with the Wizards' Guild grand secretary. Do you think you could sign this portrait to say that he was talking to Brentle Vahn.") + npc("I can and I will!") + npc.anim("human_mapping") + sound("zogre_writing") + inventory.remove("zogre_sithik_portrait_good") + addOrDrop("zogre_sithik_portrait_signed") + set("thzfe_innkeeperportraitshown", true) + item(item = "zogre_sithik_portrait_signed", text = "The Dragon Inn bartender signs the portrait.") + player("Many thanks for your help, it's really very good of you.") + npc("Not at all, just doing my part.") + } } diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/SithikInts.kt b/game/src/main/kotlin/content/area/kandarin/yanille/SithikInts.kt new file mode 100644 index 0000000000..77c9761b5d --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/yanille/SithikInts.kt @@ -0,0 +1,518 @@ +package content.area.kandarin.yanille + +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Confused +import content.entity.player.dialogue.Expression +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty +import content.entity.player.dialogue.Shock +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.items +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.quest.quest +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.sound +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.type.random + +class SithikInts : Script { + init { + objectOperate("Talk-to", "zogre_sithik_bed_entity,ogre_bedman_loc") { + when (quest("zogre_flesh_eaters")) { + "unstarted", "started", "investigate" -> sleepyOldManIntro() + "barricade", "sithik" -> conversationByVarbit488() + "potion" -> postOgreReveal() + "permanent_spell" -> postQuestProgressed() + "given_key", "killed_slash_bash", "completed" -> backToGloat() + else -> sleepyOldManIntro() // catch-all preservation of other states + } + } + + objectOperate("Search", "sithiks_drawers") { + if (noMoreSnooping()) return@objectOperate + if (!hasPermission()) { + snoopWarning() + return@objectOperate + } + searchDrawer() + } + + objectOperate("Search", "sithiks_cupboard") { + if (noMoreSnooping()) return@objectOperate + if (!hasPermission()) { + snoopWarning() + return@objectOperate + } + searchCupboard() + } + + objectOperate("Search", "sithiks_wardrobe") { + if (noMoreSnooping()) return@objectOperate + if (!hasPermission()) { + snoopWarning() + return@objectOperate + } + searchWardrobe() + } + + itemOnObjectOperate("necromancy_book", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Aha! A necromantic book! What's this doing here then?") + item(item = "necromancy_book", text = "You show the Necromantic book to Sithik.") + sithik("Oh..I'm not quite sure actually...where did you find that then?") + player("I found it in this cupboard! What do you have to say for yourself?") + sithik("Oh yes, that's right...I remember now. It's for my research, there's nothing really dangerous about it, unless it falls into the wrong hands. I'm sure it's pretty safe with me.") + player("Hmmm, likely story!") + } + + // ===== HAM book ===== + itemOnObjectOperate("book_of_ham", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("What's this then?") + item(item = "book_of_ham", text = "You show the HAM book to Sithik.") + sithik("What do you mean? It's a book by the respected HAM leader Johanhus Ulsbrecht, that man speaks for a lot of people who are unhappy with the current state of affairs.") + sithik("Can you honestly tell me that you've not had to fight for your life against the odd monster or two?") + player("Hmm, that may be true, but I don't universally hate all monsters, whereas I have a sneaking suspicion that you do...and ogres in particular!") + sithik("Hmm, that's an interesting theory, care to back it up with any facts?") + } + + // ===== Papyrus (sketching Sithik) ===== + itemOnObjectOperate("papyrus", "zogre_sithik_bed_entity,ogre_bedman_loc") { + if (questStage("zogre_flesh_eaters") >= 6) { + message("You have already created Sithik's portrait, you don't need another one.") + return@itemOnObjectOperate + } + if (!inventory.contains("charcoal")) { + statement("You have no charcoal with which to sketch this subject.") + return@itemOnObjectOperate + } + sithik("Oh lovely! You're making my portrait! Let me see it afterwards!") + statement("You begin sketching the irritable Sithik.") + anim("human_mapping") + sound("zogre_writing") + delay(2) + inventory.remove("papyrus") + val portrait = if (random.nextInt(3) == 0) "zogre_sithik_portrait_good" else "zogre_sithik_portrait_bad" // TODO use crafting level + inventory.add(portrait) + item(item = portrait, text = "You get a portrait of Sithik.") + } + + // ===== Book of portraiture ===== + itemOnObjectOperate("book_of_portraiture", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Oh, so explain this then?") + item(item = "book_of_portraiture", text = "You show the book on portraiture to Sithik.") + sithik("It's my hobby...I'm interested in portraiture, but all art in general. It's fun, you should try it.") + player("How do I do it...") + sithik("Well...you could start by reading the book!") + } + + // ===== Bad portrait ===== + itemOnObjectOperate("zogre_sithik_portrait_bad", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Here you go, what do you think?") + item(item = "zogre_sithik_portrait_bad", text = "You show the sketch...") + sithik("Hmmm, well it's an interesting interpretation, but not really classic realist representation is it? It's not my favourite, but I like the 'truth' of the work...well done.") + } + + // ===== Good portrait ===== + itemOnObjectOperate("zogre_sithik_portrait_good", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Here you go, what do you think?") + item(item = "zogre_sithik_portrait_good", text = "You show the portrait to Sithik.") + sithik("Hmmm, well it's not the most flattering of portraits, but I like the 'honesty' of the work...well done.") + } + + // ===== Strange potion ===== + itemOnObjectOperate("zogre_ogre_trans_potion", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Here, try some of this potion, it'll make you feel better!") + sithik("Err, yuck....no way am I taking any potions or medication off you...I don't trust you!") + } + + // ===== Signed portrait (the bribe scene) ===== + itemOnObjectOperate("signed_portrait", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Hey, what do you think of this? I'm going to show it to Zavistic and you're going to be in trouble!") + item(item = "signed_portrait", text = "You show the portrait to Sithik.") + sithik("Hmmm, well, I've got quite a common looking face, I'm often mistaken for other wizards, you know, when I'm wearing my wizard's hat, robes and staff. There's a lot of us around here you know.") + player("I don't think so! This is a signed picture of you, someone recognised you, you're in deep trouble!") + sithik("Ok, I'll pay you to keep this secret - how much do you want for the picture?") + player("You can't buy me Sithik!") + sithik("Ok, let's say two million...two million to keep quiet and give me the picture.") + items("coins", "coins", "Sithik shows you a chest brimming over with coins...") + player("Oh...erm...well, that is a lot of money actually...er....") + sithik("Yes, and you deserve it, you're very clever! Now, take the money...") + bribeChoice() + } + + // ===== Dragon Inn tankard ===== + itemOnObjectOperate("dragon_inn_tankard", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("What about this then? Guess where I found this?") + item(item = "dragon_inn_tankard", text = "You show the tankard to Sithik.") + sithik("You probably found it at the local brewhouse! It doesn't take a genius to figure that one out.") + player("Aha! But I found this in an old ogre tomb! I suspect it's a clue which will lead me to the suspect.") + sithik("Hmmm, well that eliminates all the local people who don't actually drink at the 'Dragon Inn'. When do you think you'll start questioning the remaining population of Yanille?") + } + + // ===== Black prism ===== + itemOnObjectOperate("black_prism", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Hey, what's this then, can you explain it?!") + item(item = "black_prism", text = "You show the black prism to Sithik.") + sithik("Err..it looks sort of familiar, did you steal it from me? Come to think of it, you have the appearance of a common thief!") + player("I found it in a place called Jiggig where some undead ogres happen to be wandering around.") + sithik("Oh, nothing to do with me then, never seen it in my life before!") + } + + // ===== Torn page ===== + itemOnObjectOperate("torn_page", "zogre_sithik_bed_entity,ogre_bedman_loc") { + player("Have you ever seen anything like this before?") + item(item = "torn_page", text = "You show the torn page to Sithik.") + sithik("It's probably a piece of rubbish someone threw away...what does it say, I can't read it?") + player("You should be able to read it, it's been torn from a book on necromancy and you're meant to be a specialist in the subject.") + sithik("Oh, no..., not really a specialist, just a hobby of mine really. Hardly know anything about it, but it does seem interesting...") + } + } + + // ===== Progress 0/2: Initial encounter ===== + + private suspend fun Player.sleepyOldManIntro() { + sithik("Hey...who gave you permission to come in here! Get out, get out I say.") + player("Alright, alright...keep your night cap on.") + } + + // ===== Progress 3/4: Branches by varbit 488 ===== + + private suspend fun Player.conversationByVarbit488() { + when (get("thzfe_prismsearch", 0)) { + 4 -> { + sithik("Hey...who gave you permission to come in here!") + zavisticIntro() + } + 5 -> { + sithik("What do you want now?") + noNeedToBeRude() + } + else -> sleepyOldManIntro() + } + } + + private suspend fun Player.zavisticIntro() { + player("Zavistic Rarve said that I could come and talk to you and ask you a few questions.") + sithik("Oh, Zavistic...why...why would he send you to me?") + sithikQuestionsMenu() + } + + suspend fun Player.sithikQuestionsMenu() { + choice { + askAboutUndeadOgres() + askWhatYouDo() + mindIfILookAround() + okThanks() + } + } + + fun ChoiceOption.askAboutUndeadOgres(): Unit = option("Do you know anything about the undead ogres at Jiggig?") { + sithik("Er...undead ogres...no, sorry, no idea what you're talking about there.") + player("Hmm, is that right...") + sithik("Well, yes, yes it is. If I knew something, I'd tell you.") + sithik("Anyway, dead ogres you say? How strange? That must be a strange sight?") + player("Very well, if you don't know anything about it, you won't mind if I look around then?") + provokedLookAround() + } + + fun ChoiceOption.askWhatYouDo(): Unit = option("What do you do?") { + sithik("I'm a scholarly student of the magical arts. When I was younger I used to be an adventurer, probably just like yourself. But I lost interest in the constant fighting, looting and gaining abilities.") + sithik("Instead I decided to focus my attention and time to study the purer form of the lost arts.") + player("The lost arts? What are they?") + sithik("Ignorant people call them the 'dark arts'. I'm talking about Necromancy, the power to bring the dead back to life - the power of the gods! Surely the most awesome power known to man.") + player("Hmm, well I guess I must be an ignorant person then, because bringing the dead back to life sounds very unnatural.") + sithikQuestionsMenu() + } + + fun ChoiceOption.mindIfILookAround(): Unit = option("Do you mind if I look around?") { + if (get("thzfe_prismsearch", 0) == 5) { + triedAlready() + } else { + provokedLookAround() + } + } + + fun ChoiceOption.okThanks(): Unit = option("Ok, thanks.") + + private suspend fun Player.provokedLookAround() { + set("thzfe_prismsearch", 5) + sithik("Well, err....well, actually yes I do mind...it's my place and I don't want strangers going through my things.") + player("Well, I'm going to have a look around anyway, if you're not involved in this whole thing, you won't have anything to hide.") + sithik("Why, if I was a few years younger I'd give you a good hiding!") + player("I'm sure!") + sithikQuestionsMenu() + } + + private suspend fun Player.triedAlready() { + sithik("I've already told you that I do! But you'll probably just ignore me again!") + player("Quite right!") + } + + // ===== "Snooping" reaction (varbit 488 == 5) ===== + + private suspend fun Player.noNeedToBeRude() { + player("Hey there's no need to be rude!") + sithik("What do you expect when you just go snooping around a person's place against their express permission.") + snoopMenu() + } + + suspend fun Player.snoopMenu() { + choice { + askWhatYouDo() + whyInBed() + okThanks() + } + } + + fun ChoiceOption.whyInBed(): Unit = option("Why do you spend most of your time in bed?") { + sithik("I'm actually quite old and not so very well and I'd like to get over this illness I have, then I'll return to my very serious and important studies.") + sithikQuestionsMenu() + } + + // ===== Progress 6: Player turned Sithik into an ogre ===== + + private suspend fun Player.postOgreReveal() { + if (get("thzfe_sithik_transformed", 0) >= 1) { + ogreFormConfession() + } else { + sithik("What do you want now?") + noNeedToBeRude() + } + } + + private suspend fun Player.ogreFormConfession() { + sithik("Arghhhh..what's happened to me...you beast!") + player("It's your own fault, you shouldn't have lied about your involvement with the undead Ogres at Jiggig. The potion will wear off once you've told the truth!") + sithik("Ok, ok, I admit it, I got Brentle Vahn to cast the spell to put an end to those awful Ogres...they're just disgusting creatures...") + player("Ok, that's a start...now I want some answers.") + confessionAnswersMenu() + } + + suspend fun Player.confessionAnswersMenu() { + choice { + removeSpellFromArea() + getRidOfOgres() + getRidOfDisease() + sorryHaveToGo() + } + } + + fun ChoiceOption.removeSpellFromArea(): Unit = option("How do I remove the effects of the spell from the area?") { + player("How do I remove the effects of the spell from the area? The ogres want to get their ceremonial dance area back and can't do that with undead walking all over it.") + if (questStage("zogre_flesh_eaters") >= 8) { + sithik("Haven't I told you this already? You can't remove the spell, it's permanent, it will last forever, the only option you have is to move the ceremonial area.") + } else { + sithik("Unfortunately you can't. The spell is permanent, it will last forever, the only option you have is to move the ceremonial area.") + set("zogre_flesh_eaters", "permanent_spell") + } + player("You're an evil man and I'm going to make you pay for this...you can stay like that forever as far as I'm concerned.") + sithik("No...no, let me try to make amends...please I can help you. Just don't leave me like this.") + confessionAnswersMenu() + } + + fun ChoiceOption.getRidOfOgres(): Unit = option("How do I get rid of the undead ogres?") { + if (get("thzfe_makebrutalarrow", false)) { + sithik("Haven't I already explained this to you once before?") + player("Humour me!") + explainBrutalArrows() + } else { + explainBrutalArrows() + } + } + + private suspend fun Player.explainBrutalArrows() { + sithik("Ok, similar spells have been cast before and the only way to deal with the resulting creatures is to cordon off the area and not go in there again.") + sithik("The undead creatures usually manifest some sort of disease so it's best to attack them from a distance with a ranged weapon.") + sithik("Normal missiles like arrows and darts do very little damage to them because they're designed to destroy internal organs. This is a waste of time with undead creatures like undead ogres.") + player("Yeah, clearly so what should we use?") + set("thzfe_makebrutalarrow", true) + sithik("From my research it looks like a flat ended arrow was designed called a 'Brutal arrow'. This does large amounts of crushing damage to the creature. You can make them by using larger arrows. ") + sithik("I think some Ogre hunters make them. But instead of adding an arrow tip, you hammer a large nail into the end of the shaft.") + confessionAnswersMenu() + } + + fun ChoiceOption.getRidOfDisease(): Unit = option("How do I get rid of the disease?") { + if (get("thzfe_makecuredisease", false)) { + sithik("Haven't I already explained this disease thing to you once before?") + val threat = if (get("thzfe_sithik_transformed", 0) == 2) { + "Just tell me again or else I'll turn you back into an ogre!" + } else { + "Just tell me again or else I'll never turn you back into a human!" + } + player(threat) + sithik("No...noo...please, I'll tell you.") + explainDiseaseCure() + } else { + explainDiseaseCure() + } + } + + private suspend fun Player.explainDiseaseCure() { + set("thzfe_makecuredisease", true) + sithik("My research shows that two jungle based herbs can be used, one is found near river tributaries and looks like a vine, the other is found in caves and grows on the wall.") + sithik("It's quite well camouflaged so it's unlikely that you'll find it.") + player("We'll see about that!") + confessionAnswersMenu() + } + + fun ChoiceOption.sorryHaveToGo(): Unit = option("Sorry, I have to go.") { + sithik("But...you can't just leave me here like this!") + } + + // ===== Progress 8: Returns post-confession ===== + + private suspend fun Player.postQuestProgressed() { + sithik("Arghhhh..what do you want now...you've turned me into a beast!") + player("I've got some questions for you...and you'd better answer them well or else!") + sithik("Ok, ok, I'll tell you anything, just turn me back into a human again!") + confessionAnswersMenu() + } + + // ===== Progress 10/12: Post-quest gloating ===== + + private suspend fun Player.backToGloat() { + sithik("Oh, so you're back then, come to gloat have you?") + player("Nope, I've just come to ask you a couple of questions.") + choice { + getRidOfOgres() + getRidOfDisease() + sorryHaveToGo() + } + } + + /** + * Returns true if the quest is past the snooping investigation phase. + * Once you're at progress 4+, the furniture has nothing of significance. + */ + private fun Player.noMoreSnooping(): Boolean { + if (questStage("zogre_flesh_eaters") >= 4) { + message("You search but find nothing of significance.") + return true + } + return false + } + + /** + * Permission to snoop is granted by varbit 488 reaching 5, + * which happens during Sithik's "Do you mind if I look around?" exchange + * after he's caught lying about the undead ogres. + */ + private fun Player.hasPermission(): Boolean = get("thzfe_prismsearch", 0) >= 5 + + private suspend fun Player.snoopWarning() { + sithik("Hey! What do you think you're doing?") + player("Erk! I'd better not start rifling through peoples things without permission.") + } + + // ===== Drawer (object 6875): papyrus, charcoal, book of portraiture ===== + + private suspend fun Player.searchDrawer() { + val hasPapyrus = inventory.contains("papyrus") // 970 + val hasCharcoal = inventory.contains("charcoal") // 973 + val ownsBook = inventory.contains("book_of_portraiture") // already has the book somewhere + + // Determine how much inventory space we need to take everything remaining + val spaceNeeded = when { + hasPapyrus && hasCharcoal -> if (ownsBook) 0 else 1 + hasPapyrus || hasCharcoal -> if (ownsBook) 1 else 2 + else -> if (ownsBook) 2 else 3 + } + + if (spaceNeeded > 0 && inventory.spaces < spaceNeeded) { + statement("You see some items in the drawer, but you need $spaceNeeded free inventory spaces to take them.") + return + } + + when { + // All items already collected + ownsBook && hasPapyrus && hasCharcoal -> { + message("You find nothing in the drawers.") + } + // Has both papyrus and charcoal — only book left + hasPapyrus && hasCharcoal -> { + addOrDrop("book_of_portraiture") + item(item = "book_of_portraiture", text = "You find a book on portraiture.") + } + // Has papyrus only — find charcoal (and maybe book) + hasPapyrus -> { + addOrDrop("charcoal") + item(item = "charcoal", text = "You find some charcoal.") + if (!ownsBook) findBookFollowup() + } + // Has charcoal only — find papyrus (and maybe book) + hasCharcoal -> { + addOrDrop("papyrus") + item(item = "papyrus", text = "You find some papyrus.") + if (!ownsBook) findBookFollowup() + } + // Has neither — find both at once (and maybe book) + else -> { + addOrDrop("charcoal") + addOrDrop("papyrus") + items("charcoal", "papyrus", "You find some charcoal and papyrus.") + if (!ownsBook) findBookFollowup() + } + } + } + + private suspend fun Player.findBookFollowup() { + addOrDrop("book_of_portraiture") + item(item = "book_of_portraiture", text = "You also find a book on portraiture.") + } + + // ===== Cupboard (object 6876): necromancy book ===== + + private suspend fun Player.searchCupboard() { + if (inventory.contains("necromancy_book")) { + statement("You search the cupboard but find nothing.") + return + } + addOrDrop("necromancy_book") + item(item = "necromancy_book", text = "You find a book on Necromancy.") + } + + // ===== Wardrobe (object 55412): book of HAM ===== + + private suspend fun Player.searchWardrobe() { + if (inventory.contains("book_of_ham")) { + statement("You search the wardrobe but find nothing.") + return + } + addOrDrop("book_of_ham") + item(item = "book_of_ham", "You find a book on Philosophy written by the 'Human's Against Monsters' leader, Johanhus Albrect.") + } + + suspend fun Player.bribeChoice() { + choice("Be bribed by Sithik for 2 million?") { + refuseBribe() + acceptBribe() + } + } + + fun ChoiceOption.refuseBribe(): Unit = option("No, I won't take the money, I'm going to bring you to justice!") { + sithik("Oh well, suit yourself! I wasn't going to give you the money anyway! No one will believe some crazy adventurer and an Inn keep.") + } + + fun ChoiceOption.acceptBribe(): Unit = option("Ok, I'll shut up for two million!") { + sithik("Ha! Ha! You believed me! I'm not going to give you all my money! No one will believe a crazy adventurer and a local Inn keep!") + player("You're a mean and cruel man Sithik, a mean and cruel man!") + } + + // Picks the right chathead id depending on whether the player has transformed Sithik into an ogre. + private suspend inline fun Player.sithik(text: String) { + val ogre = get("thzfe_sithik_transformed", 0) >= 1 + val npcId = if (ogre) "zogre_sithik_ogre" else "zogre_sithik_man" + npc(npcId, text) + } +} diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/ZavisticRarve.kt b/game/src/main/kotlin/content/area/kandarin/yanille/ZavisticRarve.kt new file mode 100644 index 0000000000..99d6b6bc7f --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/yanille/ZavisticRarve.kt @@ -0,0 +1,704 @@ +package content.area.kandarin.yanille + +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.Shifty +import content.entity.player.dialogue.Shock +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.items +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.quest.quest +import content.quest.questStage +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.ui.dialogue.talkWith +import world.gregs.voidps.engine.entity.character.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.name +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.Tile + +class ZavisticRarve : Script { + + init { + // ===== Plain Talk-to (or via bell) ===== + + npcOperate("Talk-to", "zavistic_rarve") { (target) -> + val sandProgress = get("hand_in_the_sand", 0) + val zogreProgress = questStage("zogre_flesh_eaters") + if (sandProgress < 40 && zogreProgress < 4) { + npc("What are you doing bothering me? Don't you think some of us have work to do?") + player("I thought you were here to help?") + npc("Well... I am, I suppose, anyway... we're very busy here, hurry up, what do you want?") + } else { + npc("What are you doing...Oh, it's you...sorry...didn't realise...what can I do for you?") + } + mainMenu() + } + + // ===== Bell-rung Talk-to ===== + + objectOperate("Ring", "zogre_outdoor_bell") { (target) -> + sound("zogre_bell") + target.anim("zogre_bell_ring") + var zavistic = NPCs.findOrNull(tile.regionLevel, "zavistic_rarve") + if (zavistic == null) { + zavistic = NPCs.add( + id = "zavistic_rarve", + tile = Tile(2598, 3087, 0), + ticks = 200, + ) + } + talkWith(zavistic) + + val sandProgress = get("hand_in_the_sand", 0) + val zogreProgress = questStage("zogre_flesh_eaters") + if (sandProgress < 40 && zogreProgress < 4) { + npc("What are you doing ringing that bell?! Don't you think some of us have work to do?") + player("But I was told to ring the bell if I wanted some attention.") + npc("Well...anyway...we're very busy here, hurry up what do you want?") + } else { + npc("What are you doing...Oh, it's you...sorry...didn't realise...what can I do for you?") + } + mainMenu() + } + + // ===== Item-on-Zavistic ===== + + itemOnNPCOperate("beer_hand", "zavistic_rarve") { + handInTheSandHandReveal() + } + + itemOnNPCOperate("magic_scroll", "zavistic_rarve") { + magicScrollReveal() + } + + itemOnNPCOperate("black_prism", "zavistic_rarve") { + player("I found this black prism at Jiggig where the undead ogre activity was happening?") + showBlackPrism() + } + + itemOnNPCOperate("torn_page", "zavistic_rarve") { + player("I think I've found a clue from the Jiggig area where the undead ogre activity is happening.") + showTornPage() + } + + itemOnNPCOperate("dragon_inn_tankard", "zavistic_rarve") { + item(item = "dragon_inn_tankard", text = "You show the dragon Inn Tankard to Zavistic.") + showTankard() + } + + itemOnNPCOperate("necromancy_book", "zavistic_rarve") { + item(item = "necromancy_book", text = "You show the Necromancy book to Zavistic.") + showNecromancyBook() + } + + itemOnNPCOperate("book_of_ham", "zavistic_rarve") { + item(item = "book_of_ham", text = "You show the HAM book to Zavistic.") + showHamBook() + } + + itemOnNPCOperate("zogre_sithik_portrait_signed", "zavistic_rarve") { + item(item = "zogre_sithik_portrait_signed", text = "You show the signed portrait of Sithik to Zavistic.") + showSignedPortrait() + } + + itemOnNPCOperate("zogre_sithik_portrait_good", "zavistic_rarve") { + item(item = "zogre_sithik_portrait_good", text = "You show the portrait of Sithik to Zavistic.") + npc("Hmm, great...but I already know what he looks like!") + } + + itemOnNPCOperate("zogre_sithik_portrait_bad", "zavistic_rarve") { + player("Look, I made a portrait of Sithik.") + item(item = "zogre_sithik_portrait_bad", text = "You show the sketch...") + npc("Who the demonikin is that? Is it meant to be a portrait of Sithik, it doesn't look anything like him!") + } + } + + // ===== Top-level menu router ===== + + private suspend fun Player.mainMenu() { + val sand = get("hand_in_the_sand", 0) >= 20 + val zogre = questStage("zogre_flesh_eaters") >= 3 + when { + zogre && sand -> { + choice { + aboutZogres() + aboutSand() + } + } + zogre -> sendZogreChat() + sand -> sendSandChat() + else -> guildMenu() + } + } + + fun ChoiceOption.aboutZogres(): Unit = option("I'm here about the sicks...err Zogres") { + sendZogreChat() + } + + fun ChoiceOption.aboutSand(): Unit = option("I have a rather sandy problem that I'd like to palm off on you.") { + sendSandChat() + } + + // ===== Wizards' Guild info menu (shared default) ===== + + suspend fun Player.guildMenu() { + choice { + whatIsThereToDo() + whatAreRequirements() + whatDoYouDo() + okThanks() + } + } + + suspend fun Player.guildMenuWithOrbHelp() { + choice { + whatIsThereToDo() + whatAreRequirements() + whatDoYouDo() + if (inventory.contains("magical_orb") || inventory.contains("magical_orb_active")) { + canYouHelpMore() + } else { + lostOrb() + } + } + } + + suspend fun Player.guildMenuWithLostOrb() { + choice { + whatIsThereToDo() + whatAreRequirements() + whatDoYouDo() + lostOrb() + } + } + + fun ChoiceOption.whatIsThereToDo(): Unit = option("What is there to do in the Wizards' Guild?") { + npc("This is the finest wizards' establishment in the land. We have magic portals to the other towers of wizardry around RuneScape. We have a particularly wide collection of runes in our rune shop. We sell some of") + npc("the finest mage robes in the land and we have a training area full of zombies for you to practice your magic on.") + guildMenu() + } + + fun ChoiceOption.whatAreRequirements(): Unit = option("What are the requirements to get in the Wizards' Guild?") { + npc("You need a magic level of 66, the high magic energy level is too dangerous for anyone below that level.") + guildMenu() + } + + fun ChoiceOption.whatDoYouDo(): Unit = option("What do you do in the Guild?") { + npc("I'm the Grand Secretary for the Wizards' Guild, I have lots of correspondence to keep up with, as well as attending to the discipline of the more problematic guild members.") + guildMenu() + } + + fun ChoiceOption.okThanks(): Unit = option("Ok, thanks.") + + fun ChoiceOption.canYouHelpMore(): Unit = option("Can you help me more?") { + helpMoreFlow() + } + + fun ChoiceOption.lostOrb(): Unit = option("I've lost my magical scrying orb!") { + replaceOrb() + } + + // ===== HAND IN THE SAND branches ===== + + private suspend fun Player.sendSandChat() { + when (get("hand_in_the_sand", 0)) { + 20 -> { + if (inventory.contains("beer_hand")) { + handInTheSandHandReveal() + } else { + statement("Maybe you should have the hand with you before speaking to Zavistic.") + } + } + 30, 40, 50 -> { + npc("Did you find out who killed Clarence yet?") + player("Not yet, but don't lose your head over it.") + } + 60 -> { + if (inventory.contains("magic_scroll")) { + magicScrollReveal() + } else { + statement("Perhaps you should have the scroll from Bert with you before you speak to Zavistic.") + } + } + 70 -> guildMenuWithOrbHelp() + 80, 90, 100 -> { + npc("Have you made the serum and talked to Sandy yet?") + if (inventory.contains("magical_orb")) { + player("Not yet, but don't bust a gut over it!") + } else { + player("I've lost my magical scrying orb!") + replaceOrb() + } + } + 110 -> guildMenuWithLostOrb() + 120 -> { + if (inventory.contains("magical_orb_active")) { + statement("You hand the magical scrying orb to the Wizard and watch as the recording is played back.") + npc("Well, well...I think this Sandy needs a lesson, please bring me 5 earth runes and a bucket of sand.") + runesAndSandRequest() + } else { + player("I got the whole story from Sandy... but I lost the orb.") + npc("It's ok, I saw the whole thing as the orb is connected via magic to me as I enchanted it.") + npc("I think this Sandy needs a lesson, please bring me 5 earth runes and a bucket of sand.") + runesAndSandRequest() + } + } + 130 -> { + if (inventory.contains("earth_rune", 5) && inventory.contains("bucket_of_sand")) { + player("I've brought what you wanted, what are you going to do?") + sandpitRefillCutscene() + } else { + npc("You really mean you forgot? Bring me 5 earth runes and 1 bucket of sand to help stop that moneygrabbing Sandy!") + } + } + 140 -> { + npc("Did you visit the Entrana sandpit yet? Ask the worker there if he's found an arm or a leg.") + player("Not yet no. I've been running around like a headless chicken, but I'll get to it!") + } + 150 -> { + if (inventory.contains("wizard_head")) { + item(item = "wizard_head", text = "You show the wizard the head.") + npc("Alas poor Clarence. I knew him, $name.") + npc("Thank you - we shall bury him today. I have sent word for the guards to arrest Sandy, so no one will ever see him again!") +// sendHandQuestReward() + } else { + statement("Perhaps you should have the wizard's head with you before speaking to Zavistic.") + } + } + 160 -> { + npc("Thank you so much for helping to lay Clarence to rest and lock up his murderer!") + guildMenu() + } + else -> guildMenu() + } + } + + // ===== Hand reveal flow (the murder is revealed) ===== + + private suspend fun Player.handInTheSandHandReveal() { + if (!inventory.contains("beer_hand")) { + statement("Maybe you should have the hand with you before speaking to Zavistic.") + return + } + player("Ummm... Do you have all your wizards?") + npc("All my.... whatever do you mean...?") + player("The Guard Captain asked me to see if you have any... missing... wizards.") + npc("That's silly! No one would kill a wizard... would they?") + player("Erm... no... ") + player("Well.. maybe, you see Bert found this hand and it might belong to.. a wizard!") + npc("Bert? Ahh yes, the sandman who seems to have been working very long hours recently. Let's see that hand...") + set("hand_in_the_sand", 30) + inventory.remove("beer_hand") + item(item = "beer_hand", text = "You hand it over.") + npc("Oh my! This is most definitely Clarence, my most able student! You must find out who did this!") + player("Do you have any input as to the matter at hand?") + npc("Well.... Ask Bert about the long hours he's been working, that sounds suspicious to me. Digging things up at all hours of the day isn't natural.") + } + + // ===== Magic scroll reveal (mind-altering spell) ===== + + private suspend fun Player.magicScrollReveal() { + if (!inventory.contains("magic_scroll")) { + statement("Perhaps you should have the scroll from Bert with you before you speak to Zavistic.") + return + } + player("I talked to Bert and found something very strange about his hours.") + npc("Oh? Did he kill Clarence?") + player("No, but he doesn't remember changing his hours, and his rota and the original that his boss Sandy had, are different! ") + player("... oh, and this scroll appeared when they changed - he gave it to me.") + npc("I recognise that type of scroll! It's used in a mind altering spell of some sort. Did you speak to this... Sandy guy? Perhaps he has a hand in this.") + player("I took a look around his office. I don't know about a hand in it, I think he has both hands and feet in it!") + npc("Even more suspicious! Here, take this magical scrying orb and get some Truth Serum from Betty in Port Sarim, she owes me a favour, just tell her I sent you if she complains.") + npc("Then you will be equipped to ask Sandy a few questions. Oh Clarence, I will find your murderer!") + set("hand_in_the_sand", 70) + inventory.remove("magic_scroll") + addOrDrop("magical_orb") + item("magical_orb", "You exchange the scroll for the magical scrying orb. Perhaps Zavistic can give you even more of a hand to find the murderer?") + } + + // ===== "Can you help me more?" / replace orb / teleport ===== + + private suspend fun Player.helpMoreFlow() { + if (get("handsand_tele", false)) { + npc("Unfortunately I've already helped you with one teleport, get some exercise - your legs won't fall off!") + return + } + npc("Bring me a vial and I'll help you a little more.") + if (!inventory.contains("vial")) return + player("I have a vial here for you.") + npc("Ok, would you like me to transport you to Port Sarim? I'm sticking my neck out a bit helping you like this and can only do it once though!") + choice { + yesTeleport() + noTeleport() + } + } + + fun ChoiceOption.yesTeleport(): Unit = option("Yes, that would be great!") { + npc("Off you go then, break a leg!") + portSarimTeleport() + } + + fun ChoiceOption.noTeleport(): Unit = option("No, I prefer using my legs, thanks all the same.") { + npc("Ok, suit yourself!") + } + + private suspend fun Player.replaceOrb() { + if (inventory.contains("magical_orb") || inventory.contains("magical_orb_active")) { + // Already has one + return + } + if (inventory.isFull()) { + npc("I'd give you another magical scrying orb if you had some space in your inventory.") + return + } + if (get("hand_in_the_sand", 0) == 110) { + addOrDrop("magical_orb_active") + npc("No matter, here, have another I've already activated it for you!") + } else { + addOrDrop("magical_orb") + npc("No matter, here, have another and please hurry, whoever killed Clarence must pay!") + } + } + + // ===== Port Sarim teleport cutscene ===== + + private suspend fun Player.portSarimTeleport() { + set("handsand_tele", true) + inventory.remove("vial") + // npc.anim("human_castentangle") TODO + delay(2) + gfx("pickaxe_summon_effect_spotanim", height = 92) + anim("human_shrink", delay = 4) + sound("teleport_all") + delay(3) + clearAnim() + tele(3014, 3259) + } + + // ===== Runes and sand request continuation ===== + + private suspend fun Player.runesAndSandRequest() { + set("hand_in_the_sand", 130) + inventory.remove("magical_orb_active") + player("Erm, why?") + npc("Don't question me or you'll end up as braindead as that legless Guard Captain!") + player("Umm.. ok, I'll get you the 5 earth runes and bucket of sand.") + } + + // ===== Sandpit refill cutscene (instanced) ===== + + private suspend fun Player.sandpitRefillCutscene() { + npc("Ahh excellent, let's have those! Watch and learn...") + // TODO: full instanced cutscene + // - Create instance at base (317, 386), 3x3 size + // - Spawn Bert NPC (id 3108) inside the instance + // - Fade out, start cutscene mode + // - Camera move to (2536, 3109) height 850, look at (2544, 3102) height 25 + // - Wizard chants — show info dialogue: + // "The Wizard chants and your attention is taken to the sandpit where Bert found the hand." + // - Bert walks to (2542, 3101), faces (2542, 3103) + // - Bert animates 2702, sandpit object animates 3037, sound 1591 + // - Bert says "My sand! My lovely sand" + // - Show info dialogue: + // "Something very strange happens to the Sandpit, it looks like it has filled itself up!" + // - Set varbit 278 to 1 (sandpit refilled flag) + // - Fade out, destroy instance, reset camera, fade in + // - Delete 5 earth runes, 1 bucket of sand + // - Set hand_in_the_sand to 140 + + statement("The Wizard chants and your attention is taken to the sandpit where Bert found the hand.") + statement("Something very strange happens to the Sandpit, it looks like it has filled itself up!") + inventory.remove("earth_rune", 5) + inventory.remove("bucket_of_sand") + set("hand_in_the_sand", 140) + npc("There, the sand pit will now magically refill. No more work for Bert! ") + npc("We must find the rest of Clarence, I've sent some wizards out to some of the sandpits, would you please check the Entrana sandpit?") + } + + // ===== ZOGRE FLESH EATERS branches ===== + + private suspend fun Player.sendZogreChat() { + val progress = quest("zogre_flesh_eaters") + val sithikIntro = get("thzfe_prismsearch", 0) + + when { + progress == "permanent_spell" || progress == "given_key" || progress == "killed_slash_bash" || progress == "completed" -> { + npc("Don't you worry about Sithik, he's not likely to be moving from his bed for a long time. When he eventually does get better, he's going to be sent before a disciplinary tribunal, then we'll sort out what's what.") + player("Thanks for your help with all of this.") + npc("Ooohh, no thanks required. It's I who should be thanking you my friend...your investigative mind has shown how vigilant we really should be for this type of evil use of the magical arts.") + guildMenu() + } + progress == "sithik" || progress == "potion" -> { + npc("Have you used that potion yet?") + if (progress == "potion") { + yesUsedPotion() + } else if (inventory.contains("zogre_ogre_trans_potion")) { + notYetUsedPotion() + } else { + lostPotion() + } + } + sithikIntro == 5 -> sithikInvestigationMenu() + sithikIntro == 4 -> sithikInvestigationMenuLimited() + inventory.contains("black_prism") && inventory.contains("torn_page") -> { + player("There's some undead ogre activity over at Jiggig, I've found some clues, I wondered if you'd have a look at them.") + showBothClues() + } + inventory.contains("black_prism") -> { + player("There's some undead ogre activity over at 'Jiggig', and the ogres have asked me to look into it. I think I've found a clue and I wonder if you could take a look at it for me?") + showBlackPrism() + } + inventory.contains("torn_page") -> { + player("There's some undead ogre activity over at Jiggig, I've found a clue that you may be able to help with.") + showTornPage() + } + else -> guildMenu() + } + } + + // ===== Sithik investigation menus ===== + + suspend fun Player.sithikInvestigationMenuLimited() { + val evidenceCount = evidenceCount() + choice { + whatDidYouSayShouldDo() + whereIsSithik() + if (hasEvidence()) showEvidenceOption(evidenceCount) + wantToAskAboutGuild() + sorryHaveToGo() + } + } + + suspend fun Player.sithikInvestigationMenu() { + val evidenceCount = evidenceCount() + choice { + whatDidYouSayShouldDo() + whereIsSithik() + if (hasEvidence()) showEvidenceOption(evidenceCount) else canYouHelp() + wantToAskAboutGuild() + sorryHaveToGo() + } + } + + fun ChoiceOption.whatDidYouSayShouldDo(): Unit = option("What did you say I should do?") { + npc("You should go and have a chat with Sithik Ints, he's in that house just to the north. He's a lodger and has a room upstairs. Just tell him that I sent you to see him. He should be fine once you've mentioned my name.") + sithikInvestigationMenu() + } + + fun ChoiceOption.whereIsSithik(): Unit = option("Where is Sithik?") { + npc("He's in that house just to the north, less than a few seconds walk away. He's a lodger and has a room upstairs...he's not very well though.") + sithikInvestigationMenu() + } + + fun ChoiceOption.showEvidenceOption(evidenceCount: Int) { + val text = if (evidenceCount == 1) { + "I have an item that I'd like you to look at." + } else { + "I have some items that I'd like you to look at." + } + return option(text) { + showAllEvidence() + } + } + + // Workaround helper since ChoiceOption doesn't expose Player directly in some patterns + private fun Player.evidenceCount(): Int { + var count = 0 + if (inventory.contains("necromancy_book")) count++ + if (inventory.contains("book_of_ham")) count++ + if (inventory.contains("dragon_inn_tankard")) count++ + if (inventory.contains("signed_portrait")) count++ + return count + } + + private fun Player.hasEvidence(): Boolean = evidenceCount() > 0 + + fun ChoiceOption.canYouHelp(): Unit = option("Can you help me?") { + npc("I'm happy to help as much as I can but you have to remember that I'm quite busy. If you find any more clues about what happened at Jiggig, I'll consider them with an open mind - that's as much as I can offer.") + sithikInvestigationMenu() + } + + fun ChoiceOption.wantToAskAboutGuild(): Unit = option("I want to ask about the Magic Guild.") { + npc("Sure, go ahead, ask away.") + guildMenu() + } + + fun ChoiceOption.sorryHaveToGo(): Unit = option("Sorry, I have to go.") + + // ===== Cycle through all evidence the player has ===== + + private suspend fun Player.showAllEvidence() { + // Order: necromancy book, HAM book, tankard, signed portrait + if (inventory.contains("necromancy_book")) { + item(item = "necromancy_book", text = "You show the Necromancy book to Zavistic.") + showNecromancyBook() + } + if (inventory.contains("book_of_ham")) { + item(item = "book_of_ham", text = "You show the HAM book to Zavistic.") + showHamBook() + } + if (inventory.contains("dragon_inn_tankard")) { + item(item = "dragon_inn_tankard", text = "You show the dragon Inn Tankard to Zavistic.") + showTankard() + } + if (inventory.contains("zogre_sithik_portrait_signed")) { + item(item = "zogre_sithik_portrait_signed", text = "You show the signed portrait of Sithik to Zavistic.") + showSignedPortrait() + } + } + + // ===== Individual evidence reveals ===== + + private suspend fun Player.showBlackPrism() { + item(item = "black_prism", text = "You show the black prism to the aged wizard.") + if (get("thzfe_prismsearch", 0) >= 4) { + npc("Yes, you've already showed me that, bring it to me when you've resolved the problems at Jiggig and I'll see what I can do.") + return + } + npc("Hmmm, well this is an uncommon spell component. On it's own it's useless, but with certain necromantic spells it can be very powerful. Did you find anything else there?") + if (inventory.contains("dragon_inn_tankard")) { + item(item = "dragon_inn_tankard", text = "You show the tankard to Zavistic.") + player("Well, I found this...") + npc("Hmmm, no, that's not really associated with this to be honest. Did you find anything else there?") + player("Not really.") + } else { + player("Not really.") + } + npc("I don't know what to say then, there isn't enough to go on with the clues you've shown me so far. I'd suggest going back to search a bit more, but you may just be wasting your time?") + npc("Hmm, but this prism does seem to have some magical protection. Once you've finished with this item, bring it back to me would you? I may have a reward for you!") + player("Sure...I mean, I'll try if I remember.") + } + + private suspend fun Player.showTornPage() { + item(item = "torn_page", text = "You show the necromantic half page to the aged wizard.") + npc("Hmm, this is a half torn spell page, it requires another spell component to be effective. Did you find anything else there?") + if (inventory.contains("black_prism")) { + showBothClues() + } else if (inventory.contains("dragon_inn_tankard")) { + item(item = "dragon_inn_tankard", text = "You show the tankard to Zavistic.") + player("Well, I found this...") + npc("Hmmm, no, that's not really associated with this to be honest. Did you find anything else there?") + player("Not really.") + npc("I don't know what to say then, there isn't enough to go on with the clues you've shown me so far. I'd suggest going back to search a bit more, but you may just be wasting your time?") + } else { + player("Not really.") + npc("I don't know what to say then, there isn't enough to go on with the clues you've shown me so far. I'd suggest going back to search a bit more, but you may just be wasting your time?") + } + } + + // The combined clue scene — sets up the Sithik investigation + private suspend fun Player.showBothClues() { + items("black_prism", "torn_page", "You show the prism and the necromantic half page to the aged wizard.") + npc("Hmmm, now this is interesting! Where did you get these from?") + player("I got them from a nearby Ogre tomb, it's recently been infested with zombie ogres and I'm trying to work out what happened there.") + npc("This is very troubling $name, very troubling indeed. While it's permitted for learned members of our order to research the 'dark arts', it's absolutely forbidden to make use of such magic.") + player("Do you have any leads on people that I might talk to regarding this?") + set("thzfe_prismsearch", 4) + npc("Well a wizard by the name of 'Sithik Ints' was doing some research in this area. He may know something about it. He's lodged at that guest house to the North, though he's ill and isn't able to leave his room.") + npc("Why not go and talk to him, poke around a bit and see if anything comes up. Let me know how you get on. However, I doubt that 'Sithik' had anything to do with it.") + npc("There's a severe penalty for using the 'dark arts'. If you find any evidence to the contrary, please bring it to me.") + npc("Hmm, that black prism seems to have some magical protection. Once you've finished with this item, bring it back to me would you. I may have a reward for you.") + } + + private suspend fun Player.showTankard() { + player("This is the tankard I found on the remains of Brentle Vahn!") + if (get("thzfe_innkeepermugshown", false)) { + npc("Yeah, you've shown me this before...if this is all the evidence you have?") + player("Please just look at it again...") + npc("Ok, let me look then.") + item(item = "dragon_inn_tankard", text = "You show the tankard to Zavistic, he looks at it again.") + } + set("thzfe_innkeepermugshown", true) + npc("That doesn't mean anything in itself, you could have gotten that from anywhere. Even from the Dragon Inn tavern! There isn't anything to link Brentle Vahn with Sithik Ints.") + } + + private suspend fun Player.showNecromancyBook() { + player("I have this necromancy book as evidence that Sithik is involved with the undead ogres at Jiggig.") + if (get("thzfe_shownnecrobook", false)) { + npc("Yeah, you've shown me this before...if this is all the evidence you have?") + player("Please just look at it again...") + npc("Ok, let me look then.") + } + npc("Ok, so he's researching necromancy...it doesn't mean anything in itself.") + player("Yes, but if you look, you can see that there is a half torn page which matches the page I found at Jiggig.") + set("thzfe_shownnecrobook", true) + npc("Hmm, yes, but someone could have stolen that from him and then gone and cast it without his permission or to try and deliberately implicate him.") + } + + private suspend fun Player.showHamBook() { + player("Look, this book proves that Sithik hates all monsters and most likely Ogres with a passion.") + if (get("thzfe_shownhambook", false)) { + npc("Yeah, you've shown me this before...if this is all the evidence you have?") + player("Please just look at it again...") + npc("Ok, let me look then.") + item(item = "book_of_ham", text = "You show the HAM book to Zavistic, he looks through it again.") + } + set("thzfe_shownhambook", true) + npc("So what, hating monsters isn't a crime in itself...although I suppose that it does give a motive if Sithik was involved. On its own, it's not enough evidence though.") + } + + private suspend fun Player.showSignedPortrait() { + player("This is a portrait of Sithik, signed by the landlord of the Dragon Inn saying that he saw Sithik and Brentle Vahn together.") + if (get("thzfe_shownsignedportrait", false)) { + npc("Yeah, you've shown me this before...if this is all the evidence you have?") + player("Please just look at it again...") + npc("Ok, let me look then.") + item(item = "signed_portrait", text = "You show the signed portrait of Sithik again to Zavistic.") + } + set("thzfe_shownsignedportrait", true) + npc("Hmmm, well that is interesting.") + if (showedAllEvidence()) { + handOverPotion() + } + } + + private fun Player.showedAllEvidence(): Boolean = get("thzfe_shownnecrobook", false) && get("thzfe_shownsignedportrait", false) + + // ===== The big payoff: receive the strange potion ===== + + private suspend fun Player.handOverPotion() { + npc("And I'm starting to think that Sithik may be involved. Here, take this potion and give some to Sithik. It'll bring on a change which should solicit some answers - tell him the effects won't revert until he's told the truth.") + set("zogre_flesh_eaters", "sithik") + inventory.remove("necromancy_book") + inventory.remove("torn_page") + inventory.remove("dragon_inn_tankard") + inventory.remove("zogre_sithik_portrait_signed") + inventory.remove("book_of_ham") + addOrDrop("zogre_ogre_trans_potion") + item("zogre_ogre_trans_potion", "Zavistic hands you a strange looking potion bottle and takes all the evidence you've accumulated so far.") + } + + private suspend fun Player.notYetUsedPotion() { + player("No, not yet, what was I supposed to do again?") + npc("Try to use the potion on Sithik somehow, he should undergo an interesting transformation, though you'll probably want to leave the house in case there are any side effects. Then go back and question Sithik and tell") + npc("him the effects won't wear off until he tells the truth. In fact, that's not exactly true, but I'm sure it'll be an extra incentive to get him to be honest.") + guildMenu() + } + + private suspend fun Player.lostPotion() { + player("Well, actually, I've lost it, could I have another one please?") + npc("Sure, but don't lose it this time.") + addOrDrop("zogre_ogre_trans_potion") + item(item = "zogre_ogre_trans_potion", text = "Zavistic hands you a bottle of strange potion.") + } + + private suspend fun Player.yesUsedPotion() { + player("Yes, I have in fact. I poured it into his tea.") + npc("Ok, that's good, that should work. Pop back in a little while to see Sithik and start questioning him.") + guildMenu() + } +} diff --git a/game/src/main/kotlin/content/area/karamja/ape_atroll/Daga.kt b/game/src/main/kotlin/content/area/karamja/ape_atroll/Daga.kt index 46354660dd..3d443b757d 100644 --- a/game/src/main/kotlin/content/area/karamja/ape_atroll/Daga.kt +++ b/game/src/main/kotlin/content/area/karamja/ape_atroll/Daga.kt @@ -22,52 +22,40 @@ class Daga : Script { init { npcOperate("Talk-to", "daga") { - val amulet = equipped(EquipSlot.Amulet) - - if (amulet.id == "monkeyspeak_amulet") { - - npc("Sorry, you don't have enough space in your inventory.") - - choice { - option("Yes, please.") { - openShop("dagas_scimitar_smithy") - } - option("No, thanks.") { - } - option("Do you have any Dragon Scimitars in stock?") { - npc("It just so happens I recently got a fresh delivery.
Do you want to buy one?") - choice { - option("Yes.") { - player("Yes, please.") - inventory.transaction { - remove("coin", 100_000) - add("dragon_scimitar") - } - when (inventory.transaction.error) { - is TransactionError.Full -> { - inventoryFull() - npc("Sorry, you don't have enough space in your inventory.") - } - - TransactionError.None -> { - npc("There you go. Pleasure doing business with you.") - } - - else -> npc( - "Sorry, you don't have enough coins.
It costs 100,000 gold coins.", - ) - } - - option("No.") { - player("No.") + if (amulet.id != "monkeyspeak_amulet") { + npc("Ook! Ah Uh Ah! Ook Ook! Ah!") + return@npcOperate + } + npc("Sorry, you don't have enough space in your inventory.") + choice { + option("Yes, please.") { + openShop("dagas_scimitar_smithy") + } + option("No, thanks.") + option("Do you have any Dragon Scimitars in stock?") { + npc("It just so happens I recently got a fresh delivery.
Do you want to buy one?") + choice { + option("Yes.") { + player("Yes, please.") + inventory.transaction { + remove("coin", 100_000) + add("dragon_scimitar") + } + when (inventory.transaction.error) { + is TransactionError.Full -> { + inventoryFull() + npc("Sorry, you don't have enough space in your inventory.") } + TransactionError.None -> npc("There you go. Pleasure doing business with you.") + else -> npc("Sorry, you don't have enough coins.
It costs 100,000 gold coins.") + } + option("No.") { + player("No.") } } } } - } else { - npc("Ook! Ah Uh Ah! Ook Ook! Ah!") } } diff --git a/game/src/main/kotlin/content/area/misthalin/paterdomus/Drezel.kt b/game/src/main/kotlin/content/area/misthalin/paterdomus/Drezel.kt index af2b9277c1..60ab4bf306 100644 --- a/game/src/main/kotlin/content/area/misthalin/paterdomus/Drezel.kt +++ b/game/src/main/kotlin/content/area/misthalin/paterdomus/Drezel.kt @@ -628,9 +628,7 @@ class Drezel : Script { when (ghastKills) { 1 -> npc("So you've got two more to kill then!") 2 -> npc("So you've got one more to kill then!") - 3 -> npc( - "So you've killed them all then! Go and tell him, I bet he'll be pleased.", - ) + 3 -> npc("So you've killed them all then! Go and tell him, I bet he'll be pleased.") } } diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/Benny.kt b/game/src/main/kotlin/content/area/misthalin/varrock/Benny.kt index 0a2ad61dbd..5a17cd2a41 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/Benny.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/Benny.kt @@ -49,17 +49,12 @@ class Benny : Script { purchase() } option("Varrock Herald? Never heard of it.") { - npc( - "For the illiterate amongst us, I shall elucidate. The Varrock Herald is a new newspaper. It is edited, printed and published by myself, Benny Gutenberg, and each edition promises to enthrall the reader with ", - ) + npc("For the illiterate amongst us, I shall elucidate. The Varrock Herald is a new newspaper. It is edited, printed and published by myself, Benny Gutenberg, and each edition promises to enthrall the reader with") npc("captivating material! Now, can I interest you in buying one for a mere 50 coins?") purchase() } option("Anything interesting in there?") { - npc( - - "Of course there is, mate. Packed full of thought provoking insights, contentious interviews and celebrity scandalmongering! An excellent read and all for just 50 coins! Want one?", - ) + npc("Of course there is, mate. Packed full of thought provoking insights, contentious interviews and celebrity scandalmongering! An excellent read and all for just 50 coins! Want one?") purchase() } } diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/Dealga.kt b/game/src/main/kotlin/content/area/misthalin/varrock/Dealga.kt index b4507bbef8..8176cbcfeb 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/Dealga.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/Dealga.kt @@ -36,15 +36,11 @@ class Dealga : Script { } } option("Who are you?") { - npc( - "The name's Dealga, I shipped over from Ape Atoll a while back when I heard that these 'humans' pay good money for certain wares! Thought I'd come over here, and much like the dragon scimitar... make a killing!", - ) + npc("The name's Dealga, I shipped over from Ape Atoll a while back when I heard that these 'humans' pay good money for certain wares! Thought I'd come over here, and much like the dragon scimitar... make a killing!") npc("Now, what can I do for you? A nice, keen edged dragon scimitar?") } option("What are you doing here?") { - npc( - "Like the keen edged dragon scimitar I'm slashing away the competition! If you hairless apes won't come to Ape Atoll, then I'll come to you! I'll soon be overtaking Daga in profitability!", - ) + npc("Like the keen edged dragon scimitar I'm slashing away the competition! If you hairless apes won't come to Ape Atoll, then I'll come to you! I'll soon be overtaking Daga in profitability!") npc("Now, what can I do for you? A nice, keen edged dragon scimitar?") } } diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/palace/KingRoald.kt b/game/src/main/kotlin/content/area/misthalin/varrock/palace/KingRoald.kt index 58ad8f60be..56b2f0189c 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/palace/KingRoald.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/palace/KingRoald.kt @@ -191,9 +191,7 @@ class KingRoald : Script { } private suspend fun Player.claimReward(certificate: String) { - player( - "Your majesty, I have come to claim the reward for the return of the Shield Of Arrav.", - ) + player("Your majesty, I have come to claim the reward for the return of the Shield Of Arrav.") item("certificate", "You show the certificate to the king.") if (certificate == "certificate_full") { npc("My goodness! This claim is for the reward offered by my father many years ago!") diff --git a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/FillimanTarlock.kt b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/FillimanTarlock.kt index d8104ea84f..cdb7cf5fbc 100644 --- a/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/FillimanTarlock.kt +++ b/game/src/main/kotlin/content/area/morytania/mort_myre_swamp/FillimanTarlock.kt @@ -121,10 +121,7 @@ class FillimanTarlock : Script { stage == 105 -> { npc("Hello again my friend, have you defeated three Ghasts as I asked you?") player("Yes, I've killed all three and their spirits have been released!") - npc( - "Many thanks my friend, you have completed your quest! I can now change " + - "this place into a holy sanctuary! And forever will it now be an Altar of Nature!", - ) + npc("Many thanks my friend, you have completed your quest! I can now change this place into a holy sanctuary! And forever will it now be an Altar of Nature!") sendNatureSpiritReward() } stage >= 110 -> { diff --git a/game/src/main/kotlin/content/entity/effect/toxin/Disease.kt b/game/src/main/kotlin/content/entity/effect/toxin/Disease.kt index bbb07317da..aad74ebe77 100644 --- a/game/src/main/kotlin/content/entity/effect/toxin/Disease.kt +++ b/game/src/main/kotlin/content/entity/effect/toxin/Disease.kt @@ -4,19 +4,26 @@ import content.entity.combat.hit.directHit import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.Character +import world.gregs.voidps.engine.entity.character.flagHits import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.timer.* +import world.gregs.voidps.network.login.protocol.visual.update.HitSplat import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import world.gregs.voidps.type.random import java.util.concurrent.TimeUnit import kotlin.math.sign -val Character.diseased: Boolean get() = diseaseCounter > 0 +private const val DISEASE_CYCLE = 30 // 18 seconds -val Character.antiDisease: Boolean get() = diseaseCounter < 0 +val Character.diseased: Boolean get() = diseaseDamage > 0 -var Character.diseaseCounter: Int +val Character.antiDisease: Boolean get() = diseaseDamage < 0 + +var Character.diseaseDamage: Int get() = if (this is Player) get("disease", 0) else this["disease", 0] set(value) = if (this is Player) { set("disease", value) @@ -31,22 +38,20 @@ fun Character.cureDisease(): Boolean { } fun Character.disease(target: Character, damage: Int) { - if (target.antiDisease || damage < target["disease_damage", 0]) { + if (target.antiDisease || damage < target.diseaseDamage) { return } val timers = if (target is Player) target.timers else target.softTimers if (timers.contains("disease") || timers.start("disease")) { - target.diseaseCounter = TimeUnit.SECONDS.toTicks(18) / 30 - target["disease_damage"] = damage target["disease_source"] = this + target.diseaseDamage = damage } } fun Player.antiDisease(minutes: Int) = antiDisease(minutes, TimeUnit.MINUTES) fun Player.antiDisease(duration: Int, timeUnit: TimeUnit) { - diseaseCounter = -(timeUnit.toTicks(duration) / 30) - clear("disease_damage") + diseaseDamage = -((timeUnit.toTicks(duration) / DISEASE_CYCLE)) clear("disease_source") timers.startIfAbsent("disease") } @@ -55,13 +60,13 @@ class Disease : Script { init { playerSpawn { - if (diseaseCounter != 0) { + if (diseaseDamage != 0) { timers.restart("disease") } } npcSpawn { - if (diseaseCounter != 0) { + if (diseaseDamage != 0) { softTimers.restart("disease") } } @@ -78,48 +83,81 @@ class Disease : Script { if (immune(character)) { return Timer.CANCEL } - if (!restart && character.diseaseCounter == 0) { + if (!restart && character.diseaseDamage == 0) { (character as? Player)?.message("You have been diseased.") - damage(character) } - return 30 + return DISEASE_CYCLE } fun tick(character: Character): Int { val diseased = character.diseased - character.diseaseCounter -= character.diseaseCounter.sign + val damage = character.diseaseDamage + character.diseaseDamage -= character.diseaseDamage.sign when { - character.diseaseCounter == 0 -> { + character.diseaseDamage == 0 -> { if (!diseased) { (character as? Player)?.message("Your disease resistance has worn off.") } return Timer.CANCEL } - character.diseaseCounter == -1 -> (character as? Player)?.message("Your disease resistance is about to wear off.") - diseased -> damage(character) + character.diseaseDamage == -1 -> (character as? Player)?.message("Your disease resistance is about to wear off.") + diseased -> damage(character, damage) } return Timer.CONTINUE } fun stop(character: Character, logout: Boolean) { - character.diseaseCounter = 0 - character.clear("disease_damage") + character.diseaseDamage = 0 character.clear("disease_source") } - fun immune(character: Character) = character is NPC && - character.def["immune_disease", false] || - character is Player && - character.equipped(EquipSlot.Hands).id == "inoculation_brace" + fun immune(character: Character) = character is NPC && character.def["immune_disease", false] - fun damage(character: Character) { - val damage = character["disease_damage", 0] - if (damage <= 10) { - character.cureDisease() + fun damage(character: Character, damage: Int) { + val source = character["disease_source", character] + character.sound("disease_hitsplat") + if (character is Player && character.equipped(EquipSlot.Hands).id == "inoculation_brace") { return } - character["disease_damage"] = damage - 2 - val source = character["disease_source", character] - character.directHit(source, damage, "disease") + if (character is Player) { + val skill = DRAINABLE_SKILLS[random.nextInt(DRAINABLE_SKILLS.size)] + val current = character.levels.get(skill) + if (current <= 1) { + // No skill level left to drain — bite Hitpoints instead. + character.directHit(source, damage * 10, "disease") + } else { + character.levels.drain(skill, damage) + showDiseaseSplat(character, source, damage) + } + } else { + character.directHit(source, damage, "disease") + } + } + + /** + * Adds a yellow disease hitsplat showing [amount] without deducting Constitution + * (used when disease drained a stat instead of HP). + */ + private fun showDiseaseSplat(target: Character, source: Character, amount: Int) { + val hp = target.levels.get(Skill.Constitution) + val percentage = target.levels.getPercent(Skill.Constitution, hp, 255.0).toInt() + target.visuals.hits.add( + HitSplat( + amount, + HitSplat.Mark.Diseased, + percentage, + 0, + false, + if (source is NPC) -source.index else source.index, + -1, + ), + ) + target.flagHits() + } + + companion object { + private val DRAINABLE_SKILLS: Array = Skill.entries + .filter { it != Skill.Constitution && it != Skill.Prayer } + .toTypedArray() } } diff --git a/game/src/main/kotlin/content/entity/effect/toxin/Poison.kt b/game/src/main/kotlin/content/entity/effect/toxin/Poison.kt index 9822b82713..cf7d179a7d 100644 --- a/game/src/main/kotlin/content/entity/effect/toxin/Poison.kt +++ b/game/src/main/kotlin/content/entity/effect/toxin/Poison.kt @@ -83,26 +83,43 @@ class Poison : Script { npcTimerStop("poison", ::stop) interfaceOption("Use Cure", "health_orb:poison") { - for (type in listOf("antipoison", "super_antipoison", "antipoison+")) { - val index = inventory.items.indexOfFirst { it.id.startsWith(type) } + if (poisoned) { + for (type in listOf("antipoison", "super_antipoison", "antipoison+")) { + if (drink(type)) { + return@interfaceOption + } + } + val index = inventory.indexOf("prayer_book") if (index != -1) { - val option = "Drink" + val option = "Recite-prayer" val item = inventory[index] InterfaceApi.option(this, InterfaceOption(item, index, option, item.def.options.indexOf(option), "inventory:inventory")) return@interfaceOption } + message("You don't have anything to cure the poison.") } - val index = inventory.indexOf("prayer_book") - if (index != -1) { - val option = "Recite-prayer" - val item = inventory[index] - InterfaceApi.option(this, InterfaceOption(item, index, option, item.def.options.indexOf(option), "inventory:inventory")) - return@interfaceOption + if (diseased) { + for (type in listOf("relicyms_balm", "sanfew_serum")) { + if (drink(type)) { + return@interfaceOption + } + } + message("You don't have anything to cure the disease.") } - message("You don't have anything to cure the poison.") } } + private suspend fun Player.drink(type: String): Boolean { + val index = inventory.items.indexOfFirst { it.id.startsWith(type) } + if (index != -1) { + val option = "Drink" + val item = inventory[index] + InterfaceApi.option(this, InterfaceOption(item, index, option, item.def.options.indexOf(option), "inventory:inventory")) + return true + } + return false + } + fun start(character: Character, restart: Boolean): Int { if (character.poisonImmune) { return Timer.CANCEL diff --git a/game/src/main/kotlin/content/entity/npc/combat/Attack.kt b/game/src/main/kotlin/content/entity/npc/combat/Attack.kt index b481e588d4..68c893de44 100644 --- a/game/src/main/kotlin/content/entity/npc/combat/Attack.kt +++ b/game/src/main/kotlin/content/entity/npc/combat/Attack.kt @@ -47,7 +47,7 @@ class Attack( val def = def(primaryTarget) def["combat_def", get("transform_id", def.stringId)] } else { - def["combat_def", get("transform_id", id)] + get("transform_id", id) } val definition = definitions.getOrNull(defId) ?: return@npcCombatSwing if (definition.attacks.isEmpty()) { @@ -110,12 +110,11 @@ class Attack( offense = listOf("crush", "range", "magic").random(random) defence = offense } - val spell = if (offense == "magic" || defence == "magic") attack.id else "" val damage = if (hit.max == 0) { - hit(target = target, delay = delay, offensiveType = offense, defensiveType = defence, special = hit.special, spell = spell) + hit(target = target, delay = delay, offensiveType = offense, defensiveType = defence, special = hit.special, spell = attack.id) // Reuse spell for attack name } else { val damage = Damage.roll(source = this, target = target, offensiveType = offense, weapon = Item.EMPTY, special = hit.special, defensiveType = defence, range = hit.min..hit.max, skipAccuracyRoll = !hit.accuracyRoll) - hit(target = target, delay = delay, offensiveType = offense, defensiveType = defence, special = hit.special, damage = damage, spell = spell) + hit(target = target, delay = delay, offensiveType = offense, defensiveType = defence, special = hit.special, damage = damage, spell = attack.id) } if (damage > 0) { miss = false diff --git a/game/src/main/kotlin/content/entity/player/combat/Attack.kt b/game/src/main/kotlin/content/entity/player/combat/Attack.kt index 78f248c3ac..c06d7bada1 100644 --- a/game/src/main/kotlin/content/entity/player/combat/Attack.kt +++ b/game/src/main/kotlin/content/entity/player/combat/Attack.kt @@ -8,7 +8,6 @@ import content.skill.melee.weapon.fightStyle import net.pearx.kasechange.toTitleCase import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message -import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement @@ -86,21 +85,12 @@ class Attack : Script { onNPCApproach("*_spellbook:*") { val (target, id) = it - val component = id.substringAfter(":") - val row = Rows.getOrNull("spells.$component") ?: return@onNPCApproach - val message = row.stringOrNull("npc_message") - if (message != null) { - if (message.isNotEmpty()) { - message(message) - } - return@onNPCApproach - } if (!has(Skill.Slayer, target.def["slayer_level", 0])) { message("You need a higher slayer level to know how to wound this monster.") return@onNPCApproach } approachRange(8, update = false) - spell = component + spell = id.substringAfter(":") if (target.id.endsWith("_dummy") && !handleCombatDummies(target)) { clear("spell") return@onNPCApproach @@ -114,15 +104,6 @@ class Attack : Script { onPlayerApproach("*_spellbook:*") { val (target, id) = it - val component = id.substringAfter(":") - val row = Rows.getOrNull("spells.$component") ?: return@onPlayerApproach - val message = row.stringOrNull("player_message") - if (message != null) { - if (message.isNotEmpty()) { - message(message) - } - return@onPlayerApproach - } approachRange(8, update = false) spell = id.substringAfter(":") set("attack_speed", 5) diff --git a/game/src/main/kotlin/content/entity/player/inv/item/ItemOnItems.kt b/game/src/main/kotlin/content/entity/player/inv/item/ItemOnItems.kt index 1521c0e3ff..b9168d8ce0 100644 --- a/game/src/main/kotlin/content/entity/player/inv/item/ItemOnItems.kt +++ b/game/src/main/kotlin/content/entity/player/inv/item/ItemOnItems.kt @@ -176,7 +176,9 @@ class ItemOnItems(val itemOnItemDefs: ItemOnItemDefinitions) : Script { for (item in def.requires) { if (!inventory.contains(item.id, item.amount)) { error = TransactionError.Deficient(item.amount) - return "You need a ${item.def.name.lowercase()} to ${def.type} this." + return def.requiresMessage.ifEmpty { + "You need a ${item.def.name.lowercase()} to ${def.type} this." + } } } for (item in def.remove) { diff --git a/game/src/main/kotlin/content/quest/Quest.kt b/game/src/main/kotlin/content/quest/Quest.kt index 30eb3bb988..37efca496b 100644 --- a/game/src/main/kotlin/content/quest/Quest.kt +++ b/game/src/main/kotlin/content/quest/Quest.kt @@ -28,6 +28,7 @@ val quests = setOf( "priest_in_peril", "lost_city", "tears_of_guthix", + "zogre_flesh_eaters", // mini-quests "enter_the_abyss", ) diff --git a/game/src/main/kotlin/content/quest/member/ogre/ZogreFleshEaters.kt b/game/src/main/kotlin/content/quest/member/ogre/ZogreFleshEaters.kt new file mode 100644 index 0000000000..2c42c44e8a --- /dev/null +++ b/game/src/main/kotlin/content/quest/member/ogre/ZogreFleshEaters.kt @@ -0,0 +1,699 @@ +package content.quest.member.ogre + +import content.entity.combat.killer +import content.entity.gfx.areaGfx +import content.entity.obj.door.enterDoor +import content.entity.player.bank.ownsItem +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Mad +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.type.item +import content.entity.player.dialogue.type.items +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +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.clearCamera +import world.gregs.voidps.engine.client.instruction.handle.interactPlayer +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.data.Settings.Companion.getOrNull +import world.gregs.voidps.engine.entity.character.areaSound +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.Players +import world.gregs.voidps.engine.entity.character.player.Teleport +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.queue.queue +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.random + +class ZogreFleshEaters : Script { + + init { + + // ===== Quest journal ===== + + questJournalOpen("zogre_flesh_eaters") { + val lines = when (quest("zogre_flesh_eaters")) { + "unstarted" -> notStartedJournal() + "completed" -> completedJournal() + else -> startedJournal(questStage("zogre_flesh_eaters")) + } + questJournal("Zogre Flesh Eaters", lines) + } + + // ===== Movement-triggered cutscene ===== + // The blackened/charred area cutscene plays when the player first walks into the right tile. + entered("zogre_blackened_area") { + if (get("thzfe_cut_scene", false)) { + return@entered + } + queue("zogre_blackened_cutscene") { playBlackenedCutscene() } + } + + // ===== Climb over the smashed barricade ===== + objectOperate("Climb-over", "ogre_barricade_collapsed*") { (target) -> + val enter = tile.x < target.tile.x + val direction = if (enter) Direction.EAST else Direction.WEST + anim("regicide_stepover") + exactMoveDelay( + target = tile.copy(x = tile.x + if (enter) 2 else -2), + delay = 30, + direction = direction, + ) + sound("bonewalk") + delay(2) + } + + // ===== Sithik's tea cannot be picked up — he scolds the player ===== + takeable("cup_of_tea_zogre_flesh_eaters") { _, telegrab -> + val sithik = if (get("thzfe_sithik_transformed", false)) "zogre_sithik_ogre" else "zogre_sithik_man" + if (telegrab) { + npc(sithik, "Hey! What do you think you're doing? Don't go casting that kind of spell anywhere near my tea! Leave my tea alone you telegrabbing fiend!") + } else { + npc(sithik, "Hey! What do you think you're doing? Leave my tea alone!") + } + null + } + + // ===== Entrance stairs (down into Jiggig caves) ===== + objectOperate("Climb-down", "ogre_stairs_down") { (target) -> + message("You climb down the steps.") + sound("down_stone_stairs") + open("fade_out") + delay(3) + if (target.rotation == 1) { + tele(2442, 9418, 0) + } else { + tele(2477, 9437, 2) + } + delay(1) + open("fade_in") + delay(3) + } + + // ===== Exit stairs (back up out of Jiggig caves) ===== + objectOperate("Climb-up", "ogre_stairs") { (target) -> + message("You climb up the steps.") + sound("up_stone_stairs") + open("fade_out") + delay(3) + if (target.tile.x == 2443 && target.tile.y == 9417) { + tele(2446, 9417, 2) + } else { + tele(2485, 3045, 0) + } + delay(1) + open("fade_in") + delay(3) + } + + // ===== Lectern in the tomb (search for torn page) ===== + objectOperate("Search", "zogre_lecturn") { + if (questStage("zogre_flesh_eaters") >= 4) { + return@objectOperate message("You search the lectern, but find nothing.") + } + message("You search the broken down lectern.") + anim("human_pickupfloor") + delay(2) + if (ownsItem("torn_page")) { + message("You find nothing here this time.") + return@objectOperate + } + addOrDrop("torn_page") + sound("pick") + item(item = "torn_page", text = "You find a half torn page...it has spidery writing all over it.") + } + + // ===== Skeleton corpse (spawns a zombie, then yields the backpack) ===== + objectOperate("Search", "zogre_brentle_skeleton") { + val skeletonState = get("thzfe_brentle_skele", 0) + if (skeletonState == 2) { + if (questStage("zogre_flesh_eaters") >= 4 || + inventory.contains("ruined_backpack") || + inventory.contains("dragon_inn_tankard") + ) { + return@objectOperate message("You find nothing on the corpse.") + } + addOrDrop("ruined_backpack") + item(item = "ruined_backpack", text = "You find a backpack on the corpse.") + return@objectOperate + } + + if (NPCs.findOrNull(tile.regionLevel, "zogre_human_brentle_vahn") != null) { + return@objectOperate message("You're in mortal danger, you don't have time to search!") + } + + areaGfx("smokepuff_large", Tile(2442, 9458, 2)) + delay(1) + set("thzfe_brentle_skele", 1) + message("Something screams into life right in front of you.") + sound("disease_hitsplat") // 2388 + + val zombie = NPCs.add(id = "zogre_human_brentle_vahn", tile = Tile(2442, 9458, 2), ticks = 1000, owner = this) + zombie.interactPlayer(this, "Attack") + } + + npcDespawn("zogre_human_brentle_vahn") { + areaGfx("smokepuff_large", tile) + val name: String = getOrNull("owner") ?: return@npcDespawn + val owner = Players.find(name) ?: return@npcDespawn + owner.queue("brentle_zombie_wanders") { + statement("This mindless zombie loses interest in fighting you and wanders off.") + } + } + + // ===== Knife on coffin (force the lock) ===== + itemOnObjectOperate("knife", "zogre_coffin_special") { + if (get("thzfe_prismsearch", 0) != 1) { + return@itemOnObjectOperate message("Nothing interesting happens.") + } + set("thzfe_prismsearch", 2) + sound("unlock") + item("knife", "With some skill you manage to slide the blade along the lock edge and click into place the teeth of the primitive mechanism.") + } + + // ===== Locked ogre coffin (multi-stage search) ===== + objectOperate("Search", "zogre_coffin_special*") { + when (val value = get("thzfe_prismsearch", 0)) { + 0, 1 -> { + statement("You search the coffin and find a small geometrically shaped hole in the side. It looks as if this hole was made with a considerable amount of force, maybe the thing which made the hole is still inside?") + if (value == 0) { + set("thzfe_prismsearch", 1) + statement("The lock looks quite crude, with some skill and a slender blade, you may be able to force it.") + } + } + 2 -> liftCoffinLid() + 3 -> { + if (inventory.contains("black_prism")) { + return@objectOperate message("You find nothing inside this time.") + } + if (!inventory.add("black_prism")) { + return@objectOperate statement("You see something inside, but you have no space in your inventory to store the item.") + } + item(item = "black_prism", text = "You find a creepy looking black prism inside.") + } + } + } + + // ===== Zombie NPC (just a scream) ===== + npcOperate("Talk-to", "zogre_zombie") { (target) -> + target.say("Raaarrrggghhh") + } + + npcOperate("Talk-to", "pilg") { (target) -> + npc("Dey got me in da belly, mees gutsies feel like had a dead dead dog dinner.") + } + + npcOperate("Talk-to", "grug") { (target) -> + npc("Ukk...I's dun fer...me's don't feel legsies anymore!") + } + + // ===== Item examine: ruined backpack (open it) ===== + itemOption("Open", "ruined_backpack") { + if (inventory.spaces < 3) { + return@itemOption message("You don't have enough room in your inventory for the contents of this bag.") + } + item("ruined_backpack", "Just before you open the backpack, you notice a small leather patch with the moniker: 'B.Vahn', on it.") + inventory.remove("ruined_backpack") + addOrDrop("dragon_inn_tankard") + addOrDrop("rotten_food") + addOrDrop("knife") + message("You find a knife and some rotten food.") + message("You find an interesting looking tankard.") + item(item = "dragon_inn_tankard", text = "You find an interesting looking tankard.") + items("knife", "rotten_food", "You find a knife and some rotten food, the backpack is ripped to shreds.") + } + + // ===== Item examine handlers (read books, examine items) ===== + + itemOption("Read", "torn_page") { + statement("You don't manage to understand all of it as there is only a half page here. But it seems the spell was used to place a curse on an area and for all time raise the dead.") + statement("If you look very carefully, you see what looks like a guild emblem.") + } + + itemOption("Look-at", "black_prism") { + item("black_prism", "It looks like a smokey black gem of some sort...very creepy. Some magical force must have prevented it from being shattered when it hit the coffin.") + } + + itemOption("Look-at", "dragon_inn_tankard") { + item("dragon_inn_tankard", "A stout ceramic tankard with a Dragon Emblem on the side, the words, 'Ye Olde Dragon Inn' are inscribed in the bottom.") + } + + itemOption("Look-at", "zogre_sithik_portrait_signed") { + item("signed_portrait", "You see an image of Sithik with a message underneath 'I, the bartender of the Dragon Inn, do swear that this is a true likeness of the wizzy who was talking to Brentle Vahn, my customer the other day.'") + } + + itemOption("Read", "necromancy_book") { + item("necromancy_book", "This book uses very strange language and some incomprehensible symbols. It has a very dark feeling to it. As you're looking through the book, you notice that one of the pages has been torn and half of it is missing.") + } + + itemOption("Read", "book_of_portraiture") { + item("book_of_portraiture", "All interested artisans should really consider taking up the hobby of portraiture. To do so, one uses a piece of papyrus on the intended subject to initiate a likeness drawing activity.") + } + + itemOption("Read", "book_of_ham") { + statement("You read this book for a while, it seems to be some sort of political manifesto about how the king doesn't do enough to safeguard the citizens of the realm from the monsters that still thrive within the borders. It sends out a rallying cry to all people who would want to stop monsters, to join the HAM movement.") + player("Hmmm, Sithik must really hate monsters then, I wonder if he hates ogres in particular?") + } + + // ===== Strange potion on Sithik's tea (ground item interaction) ===== + itemOnFloorItemOperate("zogre_ogre_trans_potion", "cup_of_tea_zogre_flesh_eaters") { + arriveDelay() + if (quest("zogre_flesh_eaters") != "sithik") { + return@itemOnFloorItemOperate message("Nothing interesting happens.") + } + set("zogre_flesh_eaters", "potion") + anim("human_pickuptable") + sound("drip_poison") + inventory.remove("zogre_ogre_trans_potion") + addOrDrop("sample_bottle") + delay(2) + item("zogre_ogre_trans_potion", "You pour some of the potion into the cup. Zavistic said it may take some time to have an effect.") + } + + objTeleportTakeOff("Climb-up", "basic_ladder_bottom") { obj, _ -> + if (obj.tile == Tile(2597, 3107, 0)) { + if (quest("zogre_flesh_eaters") == "potion" && get("thzfe_sithik_transformed", 0) == 0) { + set("thzfe_sithik_transformed", 1) + } + } + + Teleport.CONTINUE + } + + // ===== Ogre tomb doors (locked, need ogre gate key) ===== + objectOperate("Open", "ogre_cavedoor*") { (target) -> + enterOgreCaveDoor(target) + } + + // ===== Plinth in the tomb (Slash Bash spawn / artefact retrieval) ===== + objectOperate("Search", "zogre_stand") { (target) -> + if (NPCs.at(tile.regionLevel).any { it.id == "slash_bash" && it["owner", ""] == accountName }) { + return@objectOperate message("You're in mortal danger, you don't have time to search!") + } + + statement("You search the plinth...") + val progress = quest("zogre_flesh_eaters") + when { + progress == "completed" || inventory.contains("ogre_artefact") -> { + message("You find nothing in particular.") + } + progress == "killed_slash_bash" -> { + areaGfx("smokepuff_large", target.tile) + areaSound("smokepuff", target.tile) + addOrDrop("ogre_artefact") + item("ogre_artefact", "An ogre artefact appears in front of you. You quickly put it into your backpack.") + } + else -> { + message("Something stirs behind you!") + areaGfx("smokepuff_large", Tile(2477, 9444, 0)) + areaSound("smokepuff", Tile(2477, 9444, 0)) + NPCs.add( + id = "slash_bash", + tile = Tile(2477, 9444, 0), + ticks = 1000, + owner = this, + ) + } + } + } + + npcDeath("slash_bash") { + val killer = killer as? Player ?: return@npcDeath + if (killer.quest("zogre_flesh_eaters") == "given_key") { + killer["zogre_flesh_eaters"] = "killed_slash_bash" + } + } + + npcDeath("zogre_human_brentle_vahn") { + val killer = killer as? Player ?: return@npcDeath + areaGfx("smokepuff_large", tile) + killer["thzfe_brentle_skele"] = 2 + } + } + + // ===== Ogre tomb double doors ===== + + private suspend fun Player.enterOgreCaveDoor(target: GameObject) { + val enter = tile.y >= target.tile.y + if (enter && !inventory.contains("ogre_gate_key")) { + message("These gates are locked, you don't seem to be able to open them.") + return + } + message(if (enter) "You use the Ogre Tomb Key to unlock the door." else "You push the gates open.") + sound("strangedoor_open") + enterDoor(target) + } + + // ===== Cutscene: blackened, charred area ===== + + private suspend fun Player.playBlackenedCutscene() { + steps.clear() + set("thzfe_cut_scene", true) + message("You enter this blackened, charred area - it looks like there's been an explosion!") + moveCamera(tile = Tile(2445, 9460), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2441, 9459), height = 25, speed = 15, acceleration = 15) + statement( + "You enter this blackened, charred area - it looks like some sort of explosion has taken place.", + clickToContinue = false, + ) + delay(3) + + moveCamera(tile = Tile(2444, 9458), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2441, 9459), height = 25, speed = 15, acceleration = 15) + delay(3) + + moveCamera(tile = Tile(2442, 9457), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2442, 9459), height = 25, speed = 5, acceleration = 5) + delay(2) + + moveCamera(tile = Tile(2440, 9458), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2442, 9459), height = 25, speed = 10, acceleration = 10) + delay(2) + + moveCamera(tile = Tile(2440, 9460), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2442, 9459), height = 25, speed = 15, acceleration = 15) + delay(2) + + moveCamera(tile = Tile(2442, 9461), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2442, 9459), height = 25, speed = 10, acceleration = 10) + delay(2) + + moveCamera(tile = Tile(2444, 9460), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2437, 9459), height = 25, speed = 10, acceleration = 10) + delay(2) + + moveCamera(tile = Tile(2444, 9458), height = 400, speed = 4, acceleration = 4) + turnCamera(tile = Tile(2437, 9459), height = 25, speed = 15, acceleration = 15) + delay(1) + + statement("You enter this blackened, charred area - it looks like some sort of explosion has taken place.") + clearCamera() + } + + // ===== Coffin lid lifting (the "Urrrgggg" sequence) ===== + + private suspend fun Player.liftCoffinLid() { + if (inventory.isFull()) { + statement("You start to lift the lid and see something inside, but you have no space in your inventory to store the item.") + return + } + statement("The lid looks heavy, but now that you've unlocked it, you may be able to lift it. You prepare yourself.") + say("Urrrgggg.") + player("Urrrgggg.", clickToContinue = false) + delay(3) + say("Aarrrgghhh!") + player("Aarrrgghhh!", clickToContinue = false) + delay(3) + if (random.nextBoolean()) { + levels.drain(Skill.Strength, 2) + statement("You struggle, but just get weakened from your experience. Perhaps you should try again after you've recovered from the effort?") + } else { + set("thzfe_prismsearch", 3) + sound("coffin_open") + say("Raarrrggggg! Yes!") + player("Raarrrggggg! Yes!", clickToContinue = false) + delay(2) + statement("You eventually manage to lift the lid.") + } + } + + // ===== Journal ===== + + private fun Player.notStartedJournal(): List { + fun req(met: Boolean, text: String) = if (met) "$text" else "$text" + return listOf( + "I can start this quest by talking to Grish at the Ugrish", + "ceremonial dance place called Jiggig.", + "To start this quest I should complete these quests:-", + req(questCompleted("jungle_potion"), "Jungle Potion."), + req(questCompleted("big_chompy_bird_hunting"), "Big Chompy Bird Hunting."), + "It would help if I had the following skill levels:-", + req(has(Skill.Ranged, 30), "Ranged level: 30"), + req(has(Skill.Fletching, 30), "Fletching level: 30"), + req(has(Skill.Smithing, 4), "Smithing level: 4"), + req(has(Skill.Herblore, 4), "Herblore level: 4"), + "Must be able to defeat a level 111 foe.", + ) + } + + private fun completedJournal(): List = listOf( + "I talked to an ogre called Grish who asked me to look into", + "the problem. After some searching around in a tomb, I", + "found some clues which pointed me to the human", + "habitation of Yannile.", + "With the help of Zavistic Rarve, the grand secretary of", + "the Wizards guild I was able to piece the clues together", + "and discover that a Wizard named 'Sithik Ints' was", + "responsible.", + "Unfortunately I couldn't remove the curse from the area,", + "however, I was able to return some important artefacts to", + "Grish, who can now set up a new ceremonial dance area for", + "the ogres of Gu' Tanoth.", + "Sithik Ints also told me how to make Brutal arrows which are", + "more effective against Zogres, and he also told me how to", + "make a disease balm.", + "", + "QUEST COMPLETE!", + ) + + private fun Player.startedJournal(stage: Int): List { + val list = mutableListOf() + val search = get("thzfe_prismsearch", 0) + + // ===== Stage 2+ — initial Grish conversation ===== + if (stage < 3) { + list += "I started this quest by talking to Grish, he asked me to" + list += "check out the underground area where some Zombie ogres" + list += "(Zogres) were coming from." + list += "" + list += "I have to find a way into the ceremonial dance area and" + list += "then underground" + } else { + list += "I started this quest by talking to Grish, he asked me to" + list += "check out the underground area where some Zombie ogres" + list += "(Zogres) were coming from." + list += "I have to find a way into the ceremonial dance area and" + list += "then underground." + } + + // ===== Stage 3+ — past the barricade ===== + if (stage >= 3) { + list += "I persuaded a guard to let me past, I only had to mention" + list += "Grish's name and the guard smashed the barricade down. I" + list += "can enter now." + + // Sub-stages tracked by the sithik_intro varbit + if (search >= 4 || stage >= 4) { + list += "The guard has smashed the barricade down. I can enter" + list += "now. I need to find out what happened here" + } else if (search >= 1) { + list += "I need to find out what happened here." + } + + if (search >= 2) { + list += "I have searched a coffin, it has a funny looking hole at the" + list += "side." + } + if (search >= 3) { + list += "I have forced the lock on a coffin, maybe I can open it" + list += "now?" + } + if (search >= 4) { + list += "I've opened the coffin and retrieved a black prism, this" + list += "may be useful." + list += "I found a half torn page from a necromantic spell book," + list += "maybe this is a clue?" + } + if (search >= 5) { + list += "I have shown the prism to the grand secretary of the" + list += "wizards guild." + } + + // Tankard sub-thread (only at quest progress 3) + if (inventory.contains("dragon_inn_tankard") && stage == 3) { + if (get("thzfe_showntankard", false)) { + list += "I killed a human zombie which dropped a backpack. The" + list += "backpack had the name 'B. Vahn' on it, inside the backpack" + list += "I found a tankard." + if (inventory.contains("signed_portrait")) { + list += "The Dragon Inn Innkeeper says the tankard belongs to one" + list += "of his locals called Brentle Vahn. He was seen talking to a" + list += "wizard the other day." + } else { + list += "The 'Dragon Inn' Innkeeper says the tankard belongs to" + list += "one of his locals called Brentle Vahn. He was seen talking" + list += "to a wizard the other day." + } + } else { + list += "I killed a human zombie which dropped a backpack. The" + list += "backpack had the name 'B. Vahn' on it, inside the backpack" + list += "I found a tankard." + } + } + } + + // Current-state hint based on varbit (overrides earlier versions for the live state) + if (stage == 3 && search < 4) { + when (search) { + 0 -> list += "I need to find out what happened here." + 1 -> { + list += "I have searched a coffin, it has a funny looking hole at the" + list += "side." + } + 2 -> { + list += "I have forced the lock on a coffin, maybe I can open it" + list += "now?" + } + 3 -> { + if (inventory.contains("black_prism")) { + list += "I've opened the coffin and retrieved a black prism, this" + list += "may be useful." + } else { + list += "I've managed to lift the lid on the coffin, it was quite" + list += "heavy! Maybe there's something inside the coffin?" + } + } + } + } + + if (stage == 3 && search == 4) { + list += "I have shown the prism and the necromantic page to" + list += "Zavistic Rarve. He's told me about a wizard named Sithik" + list += "Ints who might have some information." + } + + if (stage == 3 && search == 5) { + list += "I've spoken to Sithik, I need to see if he was involved in" + list += "some way." + when { + inventory.contains("signed_portrait") -> { + list += "I've got a signed portrait of Sithik, this may help to" + list += "convince Zavistic Rarve." + } + inventory.contains("good_portrait") || inventory.contains("bad_portrait") -> { + list += "I've made a portrait of Sithik...not sure what this will do?" + } + inventory.contains("book_of_portraiture") -> { + list += "I've found a book on portraiture...what does this prove?" + } + } + if (inventory.contains("book_of_ham")) { + list += "I've found a book on HAM philosophy...what does this" + list += "prove?" + } + if (inventory.contains("necromancy_book")) { + list += "I've found a necromantic book...what does this prove?" + } + } + + // ===== Stage 4+ — Zavistic gives the potion ===== + if (stage >= 4) { + list += "I've spoken to Sithik, I need to see if he was involved with" + list += "the Undead Ogres at 'Jiggig' in some way." + list += "I talked to Zavistic Rarve regarding the prism and the torn" + list += "page, he gave some information on a student called Sithik" + list += "Ints, he may know more about what's happening here." + + if (stage >= 6) { + list += "Zavistic has given me some sort of potion, apparently I" + list += "need to give it to Sithik." + } else if (inventory.contains("zogre_ogre_trans_potion")) { + list += "Zavistic has given me some sort of potion, apparently I" + list += "need to give it to Sithik." + } else { + list += "Zavistic gave me some sort of potion, but I don't have it" + list += "on me anymore. Apparently I need to give some to Sithik." + } + } + + // ===== Stage 6+ — potion put in tea ===== + if (stage >= 6) { + if (get("thzfe_sithik_transformed", 0) >= 1) { + list += "I have put some of the potion into Sithik's tea, the potion" + list += "will take some time to act. Perhaps I should get out of here" + list += "in case there are any side effects?" + if (stage == 6) { + list += "Perhaps I should go and check on Sithik now?" + } + } else { + list += "I have put some of the potion into Sithik's tea, the potion" + list += "will take some time to act. Perhaps I should get out of here" + list += "in case there are any side effects?" + } + } + + // ===== Stage 8 — Sithik turned into ogre ===== + if (stage == 8) { + list += "I came back into Sithik's room to find that he had been" + list += "turned into an Ogre!" + list += "Sithik has told me that there is no way I can remove the" + list += "effects of the necromantic curse spell from the Jiggig" + list += "area. I'll have to go back and let Grish know." + + if (get("thzfe_makebrutalarrow", false)) { + list += "Sithik has told me how to make 'brutal arrows', which" + list += "should be more effective against Zogres." + } + + if (get("thzfe_makecuredisease", false)) { + list += "Sithik has given me some pointers on how I can make a" + list += "cure disease potion, though I'm still not sure exactly which" + list += "herbs I should use." + } + } + + // ===== Stage 10+ — ogres want artefacts ===== + if (stage >= 10) { + list += "I came back into Sithik's room to find that he had been" + list += "turned into an Ogre!" + list += "Sithik has told me how to make 'brutal arrows', which" + list += "should be more effective against Zogres." + list += "Sithik has given me some pointers on how I can make a" + list += "cure disease potion, though I'm still not sure exactly which" + list += "herbs I should use." + list += "I've told Grish to relocate the dance area, but he needs" + list += "me to get something from the tomb to so that he can do" + list += "this." + if (stage == 10) { + list += "I need to go back into the tomb and look for some 'old'" + list += "items that Grish has asked for." + } + } + + // ===== Stage 12+ — killed Slash Bash ===== + if (stage >= 12) { + list += "I've killed a monster called Slash Bash...it was a huge" + list += "Zogre!" + if (inventory.contains("ogre_artefact")) { + list += "Slash Bash was wearing some odd artefacts, I can only" + list += "assume that these were what Grish wanted." + list += "I have some artefacts which I recovered from a huge" + list += "Zogre called Slash Bash. I should return them to Grish." + } else { + list += "Slash Bash was wearing some odd artefacts, I can only" + list += "assume that these were what Grish wanted." + } + } + + return list + } +} diff --git a/game/src/main/kotlin/content/skill/firemaking/LightSource.kt b/game/src/main/kotlin/content/skill/firemaking/LightSource.kt index 315bdf0ceb..cec2bbc5d7 100644 --- a/game/src/main/kotlin/content/skill/firemaking/LightSource.kt +++ b/game/src/main/kotlin/content/skill/firemaking/LightSource.kt @@ -59,6 +59,10 @@ class LightSource : Script { } entered("*") { + if (it.tags.contains("dark")) { + open("level_one_darkness") + return@entered + } if (!it.tags.contains("darkness")) { return@entered } @@ -71,7 +75,9 @@ class LightSource : Script { } exited("*") { - if (it.tags.contains("darkness")) { + if (it.tags.contains("dark")) { + close("level_one_darkness") + } else if (it.tags.contains("darkness")) { close("level_one_darkness") close("level_three_darkness") } diff --git a/game/src/main/kotlin/content/skill/magic/book/lunar/MonsterExamine.kt b/game/src/main/kotlin/content/skill/magic/book/lunar/MonsterExamine.kt index 9b0b488aae..eba0fc824d 100644 --- a/game/src/main/kotlin/content/skill/magic/book/lunar/MonsterExamine.kt +++ b/game/src/main/kotlin/content/skill/magic/book/lunar/MonsterExamine.kt @@ -37,6 +37,7 @@ class MonsterExamine(val combatDefinitions: CombatDefinitions) : Script { sound("stat_spy") exp(Skill.Magic, Tables.int("spells.monster_examine.xp") / 10.0) open("monster_stat_spy") + clear("spell") val maxHit = maxHit(target) interfaces.sendText("monster_stat_spy", "name", target.def.name) interfaces.sendText("monster_stat_spy", "line1", "Combat level: ${target.def.combat}") @@ -59,7 +60,7 @@ class MonsterExamine(val combatDefinitions: CombatDefinitions) : Script { return defined } // No data defined; estimate with the same melee formula combat uses - val strengthBonus = npc.get("strength", 0) + 64 + val strengthBonus = npc["strength", 0] + 64 return 5 + (npc.levels.get(Skill.Strength) * strengthBonus) / 64 } } diff --git a/game/src/main/kotlin/content/skill/magic/book/lunar/StatSpy.kt b/game/src/main/kotlin/content/skill/magic/book/lunar/StatSpy.kt index 9f70f62e2e..03ef521338 100644 --- a/game/src/main/kotlin/content/skill/magic/book/lunar/StatSpy.kt +++ b/game/src/main/kotlin/content/skill/magic/book/lunar/StatSpy.kt @@ -25,7 +25,7 @@ class StatSpy : Script { if (hasClock("action_delay")) { return@onPlayerApproach } - if (!target.get("accept_aid", true)) { + if (!target["accept_aid", true]) { message("This player is not currently accepting aid.") return@onPlayerApproach } @@ -40,6 +40,7 @@ class StatSpy : Script { target.sound("stat_spy_impact") exp(Skill.Magic, Tables.int("spells.stat_spy.xp") / 10.0) open("player_stat_spy") + clear("spell") for (skill in Skill.all) { val name = name(skill) // Constitution is stored as lifepoints (x10); the interface shows levels diff --git a/game/src/main/kotlin/content/skill/summoning/pet/PetShopOwner.kt b/game/src/main/kotlin/content/skill/summoning/pet/PetShopOwner.kt index 616e096974..d83255eb4d 100644 --- a/game/src/main/kotlin/content/skill/summoning/pet/PetShopOwner.kt +++ b/game/src/main/kotlin/content/skill/summoning/pet/PetShopOwner.kt @@ -114,9 +114,7 @@ class PetShopOwner : Script { player("Isn't that a little steep?") npc( owner.id, - "Well, if we gave them away for free then people would just buy them and dump them without a care. " + - "Dogs are a big responsibility and should be cared for. If a person is unwilling to invest $PUPPY_PRICE coins, " + - "then they don't deserve to have the puppy in the first place. So, do you still want one?", + "Well, if we gave them away for free then people would just buy them and dump them without a care. Dogs are a big responsibility and should be cared for. If a person is unwilling to invest $PUPPY_PRICE coins, then they don't deserve to have the puppy in the first place. So, do you still want one?", ) choice { option("Okay, I'll take the ${breed.string("option")}.") { @@ -150,35 +148,29 @@ class PetShopOwner : Script { player("Such as?") npc( owner.id, - "Well, we sell nuts. Those can be used to feed squirrels. If you want to capture a squirrel, you'll need to use the nuts " + - "on the trap you set, as the little scamps won't be fooled by anything else.", + "Well, we sell nuts. Those can be used to feed squirrels. If you want to capture a squirrel, you'll need to use the nuts on the trap you set, as the little scamps won't be fooled by anything else.", ) player("I'll bear that in mind!") npc( owner.id, - "There are also a number of birds that live in the woodlands of the world. If you can find their eggs then you can use " + - "the incubator over there to hatch it. So long as you are the first thing they see out of the shell, they will follow " + - "you anywhere. After that, you just need to feed the chick ground fishing bait until it's old enough to eat it solid.", + "There are also a number of birds that live in the woodlands of the world. If you can find their eggs then you can use the incubator over there to hatch it. So long as you are the first thing they see out of the shell, they will follow you anywhere. After that, you just need to feed the chick ground fishing bait until it's old enough to eat it solid.", ) player("I'll make sure to keep an eye on them if I go anywhere dangerous.") npc( owner.id, - "There are also a number of fabulous and exotic lizards in Karamja. Some can be caught easily in a box trap, while others " + - "will need to be raised from an egg.", + "There are also a number of fabulous and exotic lizards in Karamja. Some can be caught easily in a box trap, while others will need to be raised from an egg.", ) player("Will the incubator work for them, too?") npc(owner.id, "Of course! I'll keep an eye on all the eggs you put in there, so they will never end up hard-boiled.") player("Thank goodness!") npc( owner.id, - "The geckos of Karamja are quite easy to trap, like raccoons. Both will investigate a trap happily without any special bait. " + - "Monkeys are a different story, however!", + "The geckos of Karamja are quite easy to trap, like raccoons. Both will investigate a trap happily without any special bait. Monkeys are a different story, however!", ) player("What do you mean?") npc( owner.id, - "Well, they are clever little things and can easily get out of a box trap, unless they are stuck. The easiest way to do that " + - "is to put a banana into the workings. They will hang on tight, and never let go, even when the trap closes!", + "Well, they are clever little things and can easily get out of a box trap, unless they are stuck. The easiest way to do that is to put a banana into the workings. They will hang on tight, and never let go, even when the trap closes!", ) player("Thanks a lot, you've been very helpful!") npc(owner.id, "It's always a pleasure to help a fellow animal-lover. Come back and visit soon.") diff --git a/game/src/test/kotlin/InstructionCalls.kt b/game/src/test/kotlin/InstructionCalls.kt index 82d03c826e..858c59c422 100644 --- a/game/src/test/kotlin/InstructionCalls.kt +++ b/game/src/test/kotlin/InstructionCalls.kt @@ -261,6 +261,11 @@ fun Player.itemOnNpc(npc: NPC, itemSlot: Int, inventory: String = "inventory") { interactItemOn(npc, inventory, inventory, item, itemSlot) } +fun Player.itemOnFloorItem(floorItem: FloorItem, itemSlot: Int, inventory: String = "inventory") { + val item = inventories.inventory(inventory)[itemSlot] + interactItemOn(floorItem, inventory, inventory, item, itemSlot) +} + fun Player.itemOnItem( firstSlot: Int, secondSlot: Int, diff --git a/game/src/test/kotlin/content/entity/effect/toxin/DiseaseTest.kt b/game/src/test/kotlin/content/entity/effect/toxin/DiseaseTest.kt new file mode 100644 index 0000000000..3bb1273a1f --- /dev/null +++ b/game/src/test/kotlin/content/entity/effect/toxin/DiseaseTest.kt @@ -0,0 +1,91 @@ +package content.entity.effect.toxin + +import FakeRandom +import WorldTest +import containsMessage +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.type.setRandom +import java.util.concurrent.TimeUnit + +class DiseaseTest : WorldTest() { + + @Test + fun `Disease fades over time`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = 0 + }) + val player = createPlayer() + player.levels.set(Skill.Constitution, 990) + player.levels.set(Skill.Attack, 3) + player.disease(player, 3) + assertTrue(player.containsMessage("You have been diseased")) + assertEquals(3, player.diseaseDamage) + assertTrue(player.timers.contains("disease")) + assertTrue(player.diseased) + tick(30) + assertEquals(1, player.levels.get(Skill.Attack)) + assertEquals(2, player.diseaseDamage) + tick(30) + assertEquals(970, player.levels.get(Skill.Constitution)) + assertEquals(1, player.diseaseDamage) + tick(30) + assertEquals(0, player.diseaseDamage) + assertFalse(player.diseased) + assertFalse(player.timers.contains("disease")) + } + + @Test + fun `Anti-disease fades over time`() { + val player = createPlayer() + player.antiDisease(36, TimeUnit.SECONDS) + assertEquals(-2, player.diseaseDamage) + assertTrue(player.timers.contains("disease")) + assertTrue(player.antiDisease) + tick(30) + assertEquals(100, player.levels.get(Skill.Constitution)) + assertEquals(-1, player.diseaseDamage) + assertTrue(player.containsMessage("Your disease resistance is about to wear off")) + tick(30) + assertEquals(100, player.levels.get(Skill.Constitution)) + assertEquals(0, player.diseaseDamage) + assertFalse(player.antiDisease) + assertFalse(player.timers.contains("disease")) + assertTrue(player.containsMessage("Your disease resistance has worn off")) + } + + @Test + fun `Can't re-disease target with lower damage`() { + val player = createPlayer() + player.levels.set(Skill.Constitution, 990) + player.disease(player, 10) + assertEquals(10, player.diseaseDamage) + assertTrue(player.timers.contains("disease")) + tick(30) + player.disease(player, 8) + assertEquals(9, player.diseaseDamage) + } + + @Test + fun `Disease resets with higher damage`() { + val player = createPlayer() + player.levels.set(Skill.Constitution, 990) + player.disease(player, 10) + assertEquals(10, player.diseaseDamage) + assertTrue(player.timers.contains("disease")) + tick(30) + player.disease(player, 11) + assertEquals(11, player.diseaseDamage) + } + + @Test + fun `Can't disease target with immunity`() { + val player = createPlayer() + player.antiDisease(1) + player.disease(player, 10) + assertEquals(-3, player.diseaseDamage) + assertFalse(player.diseased) + assertTrue(player.antiDisease) + } +} diff --git a/game/src/test/kotlin/content/quest/member/zogre_flesh_eaters/ZogreFleshEatersTest.kt b/game/src/test/kotlin/content/quest/member/zogre_flesh_eaters/ZogreFleshEatersTest.kt new file mode 100644 index 0000000000..448ff8c82b --- /dev/null +++ b/game/src/test/kotlin/content/quest/member/zogre_flesh_eaters/ZogreFleshEatersTest.kt @@ -0,0 +1,251 @@ +package content.quest.member.zogre_flesh_eaters + +import WorldTest +import content.quest.quest +import dialogueOption +import itemOnFloorItem +import itemOnNpc +import itemOnObject +import itemOption +import npcOption +import objectOption +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertNotNull +import skipDialogues +import world.gregs.voidps.engine.client.instruction.handle.interactObject +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.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.level.Level +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.setRandom +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ZogreFleshEatersTest : WorldTest() { + override var loadNpcs: Boolean = true + + @Test + fun `Complete the test`() { + val player = createPlayer(Tile(2445, 3052)) + player.levels.set(Skill.Ranged, 30) + player.experience.set(Skill.Ranged, Level.experience(30)) + player["chompy_birds"] = 65 + player["jungle_potion"] = "completed" + val grish = NPCs.findBySpawn(Tile(2443, 3051), "grish") + + player.npcOption(grish, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(2) // Sickies? + player.skipDialogues() + player.dialogueOption(4) // Can I help? + player.skipDialogues() + player.dialogueOption(2) // Ok + player.skipDialogues() + player.dialogueOption(1) // Really sure + player.skipDialogues() + tick(4) + + assertEquals("investigate", player.quest("zogre_flesh_eaters")) + assertEquals(3, player.inventory.count("cooked_chompy")) + assertEquals(2, player.inventory.count("super_restore_3")) + + val guard = NPCs.findBySpawn(Tile(2454, 3047), "zogre_ogre_guard") + player.tele(2453, 3048) + player.npcOption(guard, "Talk-to") + tick(1) + player.skipDialogues() + tick(6) + assertEquals("barricade", player.quest("zogre_flesh_eaters")) + + val barricade = GameObjects.find(Tile(2456, 3049), "ogre_barricade_collapsed") + player.objectOption(barricade, "Climb-over") + tick(6) + assertEquals(Tile(2457, 3049), player.tile) + + player.tele(2443, 9460, 2) + val lecturn = GameObjects.find(Tile(2443, 9459, 2), "zogre_lecturn") + player.objectOption(lecturn, "Search") + tick(3) + assertTrue(player.inventory.contains("torn_page")) + + player.tele(2442, 9459, 2) + player["insta_kill"] = true + player["auto_retaliate"] = true + val skeleton = GameObjects.find(Tile(2442, 9459, 2), "zogre_brentle_skeleton") + player.objectOption(skeleton, "Search") + tick(15) + + assertTrue(FloorItems.at(player.tile.zone).any { list -> list.any { item -> item.id == "ruined_backpack" } }) + player.inventory.add("ruined_backpack") + + player.itemOption("Open", "ruined_backpack") + tick(1) + player.skipDialogues() + assertTrue(player.inventory.contains("dragon_inn_tankard")) + assertTrue(player.inventory.contains("rotten_food")) + assertTrue(player.inventory.contains("knife")) + + player.tele(2440, 9459, 2) + val coffin = GameObjects.find(Tile(2438, 9458, 2), "zogre_coffin_base") + player.interactObject(coffin, "Search") + tick(1) + player.skipDialogues() + player.itemOnObject(coffin, player.inventory.indexOf("knife")) + tick(4) + player.interactObject(coffin, "Search") + tick(1) + player.dialogueOption("continue") + setRandom(object : Random() { + override fun nextBits(bitCount: Int) = 0 + }) + tick(8) + player.skipDialogues() + tick(2) + player.interactObject(coffin, "Search") + tick(4) + assertTrue(player.inventory.contains("black_prism")) + assertEquals(3, player["thzfe_prismsearch", 0]) + + player.tele(2556, 3079, 0) + val bartender = NPCs.findBySpawn(Tile(2556, 3078), "bartender_dragon_inn") + player.itemOnNpc(bartender, player.inventory.indexOf("dragon_inn_tankard")) + tick(1) + player.skipDialogues() + assertTrue(player["thzfe_showntankard", false]) + + player.tele(2598, 3086) + val bell = GameObjects.find(Tile(2598, 3085), "zogre_outdoor_bell") + player.objectOption(bell, "Ring") + tick(1) + player.skipDialogues() + assertEquals(4, player["thzfe_prismsearch", 0]) + + player.tele(2590, 3104, 1) + val sithik = GameObjects.find(Tile(2591, 3103, 1), "zogre_sithik_bed") + player.objectOption(sithik, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(1) + player.skipDialogues() + player.objectOption(sithik, "Talk-to") + tick(1) + player.dialogueOption(2) + player.skipDialogues() + assertEquals(5, player["thzfe_prismsearch", 0]) + + player.tele(2590, 3104, 1) + val wardrobe = GameObjects.find(Tile(2590, 3103, 1), "sithiks_wardrobe") + player.objectOption(wardrobe, "Search") + tick(1) + assertTrue(player.inventory.contains("book_of_ham")) + + player.tele(2593, 3105, 1) + val cupboard = GameObjects.find(Tile(2594, 3104, 1), "sithiks_cupboard") + player.objectOption(cupboard, "Search") + tick(1) + assertTrue(player.inventory.contains("necromancy_book")) + + player.tele(2593, 3103, 1) + val drawers = GameObjects.find(Tile(2594, 3103, 1), "sithiks_drawers") + player.objectOption(drawers, "Search") + tick(1) + player.skipDialogues() + assertTrue(player.inventory.contains("charcoal")) + assertTrue(player.inventory.contains("papyrus")) + assertTrue(player.inventory.contains("book_of_portraiture")) + + player.itemOnObject(sithik, player.inventory.indexOf("papyrus")) + tick(1) + player.skipDialogues() + tick(2) + assertTrue(player.inventory.contains("zogre_sithik_portrait_good")) + + player.tele(2556, 3079, 0) + player.itemOnNpc(bartender, player.inventory.indexOf("zogre_sithik_portrait_good")) + tick(1) + player.skipDialogues() + assertTrue(player["thzfe_innkeeperportraitshown", false]) + + player.tele(2588, 3090, 1) + val rarve = NPCs.findBySpawn(Tile(2588, 3091, 1), "zavistic_rarve") + player.npcOption(rarve, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(3) + player.skipDialogues() + assertEquals("sithik", player.quest("zogre_flesh_eaters")) + assertTrue(player.inventory.contains("zogre_ogre_trans_potion")) + assertFalse(player.inventory.contains("book_of_ham")) + assertFalse(player.inventory.contains("necromancy_book")) + assertFalse(player.inventory.contains("zogre_sithik_portrait_signed")) + + player.tele(2593, 3103, 1) + val floorItem = FloorItems.add(Tile(2594, 3103, 1), "cup_of_tea_zogre_flesh_eaters") + player.itemOnFloorItem(floorItem, player.inventory.indexOf("zogre_ogre_trans_potion")) + tick(3) + player.skipDialogues() + assertEquals("potion", player.quest("zogre_flesh_eaters")) + + player.tele(2597, 3108, 0) + val ladder = GameObjects.find(Tile(2597, 3107), "basic_ladder_bottom") + player.objectOption(ladder, "Climb-up") + tick(2) + assertEquals(1, player["thzfe_sithik_transformed", 0]) + + player.tele(2593, 3103, 1) + player.objectOption(sithik, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(1) + player.skipDialogues() + assertEquals("permanent_spell", player.quest("zogre_flesh_eaters")) + player.dialogueOption(2) + player.skipDialogues() + assertTrue(player["thzfe_makebrutalarrow", false]) + player.dialogueOption(3) + player.skipDialogues() + assertTrue(player["thzfe_makecuredisease", false]) + + player.tele(2445, 3052, 0) + player.npcOption(grish, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(1) + player.skipDialogues() + assertTrue(player.inventory.contains("ogre_gate_key")) + assertEquals("given_key", player.quest("zogre_flesh_eaters")) + + player.tele(2482, 9445) + val stand = GameObjects.find(Tile(2483, 9445), "zogre_stand") + player.objectOption(stand, "Search") + tick(1) + player.skipDialogues() + tick(15) + + assertEquals("killed_slash_bash", player.quest("zogre_flesh_eaters")) + val artifact = FloorItems.firstOrNull(Tile(2477, 9444), "ogre_artefact") + assertNotNull(artifact) + + player.objectOption(stand, "Search") + tick(4) + player.skipDialogues() + assertTrue(player.inventory.contains("ogre_artefact")) + + player.tele(2445, 3052, 0) + player.npcOption(grish, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(1) + player.skipDialogues() + tick(1) + assertEquals("completed", player.quest("zogre_flesh_eaters")) + } +}