diff --git a/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npc-spawns.toml b/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npc-spawns.toml deleted file mode 100644 index dc348a43a9..0000000000 --- a/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npc-spawns.toml +++ /dev/null @@ -1,39 +0,0 @@ -spawns = [ - { id = "flying_book", x = 3375, y = 9640, level = 2, members = true }, - { id = "flying_book", x = 3335, y = 9678, members = true }, - { id = "flying_book", x = 3351, y = 9720, members = true }, - { id = "flying_book", x = 3337, y = 9720, level = 1, members = true }, - { id = "flying_book_2", x = 3354, y = 9636, level = 2, members = true }, - { id = "flying_book_2", x = 3339, y = 9692, level = 1, members = true }, - { id = "flying_book_2", x = 3349, y = 9721, level = 2, members = true }, - { id = "telekinetic_guardian", x = 3333, y = 9719, members = true }, - { id = "telekinetic_guardian", x = 3347, y = 9687, members = true }, - { id = "telekinetic_guardian", x = 3370, y = 9697, members = true }, - { id = "telekinetic_guardian", x = 3378, y = 9718, members = true }, - { id = "telekinetic_guardian", x = 3353, y = 9712, level = 1, members = true }, - { id = "telekinetic_guardian", x = 3361, y = 9690, level = 1, members = true }, - { id = "telekinetic_guardian", x = 3380, y = 9690, level = 1, members = true }, - { id = "telekinetic_guardian", x = 3387, y = 9710, level = 1, members = true }, - { id = "alchemy_guardian", x = 3364, y = 9626, level = 2, members = true }, - { id = "enchantment_guardian", x = 3364, y = 9647, members = true }, - { id = "graveyard_guardian", x = 3362, y = 9644, level = 1, members = true }, - { id = "maze_guardian", x = 3354, y = 9679, level = 2, members = true }, - { id = "maze_guardian", x = 3357, y = 9707, level = 2, members = true }, - { id = "charmed_warrior", x = 3382, y = 9631 }, - { id = "charmed_warrior", x = 3362, y = 9650, level = 2 }, - { id = "charmed_warrior", x = 3349, y = 9701 }, - { id = "charmed_warrior", x = 3343, y = 9688, level = 2 }, - { id = "charmed_warrior", x = 3367, y = 9713, level = 2 }, - { id = "charmed_warrior_2", x = 3382, y = 9655 }, - { id = "charmed_warrior_2", x = 3365, y = 9669 }, - { id = "charmed_warrior_2", x = 3339, y = 9675, level = 1 }, - { id = "charmed_warrior_3", x = 3350, y = 9652 }, - { id = "charmed_warrior_3", x = 3367, y = 9650, level = 2 }, - { id = "charmed_warrior_3", x = 3383, y = 9669 }, - { id = "charmed_warrior_3", x = 3387, y = 9722, level = 1 }, - { id = "charmed_warrior_3", x = 3363, y = 9683, level = 2 }, - { id = "charmed_warrior_4", x = 3347, y = 9623, members = true }, - { id = "charmed_warrior_4", x = 3362, y = 9709, members = true }, - { id = "charmed_warrior_4", x = 3374, y = 9699, level = 1, members = true }, - { id = "sweeper", x = 3364, y = 9638, level = 2 }, -] diff --git a/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npcs.toml b/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npcs.toml deleted file mode 100644 index a4f9bee611..0000000000 --- a/data/area/kharidian_desert/al_kharid/mage_training_arena/mage_training_arena.npcs.toml +++ /dev/null @@ -1,23 +0,0 @@ -[telekinetic_guardian] -id = 3098 -examine = "A guardian of the arena." - -[alchemy_guardian] -id = 3099 -examine = "A guardian of the arena." - -[enchantment_guardian] -id = 3100 -examine = "A guardian of the arena." - -[graveyard_guardian] -id = 3101 -examine = "A guardian of the arena." - -[maze_guardian] -id = 3102 -examine = "A guardian of the arena." - -[sweeper] -id = 3298 -examine = "Keeps this magic area tidy." diff --git a/data/minigame/mage_training_arena/mage_training_arena.anims.toml b/data/minigame/mage_training_arena/mage_training_arena.anims.toml new file mode 100644 index 0000000000..17d74cb20e --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.anims.toml @@ -0,0 +1,2 @@ +[mta_search_cupboard] +id = 3032 diff --git a/data/minigame/mage_training_arena/mage_training_arena.areas.toml b/data/minigame/mage_training_arena/mage_training_arena.areas.toml index d044565e15..4dc517fcfd 100644 --- a/data/minigame/mage_training_arena/mage_training_arena.areas.toml +++ b/data/minigame/mage_training_arena/mage_training_arena.areas.toml @@ -3,3 +3,30 @@ x = [3392, 3417] y = [3264, 3334] tags = ["penguin_area"] hint = "to the east of the Mage Training Arena." + +[mage_training_arena_lobby] +x = [3328, 3391] +y = [3264, 3327] + +[mage_training_arena_telekinetic_theatre] +x = [3328, 3391] +y = [9664, 9727] +tags = ["mage_training_arena", "no_random_events", "no_trading"] + +[mage_training_arena_enchanting_chamber] +x = [3335, 3389] +y = [9612, 9663] +level = 0 +tags = ["mage_training_arena", "no_random_events", "no_trading"] + +[mage_training_arena_creature_graveyard] +x = [3333, 3390] +y = [9610, 9663] +level = 1 +tags = ["mage_training_arena", "no_random_events", "no_trading"] + +[mage_training_arena_alchemists_playground] +x = [3341, 3391] +y = [9618, 9654] +level = 2 +tags = ["mage_training_arena", "no_random_events", "no_trading"] diff --git a/data/minigame/mage_training_arena/mage_training_arena.gfx.toml b/data/minigame/mage_training_arena/mage_training_arena.gfx.toml new file mode 100644 index 0000000000..290bbaa59d --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.gfx.toml @@ -0,0 +1,6 @@ +[mta_falling_bones] +id = 520 + +[mta_enchant_orb] +id = 237 +height = 110 diff --git a/data/minigame/mage_training_arena/mage_training_arena.ifaces.toml b/data/minigame/mage_training_arena/mage_training_arena.ifaces.toml new file mode 100644 index 0000000000..e774a5938c --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.ifaces.toml @@ -0,0 +1,84 @@ +[mage_training_arena_alchemist] +id = 194 +type = "game_screen_overlay" + +[.points] +id = 3 + +[.value_leather_boots_mage_training_arena] +id = 9 + +[.value_adamant_kiteshield_mage_training_arena] +id = 10 + +[.value_adamant_helm_mage_training_arena] +id = 11 + +[.value_emerald_mage_training_arena] +id = 12 + +[.value_rune_longsword_mage_training_arena] +id = 13 + +[.free_leather_boots_mage_training_arena] +id = 14 + +[.free_adamant_kiteshield_mage_training_arena] +id = 15 + +[.free_adamant_helm_mage_training_arena] +id = 16 + +[.free_emerald_mage_training_arena] +id = 17 + +[.free_rune_longsword_mage_training_arena] +id = 18 + +[mage_training_arena_enchanting] +id = 195 +type = "game_screen_overlay" + +[.bonus_cube] +id = 1 + +[.bonus_cylinder] +id = 2 + +[.bonus_icosahedron] +id = 3 + +[.bonus_pentamid] +id = 4 + +[.points] +id = 9 + +[mage_training_arena_graveyard] +id = 196 +type = "game_screen_overlay" + +[.points] +id = 12 + +[mage_training_arena_rewards] +id = 197 +type = "main_screen" + +[.close] +id = 15 + +[.stock] +id = 16 +inventory = "mage_training_arena" +options = { Value = 0, Buy = 1, Examine = 9 } + +[mage_training_arena_telekinetic] +id = 198 +type = "game_screen_overlay" + +[.points] +id = 4 + +[.solved] +id = 7 diff --git a/data/minigame/mage_training_arena/mage_training_arena.npc-spawns.toml b/data/minigame/mage_training_arena/mage_training_arena.npc-spawns.toml index d40768171b..b2ea5b6a6a 100644 --- a/data/minigame/mage_training_arena/mage_training_arena.npc-spawns.toml +++ b/data/minigame/mage_training_arena/mage_training_arena.npc-spawns.toml @@ -9,4 +9,31 @@ spawns = [ { id = "charmed_warrior_3", x = 3363, y = 3307, level = 1 }, { id = "charmed_warrior_4", x = 3364, y = 3311, members = true }, { id = "wizard_shug", x = 3365, y = 3321, level = 1, members = true }, + { id = "flying_book", x = 3375, y = 9640, level = 2, members = true }, + { id = "flying_book", x = 3335, y = 9678, members = true }, + { id = "flying_book", x = 3351, y = 9720, members = true }, + { id = "flying_book", x = 3337, y = 9720, level = 1, members = true }, + { id = "flying_book_2", x = 3354, y = 9636, level = 2, members = true }, + { id = "flying_book_2", x = 3339, y = 9692, level = 1, members = true }, + { id = "flying_book_2", x = 3349, y = 9721, level = 2, members = true }, + { id = "alchemy_guardian", x = 3364, y = 9626, level = 2, members = true }, + { id = "enchantment_guardian", x = 3364, y = 9647, members = true }, + { id = "graveyard_guardian", x = 3362, y = 9644, level = 1, members = true }, + { id = "charmed_warrior", x = 3382, y = 9631 }, + { id = "charmed_warrior", x = 3362, y = 9650, level = 2 }, + { id = "charmed_warrior", x = 3349, y = 9701 }, + { id = "charmed_warrior", x = 3343, y = 9688, level = 2 }, + { id = "charmed_warrior", x = 3367, y = 9713, level = 2 }, + { id = "charmed_warrior_2", x = 3382, y = 9655 }, + { id = "charmed_warrior_2", x = 3365, y = 9669 }, + { id = "charmed_warrior_2", x = 3339, y = 9675, level = 1 }, + { id = "charmed_warrior_3", x = 3350, y = 9652 }, + { id = "charmed_warrior_3", x = 3367, y = 9650, level = 2 }, + { id = "charmed_warrior_3", x = 3383, y = 9669 }, + { id = "charmed_warrior_3", x = 3387, y = 9722, level = 1 }, + { id = "charmed_warrior_3", x = 3363, y = 9683, level = 2 }, + { id = "charmed_warrior_4", x = 3347, y = 9623, members = true }, + { id = "charmed_warrior_4", x = 3362, y = 9709, members = true }, + { id = "charmed_warrior_4", x = 3374, y = 9699, level = 1, members = true }, + { id = "sweeper", x = 3364, y = 9638, level = 2 }, ] diff --git a/data/minigame/mage_training_arena/mage_training_arena.npcs.toml b/data/minigame/mage_training_arena/mage_training_arena.npcs.toml index 66cc7a29af..8486653d29 100644 --- a/data/minigame/mage_training_arena/mage_training_arena.npcs.toml +++ b/data/minigame/mage_training_arena/mage_training_arena.npcs.toml @@ -31,3 +31,30 @@ id = 3107 [wizard_shug] id = 5027 examine = "A tired old wizard." + +[pizazz_hat] +id = 3096 + +[telekinetic_guardian] +id = 3098 +examine = "A guardian of the arena." + +[alchemy_guardian] +id = 3099 +examine = "A guardian of the arena." + +[enchantment_guardian] +id = 3100 +examine = "A guardian of the arena." + +[graveyard_guardian] +id = 3101 +examine = "A guardian of the arena." + +[maze_guardian] +id = 3102 +examine = "A guardian of the arena." + +[sweeper] +id = 3298 +examine = "Keeps this magic area tidy." diff --git a/data/minigame/mage_training_arena/mage_training_arena.objs.toml b/data/minigame/mage_training_arena/mage_training_arena.objs.toml new file mode 100644 index 0000000000..aff2bee3cd --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.objs.toml @@ -0,0 +1,125 @@ +[doorway_mage_training_arena] +id = 10721 + +[telekinetic_portal] +id = 10778 + +[enchanters_portal] +id = 10779 + +[alchemists_portal] +id = 10780 + +[graveyard_portal] +id = 10781 + +[exit_portal_mage_training_arena] +id = 10782 + +[bones_mage_training_arena_1] +id = 10725 +examine = "Various animals' bones." + +[bones_mage_training_arena_2] +id = 10726 +examine = "Various animals' bones." + +[bones_mage_training_arena_3] +id = 10727 +examine = "Various animals' bones." + +[bones_mage_training_arena_4] +id = 10728 +examine = "Various animals' bones." + +[coin_collector] +id = 10734 +examine = "Deposit your coins here." + +[food_chute] +id = 10735 +examine = "Put bananas or peaches in here." + +[cupboard_mage_training_arena_1] +id = 10797 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_1_open] +id = 10798 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_2] +id = 10795 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_2_open] +id = 10796 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_3] +id = 10793 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_3_open] +id = 10794 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_4] +id = 10791 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_4_open] +id = 10792 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_5] +id = 10789 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_5_open] +id = 10790 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_6] +id = 10787 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_6_open] +id = 10788 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_7] +id = 10785 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_7_open] +id = 10786 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_8] +id = 10783 +examine = "This may be worth opening." + +[cupboard_mage_training_arena_8_open] +id = 10784 +examine = "This may be worth opening." + +[cube_pile] +id = 10799 +examine = "Some simple cubes." + +[cylinder_pile] +id = 10800 +examine = "A pile of cylindrical shapes." + +[icosahedron_pile] +id = 10801 +examine = "A pile of Icosahedrons." + +[pentamid_pile] +id = 10802 +examine = "A pile of pentamids." + +[orb_hole] +id = 10803 +examine = "Put the enchanted orbs in here." diff --git a/data/minigame/mage_training_arena/mage_training_arena.sounds.toml b/data/minigame/mage_training_arena/mage_training_arena.sounds.toml new file mode 100644 index 0000000000..881e4c266b --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.sounds.toml @@ -0,0 +1,17 @@ +[mta_barrier] +id = 1657 + +[mta_deposit_bone] +id = 1660 + +[mta_bone_fall] +id = 1661 + +[mta_deposit_fruit] +id = 1663 + +[mta_deposit_orb] +id = 1664 + +[mta_move_statue] +id = 1666 diff --git a/data/minigame/mage_training_arena/mage_training_arena.tables.toml b/data/minigame/mage_training_arena/mage_training_arena.tables.toml new file mode 100644 index 0000000000..b23f1af717 --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.tables.toml @@ -0,0 +1,556 @@ +[mta_rooms] +portal = "obj" +level = "int" +enter = "tile" +exit = "tile" +overlay = "string" +area = "string" +name = "string" +spell = "string" + +[.telekinetic] +portal = "telekinetic_portal" +level = 33 +enter = { x = 3363, y = 3316 } +exit = { x = 3363, y = 3316 } +overlay = "mage_training_arena_telekinetic" +area = "mage_training_arena_telekinetic_theatre" +name = "Telekinetic Theatre" +spell = "Telekinetic Grab" + +[.alchemist] +portal = "alchemists_portal" +level = 21 +enter = { x = 3366, y = 9623, level = 2 } +exit = { x = 3363, y = 3320 } +overlay = "mage_training_arena_alchemist" +area = "mage_training_arena_alchemists_playground" +name = "Alchemists' Playground" +spell = "Low Alchemy" + +[.enchanting] +portal = "enchanters_portal" +level = 7 +enter = { x = 3363, y = 9649 } +exit = { x = 3361, y = 3318 } +overlay = "mage_training_arena_enchanting" +area = "mage_training_arena_enchanting_chamber" +name = "Enchanting Chamber" +spell = "Lvl-1 Enchant" + +[.graveyard] +portal = "graveyard_portal" +level = 15 +enter = { x = 3363, y = 9639, level = 1 } +exit = { x = 3365, y = 3318 } +overlay = "mage_training_arena_graveyard" +area = "mage_training_arena_creature_graveyard" +name = "Creature Graveyard" +spell = "Bones to Bananas" + +# Tiles are offsets from the south-west corner of region 13463 (3328, 9664). +# north/south are local y thresholds, west/east local x thresholds used to pick the statue direction. +[mta_mazes] +index = "int" +base = "tile" +statue = "tile" +guardian = "tile" +end = "tile" +camera = "tile" +north = "int" +south = "int" +west = "int" +east = "int" + +[.maze_1] +index = 0 +base = { x = 8, y = 54 } +statue = { x = 15, y = 41 } +guardian = { x = 8, y = 39 } +end = { x = 19, y = 50 } +camera = { x = 15, y = 45 } +north = 51 +south = 40 +west = 9 +east = 20 + +[.maze_2] +index = 1 +base = { x = 34, y = 49, level = 1 } +statue = { x = 22, y = 53, level = 1 } +guardian = { x = 26, y = 48, level = 1 } +end = { x = 13, y = 44, level = 1 } +camera = { x = 17, y = 48, level = 1 } +north = 54 +south = 43 +west = 12 +east = 23 + +[.maze_3] +index = 2 +base = { x = 54, y = 34, level = 1 } +statue = { x = 48, y = 22, level = 1 } +guardian = { x = 45, y = 15, level = 1 } +end = { x = 57, y = 22, level = 1 } +camera = { x = 53, y = 17, level = 1 } +north = 23 +south = 12 +west = 47 +east = 58 + +[.maze_4] +index = 3 +base = { x = 50, y = 42, level = 1 } +statue = { x = 46, y = 49, level = 1 } +guardian = { x = 57, y = 41, level = 1 } +end = { x = 55, y = 49, level = 1 } +camera = { x = 50, y = 53, level = 1 } +north = 59 +south = 48 +west = 45 +east = 56 + +[.maze_5] +index = 4 +base = { x = 46, y = 32 } +statue = { x = 45, y = 14 } +guardian = { x = 45, y = 23 } +end = { x = 47, y = 18 } +camera = { x = 45, y = 13 } +north = 19 +south = 8 +west = 40 +east = 51 + +[.maze_6] +index = 5 +base = { x = 26, y = 26 } +statue = { x = 15, y = 16 } +guardian = { x = 23, y = 24 } +end = { x = 14, y = 20 } +camera = { x = 14, y = 15 } +north = 21 +south = 10 +west = 9 +east = 10 + +[.maze_7] +index = 6 +base = { x = 51, y = 52 } +statue = { x = 40, y = 48 } +guardian = { x = 55, y = 50 } +end = { x = 39, y = 56 } +camera = { x = 43, y = 51 } +north = 57 +south = 46 +west = 37 +east = 48 + +[.maze_8] +index = 7 +base = { x = 31, y = 37, level = 2 } +statue = { x = 18, y = 54, level = 2 } +guardian = { x = 28, y = 41, level = 2 } +end = { x = 17, y = 54, level = 2 } +camera = { x = 19, y = 49, level = 2 } +north = 55 +south = 44 +west = 14 +east = 25 + +[.maze_9] +index = 8 +base = { x = 40, y = 16, level = 2 } +statue = { x = 20, y = 10, level = 2 } +guardian = { x = 34, y = 15, level = 2 } +end = { x = 11, y = 19, level = 2 } +camera = { x = 16, y = 14, level = 2 } +north = 20 +south = 9 +west = 10 +east = 21 + +[.maze_10] +index = 9 +base = { x = 27, y = 29, level = 1 } +statue = { x = 23, y = 20, level = 1 } +guardian = { x = 31, y = 25, level = 1 } +end = { x = 25, y = 16, level = 1 } +camera = { x = 23, y = 20, level = 1 } +north = 26 +south = 15 +west = 17 +east = 28 + +# Cupboards ring clockwise from the entrance: slot 0-7, only slots 0-4 hold an item after rotation. +[mta_alchemist_items] +item = "item" +slot = "int" +name = "string" + +[.leather_boots] +item = "leather_boots_mage_training_arena" +slot = 0 +name = "leather boots" + +[.adamant_kiteshield] +item = "adamant_kiteshield_mage_training_arena" +slot = 1 +name = "adamant kiteshield" + +[.adamant_helm] +item = "adamant_helm_mage_training_arena" +slot = 2 +name = "adamant helm" + +[.emerald] +item = "emerald_mage_training_arena" +slot = 3 +name = "emerald" + +[.rune_longsword] +item = "rune_longsword_mage_training_arena" +slot = 4 +name = "rune longsword" + +[mta_alchemist] +values = "list" +interval = "int" +reward_cap = "int" +eject_amount = "int" + +[.settings] +values = [1, 5, 8, 15, 30] +interval = 70 +reward_cap = 1000 +eject_amount = 12000 + +[mta_shapes] +pile = "obj" +item = "item" + +[.cube] +pile = "cube_pile" +item = "cube" + +[.cylinder] +pile = "cylinder_pile" +item = "cylinder" + +[.icosahedron] +pile = "icosahedron_pile" +item = "icosahedron" + +[.pentamid] +pile = "pentamid_pile" +item = "pentamid" + +[mta_enchanting] +dragonstones = "list" +bonus_interval = "int" +dragonstone_respawn = "int" +orbs_per_reward = "int" +rune_rewards = "list" +rune_amount = "int" +shapes_per_reward = "int" + +[.settings] +dragonstones = [ + { x = 3354, y = 9645 }, + { x = 3353, y = 9635 }, + { x = 3359, y = 9632 }, + { x = 3375, y = 9633 }, + { x = 3374, y = 9643 }, + { x = 3373, y = 9651 }, +] +bonus_interval = 40 +dragonstone_respawn = 500 +orbs_per_reward = 20 +rune_rewards = ["death_rune", "blood_rune", "cosmic_rune"] +rune_amount = 3 +shapes_per_reward = 10 + +[mta_bones] +pile = "obj" +item = "item" +multiplier = "int" +next = "obj" + +[.bones_1] +pile = "bones_mage_training_arena_1" +item = "animals_bones_1" +multiplier = 1 +next = "bones_mage_training_arena_2" + +[.bones_2] +pile = "bones_mage_training_arena_2" +item = "animals_bones_2" +multiplier = 2 +next = "bones_mage_training_arena_3" + +[.bones_3] +pile = "bones_mage_training_arena_3" +item = "animals_bones_3" +multiplier = 3 +next = "bones_mage_training_arena_4" + +[.bones_4] +pile = "bones_mage_training_arena_4" +item = "animals_bones_4" +multiplier = 4 +next = "bones_mage_training_arena_1" + +[mta_graveyard] +bone_drops = "list" +fruit_per_reward = "int" +xp = "int" +rune_rewards = "list" +hit = "int" +interval = "int" +death_penalty = "int" +pile_lives = "int" + +[.settings] +bone_drops = [ + { x = 3370, y = 9634, level = 1 }, + { x = 3359, y = 9634, level = 1 }, + { x = 3360, y = 9643, level = 1 }, + { x = 3363, y = 9643, level = 1 }, + { x = 3363, y = 9633, level = 1 }, + { x = 3367, y = 9637, level = 1 }, + { x = 3366, y = 9642, level = 1 }, + { x = 3380, y = 9647, level = 1 }, + { x = 3375, y = 9648, level = 1 }, + { x = 3370, y = 9649, level = 1 }, + { x = 3372, y = 9644, level = 1 }, + { x = 3379, y = 9646, level = 1 }, + { x = 3381, y = 9651, level = 1 }, + { x = 3379, y = 9656, level = 1 }, + { x = 3373, y = 9658, level = 1 }, + { x = 3353, y = 9648, level = 1 }, + { x = 3356, y = 9653, level = 1 }, + { x = 3346, y = 9657, level = 1 }, + { x = 3347, y = 9655, level = 1 }, + { x = 3354, y = 9634, level = 1 }, + { x = 3352, y = 9631, level = 1 }, + { x = 3355, y = 9627, level = 1 }, + { x = 3345, y = 9631, level = 1 }, + { x = 3345, y = 9627, level = 1 }, + { x = 3347, y = 9623, level = 1 }, + { x = 3351, y = 9621, level = 1 }, + { x = 3368, y = 9624, level = 1 }, + { x = 3368, y = 9631, level = 1 }, + { x = 3375, y = 9630, level = 1 }, + { x = 3375, y = 9635, level = 1 }, + { x = 3371, y = 9621, level = 1 }, + { x = 3377, y = 9621, level = 1 }, + { x = 3381, y = 9624, level = 1 }, + { x = 3382, y = 9629, level = 1 }, + { x = 3371, y = 9635, level = 1 }, + { x = 3365, y = 9634, level = 1 }, + { x = 3361, y = 9641, level = 1 }, + { x = 3363, y = 9644, level = 1 }, +] +fruit_per_reward = 16 +xp = 500 +rune_rewards = ["nature_rune", "water_rune", "earth_rune"] +hit = 20 +interval = 10 +death_penalty = 10 +pile_lives = 4 + +# Pizazz costs: telekinetic, alchemist, enchanting, graveyard. Wands consume the previous tier. +[mta_rewards] +item = "item" +telekinetic = "int" +alchemist = "int" +enchanting = "int" +graveyard = "int" +coins = "int" +requires = "item" + +[.beginner_wand] +item = "beginner_wand" +telekinetic = 30 +alchemist = 30 +enchanting = 300 +graveyard = 30 + +[.apprentice_wand] +item = "apprentice_wand" +telekinetic = 60 +alchemist = 60 +enchanting = 600 +graveyard = 60 +requires = "beginner_wand" + +[.teacher_wand] +item = "teacher_wand" +telekinetic = 150 +alchemist = 200 +enchanting = 1500 +graveyard = 150 +requires = "apprentice_wand" + +[.master_wand] +item = "master_wand" +telekinetic = 240 +alchemist = 240 +enchanting = 2400 +graveyard = 240 +requires = "teacher_wand" + +[.infinity_top] +item = "infinity_top" +telekinetic = 400 +alchemist = 450 +enchanting = 4000 +graveyard = 400 + +[.infinity_hat] +item = "infinity_hat" +telekinetic = 350 +alchemist = 400 +enchanting = 3000 +graveyard = 350 + +[.infinity_boots] +item = "infinity_boots" +telekinetic = 120 +alchemist = 120 +enchanting = 1200 +graveyard = 120 + +[.infinity_gloves] +item = "infinity_gloves" +telekinetic = 175 +alchemist = 225 +enchanting = 1500 +graveyard = 175 + +[.infinity_bottoms] +item = "infinity_bottoms" +telekinetic = 450 +alchemist = 500 +enchanting = 5000 +graveyard = 450 + +[.mages_book] +item = "mages_book" +telekinetic = 500 +alchemist = 550 +enchanting = 6000 +graveyard = 500 + +[.bones_to_peaches_spell] +item = "bones_to_peaches_spell" +telekinetic = 200 +alchemist = 300 +enchanting = 2000 +graveyard = 200 + +[.battlestaff] +item = "battlestaff" +telekinetic = 1 +alchemist = 2 +enchanting = 20 +graveyard = 2 + +[.mist_rune] +item = "mist_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.dust_rune] +item = "dust_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.mud_rune] +item = "mud_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.smoke_rune] +item = "smoke_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.steam_rune] +item = "steam_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.lava_rune] +item = "lava_rune" +telekinetic = 1 +alchemist = 1 +enchanting = 15 +graveyard = 1 + +[.cosmic_rune] +item = "cosmic_rune" +telekinetic = 0 +alchemist = 0 +enchanting = 5 +graveyard = 0 + +[.chaos_rune] +item = "chaos_rune" +telekinetic = 0 +alchemist = 1 +enchanting = 5 +graveyard = 1 + +[.nature_rune] +item = "nature_rune" +telekinetic = 0 +alchemist = 1 +enchanting = 0 +graveyard = 1 + +[.death_rune] +item = "death_rune" +telekinetic = 2 +alchemist = 1 +enchanting = 20 +graveyard = 1 + +[.law_rune] +item = "law_rune" +telekinetic = 2 +alchemist = 0 +enchanting = 0 +graveyard = 0 + +[.soul_rune] +item = "soul_rune" +telekinetic = 2 +alchemist = 2 +enchanting = 25 +graveyard = 2 + +[.blood_rune] +item = "blood_rune" +telekinetic = 2 +alchemist = 2 +enchanting = 25 +graveyard = 2 + +[.arena_book] +item = "arena_book" +telekinetic = 0 +alchemist = 0 +enchanting = 0 +graveyard = 0 +coins = 200 diff --git a/data/minigame/mage_training_arena/mage_training_arena.varbits.toml b/data/minigame/mage_training_arena/mage_training_arena.varbits.toml new file mode 100644 index 0000000000..0b8e414b86 --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.varbits.toml @@ -0,0 +1,65 @@ +# Varps 624-629; cs2 27 on the rewards interface reads 1485/1489/1488/1486 for the point labels. +[mage_training_arena_telekinetic_points] +id = 1485 +format = "int" +persist = true + +[mage_training_arena_graveyard_points] +id = 1486 +format = "int" +persist = true + +[mage_training_arena_telekinetic_maze] +id = 1487 +format = "int" +persist = true + +[mage_training_arena_enchanting_points] +id = 1488 +format = "int" +persist = true + +[mage_training_arena_alchemist_points] +id = 1489 +format = "int" +persist = true + +[mage_training_arena_maze_streak] +id = 1493 +format = "int" +persist = true + +[mage_training_arena_fruit_deposited] +id = 1494 +format = "int" +persist = true + +[mage_training_arena_orbs_deposited] +id = 1495 +format = "int" +persist = true + +[mage_training_arena_started] +id = 1499 +format = "boolean" +persist = true + +[mage_training_arena_alchemist_reward] +id = 1504 +format = "int" +persist = true + +[bones_to_peaches] +id = 1505 +format = "boolean" +persist = true + +[mage_training_arena_shapes_converted] +id = 1506 +format = "int" +persist = true + +[mage_training_arena_camera] +id = 1508 +format = "boolean" +transmit = false diff --git a/data/minigame/mage_training_arena/mage_training_arena.vars.toml b/data/minigame/mage_training_arena/mage_training_arena.vars.toml new file mode 100644 index 0000000000..18bd88494d --- /dev/null +++ b/data/minigame/mage_training_arena/mage_training_arena.vars.toml @@ -0,0 +1,7 @@ +[mage_training_arena_room] +format = "string" + +# Bitmask of mazes solved in the current set of ten; refilled when all are solved. +[mage_training_arena_mazes_solved] +format = "int" +persist = true diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/AlchemistsPlayground.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/AlchemistsPlayground.kt new file mode 100644 index 0000000000..c9fe1a1766 --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/AlchemistsPlayground.kt @@ -0,0 +1,260 @@ +package content.minigame.mage_training_arena + +import content.entity.player.bank.bank +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.entity.player.modal.Tab +import content.entity.player.modal.tab +import content.skill.magic.Magic +import content.skill.magic.spell.SpellRunes +import content.skill.magic.spell.SpellRunes.removeItems +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.ItemDefinitions +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.World +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.chat.inventoryFull +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.has +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.replace +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.transact.TransactionError +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove +import world.gregs.voidps.engine.timer.Timer + +/** + * Eight cupboards whose contents rotate; alching the arena items makes arena coins that are + * deposited for points, with a share banked as real coins on the way out. + */ +class AlchemistsPlayground : Script { + + init { + objectOperate("Search", "cupboard_mage_training_arena_*") { (target) -> + val slot = target.id.removePrefix("cupboard_mage_training_arena_").removeSuffix("_open").toInt() - 1 + if (!target.id.endsWith("_open")) { + anim("mta_search_cupboard") + target.replace("${target.id}_open", ticks = 35) + } + val item = itemAt(slot) + if (item == null) { + message("The cupboard is empty.") + return@objectOperate + } + if (!inventory.add(item)) { + message("You have no free space to hold any more items.") + return@objectOperate + } + message("You found: ${ItemDefinitions.get(item).name}") + } + + onItem("modern_spellbook:*_level_alchemy", items.joinToString(",")) { item, id -> + if (!MageTrainingArena.inRoom(this, "alchemist")) { + message("This item isn't yours to alch, it belongs to the arena!") + return@onItem + } + val spell = id.substringAfter(":") + val level = SpellRunes.magicLevel("modern_spellbook", spell) ?: 0 + if (!has(Skill.Magic, level, message = true)) { + return@onItem + } + val value = values[item.id] ?: return@onItem + tab(Tab.Inventory) + val eject = Tables.int("mta_alchemist.settings.eject_amount") + if (inventory.count("coins_mage_training_arena") + value > eject) { + message("Warning: You can't deposit more than $eject coins at a time.") + } + inventory.transaction { + if (item.id != freeItem) { + removeItems(this@onItem, spell) + } + remove(item.id) + add("coins_mage_training_arena", value) + } + when (inventory.transaction.error) { + is TransactionError.Full -> inventoryFull("room in your inventory") + TransactionError.None -> { + val row = Rows.get("spells.$spell") + anim(Magic.animation(this, row)) + gfx(Magic.graphic(this, row)) + sound(spell) + exp(Skill.Magic, row.int("xp") / 10.0) + tab(Tab.MagicSpellbook) + } + else -> return@onItem + } + } + + objectOperate("Deposit", "coin_collector") { + val deposit = inventory.count("coins_mage_training_arena") + if (deposit == 0) { + statement("You don't have any coins to deposit.") + return@objectOperate + } + val eject = Tables.int("mta_alchemist.settings.eject_amount") + if (deposit >= eject) { + tele(MageTrainingArena.lobby) + statement("You have been ejected from the arena! You were warned not to deposit more than $eject coins at once.") + return@objectOperate + } + if (!inventory.remove("coins_mage_training_arena", deposit)) { + return@objectOperate + } + val points = deposit / 100 + val xp = deposit * 2 + val reward = inc("mage_training_arena_alchemist_reward", points * 10, max = Tables.int("mta_alchemist.settings.reward_cap")) + PizazzPoints.add(this, "alchemist", points) + exp(Skill.Magic, xp.toDouble()) + AuditLog.event(this, "mta_alchemist_deposit", deposit, points, reward) + statement("You've just deposited $deposit coins, earning you $points Alchemist Pizazz Points and $xp magic XP. So far you're taking $reward coins as a reward when you leave!") + } + + npcOperate("Talk-to", "alchemy_guardian") { + player("Hi.") + npc("Greetings young one. What wisdom do you seek?") + guardianMenu() + } + + entered("mage_training_arena_alchemists_playground") { + if (values.isEmpty()) { + shuffle() + } + World.timers.startIfAbsent("mta_alchemist") + refresh(this) + } + + exited("mage_training_arena_alchemists_playground") { + payout(this) + } + + worldTimerStart("mta_alchemist") { + Tables.int("mta_alchemist.settings.interval") + } + + worldTimerTick("mta_alchemist") { + if (Players.none { MageTrainingArena.inRoom(it, "alchemist") }) { + return@worldTimerTick Timer.CANCEL + } + rotate() + Timer.CONTINUE + } + } + + private suspend fun Player.guardianMenu() { + choice { + option("What do I have to do in this room?") { + npc("In this room you will see various cupboards. It is your task to search the cupboards to find items to turn into gold using your low or high alchemy spells. You must deposit the money in the receptacle at the end of the") + npc("hall in order to receive your Alchemist Pizazz Points, otherwise the money will be taken from you as you leave through the portal. This money is used for the upkeep of the training arena as well as magic shops all") + npc("around Gielinor. Keep an eye on the cost of each items as these will change from time-to-time, as will the location of the items. Occasionally one of the items will be indicated as costing no runestones to convert to money.") + guardianMenu() + } + option("What are the rewards?") { + npc("You will get experience from casting the alchemist spells, as well as 1 Alchemist Pizazz Point for every 100 coins you deposit, and 10% of the coins you deposit will be given to you as you leave. Keep in mind that you") + npc("will not be able to take more than 1000 coins back out with you.") + guardianMenu() + } + option("Got any tips that may help me?") { + npc("You must remember to keep an eye on the various costs of the items. If you watch the movements of the other players, you might be able to guess which are the best places to visit. You will get 1 Pizazz Point for") + npc("every 100 coins, so if you have 190 coins, why not get an extra 10?") + player("I see.") + npc("Oh, and a word of warning: should you decide to leave this room by a method other than the exit portals, you will be teleported to the entrance and have any items that you picked up in the room removed.") + guardianMenu() + } + option("Thanks, bye!") + } + } + + /** + * Converts leftover arena coins into points on logout and banks the accumulated reward. + */ + private fun payout(player: Player) { + val coins = player.inventory.count("coins_mage_training_arena") + if (coins > 0) { + if (player["logged_out", false]) { + PizazzPoints.add(player, "alchemist", coins / 100) + } + player.inventory.remove("coins_mage_training_arena", coins) + } + val reward = player["mage_training_arena_alchemist_reward", 0] + if (reward <= 0) { + return + } + player["mage_training_arena_alchemist_reward"] = 0 + val banked = player.bank.transaction { add("coins", reward) } + if (!banked) { + player.addOrDrop("coins", reward) + } + AuditLog.event(player, "mta_alchemist_reward", reward, banked) + player.message("You've been awarded $reward coins straight into your bank as a reward!") + } + + companion object { + var rotation = 0 + val values = HashMap() + var freeItem: String? = null + + private val rows: List + get() = Tables.get("mta_alchemist_items").rows() + + val items: List + get() = rows.map { it.item("item") } + + /** + * Which item a cupboard holds after the current rotation; slots past the item count are empty. + */ + fun itemAt(slot: Int): String? { + val index = (slot - rotation).mod(8) + return rows.firstOrNull { it.int("slot") == index }?.item("item") + } + + fun shuffle() { + val list = Tables.intList("mta_alchemist.settings.values").shuffled() + for ((index, item) in items.withIndex()) { + values[item] = list[index % list.size] + } + } + + fun rotate() { + rotation = (rotation + 1) % 8 + shuffle() + val guardian = NPCs.firstOrNull { it.id == "alchemy_guardian" } + if (freeItem == null) { + val row = rows.random() + freeItem = row.item("item") + val name = row.string("name") + guardian?.say("The $name ${if (name.endsWith("s")) "are" else "is"} free to convert!") + } else { + freeItem = null + guardian?.say("The costs are changing!") + } + for (player in Players) { + if (MageTrainingArena.inRoom(player, "alchemist")) { + refresh(player) + } + } + } + + fun refresh(player: Player) { + val overlay = "mage_training_arena_alchemist" + for (item in items) { + player.interfaces.sendText(overlay, "value_$item", (values[item] ?: 0).toString()) + player.interfaces.sendVisibility(overlay, "free_$item", item == freeItem) + } + } + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/CreatureGraveyard.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/CreatureGraveyard.kt new file mode 100644 index 0000000000..dc73ce4813 --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/CreatureGraveyard.kt @@ -0,0 +1,161 @@ +package content.minigame.mage_training_arena + +import content.entity.combat.hit.damage +import content.entity.gfx.areaGfx +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.chat.an +import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.ItemDefinitions +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.World +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.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.obj.replace +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.timer.Timer +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.random + +/** + * Bones grabbed from piles are converted to fruit and fed through the food chute for points while + * bones rain down on everyone in the room. + */ +class CreatureGraveyard : Script { + + private val lives = HashMap() + + init { + objectOperate("Grab", "bones_mage_training_arena_*") { (target) -> + val row = bones.firstOrNull { it.obj("pile") == target.id } ?: return@objectOperate + if (!inventory.add(row.item("item"))) { + message("You have no free space!") + return@objectOperate + } + anim("climb_down") + sound("mta_deposit_bone") + val remaining = lives.getOrDefault(target.tile, Tables.int("mta_graveyard.settings.pile_lives")) - 1 + if (remaining > 0) { + lives[target.tile] = remaining + return@objectOperate + } + lives.remove(target.tile) + target.replace(row.obj("next")) + } + + objectOperate("Deposit", "food_chute") { + val bananas = inventory.count("banana") + val peaches = inventory.count("peach") + if (bananas + peaches == 0) { + statement("You don't have any bananas or peaches to deposit.") + return@objectOperate + } + if (bananas > 0 && !inventory.remove("banana", bananas)) { + return@objectOperate + } + if (peaches > 0 && !inventory.remove("peach", peaches)) { + return@objectOperate + } + val perReward = Tables.int("mta_graveyard.settings.fruit_per_reward") + var total = get("mage_training_arena_fruit_deposited", 0) + bananas + peaches + AuditLog.event(this, "mta_fruit_deposited", bananas + peaches, total) + while (total >= perReward) { + total -= perReward + set("mage_training_arena_fruit_deposited", total) + val rune = Tables.itemList("mta_graveyard.settings.rune_rewards").random() + addOrDrop(rune) + PizazzPoints.add(this, "graveyard", 1) + exp(Skill.Magic, Tables.int("mta_graveyard.settings.xp") / 10.0) + sound("mta_deposit_fruit") + statement("Congratulations - you've been awarded ${ItemDefinitions.get(rune).name.lowercase().an()} and extra magic XP.") + } + set("mage_training_arena_fruit_deposited", total) + } + + npcOperate("Talk-to", "graveyard_guardian") { + player("Hello.") + guardianMenu() + } + + entered("mage_training_arena_creature_graveyard") { + World.timers.startIfAbsent("mta_graveyard") + } + + playerDeath { + if (!MageTrainingArena.inRoom(this, "graveyard")) { + return@playerDeath + } + val lost = PizazzPoints.get(this, "graveyard").coerceAtMost(Tables.int("mta_graveyard.settings.death_penalty")) + if (lost <= 0) { + return@playerDeath + } + PizazzPoints.remove(this, "graveyard", lost) + message("You lost $lost Pizazz Points upon death!") + } + + worldTimerStart("mta_graveyard") { + Tables.int("mta_graveyard.settings.interval") + } + + worldTimerTick("mta_graveyard") { + val players = Players.filter { MageTrainingArena.inRoom(it, "graveyard") } + if (players.isEmpty()) { + return@worldTimerTick Timer.CANCEL + } + for (tile in Tables.tileList("mta_graveyard.settings.bone_drops")) { + if (random.nextInt(12) < 8) { + areaGfx("mta_falling_bones", tile) + } + } + val hit = Tables.int("mta_graveyard.settings.hit") + for (player in players) { + player.sound("mta_bone_fall") + if (player.dialogue == null) { + player.damage(hit) + } + } + Timer.CONTINUE + } + } + + private suspend fun Player.guardianMenu() { + choice { + option("What do I have to do in this room?") { + npc("Have you noticed all the bones around the room? These are teleported here from all over Gielinor to help clean up the landscape of countless bones left behind from combat. What better use for these bones than to") + npc("convert them to nutritious fruit to be eaten by you mortals? You have to use your Bones to Bananas spell to convert the bones and then place them in the holes on the walls to earn Graveyard Pizazz Points.") + npc("Unluckily for you, your health will constantly decrease from getting hit by these dropping bones so you will probably want to eat some of the bananas yourself to increase your stay here.") + guardianMenu() + } + option("What are the rewards?") { + npc("You will get experience from casting your bones to bananas spell and you will get Graveyard Pizazz Points when you put the bananas though the wall. Occasionally you will also be credited with a runestone") + npc("to help you in your future spell casting.") + guardianMenu() + } + option("Got any tips that may help me?") { + npc("Different bones will provide you with different numbers of bananas, so try to find the best type. Collect enough points and you will be able to buy a 'Bones to Peaches' spell from my fellow guardian above the entrance hall.") + npc("This spell can be used here just like the bones to bananas spell, except this spell will give you even more experience and the peaches will restore more health!") + player("I see.") + npc("Oh, and a word of warning: should you decide to leave this room by a method other than the exit portals, you will be teleported to the entrance and have any items that you picked up in the room removed.") + } + option("Thanks, bye!") + } + } + + companion object { + val bones: List + get() = Tables.get("mta_bones").rows() + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/EnchantingChamber.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/EnchantingChamber.kt new file mode 100644 index 0000000000..1943a9abdf --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/EnchantingChamber.kt @@ -0,0 +1,240 @@ +package content.minigame.mage_training_arena + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.skill.magic.spell.SpellRunes.removeItems +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.variable.hasClock +import world.gregs.voidps.engine.client.variable.start +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.World +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.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +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.item.floor.FloorItem +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.transact.TransactionError +import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace +import world.gregs.voidps.engine.timer.Timer + +/** + * Shapes taken from piles are enchanted into orbs for points; a bonus shape rotates on a world + * timer and private dragonstones respawn around the room for double points. + */ +class EnchantingChamber : Script { + + init { + objectOperate("Take-from", "cube_pile,cylinder_pile,icosahedron_pile,pentamid_pile") { (target) -> + val row = shapes.firstOrNull { it.obj("pile") == target.id } ?: return@objectOperate + if (!inventory.add(row.item("item"))) { + message("You have no space left in your inventory.") + return@objectOperate + } + anim("climb_down") + } + + onItem("modern_spellbook:enchant_level_*", "cube,cylinder,icosahedron,pentamid,dragonstone_mage_training_arena") { item, id -> + if (!MageTrainingArena.inRoom(this, "enchanting")) { + message("You can't use this spell on this item.") + return@onItem + } + if (hasClock("action_delay")) { + return@onItem + } + val spell = id.substringAfter(":") + val level = Tables.int("jewellery_enchant.$spell.level") + if (!has(Skill.Magic, level, message = true)) { + return@onItem + } + inventory.transaction { + removeItems(this@onItem, spell, message = false) + replace(item.id, "orb") + } + when (inventory.transaction.error) { + is TransactionError.Deficient -> { + message("You do not have the required items to cast this spell.") + return@onItem + } + TransactionError.None -> {} + else -> return@onItem + } + start("action_delay", 1) + val type = Tables.string("jewellery_enchant.$spell.type") + gfx("enchant_ring") + gfx("mta_enchant_orb") + anim("enchant_ring") + sound("enchant_${type}_ring") + exp(Skill.Magic, Tables.int("jewellery_enchant.$spell.xp") / 10.0 * 0.75) + val points = points(this, item.id, spell.removePrefix("enchant_level_").toInt()) + PizazzPoints.add(this, "enchanting", points) + } + + objectOperate("Deposit", "orb_hole") { + val orbs = inventory.count("orb") + if (orbs == 0) { + message("You don't have any orbs to deposit.") + return@objectOperate + } + if (!inventory.remove("orb", orbs)) { + return@objectOperate + } + anim("take") + sound("mta_deposit_orb") + val perReward = Tables.int("mta_enchanting.settings.orbs_per_reward") + var total = get("mage_training_arena_orbs_deposited", 0) + orbs + AuditLog.event(this, "mta_orbs_deposited", orbs, total) + while (total >= perReward) { + total -= perReward + set("mage_training_arena_orbs_deposited", total) + addOrDrop(Tables.itemList("mta_enchanting.settings.rune_rewards").random(), Tables.int("mta_enchanting.settings.rune_amount")) + statement("Congratulations! You've been rewarded with an item for your efforts.") + } + set("mage_training_arena_orbs_deposited", total) + } + + npcOperate("Talk-to", "enchantment_guardian") { + npc("Greetings young one. How can I enlighten you?") + guardianMenu() + } + + taken("dragonstone_mage_training_arena") { floorItem -> + val stones = dragonstones[index] ?: return@taken + stones.remove(floorItem) + val tile = floorItem.tile + World.queue("mta_dragonstone_${index}_${tile.id}", Tables.int("mta_enchanting.settings.dragonstone_respawn")) { + if (!MageTrainingArena.inRoom(this, "enchanting")) { + return@queue + } + dragonstones.getOrPut(index) { mutableListOf() }.add(FloorItems.add(tile, "dragonstone_mage_training_arena", owner = this)) + } + } + + entered("mage_training_arena_enchanting_chamber") { + World.timers.startIfAbsent("mta_enchanting") + spawnDragonstones(this) + refresh(this) + } + + exited("mage_training_arena_enchanting_chamber") { + removeDragonstones(this) + } + + worldTimerStart("mta_enchanting") { + Tables.int("mta_enchanting.settings.bonus_interval") + } + + worldTimerTick("mta_enchanting") { + if (Players.none { MageTrainingArena.inRoom(it, "enchanting") }) { + return@worldTimerTick Timer.CANCEL + } + changeBonus() + Timer.CONTINUE + } + } + + private suspend fun Player.guardianMenu() { + choice { + option("What do I have to do in this room?") { + npc("In this chamber you will see various piles of shapes. You can pick up these shapes and enchant them using the enchant jewelry spells. By enchanting these shapes, you'll be converting them into orbs which can be") + npc("placed in the hole in the centre. You will get Enchantment Pizazz Points for every ten shapes that you convert and the points you get depends on the level of enchantment spell you cast. You will get a") + npc("bonus Pizazz Point for enchanting a certain shape at a certain time, which I will periodically shout out. You will also be rewarded with an item for every so many orbs you deposit into the hole.") + guardianMenu() + } + option("What are the rewards?") { + npc("As well as the magic experience from casting your enchantment spells, you will get Enchantment Pizazz Points for converting so many shapes, plus a bonus point for converting shape of the correct type. You") + npc("should also note that you will occasionally be rewarded with items when you put one of the enchanted orbs in the floor.") + guardianMenu() + } + option("Got any tips that may help me?") { + npc("Try to guess or keep track of the time between the change of the best shape to enchant. This means you can be ready to run to a different shape when you know it is about to change. Look out for the dragon") + npc("gems, as these will get you more Pizazz Points!") + player("I see.") + npc("Oh, and a word of warning: should you decide to leave this room by a method other than the exit portals, you will be teleported to the entrance and have any items that you picked up in the room removed.") + } + option("Thanks, bye!") + } + } + + companion object { + var bonus: String = "cube" + + private val dragonstones = HashMap>() + + val shapes: List + get() = Tables.get("mta_shapes").rows() + + fun shape(item: String): String? = shapes.firstOrNull { it.item("item") == item }?.rowId + + /** + * Dragonstones score double immediately; shapes score every tenth conversion plus one for the bonus shape. + */ + fun points(player: Player, item: String, spellLevel: Int): Int { + if (item == "dragonstone_mage_training_arena") { + return spellLevel * 2 + } + val shape = shape(item) ?: return 0 + var points = 0 + val converted = player.inc("mage_training_arena_shapes_converted") + if (converted >= Tables.int("mta_enchanting.settings.shapes_per_reward")) { + player["mage_training_arena_shapes_converted"] = 0 + points += spellLevel + } + if (shape == bonus) { + points += 1 + player.message("You get $points bonus point${if (points != 1) "s" else ""}!") + } + return points + } + + fun changeBonus() { + val next = shapes.map { it.rowId }.filter { it != bonus }.random() + bonus = next + NPCs.firstOrNull { it.id == "enchantment_guardian" }?.say("The bonus shape has changed to the $next.") + for (player in Players) { + if (MageTrainingArena.inRoom(player, "enchanting")) { + refresh(player) + } + } + } + + fun refresh(player: Player) { + for (shape in shapes) { + player.interfaces.sendVisibility("mage_training_arena_enchanting", "bonus_${shape.rowId}", shape.rowId == bonus) + } + } + + fun dragonstones(player: Player): List = dragonstones[player.index] ?: emptyList() + + private fun spawnDragonstones(player: Player) { + removeDragonstones(player) + val stones = mutableListOf() + for (tile in Tables.tileList("mta_enchanting.settings.dragonstones")) { + stones.add(FloorItems.add(tile, "dragonstone_mage_training_arena", owner = player)) + } + dragonstones[player.index] = stones + } + + private fun removeDragonstones(player: Player) { + for (stone in dragonstones.remove(player.index) ?: return) { + FloorItems.remove(stone) + } + for (tile in Tables.tileList("mta_enchanting.settings.dragonstones")) { + World.clearQueue("mta_dragonstone_${player.index}_${tile.id}") + } + } + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArena.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArena.kt new file mode 100644 index 0000000000..eb9ae9521d --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArena.kt @@ -0,0 +1,264 @@ +package content.minigame.mage_training_arena + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.skill.summoning.follower +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.command.adminCommand +import world.gregs.voidps.engine.client.command.intArg +import world.gregs.voidps.engine.client.command.stringArg +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.close +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.character.move.tele +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.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Tile + +/** + * Entrance hall, portals and the rules shared by every room of the Mage Training Arena. + */ +class MageTrainingArena : Script { + + init { + objectOperate("Enter", "doorway_mage_training_arena") { + if (levels.get(Skill.Magic) < 7) { + statement("You need a Magic level of at least 7 to enter the guild.") + return@objectOperate + } + val direction = if (tile.y < 3300) Direction.NORTH else Direction.SOUTH + sound("mta_barrier") + anim("pass_through_barrier") + exactMoveDelay(tile.add(direction.delta.x * 2, direction.delta.y * 2), delay = 90, direction = direction, startDelay = 30) + } + + objectOperate("Enter", "telekinetic_portal,alchemists_portal,enchanters_portal,graveyard_portal") { (target) -> + val room = rooms.firstOrNull { it.obj("portal") == target.id } ?: return@objectOperate + enterRoom(room) + } + + objectOperate("Enter", "exit_portal_mage_training_arena") { + val room = currentRoom(this) ?: return@objectOperate + tele(room.tile("exit")) + } + + for (room in rooms) { + entered(room.string("area")) { + set("mage_training_arena_room", room.rowId) + open(room.string("overlay")) + PizazzPoints.refresh(this, room.rowId) + } + exited(room.string("area")) { + close(room.string("overlay")) + clear("mage_training_arena_room") + confiscate(this) + } + } + + playerDeath { + if (!contains("mage_training_arena_room")) { + return@playerDeath + } + it.dropItems = false + it.teleport = lobby + } + + for (type in listOf("modern", "ancient", "lunar", "dungeoneering", "jewellery", "tablet", "scroll", "home_teleport", "ectophial", "skull_sceptre")) { + teleportTakeOff(type) { + if (!contains("mage_training_arena_room")) { + return@teleportTakeOff true + } + message("You can't teleport out of the training arena!") + false + } + } + + adminCommand("pizazz", intArg("amount", desc = "Points to add, negative to remove"), stringArg("room", optional = true, autofill = PizazzPoints.rooms.toSet() + "all"), desc = "Add Pizazz Points to one room or all four") { args -> + val amount = args[0].toInt() + val room = args.getOrNull(1) ?: "all" + val rooms = if (room == "all") PizazzPoints.rooms else listOf(room) + for (name in rooms) { + if (name !in PizazzPoints.rooms) { + message("Unknown room '$name'; use ${PizazzPoints.rooms.joinToString()} or all.") + return@adminCommand + } + if (amount >= 0) { + PizazzPoints.add(this, name, amount) + } else { + PizazzPoints.remove(this, name, -amount) + } + } + message("Pizazz Points: ${PizazzHat.points(this)}") + } + + npcOperate("Talk-to", "charmed_warrior") { + player("Is there anybody there?") + npc("What do you think?") + } + + npcOperate("Talk-to", "charmed_warrior_2") { + player("Is there anybody there?") + npc("Wooo wooo! Be afraid for I'm a scary ghost. Wooo!") + player("Er, whatever.") + } + + npcOperate("Talk-to", "charmed_warrior_3") { + player("Hello?") + npc("Hey! You haven't paid for your Magic Training Arena Membership money!") + player("You're lying. I can see right through you!") + npc("Oh, HA HA, very funny.") + } + + npcOperate("Talk-to", "charmed_warrior_4") { + player("Hello?") + npc("Can't you see I'm busy?") + player("Well, I can't really see YOU.") + } + } + + private suspend fun Player.enterRoom(room: RowDefinition) { + if (!get("mage_training_arena_started", false) || !hasProgressHat(this)) { + statement("You need a Pizazz Progress Hat in order to enter. Talk to the Entrance Guardian if you don't have one.") + return + } + if (follower != null) { + statement("You can't take a familiar into the arena.") + return + } + if (levels.get(Skill.Magic) < room.int("level")) { + statement("You need to be able to cast the ${room.string("spell")} spell in order to enter.") + return + } + when (room.rowId) { + "alchemist" -> if (inventory.contains("coins")) { + statement("You cannot take money into the Alchemists' Playground.") + return + } + "graveyard" -> if (inventory.contains("banana") || inventory.contains("peach")) { + statement("You can't take bananas or peaches into the arena.") + return + } + } + if (room.rowId == "telekinetic") { + TelekineticTheatre.start(this) + } else { + tele(room.tile("enter")) + } + message("You've entered the ${room.string("name")}.") + } + + companion object { + val lobby = Tile(3363, 3302, 0) + + val rooms: List + get() = Tables.get("mta_rooms").rows() + + /** + * Items that belong to the arena and are removed when a player leaves a room by any means. + */ + private val arenaItems = listOf( + "leather_boots_mage_training_arena", + "adamant_kiteshield_mage_training_arena", + "adamant_helm_mage_training_arena", + "emerald_mage_training_arena", + "rune_longsword_mage_training_arena", + "cube", + "cylinder", + "icosahedron", + "pentamid", + "dragonstone_mage_training_arena", + "orb", + "animals_bones_1", + "animals_bones_2", + "animals_bones_3", + "animals_bones_4", + "banana", + "peach", + ) + + fun currentRoom(player: Player): RowDefinition? { + val room: String = player.get("mage_training_arena_room") ?: return null + return Rows.getOrNull("mta_rooms.$room") + } + + fun inRoom(player: Player, room: String): Boolean = player["mage_training_arena_room", ""] == room + + fun hasProgressHat(player: Player): Boolean = PizazzHat.hats.any { player.inventory.contains(it) || player.equipment.contains(it) } + + fun confiscate(player: Player) { + for (item in arenaItems) { + val held = player.inventory.count(item) + if (held > 0) { + player.inventory.remove(item, held) + } + val worn = player.equipment.count(item) + if (worn > 0) { + player.equipment.remove(item, worn) + } + } + } + } +} + +/** + * Pizazz points are stored in four varbits read by the rewards interface and shown on each room's overlay. + */ +object PizazzPoints { + val rooms = listOf("telekinetic", "alchemist", "enchanting", "graveyard") + + private val limits = mapOf( + "telekinetic" to 8191, + "alchemist" to 8191, + "enchanting" to 32767, + "graveyard" to 16383, + ) + + fun key(room: String) = "mage_training_arena_${room}_points" + + fun get(player: Player, room: String): Int = player[key(room), 0] + + fun total(player: Player): Int = rooms.sumOf { get(player, it) } + + fun add(player: Player, room: String, amount: Int) { + if (amount <= 0) { + return + } + player.inc(key(room), amount, max = limits.getValue(room)) + refresh(player, room) + PizazzHat.refresh(player) + } + + fun remove(player: Player, room: String, amount: Int) { + if (amount <= 0) { + return + } + player.dec(key(room), amount, min = 0) + refresh(player, room) + } + + fun reset(player: Player) { + for (room in rooms) { + player.set(key(room), 0) + refresh(player, room) + } + } + + fun refresh(player: Player, room: String) { + val overlay = Rows.getOrNull("mta_rooms.$room")?.string("overlay") ?: return + if (!player.hasOpen(overlay)) { + return + } + player.interfaces.sendText(overlay, "points", get(player, room).toString()) + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewards.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewards.kt new file mode 100644 index 0000000000..fce90c5b9b --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewards.kt @@ -0,0 +1,182 @@ +package content.minigame.mage_training_arena + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.InventoryDefinitions +import world.gregs.voidps.engine.data.definition.ItemDefinitions +import world.gregs.voidps.engine.data.definition.Tables +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.inv.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.sendInventory +import world.gregs.voidps.engine.inv.transact.TransactionError +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove + +/** + * The Rewards Guardian trades Pizazz Points from all four rooms for wands, infinity robes, runes + * and the Bones to Peaches spell. + */ +class MageTrainingArenaRewards(private val inventoryDefinitions: InventoryDefinitions) : Script { + + init { + npcOperate("Talk-to", "rewards_guardian") { + if (!get("mage_training_arena_started", false)) { + npc("Greetings. Have you spoken to my fellow Guardian downstairs?") + player("Nope.") + npc("Well, you need to talk to him first.") + return@npcOperate + } + player("Hi.") + npc("Greetings. What wisdom do you seek?") + menu() + } + + npcOperate("Trade-with", "rewards_guardian") { + if (!get("mage_training_arena_started", false)) { + npc("Have you spoken to my fellow guardian downstairs?") + player("Nope.") + npc("Well, you need to talk to him first.") + return@npcOperate + } + openRewards() + } + + interfaceOption("Value", "mage_training_arena_rewards:stock") { (item) -> + val row = reward(item.id) ?: return@interfaceOption + val coins = row.intOrNull("coins") ?: 0 + if (coins > 0) { + message("The ${item.def.name.lowercase()} costs $coins gold coins.") + return@interfaceOption + } + message("The ${item.def.name} costs ${row.int("telekinetic")} Telekinetic, ${row.int("alchemist")} Alchemist,") + message("${row.int("enchanting")} Enchantment and ${row.int("graveyard")} Graveyard Pizazz Points.") + } + + interfaceOption("Buy", "mage_training_arena_rewards:stock") { (item) -> + val row = reward(item.id) ?: return@interfaceOption + buy(this, row) + } + + interfaceOption("Examine", "mage_training_arena_rewards:stock") { (item) -> + message(item.def.getOrNull("examine") ?: return@interfaceOption, ChatType.ItemExamine) + } + } + + private suspend fun Player.menu() { + choice { + option("Who are you?") { + npc("Me? I'm here to grant you rewards for any of the Pizazz Points you may have earned in this training arena. Like my fellow Guardians, I am part of the arena and live to ensure its safe running.") + player("I see.") + menu() + } + option("Can I trade my Pizazz Points please?") { + npc("Why of course.") + openRewards() + } + option("Got anything else I can buy?") { + npc("Well, we do stock a special book that you may be interested in, which provides a comprehensive guide to this training arena. It costs 200gp. Would like one?") + choice { + option("Yes please.") { + buy(this, Tables.get("mta_rewards").rows().first { it.item("item") == "arena_book" }) + } + option("No thanks.") + } + } + } + } + + private fun Player.openRewards() { + val id = "mage_training_arena" + val new = !inventories.contains(id) + val inventory = inventories.inventory(id) + if (new) { + val definition = inventoryDefinitions.get(id) + for (index in 0 until definition.length) { + val itemId = definition.ids?.getOrNull(index) ?: continue + val amount = definition.amounts?.getOrNull(index) ?: continue + inventory.transaction { set(index, Item(ItemDefinitions.get(itemId).stringId, amount)) } + } + } + open("mage_training_arena_rewards") + sendInventory(id) + interfaceOptions.unlockAll("mage_training_arena_rewards", "stock", 0 until inventory.size) + } + + private suspend fun buy(player: Player, row: RowDefinition) { + val item = row.item("item") + val coins = row.intOrNull("coins") ?: 0 + if (coins > 0) { + if (!player.inventory.remove("coins", coins)) { + player.message("You don't have enough coins.") + return + } + player.addOrDrop(item) + AuditLog.event(player, "mta_reward", item, coins) + return + } + if (item == "bones_to_peaches_spell" && player["bones_to_peaches", false]) { + player.message("You already unlocked that spell.") + return + } + for (room in PizazzPoints.rooms) { + if (PizazzPoints.get(player, room) < row.int(room)) { + player.message("You cannot afford that item.") + return + } + } + val required = row.itemOrNull("requires") + val previous = if (required == null) { + null + } else if (player.inventory.contains(required)) { + player.inventory + } else if (player.equipment.contains(required)) { + player.equipment + } else if (player.inventory.contains("master_wand") || player.equipment.contains("master_wand")) { + null + } else { + player.message("You don't have the required wand to buy this upgrade.") + return + } + if (item != "bones_to_peaches_spell") { + player.inventory.transaction { + if (previous === player.inventory && required != null) { + remove(required) + } else if (previous != null && required != null) { + link(previous).remove(required) + } + add(item) + } + when (player.inventory.transaction.error) { + is TransactionError.Full -> { + player.message("You don't have enough inventory space.") + return + } + TransactionError.None -> {} + else -> return + } + } + for (room in PizazzPoints.rooms) { + PizazzPoints.remove(player, room, row.int(room)) + } + AuditLog.event(player, "mta_reward", item, row.int("telekinetic"), row.int("alchemist"), row.int("enchanting"), row.int("graveyard")) + if (item == "bones_to_peaches_spell") { + player["bones_to_peaches"] = true + player.statement("The Guardian teaches you how to use the Bones to Peaches spell!") + } + } + + private fun reward(item: String): RowDefinition? = Tables.get("mta_rewards").rows().firstOrNull { it.item("item") == item } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaWizards.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaWizards.kt new file mode 100644 index 0000000000..b09b7f68ba --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/MageTrainingArenaWizards.kt @@ -0,0 +1,49 @@ +package content.minigame.mage_training_arena + +import content.entity.player.dialogue.Angry +import content.entity.player.dialogue.Asleep +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import world.gregs.voidps.engine.Script + +/** + * The wizards on the arena's top floor and in its basement. Their Rune Mechanics quest is not + * implemented, so these are their pre-quest conversations only. + */ +class MageTrainingArenaWizards : Script { + + init { + npcOperate("Talk-to", "wizard_shug") { + player("Hello?") + npc("Zzz zzz zzzzz") + player("He's out like a light.") + } + + npcOperate("Talk-to", "apprentice_clerval") { + player("Hello.") + npc("Hello?") + player("What are you doing down here?") + npc("Well, I'm supposed to be training my magic, but I got a bit...distracted. I came down here to think.") + player("What are you thinking about?") + npc("Rune guardians! Aren't they just fascinating? I was speaking to one earlier and now I just can't stop thinking about them.") + player("What exactly is so fascinating about them?") + npc("Can't you see it? They're walking, talking ROCKS!") + npc("I've been pondering how I could make my own rune guardian, then I would always have a friend with me. I just don't know how to go about doing it.") + player("Well, why don't you TALK to the wizards?") + npc("NO! They'd yell at me and tell me to get back to the training rooms. I'm terrible at casting spells under duress. I get nervous and end up casting spells on myself.") + } + + npcOperate("Talk-to", "wizard_dougal") { + player("Hello.") + npc("Not another one. Don't you useless apprentices have anything better to do than bother me? First that bumbling Clerval and now you.") + } + + npcOperate("Talk-to", "wizard_edvin") { + player("Hello.") + npc("Hmm, it's still just not right. I think I've been working on it too long.") + npc("I got rid of the lava...so that should make it safer. No one's gotten trapped between teleports since I fixed the abyssal quadrangle algorithm.") + } + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/PizazzHat.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/PizazzHat.kt new file mode 100644 index 0000000000..3864773bd5 --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/PizazzHat.kt @@ -0,0 +1,220 @@ +package content.minigame.mage_training_arena + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +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.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace + +/** + * The Pizazz Progress Hat handed out by the Entrance Guardian; its tier reflects total points earned. + */ +class PizazzHat : Script { + + init { + npcOperate("Talk-to", "entrance_guardian") { + player("Hi.") + if (get("mage_training_arena_started", false)) { + menu() + } else { + npc("Greetings. What wisdom do you seek?") + choice { + option("I'm new to this place. Where am I?") { + about() + } + option("None, I don't really care.") + } + } + } + + itemOption("Talk-to", "progress_hat*") { + talk() + } + + itemOption("Talk-to", "progress_hat*", inventory = "worn_equipment") { + talk() + } + + itemOption("Destroy", "progress_hat*") { (item, slot) -> + npc("pizazz_hat", "How dare you destroy me? You'll lose your Pizazz Points!") + choice("Destroy Hat?") { + option("Yes") { + if (!inventory.remove(slot, item.id, item.amount)) { + return@option + } + sound("destroy_object") + PizazzPoints.reset(this) + statement("The hat whispers as you destroy it. You can get another from the Entrance Guardian.") + } + option("No") { + npc("pizazz_hat", "I think so too!") + } + } + } + } + + private suspend fun Player.talk() { + val greeting = greetings.random() + player(greeting.first) + npc("pizazz_hat", greeting.second) + player(greeting.third) + npc("pizazz_hat", "Ok, I suppose it's my job. You have: ${points(this)}") + player("Thank you!") + } + + private suspend fun Player.menu() { + choice { + option("Can you tell me about this place again?") { + about() + } + option("Can you explain the different portals?") { + portals() + } + option("About the progress hat...") { + if (MageTrainingArena.hasProgressHat(this)) { + npc("Which you have stored somewhere I'm sure.") + player("Yes. What's it for?") + npc("Collect Pizazz Points from the various areas and the hat will remember your totals. Talk to the hat at any time to find out what points you have. Go upstairs and talk to the Rewards Guardian to claim items for the points.") + } else { + npc("You want another one don't you.") + player("Sorry, can I?") + give(this) + npc("Here you go. Talk to the hat to find out your current Pizazz Point totals.") + } + menu() + } + option("Thanks, bye!") + } + } + + private suspend fun Player.about() { + npc("Well young one, you have entered the Magic Training Arena. It was built at the start of the Fifth Age, when runestones were first discovered. It was made because of the many pointless accidents caused by inexperienced mages.") + player("Who created it?") + npc("Good question. It was originally made by the ancestors of the wizards in the Wizards Tower. However, it was destroyed by melee and ranged warriors who took offence at the use of this new 'Magic Art'. Recently") + npc("the current denizens of the Wizards Tower have resurrected the arena including various Guardians you will see as you look around. We are here to help and to ensure things run smoothly.") + player("Interesting. So what can I do here?") + npc("You may train up your skills in the magic arts by travelling through one of the portals at the back of this entrance hall. By training up in one of these areas you will be awarded special Pizazz Points unique to each room.") + npc("With these points you may claim a variety of items from my fellow guardian up the stairs.") + player("How do you record the points I have earned?") + if (get("mage_training_arena_started", false)) { + npc("With the Pizazz Progress Hat I gave you, of course.") + player("OK.") + npc("Oh, and a word of warning: should you decide to log out whilst in any of the rooms in the arena, you will be teleported to the entrance and have any items that you picked up in the room removed.") + menu() + return + } + npc("You really are full of questions! You will need a special Pizzaz Progress Hat! I can give you one if you so wish to train here.") + player("Yes Please!") + set("mage_training_arena_started", true) + give(this) + npc("Here you go. Talk to the hat to find out your current Pizzaz Points totals.") + player("Talk to it?") + npc("Well of course, it's a magic Pizazz Progress Hat! Mind your manners though, hats have feelings too!") + player("Er... if you insist.") + npc("Oh, and a word of warning: should you decide to leave the rooms by any method other than the exit portals, you will be teleported to the entrance and have any items that you picked up in the room removed.") + player("Okay. Thanks!") + } + + private suspend fun Player.portals() { + npc("They lead to four areas to train your magic: The Telekinetic Theatre, The Alchemists' Playground, The Enchanting Chamber, and The Creature Graveyard.") + choice { + option("What's the Telekinetic Theatre?") { + npc("In there you can earn Telekinetic Pizazz Points for trade upstairs.") + player("What will I be doing in there?") + npc("That depends on how much of a time-waster you are! You are required to use the Telekinetic Grab spell in order to move a statue through a maze. Casting the spell will move the statue towards you until it reaches a wall.") + npc("So by standing on the different sides of the maze you can move the statue North, East, South or West. You will be rewarded Pizazz Points for each maze successfully solved.") + portals() + } + option("What's the Alchemists' Playground?") { + npc("In the playground you can earn Alchemist Pizazz Points for trading with the rewards guardian upstairs.") + player("What's in there?") + npc("You'll find eight cupboards containing items you can turn to gold using the low and high alchemy spells. The money you earn will be taken from you upon leaving the playground to pay for the upkeep of this training") + npc("arena and to help fund magic shops around Gielinor. You will be rewarded with 1 Pizazz Point for every 100 coins deposited and a percentage of the money you create. Keep in mind that you will not be able to take more than 1000 coins back out with you.") + player("Sounds simple.") + portals() + } + option("What's the Enchanting Chamber?") { + npc("In here you will be able to earn Enchantment Pizazz Points for trade upstairs.") + player("What will I have to do?") + npc("You will find yourself amongst various piles of shapes. You can pick up these shapes and use one of your enchanting spells upon it to morph it into an orb. You'll also see a hole in the centre of the room; put the orbs") + npc("in here and for every 20 we will credit you with a gift. You will get Pizazz Points for every 10 shapes that you convert and the amount of points depends on the spell you use.") + portals() + } + option("What's the Creature Graveyard?") { + npc("In here you will be able to earn Graveyard Pizazz Points for trade upstairs.") + player("But what do I have to do in there?") + npc("Patience young one. A great many creatures die in this world and much of their remains can cause clutter. We have taken it upon ourselves to teleport many bones from these creatures into this graveyard for a") + npc("use. It's up to you to practice your bones to bananas spell in order to put these bones into immediate use - nutritious food for monsters! Just convert the bones and put them in the holes in the walls. There is a") + npc("drawback to this room however, the bones often fall on people so you may want to eat some of those bananas to stay alive. When people die we confiscate Pizazz Points for the effort of teleporting incompetent mages.") + portals() + } + option("Thanks, bye!") + } + } + + companion object { + val hats = listOf("progress_hat", "progress_hat_2", "progress_hat_3") + + private val greetings = listOf( + Triple("Hello... Mr Progress Hat?", "That's me, why are you bothering me human?", "Sorry, but do you think you could tell me my Pizazz Points?"), + Triple("Erm, excuse me Mr Progress Hat?", "Can't you see I'm busy?", "But you're just a hat? Can you tell me my Pizazz Point totals?"), + Triple("Mr Progress Hat... sir?", "What do you want?", "Do you think you could tell me my Pizazz Points?"), + Triple("Mr Progress Hat? Hello?", "I suppose you want to know your Pizazz Points.", "That would be nice, yes."), + ) + + fun points(player: Player): String = "${PizazzPoints.get(player, "telekinetic")} Telekinetic, ${PizazzPoints.get(player, "alchemist")} Alchemist, ${PizazzPoints.get(player, "enchanting")} Enchantment, and ${PizazzPoints.get(player, "graveyard")} Graveyard Pizazz Points." + + fun tier(player: Player): String { + val total = PizazzPoints.total(player) + return when { + total > 600 -> "progress_hat_3" + total > 300 -> "progress_hat_2" + else -> "progress_hat" + } + } + + /** + * Hands the player a fresh hat of the correct tier, replacing any they already carry. + */ + fun give(player: Player) { + for (hat in hats) { + val held = player.inventory.count(hat) + if (held > 0) { + player.message("The hat shifts unexpectedly.") + player.inventory.remove(hat, held) + } + } + player.addOrDrop(tier(player)) + } + + /** + * Swaps a carried or worn hat for the tier matching the player's current points. + */ + fun refresh(player: Player) { + val tier = tier(player) + for (hat in hats) { + if (hat == tier) { + continue + } + if (player.inventory.contains(hat)) { + player.inventory.transaction { replace(hat, tier) } + player.message("Your progress hat updates to reflect your achievements.") + } + if (player.equipment.contains(hat)) { + player.equipment.transaction { replace(hat, tier) } + player.message("Your progress hat updates to reflect your achievements.") + } + } + } + } +} diff --git a/game/src/main/kotlin/content/minigame/mage_training_arena/TelekineticTheatre.kt b/game/src/main/kotlin/content/minigame/mage_training_arena/TelekineticTheatre.kt new file mode 100644 index 0000000000..3d22b2d690 --- /dev/null +++ b/game/src/main/kotlin/content/minigame/mage_training_arena/TelekineticTheatre.kt @@ -0,0 +1,317 @@ +package content.minigame.mage_training_arena + +import content.entity.gfx.areaGfx +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.dialogue.type.statement +import content.entity.player.inv.item.addOrDrop +import content.entity.proj.shoot +import content.quest.clearInstance +import content.quest.instanceOffset +import content.quest.setInstanceLogout +import content.quest.smallInstance +import content.skill.magic.spell.removeSpellItems +import org.rsmod.game.pathfinder.StepValidator +import org.rsmod.game.pathfinder.collision.CollisionStrategies +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.clearCamera +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.moveCamera +import world.gregs.voidps.engine.client.turnCamera +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.client.variable.hasClock +import world.gregs.voidps.engine.client.variable.start +import world.gregs.voidps.engine.data.config.RowDefinition +import world.gregs.voidps.engine.data.definition.Tables +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.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.exp.exp +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.queue.strongQueue +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Region +import world.gregs.voidps.type.Tile + +/** + * Ten mazes copied into a private instance of region 13463; the player casts Telekinetic Grab on a + * statue to slide it towards the side of the maze they stand on until it reaches the exit square. + */ +class TelekineticTheatre(private val stepValidator: StepValidator) : Script { + + init { + onFloorItemApproach("modern_spellbook:telekinetic_grab", "guardian_statue") { + approachRange(10) + steps.clear() + val statue = it.target + face(statue.tile) + if (hasClock("action_delay")) { + return@onFloorItemApproach + } + val maze = currentMaze(this) ?: return@onFloorItemApproach + val direction = direction(this, maze) + if (!removeSpellItems("telekinetic_grab")) { + return@onFloorItemApproach + } + start("action_delay", 3) + anim("tele_grab_cast") + gfx("tele_grab_cast") + sound("tele_grab_cast") + exp(Skill.Magic, Tables.int("spells.telekinetic_grab.xp") / 10.0) + val clientTicks = shoot("tele_grab_travel", statue.tile) + areaSound("tele_grab_impact", statue.tile, delay = clientTicks, radius = 10) + areaGfx("tele_grab_impact", statue.tile, delay = clientTicks) + delay(3) + if (direction == null) { + message("Invalid move!") + return@onFloorItemApproach + } + strongQueue("mta_statue") { + slide(this, maze, direction) + } + } + + // The statue sits inside the maze walls, so these work from a distance rather than walking to it. + floorItemApproach("Observe") { (target) -> + if (target.id != "guardian_statue") { + return@floorItemApproach + } + steps.clear() + observe(this) + } + + floorItemApproach("Reset") { (target) -> + if (target.id != "guardian_statue") { + return@floorItemApproach + } + steps.clear() + val maze = currentMaze(this) ?: return@floorItemApproach + placeStatue(this, local(this, maze.tile("statue"))) + } + + npcOperate("Talk-to", "maze_guardian") { (target) -> + player("Hi!") + npc("Well done on releasing me. Would you like to try another maze?") + choice { + option("Yes please!") { + npc("Very well, I shall teleport you.") + NPCs.remove(target) + nextMaze(this) + } + option("No thanks.") { + npc("Very well. Talk to me if you want to move onto the next maze, or you can return to the entrance hall through the portal.") + } + } + } + + npcOperate("Talk-to", "telekinetic_guardian") { + player("Hi.") + guardianMenu() + } + + entered("mage_training_arena_telekinetic_theatre") { + refreshSolved(this) + } + + exited("mage_training_arena_telekinetic_theatre") { + leave(this) + } + } + + private suspend fun Player.guardianMenu() { + choice { + option("What do I have to do in this room?") { + npc("In this room you will see a maze within which one of my fellow Guardians has been turned to stone for the purpose of this exercise. You must move the statue using your telekinetic grab spell to the exit square at") + npc("the edge of the maze to bring the Guardian back to life. Simply stand on the side that you wish for the statue to travel towards and cast the spell on the statue. Once you have solved the maze, the statue will change back") + npc("into the Guardian and he will award you with Telekinetic Pizazz Points and teleport you to the next maze. You can switch to a better view of the maze by selecting the 'Observe' option on the statue and return") + npc("your view to normal by selecting the same option again. There is also a 'Reset' option on the statue just in case things aren't going too well.") + guardianMenu() + } + option("What are the rewards?") { + npc("As well as the experience in casting magic, you will get Telekinetic Pizazz Points for each maze successfully solved and bonus points for completing five mazes in a row without returning to the entrance.") + guardianMenu() + } + option("Got any tips that may help me?") { + npc("Have a good look at the maze before you try to solve it because this can save you time and runes required to navigate the maze. Although you will still be getting magic experience for moving the statue incorrectly, you") + npc("won't be progressing towards collecting Telekinetic Pizazz Points. Lastly, all the mazes can be solved in ten moves or less.") + player("I see.") + } + option("Thanks, bye!") + } + } + + /** + * Slides the statue one tile per tick in [direction] until it hits a wall or reaches the exit. + */ + private suspend fun slide(player: Player, maze: RowDefinition, direction: Direction) { + val end = local(player, maze.tile("end")) + while (true) { + val statue = statues[player.index] ?: return + val tile = statue.tile + val blocked = !stepValidator.canTravel( + level = tile.level, + x = tile.x, + z = tile.y, + offsetX = direction.delta.x, + offsetZ = direction.delta.y, + size = 1, + extraFlag = 0, + collision = CollisionStrategies.Normal, + ) + if (blocked) { + return + } + val next = tile.add(direction) + player.sound("mta_move_statue") + placeStatue(player, next) + if (next == end) { + win(player, maze) + return + } + player.delay(1) + } + } + + private suspend fun win(player: Player, maze: RowDefinition) { + statues.remove(player.index)?.let { FloorItems.remove(it) } + NPCs.remove(guardians.remove(player.index)) + player["mage_training_arena_mazes_solved"] = player["mage_training_arena_mazes_solved", 0] or (1 shl maze.int("index")) + val streak = player.inc("mage_training_arena_maze_streak") + val bonus = streak >= 5 + if (bonus) { + player["mage_training_arena_maze_streak"] = 0 + player.exp(Skill.Magic, 1000.0) + player.addOrDrop("law_rune", 10) + } + PizazzPoints.add(player, "telekinetic", if (bonus) 10 else 2) + if (player["mage_training_arena_camera", false]) { + player["mage_training_arena_camera"] = false + player.clearCamera() + } + refreshSolved(player) + NPCs.add("maze_guardian", local(player, maze.tile("end"))) + AuditLog.event(player, "mta_maze_solved", maze.rowId, streak) + player.statement("Congratulations! You have received two Telekinetic Pizazz Points!") + if (bonus) { + player.statement("Congratulations on solving five mazes in a row, have 8 bonus points, 10 law runes and extra magic XP!") + } + } + + private fun observe(player: Player) { + if (player["mage_training_arena_camera", false]) { + player["mage_training_arena_camera"] = false + player.clearCamera() + return + } + val maze = currentMaze(player) ?: return + player.message("Click observe on the guardian to reset your camera.") + player["mage_training_arena_camera"] = true + val camera = local(player, maze.tile("camera")) + if (maze.rowId == "maze_4") { + player.moveCamera(camera.add(11, 5), 799, 95, 95) + player.turnCamera(camera.add(-44, -10), 799, 95, 95) + return + } + player.moveCamera(camera.addY(-10), 798, 95, 95) + player.turnCamera(camera.addY(-10), 798, 95, 95) + } + + private fun leave(player: Player) { + statues.remove(player.index) + guardians.remove(player.index) + if (player["mage_training_arena_camera", false]) { + player["mage_training_arena_camera"] = false + player.clearCamera() + } + player.clearInstance() + if (player["logged_out", false]) { + player.tele(MageTrainingArena.lobby) + } + } + + companion object { + val region = Region(13463) + + private val statues = HashMap() + private val guardians = HashMap() + + private val mazes: List + get() = Tables.get("mta_mazes").rows() + + suspend fun start(player: Player) { + player.smallInstance(region, levels = 3) + player.setInstanceLogout(MageTrainingArena.lobby) + player.delay(1) + nextMaze(player) + } + + /** + * Picks an unsolved maze other than the current one, refilling the set once all ten are solved. + */ + fun nextMaze(player: Player) { + val all = mazes + var solved = player["mage_training_arena_mazes_solved", 0] + if (all.indices.all { solved and (1 shl it) != 0 }) { + solved = 0 + player["mage_training_arena_mazes_solved"] = 0 + } + val current = player["mage_training_arena_telekinetic_maze", 0] + val remaining = all.filter { solved and (1 shl it.int("index")) == 0 } + val choices = remaining.filter { it.int("index") + 1 != current }.ifEmpty { remaining } + val maze = choices.random() + player["mage_training_arena_telekinetic_maze"] = maze.int("index") + 1 + NPCs.remove(guardians.remove(player.index)) + placeStatue(player, local(player, maze.tile("statue"))) + guardians[player.index] = NPCs.add("telekinetic_guardian", local(player, maze.tile("guardian"))) + player.tele(local(player, maze.tile("base"))) + } + + fun currentMaze(player: Player): RowDefinition? { + val index = player["mage_training_arena_telekinetic_maze", 0] - 1 + return mazes.firstOrNull { it.int("index") == index } + } + + fun statue(player: Player): FloorItem? = statues[player.index] + + fun placeStatue(player: Player, tile: Tile) { + statues.remove(player.index)?.let { FloorItems.remove(it) } + statues[player.index] = FloorItems.add(tile, "guardian_statue", owner = player) + } + + /** + * Converts a maze offset into a tile inside the player's instance. + */ + fun local(player: Player, offset: Tile): Tile = region.tile.add(offset).add(player.instanceOffset()) + + /** + * The side of the maze the player stands on decides where the statue travels. + */ + fun direction(player: Player, maze: RowDefinition): Direction? { + val origin = region.tile.add(player.instanceOffset()) + val x = player.tile.x - origin.x + val y = player.tile.y - origin.y + return when { + y >= maze.int("north") -> Direction.NORTH + y <= maze.int("south") -> Direction.SOUTH + x <= maze.int("west") -> Direction.WEST + x >= maze.int("east") -> Direction.EAST + else -> null + } + } + + fun refreshSolved(player: Player) { + if (!player.hasOpen("mage_training_arena_telekinetic")) { + return + } + player.interfaces.sendText("mage_training_arena_telekinetic", "solved", player["mage_training_arena_maze_streak", 0].toString()) + } + } +} diff --git a/game/src/main/kotlin/content/skill/magic/book/modern/Alchemy.kt b/game/src/main/kotlin/content/skill/magic/book/modern/Alchemy.kt index aa27a1ae4d..1226675207 100644 --- a/game/src/main/kotlin/content/skill/magic/book/modern/Alchemy.kt +++ b/game/src/main/kotlin/content/skill/magic/book/modern/Alchemy.kt @@ -10,6 +10,7 @@ import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.variable.hasClock import world.gregs.voidps.engine.client.variable.start import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.inventoryFull @@ -26,6 +27,10 @@ import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove class Alchemy : Script { init { onItem("modern_spellbook:*_level_alchemy") { item, id -> + if (tile in Areas["mage_training_arena_alchemists_playground"]) { + message("You can only alch items from the cupboards!") + return@onItem + } if (item.def.contains("destroy")) { message("This spell can not be cast on this item.") return@onItem diff --git a/game/src/main/kotlin/content/skill/magic/book/modern/BonesTo.kt b/game/src/main/kotlin/content/skill/magic/book/modern/BonesTo.kt index 125bfc8b5e..ebd8f842e2 100644 --- a/game/src/main/kotlin/content/skill/magic/book/modern/BonesTo.kt +++ b/game/src/main/kotlin/content/skill/magic/book/modern/BonesTo.kt @@ -5,11 +5,15 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.variable.hasClock import world.gregs.voidps.engine.client.variable.start +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.Tables +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.sound import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.transact.operation.AddItemLimit.addToLimit +import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace class BonesTo : Script { @@ -18,33 +22,65 @@ class BonesTo : Script { if (hasClock("action_delay")) { return@interfaceOption } - if (!inventory.contains("bones") && !inventory.contains("big_bones")) { - message("You don't have any bones to cast this spell on.") + if (it.component == "bones_to_peaches" && !get("bones_to_peaches", false)) { + message("You can only learn this spell from the Mage Training Arena.") return@interfaceOption } - val spell = it.component - val produce = if (spell == "bones_to_peaches") "peach" else "banana" - val success = inventory.transaction { - removeItems(this@interfaceOption, spell) - var next = inventory.indexOf("bones") - while (next != -1 && !failed) { - replace(next, "bones", produce) - next = inventory.indexOf("bones") - } - next = inventory.indexOf("big_bones") + convert(this, it.component, tabletSlot = -1) + } + + itemOption("Break", "bones_to_bananas,bones_to_peaches") { (item, slot) -> + if (hasClock("action_delay")) { + return@itemOption + } + if (inGraveyard(this)) { + message("You can not use this tablet in the Mage Training Arena.") + return@itemOption + } + convert(this, item.id, tabletSlot = slot) + } + } + + private fun convert(player: Player, spell: String, tabletSlot: Int) { + val produce = if (spell == "bones_to_peaches") "peach" else "banana" + val bones = if (inGraveyard(player)) graveyardBones() else mapOf("bones" to 1, "big_bones" to 1) + if (bones.keys.none { player.inventory.contains(it) }) { + player.message("You don't have any bones to cast this spell on.") + return + } + val success = player.inventory.transaction { + if (tabletSlot == -1) { + removeItems(player, spell) + } else { + remove(tabletSlot, spell, 1) + } + for ((bone, multiplier) in bones) { + var next = player.inventory.indexOf(bone) while (next != -1 && !failed) { - replace(next, "big_bones", produce) - next = inventory.indexOf("big_bones") + replace(next, bone, produce) + if (multiplier > 1) { + addToLimit(produce, multiplier - 1) + } + next = player.inventory.indexOf(bone) } } - if (!success) { - return@interfaceOption - } - start("action_delay", 1) - anim("bones_to_spell") - gfx("bones_to_spell") - sound("bones_to_spell") - exp(Skill.Magic, Tables.int("spells.$spell.xp") / 10.0) + } + if (!success) { + return + } + player.start("action_delay", 1) + player.anim("bones_to_spell") + player.gfx("bones_to_spell") + player.sound("bones_to_spell") + if (tabletSlot == -1) { + player.exp(Skill.Magic, Tables.int("spells.$spell.xp") / 10.0) } } + + private fun inGraveyard(player: Player): Boolean = player.tile in Areas["mage_training_arena_creature_graveyard"] + + /** + * Each type of animals' bones in the Creature Graveyard yields a different amount of fruit. + */ + private fun graveyardBones(): Map = Tables.get("mta_bones").rows().associate { it.item("item") to it.int("multiplier") } } diff --git a/game/src/main/kotlin/content/social/trade/TradeRequest.kt b/game/src/main/kotlin/content/social/trade/TradeRequest.kt index ae7c97a769..be80774c6d 100644 --- a/game/src/main/kotlin/content/social/trade/TradeRequest.kt +++ b/game/src/main/kotlin/content/social/trade/TradeRequest.kt @@ -8,6 +8,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.sendScript import world.gregs.voidps.engine.client.ui.closeType +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.ChatType import world.gregs.voidps.engine.entity.character.player.name @@ -26,6 +27,10 @@ class TradeRequest : Script { if (filter == "off" || (filter == "friends" && !target.friend(this))) { return@playerOperate } + if (tradingBlocked(this) || tradingBlocked(target)) { + message("You can't do that right now.") + return@playerOperate + } if (target.hasRequest(this, "trade")) { message("Sending trade offer...", ChatType.Trade) } else { @@ -53,6 +58,8 @@ class TradeRequest : Script { * When an offer is updated the change is persisted to the other player */ + private fun tradingBlocked(player: Player): Boolean = Areas.get(player.tile.zone).any { it.tags.contains("no_trading") && player.tile in it.area } + fun startTrade(player: Player, partner: Player) { reset(player, partner) player["other_trader_name"] = partner.name diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/AlchemistsPlaygroundTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/AlchemistsPlaygroundTest.kt new file mode 100644 index 0000000000..b1c370e79e --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/AlchemistsPlaygroundTest.kt @@ -0,0 +1,147 @@ +package content.minigame.mage_training_arena + +import WorldTest +import containsMessage +import content.entity.player.bank.bank +import interfaceOnItem +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile + +internal class AlchemistsPlaygroundTest : WorldTest() { + + @Test + fun `Cupboards hold the item for their rotated slot`() { + val player = enter("mta-alch-cupboard") + val cupboard = MageTrainingArenaTest.findObject("cupboard_mage_training_arena_2", Tile(3341, 9618, 2), Tile(3391, 9654, 2)) + player.tele(cupboard.tile.addY(-1)) + tick(2) + + player.objectOption(cupboard, "Search") + tick(5) + + assertTrue(player.inventory.contains("adamant_kiteshield_mage_training_arena")) + assertTrue(player.containsMessage("You found: Adamant kiteshield")) + } + + @Test + fun `Alching an arena item makes arena coins worth its current value`() { + val player = enter("mta-alch-cast") + player.inventory.add("leather_boots_mage_training_arena") + player.inventory.add("fire_rune", 3) + player.inventory.add("nature_rune", 1) + + player.interfaceOnItem("modern_spellbook", "low_level_alchemy", Item("leather_boots_mage_training_arena"), player.inventory.indexOf("leather_boots_mage_training_arena")) + tick(3) + + assertEquals(30, player.inventory.count("coins_mage_training_arena")) + assertEquals(0, player.inventory.count("nature_rune")) + assertEquals(31.0, player.experience.get(Skill.Magic)) + } + + @Test + fun `The free item costs no runes`() { + val player = enter("mta-alch-free") + AlchemistsPlayground.freeItem = "emerald_mage_training_arena" + player.inventory.add("emerald_mage_training_arena") + + player.interfaceOnItem("modern_spellbook", "low_level_alchemy", Item("emerald_mage_training_arena"), player.inventory.indexOf("emerald_mage_training_arena")) + tick(3) + + assertEquals(8, player.inventory.count("coins_mage_training_arena")) + } + + @Test + fun `Normal items can't be alched in the playground`() { + val player = enter("mta-alch-normal") + player.inventory.add("bronze_sword") + player.inventory.add("fire_rune", 3) + player.inventory.add("nature_rune", 1) + + player.interfaceOnItem("modern_spellbook", "low_level_alchemy", Item("bronze_sword"), player.inventory.indexOf("bronze_sword")) + tick(3) + + assertTrue(player.inventory.contains("bronze_sword")) + assertTrue(player.containsMessage("You can only alch items from the cupboards!")) + } + + @Test + fun `Depositing coins gives points, experience and a reward share`() { + val player = enter("mta-alch-deposit") + player.inventory.add("coins_mage_training_arena", 250) + val collector = MageTrainingArenaTest.findObject("coin_collector", Tile(3341, 9618, 2), Tile(3391, 9654, 2)) + player.tele(collector.tile.addY(-2)) + tick(2) + + player.objectOption(collector, "Deposit") + tick(6) + player.skipDialogues() + + assertEquals(0, player.inventory.count("coins_mage_training_arena")) + assertEquals(2, PizazzPoints.get(player, "alchemist")) + assertEquals(500.0, player.experience.get(Skill.Magic)) + assertEquals(20, player["mage_training_arena_alchemist_reward", 0]) + } + + @Test + fun `Depositing too many coins ejects the player`() { + val player = enter("mta-alch-eject") + player.inventory.add("coins_mage_training_arena", 12000) + val collector = MageTrainingArenaTest.findObject("coin_collector", Tile(3341, 9618, 2), Tile(3391, 9654, 2)) + player.tele(collector.tile.addY(-2)) + tick(2) + + player.objectOption(collector, "Deposit") + tick(6) + + assertEquals(MageTrainingArena.lobby, player.tile) + assertEquals(0, player.inventory.count("coins_mage_training_arena")) + assertEquals(0, PizazzPoints.get(player, "alchemist")) + } + + @Test + fun `Leaving banks the reward and removes leftover coins`() { + val player = enter("mta-alch-exit") + player["mage_training_arena_alchemist_reward"] = 20 + player.inventory.add("coins_mage_training_arena", 50) + val exit = MageTrainingArenaTest.findObject("exit_portal_mage_training_arena", Tile(3341, 9618, 2), Tile(3391, 9654, 2)) + player.tele(exit.tile.addY(-1)) + tick(2) + + player.objectOption(exit, "Enter") + tick(10) + + assertEquals(Tile(3363, 3320), player.tile) + assertEquals(0, player.inventory.count("coins_mage_training_arena")) + assertEquals(20, player.bank.count("coins")) + assertEquals(0, player["mage_training_arena_alchemist_reward", 0]) + assertTrue(player.containsMessage("You've been awarded 20 coins straight into your bank as a reward!")) + } + + private fun enter(name: String): Player { + val player = createPlayer(Tile(3361, 3316), name) + player.levels.set(Skill.Magic, 55) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + player.tele(Tile(3366, 9623, 2)) + tick(2) + assertEquals("alchemist", player["mage_training_arena_room", ""]) + AlchemistsPlayground.rotation = 0 + AlchemistsPlayground.freeItem = null + AlchemistsPlayground.values["leather_boots_mage_training_arena"] = 30 + AlchemistsPlayground.values["adamant_kiteshield_mage_training_arena"] = 15 + AlchemistsPlayground.values["adamant_helm_mage_training_arena"] = 5 + AlchemistsPlayground.values["emerald_mage_training_arena"] = 8 + AlchemistsPlayground.values["rune_longsword_mage_training_arena"] = 1 + return player + } +} diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/CreatureGraveyardTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/CreatureGraveyardTest.kt new file mode 100644 index 0000000000..b013e162ca --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/CreatureGraveyardTest.kt @@ -0,0 +1,128 @@ +package content.minigame.mage_training_arena + +import WorldTest +import containsMessage +import content.entity.combat.hit.directHit +import interfaceOption +import itemOption +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.entity.character.move.tele +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.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile + +internal class CreatureGraveyardTest : WorldTest() { + + @Test + fun `Bone piles hand out bones then change type`() { + val player = enter("mta-grave-pile") + val pile = MageTrainingArenaTest.findObject("bones_mage_training_arena_1", Tile(3333, 9610, 1), Tile(3390, 9663, 1)) + player.tele(pile.tile.addY(-1)) + tick(2) + + repeat(4) { + player.objectOption(GameObjects.find(pile.tile) { it.id.startsWith("bones_mage_training_arena") }, "Grab") + tick(3) + } + + assertEquals(4, player.inventory.count("animals_bones_1")) + assertNotNull(GameObjects.findOrNull(pile.tile, "bones_mage_training_arena_2")) + } + + @Test + fun `Animal bones convert to a multiple of fruit`() { + val player = enter("mta-grave-cast") + player.inventory.add("animals_bones_3", 2) + player.inventory.add("nature_rune") + player.inventory.add("water_rune", 2) + player.inventory.add("earth_rune", 2) + + player.interfaceOption("modern_spellbook", "bones_to_bananas", "Cast") + tick(1) + + assertEquals(0, player.inventory.count("animals_bones_3")) + assertEquals(6, player.inventory.count("banana")) + assertEquals(0, player.inventory.count("nature_rune")) + assertEquals(25.0, player.experience.get(Skill.Magic)) + } + + @Test + fun `Tablets can't be broken in the graveyard`() { + val player = enter("mta-grave-tablet") + player.inventory.add("animals_bones_1") + player.inventory.add("bones_to_bananas") + + player.itemOption("Break", "bones_to_bananas") + tick(1) + + assertTrue(player.inventory.contains("bones_to_bananas")) + assertTrue(player.inventory.contains("animals_bones_1")) + assertTrue(player.containsMessage("You can not use this tablet in the Mage Training Arena.")) + } + + @Test + fun `Sixteen fruit through the chute rewards a point, a rune and experience`() { + val player = enter("mta-grave-chute") + player["mage_training_arena_fruit_deposited"] = 10 + player.inventory.add("banana", 4) + player.inventory.add("peach", 4) + val chute = MageTrainingArenaTest.findObject("food_chute", Tile(3333, 9610, 1), Tile(3390, 9663, 1)) + player.tele(chute.tile.addY(-1)) + tick(2) + + player.objectOption(chute, "Deposit") + tick(5) + player.skipDialogues() + + assertEquals(0, player.inventory.count("banana")) + assertEquals(0, player.inventory.count("peach")) + assertEquals(2, player["mage_training_arena_fruit_deposited", 0]) + assertEquals(1, PizazzPoints.get(player, "graveyard")) + assertEquals(50.0, player.experience.get(Skill.Magic)) + assertEquals(1, listOf("nature_rune", "water_rune", "earth_rune").sumOf { player.inventory.count(it) }) + } + + @Test + fun `Falling bones damage everyone in the room`() { + val player = enter("mta-grave-bones") + val before = player.levels.get(Skill.Constitution) + + tick(12) + + assertTrue(player.levels.get(Skill.Constitution) < before) { "Expected damage below $before" } + } + + @Test + fun `Dying in the graveyard costs up to ten points and keeps items`() { + val player = enter("mta-grave-death") + player["mage_training_arena_graveyard_points"] = 15 + player.inventory.add("banana", 3) + + player.directHit(player.levels.get(Skill.Constitution)) + tick(10) + + assertEquals(5, PizazzPoints.get(player, "graveyard")) + assertTrue(player.containsMessage("You lost 10 Pizazz Points upon death!")) + assertEquals(MageTrainingArena.lobby, player.tile) + assertTrue(player.inventory.contains("progress_hat")) + } + + private fun enter(name: String): Player { + val player = createPlayer(Tile(3361, 3316), name) + player.levels.set(Skill.Magic, 50) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + player.tele(Tile(3363, 9639, 1)) + tick(2) + assertEquals("graveyard", player["mage_training_arena_room", ""]) + return player + } +} diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/EnchantingChamberTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/EnchantingChamberTest.kt new file mode 100644 index 0000000000..a9b3e40a7e --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/EnchantingChamberTest.kt @@ -0,0 +1,137 @@ +package content.minigame.mage_training_arena + +import WorldTest +import containsMessage +import interfaceOnItem +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.entity.character.move.tele +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.player.skill.Skill +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile + +internal class EnchantingChamberTest : WorldTest() { + + @Test + fun `Entering spawns private dragonstones and shows the bonus shape`() { + val player = enter("mta-ench-enter") + + assertEquals(6, EnchantingChamber.dragonstones(player).size) + val stone = FloorItems.at(Tile(3354, 9645)).firstOrNull { it.id == "dragonstone_mage_training_arena" } + assertEquals(player.name, stone?.owner) + } + + @Test + fun `Shapes are taken from piles`() { + val player = enter("mta-ench-pile") + val pile = MageTrainingArenaTest.findObject("cube_pile", Tile(3335, 9612), Tile(3389, 9663)) + player.tele(pile.tile.addY(-1)) + tick(2) + + player.objectOption(pile, "Take-from") + tick(5) + + assertTrue(player.inventory.contains("cube")) + } + + @Test + fun `Enchanting a shape makes an orb with reduced experience`() { + val player = enter("mta-ench-cast") + EnchantingChamber.bonus = "pentamid" + player.inventory.add("cube") + player.inventory.add("cosmic_rune") + player.inventory.add("water_rune") + + enchant(player, "cube") + + assertTrue(player.inventory.contains("orb")) + assertEquals(0, player.inventory.count("cosmic_rune")) + assertEquals(13.1, player.experience.get(Skill.Magic)) + assertEquals(1, player["mage_training_arena_shapes_converted", 0]) + assertEquals(0, PizazzPoints.get(player, "enchanting")) + } + + @Test + fun `Every tenth shape scores the spell level`() { + val player = enter("mta-ench-tenth") + EnchantingChamber.bonus = "pentamid" + player["mage_training_arena_shapes_converted"] = 9 + player.inventory.add("cylinder") + player.inventory.add("cosmic_rune") + player.inventory.add("water_rune") + + enchant(player, "cylinder") + + assertEquals(1, PizazzPoints.get(player, "enchanting")) + assertEquals(0, player["mage_training_arena_shapes_converted", 0]) + } + + @Test + fun `The bonus shape scores an extra point`() { + val player = enter("mta-ench-bonus") + EnchantingChamber.bonus = "icosahedron" + player.inventory.add("icosahedron") + player.inventory.add("cosmic_rune") + player.inventory.add("water_rune") + + enchant(player, "icosahedron") + + assertEquals(1, PizazzPoints.get(player, "enchanting")) + assertTrue(player.containsMessage("You get 1 bonus point!")) + } + + @Test + fun `Dragonstones score double the spell level immediately`() { + val player = enter("mta-ench-dragonstone") + player.inventory.add("dragonstone_mage_training_arena") + player.inventory.add("cosmic_rune") + player.inventory.add("water_rune") + + enchant(player, "dragonstone_mage_training_arena") + + assertEquals(2, PizazzPoints.get(player, "enchanting")) + assertTrue(player.inventory.contains("orb")) + } + + @Test + fun `Twenty orbs in the hole reward runes`() { + val player = enter("mta-ench-hole") + player["mage_training_arena_orbs_deposited"] = 5 + player.inventory.add("orb", 15) + val hole = MageTrainingArenaTest.findObject("orb_hole", Tile(3335, 9612), Tile(3389, 9663)) + player.tele(hole.tile.addY(-1)) + tick(2) + + player.objectOption(hole, "Deposit") + tick(5) + player.skipDialogues() + + assertEquals(0, player.inventory.count("orb")) + assertEquals(0, player["mage_training_arena_orbs_deposited", 0]) + assertEquals(3, listOf("death_rune", "blood_rune", "cosmic_rune").sumOf { player.inventory.count(it) }) + } + + private fun enchant(player: Player, item: String) { + player.interfaceOnItem("modern_spellbook", "enchant_level_1", Item(item), player.inventory.indexOf(item)) + tick(3) + } + + private fun enter(name: String): Player { + val player = createPlayer(Tile(3361, 3316), name) + player.levels.set(Skill.Magic, 50) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + player.tele(Tile(3363, 9649)) + tick(2) + assertEquals("enchanting", player["mage_training_arena_room", ""]) + return player + } +} diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewardsTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewardsTest.kt new file mode 100644 index 0000000000..9cb13a84e3 --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaRewardsTest.kt @@ -0,0 +1,125 @@ +package content.minigame.mage_training_arena + +import WorldTest +import containsMessage +import interfaceOption +import npcOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile + +internal class MageTrainingArenaRewardsTest : WorldTest() { + + @Test + fun `Buying deducts points from every room`() { + val player = open("mta-reward-buy", 30, 30, 300, 30) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("beginner_wand"), slot = 0) + tick(1) + + assertTrue(player.inventory.contains("beginner_wand")) + assertEquals(0, PizazzPoints.total(player)) + } + + @Test + fun `Can't afford without enough points in each room`() { + val player = open("mta-reward-poor", 30, 30, 299, 30) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("beginner_wand"), slot = 0) + tick(1) + + assertFalse(player.inventory.contains("beginner_wand")) + assertTrue(player.containsMessage("You cannot afford that item.")) + assertEquals(389, PizazzPoints.total(player)) + } + + @Test + fun `Wand upgrades consume the previous wand`() { + val player = open("mta-reward-wand", 60, 60, 600, 60) + player.equipment.add("beginner_wand") + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("apprentice_wand"), slot = 1) + tick(1) + + assertTrue(player.inventory.contains("apprentice_wand")) + assertFalse(player.equipment.contains("beginner_wand")) + assertEquals(0, PizazzPoints.total(player)) + } + + @Test + fun `Wand upgrades need the previous wand`() { + val player = open("mta-reward-no-wand", 60, 60, 600, 60) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("apprentice_wand"), slot = 1) + tick(1) + + assertFalse(player.inventory.contains("apprentice_wand")) + assertTrue(player.containsMessage("You don't have the required wand to buy this upgrade.")) + } + + @Test + fun `Bones to Peaches is learnt once`() { + val player = open("mta-reward-peaches", 400, 600, 4000, 400) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("bones_to_peaches_spell"), slot = 10) + tick(1) + player.skipDialogues() + + assertTrue(player["bones_to_peaches", false]) + assertFalse(player.inventory.contains("bones_to_peaches_spell")) + assertEquals(200, PizazzPoints.get(player, "telekinetic")) + assertEquals(300, PizazzPoints.get(player, "alchemist")) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("bones_to_peaches_spell"), slot = 10) + tick(1) + + assertTrue(player.containsMessage("You already unlocked that spell.")) + assertEquals(200, PizazzPoints.get(player, "telekinetic")) + } + + @Test + fun `The arena book costs coins`() { + val player = open("mta-reward-book", 0, 0, 0, 0) + player.inventory.add("coins", 250) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Buy", item = Item("arena_book"), slot = 25) + tick(1) + + assertTrue(player.inventory.contains("arena_book")) + assertEquals(50, player.inventory.count("coins")) + } + + @Test + fun `Value lists the cost per room`() { + val player = open("mta-reward-value", 0, 0, 0, 0) + + player.interfaceOption("mage_training_arena_rewards", "stock", "Value", item = Item("master_wand"), slot = 3) + tick(1) + + assertTrue(player.containsMessage("The Master wand costs 240 Telekinetic, 240 Alchemist,")) + assertTrue(player.containsMessage("2400 Enchantment and 240 Graveyard Pizazz Points.")) + } + + private fun open(name: String, telekinetic: Int, alchemist: Int, enchanting: Int, graveyard: Int): Player { + val player = createPlayer(Tile(3362, 3319, 1), name) + player["mage_training_arena_started"] = true + player["mage_training_arena_telekinetic_points"] = telekinetic + player["mage_training_arena_alchemist_points"] = alchemist + player["mage_training_arena_enchanting_points"] = enchanting + player["mage_training_arena_graveyard_points"] = graveyard + val guardian = createNPC("rewards_guardian", Tile(3362, 3318, 1)) + player.npcOption(guardian, "Trade-with") + tick(2) + assertTrue(player.hasOpen("mage_training_arena_rewards")) + return player + } +} diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaTest.kt new file mode 100644 index 0000000000..39c1b7c367 --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/MageTrainingArenaTest.kt @@ -0,0 +1,169 @@ +package content.minigame.mage_training_arena + +import WorldTest +import containsMessage +import dialogueOption +import itemOption +import npcOption +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import skipDialogues +import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.player.Player +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.obj.GameObject +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.equipment +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import world.gregs.voidps.type.Tile + +internal class MageTrainingArenaTest : WorldTest() { + + @Test + fun `Entrance guardian hands out a progress hat`() { + val player = createPlayer(Tile(3363, 3305), "mta-hat") + val guardian = createNPC("entrance_guardian", Tile(3363, 3304)) + + player.npcOption(guardian, "Talk-to") + tick(1) + player.skipDialogues() + player.dialogueOption(1) + player.skipDialogues() + + assertTrue(player["mage_training_arena_started", false]) + assertTrue(player.inventory.contains("progress_hat")) + assertEquals("progress_hat", PizazzHat.tier(player)) + } + + @Test + fun `Portals refuse players without a progress hat`() { + val player = createPlayer(Tile(3361, 3316), "mta-no-hat") + player.levels.set(Skill.Magic, 50) + val portal = findObject("enchanters_portal", Tile(3355, 3310), Tile(3372, 3325)) + + player.objectOption(portal, "Enter") + tick(3) + + assertEquals(0, player.tile.level) + assertTrue(player.tile.y < 3330) + assertFalse(player.contains("mage_training_arena_room")) + } + + @Test + fun `Portals take hat holders into the room and open the overlay`() { + val player = createPlayer(Tile(3361, 3316), "mta-enter") + player.levels.set(Skill.Magic, 50) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + val portal = findObject("enchanters_portal", Tile(3355, 3310), Tile(3372, 3325)) + + player.objectOption(portal, "Enter") + tick(3) + + assertEquals(Tile(3363, 9649), player.tile) + assertEquals("enchanting", player["mage_training_arena_room", ""]) + assertTrue(player.hasOpen("mage_training_arena_enchanting")) + assertTrue(player.containsMessage("You've entered the Enchanting Chamber.")) + } + + @Test + fun `Exit portal returns to the lobby and confiscates arena items`() { + val player = enter("mta-exit", "enchanting", Tile(3363, 9649)) + player.inventory.add("cube", 3) + val exit = findObject("exit_portal_mage_training_arena", Tile(3335, 9612), Tile(3389, 9663)) + + player.objectOption(exit, "Enter") + tick(10) + + assertEquals(Tile(3361, 3318), player.tile) + assertFalse(player.inventory.contains("cube")) + assertFalse(player.contains("mage_training_arena_room")) + assertFalse(player.hasOpen("mage_training_arena_enchanting")) + } + + @Test + fun `Teleport spells are blocked inside the arena`() { + val player = enter("mta-teleport", "enchanting", Tile(3363, 9649)) + + Teleport.teleport(player, Tile(3212, 3424), "modern", "varrock_teleport") + tick(5) + + assertEquals(Tile(3363, 9649), player.tile) + assertTrue(player.containsMessage("You can't teleport out of the training arena!")) + } + + @Test + fun `Pizazz points upgrade the hat tier`() { + val player = createPlayer(Tile(3363, 3305), "mta-tier") + player.inventory.add("progress_hat") + + PizazzPoints.add(player, "enchanting", 301) + + assertTrue(player.inventory.contains("progress_hat_2")) + assertFalse(player.inventory.contains("progress_hat")) + + PizazzPoints.add(player, "graveyard", 300) + + assertTrue(player.inventory.contains("progress_hat_3")) + assertEquals(601, PizazzPoints.total(player)) + } + + @Test + fun `The hat can be talked to while worn`() { + val player = createPlayer(Tile(3363, 3305), "mta-worn-hat") + player.equipment.add("progress_hat") + + player.itemOption("Talk-to", "progress_hat", id = "worn_equipment", component = "hat_slot", optionIndex = 1, inventory = "worn_equipment", slot = EquipSlot.Hat.index) + tick(1) + + assertNotNull(player.dialogue) + player.skipDialogues() + assertNull(player.dialogue) + } + + @Test + fun `The arena wizards have something to say`() { + val player = createPlayer(Tile(3365, 3322, 1), "mta-wizards") + for (id in listOf("wizard_shug", "apprentice_clerval", "wizard_dougal", "wizard_edvin")) { + val npc = createNPC(id, player.tile.addX(1)) + player.npcOption(npc, "Talk-to") + tick(1) + assertNotNull(player.dialogue) { id } + player.skipDialogues() + assertNull(player.dialogue) { id } + } + } + + private fun enter(name: String, room: String, tile: Tile): Player { + val player = createPlayer(Tile(3361, 3316), name) + player.levels.set(Skill.Magic, 50) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + player.tele(tile) + tick(2) + assertEquals(room, player["mage_training_arena_room", ""]) + return player + } + + companion object { + fun findObject(id: String, from: Tile, to: Tile): GameObject { + for (x in from.x..to.x) { + for (y in from.y..to.y) { + val obj = GameObjects.findOrNull(Tile(x, y, from.level), id) ?: continue + return obj + } + } + error("Object $id not found between $from and $to") + } + } +} diff --git a/game/src/test/kotlin/content/minigame/mage_training_arena/TelekineticTheatreTest.kt b/game/src/test/kotlin/content/minigame/mage_training_arena/TelekineticTheatreTest.kt new file mode 100644 index 0000000000..fa73ddee10 --- /dev/null +++ b/game/src/test/kotlin/content/minigame/mage_training_arena/TelekineticTheatreTest.kt @@ -0,0 +1,168 @@ +package content.minigame.mage_training_arena + +import WorldTest +import content.quest.instance +import floorItemOption +import interfaceOnFloorItem +import objectOption +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.rsmod.game.pathfinder.StepValidator +import org.rsmod.game.pathfinder.collision.CollisionStrategies +import skipDialogues +import world.gregs.voidps.engine.client.ui.hasOpen +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.get +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.Tile + +internal class TelekineticTheatreTest : WorldTest() { + + @Test + fun `Entering the portal instances the first unsolved maze`() { + val player = enter("mta-maze-enter") + + assertNotNull(player.instance()) + assertEquals("telekinetic", player["mage_training_arena_room", ""]) + assertEquals(1, player["mage_training_arena_telekinetic_maze", 0]) + assertEquals(TelekineticTheatre.local(player, Tile(8, 54)), player.tile) + assertEquals(TelekineticTheatre.local(player, Tile(15, 41)), TelekineticTheatre.statue(player)?.tile) + assertTrue(player.hasOpen("mage_training_arena_telekinetic")) + assertTrue(NPCs.any { it.id == "telekinetic_guardian" && it.tile == TelekineticTheatre.local(player, Tile(8, 39)) }) + } + + @Test + fun `Telegrab slides the statue towards the side the player stands on`() { + val player = enter("mta-maze-slide") + player.inventory.add("law_rune", 5) + player.inventory.add("air_rune", 5) + player.tele(TelekineticTheatre.local(player, Tile(15, 51))) + tick(2) + val statue = TelekineticTheatre.statue(player)!! + val before = statue.tile + + player.interfaceOnFloorItem("modern_spellbook", "telekinetic_grab", statue) + tick(15) + + val after = TelekineticTheatre.statue(player)!!.tile + assertTrue(after.y > before.y) { "Statue didn't move north: $before -> $after" } + assertEquals(before.x, after.x) + assertEquals(4, player.inventory.count("law_rune")) + assertEquals(43.0, player.experience.get(Skill.Magic)) + } + + @Test + fun `Reaching the exit square awards points and frees the guardian`() { + val player = enter("mta-maze-win") + player.inventory.add("law_rune", 5) + player.inventory.add("air_rune", 5) + val end = solve(player) + tick(15) + player.skipDialogues() + tick(1) + + assertEquals(2, PizazzPoints.get(player, "telekinetic")) + assertEquals(1, player["mage_training_arena_maze_streak", 0]) + assertEquals(1023, player["mage_training_arena_mazes_solved", 0]) + assertNull(TelekineticTheatre.statue(player)) + assertTrue(NPCs.any { it.id == "maze_guardian" && it.tile == end }) + } + + @Test + fun `Fifth maze in a row gives bonus points, law runes and experience`() { + val player = enter("mta-maze-bonus") + player.inventory.add("law_rune", 5) + player.inventory.add("air_rune", 5) + player["mage_training_arena_maze_streak"] = 4 + solve(player) + tick(15) + player.skipDialogues() + tick(1) + + assertEquals(10, PizazzPoints.get(player, "telekinetic")) + assertEquals(0, player["mage_training_arena_maze_streak", 0]) + assertEquals(14, player.inventory.count("law_rune")) + assertEquals(1043.0, player.experience.get(Skill.Magic)) + } + + @Test + fun `Exit portal frees the instance and returns to the lobby`() { + val player = enter("mta-maze-exit") + val base = TelekineticTheatre.local(player, Tile(0, 0)) + val exit = MageTrainingArenaTest.findObject("exit_portal_mage_training_arena", base, base.add(63, 63)) + player.tele(exit.tile.addY(-1)) + tick(2) + + player.objectOption(exit, "Enter") + tick(10) + + assertNull(player.instance()) + assertEquals(Tile(3363, 3316), player.tile) + } + + @Test + fun `Observe and Reset work on the statue from across the maze`() { + val player = enter("mta-maze-observe") + player.tele(TelekineticTheatre.local(player, Tile(15, 51))) + tick(2) + val start = TelekineticTheatre.statue(player)!!.tile + + player.floorItemOption(TelekineticTheatre.statue(player)!!, "Observe") + tick(3) + + assertTrue(player["mage_training_arena_camera", false]) + assertEquals(TelekineticTheatre.local(player, Tile(15, 51)), player.tile) + + TelekineticTheatre.placeStatue(player, start.addY(1)) + player.floorItemOption(TelekineticTheatre.statue(player)!!, "Reset") + tick(3) + + assertEquals(start, TelekineticTheatre.statue(player)!!.tile) + } + + /** + * Places the statue one step from the exit and casts from the matching side of the maze. + */ + private fun solve(player: Player): Tile { + val end = TelekineticTheatre.local(player, Tile(19, 50)) + val validator = get() + val direction = listOf(Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST).first { direction -> + val from = end.minus(direction) + validator.canTravel(level = from.level, x = from.x, z = from.y, offsetX = direction.delta.x, offsetZ = direction.delta.y, size = 1, extraFlag = 0, collision = CollisionStrategies.Normal) + } + val statueTile = end.minus(direction) + TelekineticTheatre.placeStatue(player, statueTile) + val origin = TelekineticTheatre.local(player, Tile(0, 0)) + val side = when (direction) { + Direction.NORTH -> Tile(statueTile.x, origin.y + 51 + 1, statueTile.level) + Direction.SOUTH -> Tile(statueTile.x, origin.y + 40 - 1, statueTile.level) + Direction.EAST -> Tile(origin.x + 20 + 1, statueTile.y, statueTile.level) + else -> Tile(origin.x + 9 - 1, statueTile.y, statueTile.level) + } + player.tele(side) + tick(2) + player.interfaceOnFloorItem("modern_spellbook", "telekinetic_grab", TelekineticTheatre.statue(player)!!) + return end + } + + private fun enter(name: String): Player { + val player = createPlayer(Tile(3361, 3316), name) + player.levels.set(Skill.Magic, 50) + player["mage_training_arena_started"] = true + player.inventory.add("progress_hat") + player["mage_training_arena_mazes_solved"] = 1022 + val portal = MageTrainingArenaTest.findObject("telekinetic_portal", Tile(3355, 3310), Tile(3372, 3325)) + player.objectOption(portal, "Enter") + tick(10) + assertEquals("telekinetic", player["mage_training_arena_room", ""]) + return player + } +} diff --git a/game/src/test/kotlin/content/skill/magic/book/modern/BonesToTest.kt b/game/src/test/kotlin/content/skill/magic/book/modern/BonesToTest.kt index 0ec508cc73..788d358b6a 100644 --- a/game/src/test/kotlin/content/skill/magic/book/modern/BonesToTest.kt +++ b/game/src/test/kotlin/content/skill/magic/book/modern/BonesToTest.kt @@ -1,8 +1,10 @@ package content.skill.magic.book.modern import WorldTest +import containsMessage import interfaceOption import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import world.gregs.voidps.engine.entity.character.player.skill.Skill @@ -15,6 +17,7 @@ class BonesToTest : WorldTest() { @ValueSource(strings = ["bananas", "peaches"]) fun `Convert all bones and big bones`(type: String) { val player = createPlayer() + player["bones_to_peaches"] = true player.levels.set(Skill.Magic, 60) player.inventory.add("nature_rune", 2) player.inventory.add("water_rune", 4) @@ -46,6 +49,7 @@ class BonesToTest : WorldTest() { @ValueSource(strings = ["bananas", "peaches"]) fun `Can't cast without runes`(type: String) { val player = createPlayer() + player["bones_to_peaches"] = true player.levels.set(Skill.Magic, 60) player.inventory.add("nature_rune", 2) player.inventory.add("water_rune", 1) @@ -69,6 +73,7 @@ class BonesToTest : WorldTest() { @ValueSource(strings = ["bananas", "peaches"]) fun `Can't cast without level`(type: String) { val player = createPlayer() + player["bones_to_peaches"] = true player.levels.set(Skill.Magic, 14) player.inventory.add("nature_rune", 2) player.inventory.add("water_rune", 4) @@ -92,6 +97,7 @@ class BonesToTest : WorldTest() { @ValueSource(strings = ["bananas", "peaches"]) fun `Can't cast without bones`(type: String) { val player = createPlayer() + player["bones_to_peaches"] = true player.levels.set(Skill.Magic, 10) player.inventory.add("nature_rune", 2) player.inventory.add("water_rune", 4) @@ -106,4 +112,21 @@ class BonesToTest : WorldTest() { assertEquals(0.0, player.experience.get(Skill.Magic)) assertEquals(0, player.inventory.count(if (type == "bananas") "banana" else "peach")) } + + @Test + fun `Bones to Peaches must be learnt at the Mage Training Arena`() { + val player = createPlayer() + player.levels.set(Skill.Magic, 60) + player.inventory.add("nature_rune", 2) + player.inventory.add("water_rune", 4) + player.inventory.add("earth_rune", 4) + player.inventory.add("bones") + + player.interfaceOption("modern_spellbook", "bones_to_peaches", "Cast") + tick(1) + + assertEquals(1, player.inventory.count("bones")) + assertEquals(2, player.inventory.count("nature_rune")) + assertTrue(player.containsMessage("You can only learn this spell from the Mage Training Arena.")) + } }