From 612ccc677678902e7f6270574c1f5a07be44cc75 Mon Sep 17 00:00:00 2001 From: Oven Bread Date: Tue, 11 Feb 2025 13:03:30 +0000 Subject: [PATCH] Implemented Maze random event --- .../main/content/global/ame/RandomEvents.kt | 2 + .../global/ame/events/maze/MazeInterface.kt | 279 ++++++++++++++++++ .../content/global/ame/events/maze/MazeNPC.kt | 58 ++++ .../game/global/action/DoorActionHandler.java | 4 +- .../main/core/net/packet/PacketProcessor.kt | 6 + 5 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 Server/src/main/content/global/ame/events/maze/MazeInterface.kt create mode 100644 Server/src/main/content/global/ame/events/maze/MazeNPC.kt diff --git a/Server/src/main/content/global/ame/RandomEvents.kt b/Server/src/main/content/global/ame/RandomEvents.kt index 9ab78c468..9addefbe4 100644 --- a/Server/src/main/content/global/ame/RandomEvents.kt +++ b/Server/src/main/content/global/ame/RandomEvents.kt @@ -8,6 +8,7 @@ import content.global.ame.events.drunkendwarf.DrunkenDwarfNPC import content.global.ame.events.evilbob.EvilBobNPC import content.global.ame.events.evilchicken.EvilChickenNPC import content.global.ame.events.freakyforester.FreakyForesterNPC +import content.global.ame.events.maze.MazeNPC import content.global.ame.events.genie.GenieNPC import content.global.ame.events.pillory.PilloryNPC import content.global.ame.events.rickturpentine.RickTurpentineNPC @@ -45,6 +46,7 @@ enum class RandomEvents(val npc: RandomEventNPC, val loot: WeightBasedTable? = n WeightedItem(Items.TOOTH_HALF_OF_A_KEY_985,1,1,0.1), WeightedItem(Items.LOOP_HALF_OF_A_KEY_987,1,1,0.1) )), + MAZE(npc = MazeNPC()), DRILL_DEMON(npc = SeargentDamienNPC()), EVIL_CHICKEN(npc = EvilChickenNPC()), STRANGE_PLANT(npc = StrangePlantNPC()), diff --git a/Server/src/main/content/global/ame/events/maze/MazeInterface.kt b/Server/src/main/content/global/ame/events/maze/MazeInterface.kt new file mode 100644 index 000000000..8a317e57a --- /dev/null +++ b/Server/src/main/content/global/ame/events/maze/MazeInterface.kt @@ -0,0 +1,279 @@ +package content.global.ame.events.maze + +import core.api.* +import core.api.utils.WeightBasedTable +import core.api.utils.WeightedItem +import core.game.event.EventHook +import core.game.event.TickEvent +import core.game.global.action.DoorActionHandler +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.interaction.QueueStrength +import core.game.node.entity.Entity +import core.game.node.entity.player.Player +import core.game.node.scenery.SceneryBuilder +import core.game.system.task.Pulse +import core.game.world.GameWorld.Pulser +import core.game.world.map.Location +import core.game.world.map.zone.ZoneBorders +import core.game.world.map.zone.ZoneRestriction +import core.game.world.update.flag.context.Graphics +import org.rs09.consts.* + +class MazeInterface : InteractionListener, EventHook, MapArea { + companion object { + const val MAZE_TIMER_INTERFACE = Components.MAZETIMER_209 + const val MAZE_TIMER_VARP = 531 // Interface 209 child 2 config: [531, 0] + const val MAZE_ATTRIBUTE_RETURN_LOC = "/save:original-loc" + const val MAZE_ATTRIBUTE_TICKS_LEFT = "maze:percent-ticks-left" + const val MAZE_ATTRIBUTE_CHESTS_OPEN = "/save:maze:chests-opened" + + val STARTING_POINTS = arrayOf( + Location(2928, 4553, 0), + Location(2917, 4553, 0), + Location(2908, 4555, 0), + Location(2891, 4589, 0), + Location(2891, 4595, 0), + Location(2891, 4595, 0), + Location(2926, 4597, 0), + Location(2931, 4597, 0), + // There's 2 more, but there isn't a door for them... + ) + + val REWARD_ITEM = intArrayOf( + Items.COINS_995, + Items.FEATHER_314, + Items.IRON_ARROW_884, + Items.CHAOS_RUNE_562, + Items.STEEL_ARROW_886, + Items.DEATH_RUNE_560, + Items.COAL_454, + Items.NATURE_RUNE_561, + Items.MITHRIL_ORE_448 + ) + + val ITEM_DIVISOR = arrayOf( + 1.0, + 2.0, + 3.0, + 9.0, + 12.0, + 18.0, + 45.0, + 162.0, + 180.0, + ) + + val CHEST_REWARDS = WeightBasedTable.create( + WeightedItem(Items.AIR_RUNE_556,15,15,1.0), + WeightedItem(Items.WATER_RUNE_555,10,10,1.0), + WeightedItem(Items.EARTH_RUNE_557,10,10,1.0), + WeightedItem(Items.FIRE_RUNE_554,10,10,1.0), + WeightedItem(Items.BRONZE_ARROW_882,20,20,1.0), + WeightedItem(Items.BRONZE_BOLTS_877,10,10,1.0), + WeightedItem(Items.IRON_ARROW_884,15,15,1.0), + WeightedItem(Items.ATTACK_POTION2_123,1,1,1.0), + WeightedItem(Items.STRENGTH_POTION2_117,1,1,1.0), + WeightedItem(Items.DEFENCE_POTION2_135,1,1,1.0), + ) + + fun initMaze(player: Player) { + setAttribute(player, MAZE_ATTRIBUTE_TICKS_LEFT, 300) + setVarp(player, MAZE_TIMER_VARP, (getAttribute(player, MAZE_ATTRIBUTE_TICKS_LEFT, 0) / 3),false) + openOverlay(player, MAZE_TIMER_INTERFACE) + sendMessage(player, "You need to reach the maze center, then you'll be returned to where you were.") + sendNPCDialogue(player, NPCs.MYSTERIOUS_OLD_MAN_410, "You need to reach the maze center, then you'll be returned to where you were.") + } + + fun calculateLoot(player: Player) { + val randomNumber = (0..8).random() + val totalLevel = player.getSkills().totalLevel.toDouble() + val rewardPotential = getAttribute(player, MAZE_ATTRIBUTE_TICKS_LEFT, 0).toDouble() / 300.0 + val itemDivisor = ITEM_DIVISOR[randomNumber] + val itemQuantity = (totalLevel * rewardPotential * 3.33) / itemDivisor + // sendMessage(player, "Maze reward calculation: $totalLevel * $rewardPotential * 3.33 / $itemDivisor = $itemQuantity") + if (itemQuantity.toInt() > 0) { + addItemOrDrop(player, REWARD_ITEM[randomNumber], itemQuantity.toInt()) + } + } + + /** + * Chest Location to rotation mapping. + * This is needed as it is impossible to obtain the underlying chest scenery for the rotation. + * 0: Facing North, 1: Facing East, 2: Facing South, 3: Facing West + */ + val chestLocationRotationMap = mapOf( + Location(2930, 4595, 0).toString() to 2, + Location(2924, 4572, 0).toString() to 2, + Location(2925, 4573, 0).toString() to 0, + Location(2900, 4578, 0).toString() to 2, + Location(2901, 4560, 0).toString() to 1, + Location(2890, 4599, 0).toString() to 2, + Location(2896, 4591, 0).toString() to 2, + Location(2895, 4592, 0).toString() to 1, + Location(2901, 4560, 0).toString() to 3, + Location(2918, 4590, 0).toString() to 1, + Location(2917, 4590, 0).toString() to 3, + ) + + /** + * Chest Interaction workaround + * + * The issue here is that the walls(3626) of the Maze are overlapping some(not all) chest sceneries. + * + * The types for the wallScenery: + * Type 0 - flat panel | + * Type 2 - right angle panel with rotation 0:r 1:7 2:> 3:L + * Type 3 - corner post . for angle edges of walls + */ + fun overrideScenery(wallScenery: core.game.node.scenery.Scenery, chestSceneryId: Int): core.game.node.scenery.Scenery { + if (wallScenery.id == chestSceneryId) { + replaceScenery(wallScenery, Scenery.CHEST_3636, 30) + wallScenery.isActive = true + return wallScenery // Return the chest scenery as the wallScenery isn't there. + } + + addScenery(Scenery.CHEST_3636, wallScenery.location, chestLocationRotationMap[wallScenery.location.toString()] ?: 0, 10) + addScenery(wallScenery) + // replaceScenery(newChestScenery, Scenery.CHEST_3636, 3) // didn't work for an underlying scenery + // I did a world pulse since everyone will get to see the chest open. + Pulser.submit(object : Pulse(30) { + override fun pulse(): Boolean { + addScenery(Scenery.CHEST_3635, wallScenery.location, chestLocationRotationMap[wallScenery.location.toString()] ?: 0, 10) + addScenery(wallScenery) + return true + } + }) + // Return the chest scenery to replace PacketProcessor so that MISMATCH will not happen. + return core.game.node.scenery.Scenery( + chestSceneryId, + wallScenery.location, + chestLocationRotationMap[wallScenery.location.toString()] ?: 0 + ) + } + } + + override fun defineListeners() { + + // This somehow doesn't trigger as the scenery.id != objId (3626 != 3635) + on(Scenery.CHEST_3635, IntType.SCENERY, "open") { player, node -> + if (getAttribute(player, MAZE_ATTRIBUTE_TICKS_LEFT, 0) > 0 && getAttribute(player, MAZE_ATTRIBUTE_CHESTS_OPEN, 0) < 10) { + animate(player, 536) + // val actualScenery = RegionManager.getObject(node.location.z, node.location.x, node.location.y, 3626) + val tableRoll = CHEST_REWARDS.roll() + addItemOrBank(player, tableRoll[0].id) + when (tableRoll[0].id){ + Items.AIR_RUNE_556 -> sendItemDialogue(player, Items.AIR_RUNE_556, "You've found some air runes!") + Items.WATER_RUNE_555 -> sendItemDialogue(player, Items.WATER_RUNE_555, "You've found some water runes!") + Items.EARTH_RUNE_557 -> sendItemDialogue(player, Items.EARTH_RUNE_557, "You've found some earth runes!") + Items.FIRE_RUNE_554 -> sendItemDialogue(player, Items.FIRE_RUNE_554, "You've found some fire runes!") + Items.BRONZE_ARROW_882 -> sendItemDialogue(player, Items.BRONZE_ARROW_882, "You've found some bronze arrows!") + Items.BRONZE_BOLTS_877 -> sendItemDialogue(player, Items.BRONZE_BOLTS_877, "You've found some bronze bolts!") + Items.IRON_ARROW_884 -> sendItemDialogue(player, Items.IRON_ARROW_884, "You've found some iron arrows!") + Items.ATTACK_POTION2_123 -> sendItemDialogue(player, Items.ATTACK_POTION2_123, "You've found an attack potion!") + Items.STRENGTH_POTION2_117 -> sendItemDialogue(player, Items.STRENGTH_POTION2_117, "You've found a strength potion!") + Items.DEFENCE_POTION2_135 -> sendItemDialogue(player, Items.DEFENCE_POTION2_135, "You've found a defence potion!") + } + setAttribute(player, MAZE_ATTRIBUTE_CHESTS_OPEN, getAttribute(player, MAZE_ATTRIBUTE_CHESTS_OPEN, 0)) + } else { + sendMessage(player,"You find nothing of interest.") + } + return@on true + } + + on(Scenery.CHEST_3636, SCENERY, "search") { player, node -> + sendMessage(player,"You find nothing of interest.") + return@on true + } + + on(Scenery.WALL_3626, IntType.SCENERY, "open") { player, node -> + sendMessage(player, "That bit doesn't open.") // 0xBrLo9woIY + return@on true + } + + on(Scenery.WALL_3628, IntType.SCENERY, "open") { player, node -> + // Door opening workaround + // Ignore 3629(WALL_3629) and 3630(WALL_3630) in handleAutowalkDoor ignoreSecondDoor + DoorActionHandler.handleAutowalkDoor(player, node as core.game.node.scenery.Scenery) + return@on true + } + + on(Scenery.STRANGE_SHRINE_3634, IntType.SCENERY, "touch") { player, node -> + player.unhook(this) + closeOverlay(player) + queueScript(player, 0, QueueStrength.SOFT) { stage: Int -> + when (stage) { + 0 -> { + lock(player, 6) + sendGraphics(Graphics(86, 0, 3), player.location) + animate(player,862) + return@queueScript delayScript(player, 6) + } + 1 -> { + lock(player, 6) + sendGraphics(Graphics(1576, 0, 0), player.location) + animate(player,8939) + playAudio(player, Sounds.TELEPORT_ALL_200) + return@queueScript delayScript(player, 3) + } + 2 -> { + teleport(player, getAttribute(player, MAZE_ATTRIBUTE_RETURN_LOC, Location.create(3222, 3218, 0))) + sendGraphics(Graphics(1577, 0, 0), player.location) + removeAttribute(player, MAZE_ATTRIBUTE_RETURN_LOC) + animate(player,8941) + closeOverlay(player) + return@queueScript delayScript(player, 1) + } + 3 -> { + calculateLoot(player) + removeAttribute(player, MAZE_ATTRIBUTE_TICKS_LEFT) + removeAttribute(player, MAZE_ATTRIBUTE_CHESTS_OPEN) + return@queueScript stopExecuting(player) + } + else -> return@queueScript stopExecuting(player) + } + } + + return@on true + } + } + + override fun process(entity: Entity, event: TickEvent) { + if (entity is Player) { + if (getAttribute(entity, MAZE_ATTRIBUTE_TICKS_LEFT, 0) > 0) { + setAttribute(entity, MAZE_ATTRIBUTE_TICKS_LEFT, getAttribute(entity, MAZE_ATTRIBUTE_TICKS_LEFT, 0) - 1) + } + setVarp(entity, MAZE_TIMER_VARP, (getAttribute(entity, MAZE_ATTRIBUTE_TICKS_LEFT, 0) / 3), false) + } + } + + override fun defineAreaBorders(): Array { + return arrayOf(getRegionBorders(11591)) + } + + override fun getRestrictions(): Array { + return arrayOf(ZoneRestriction.RANDOM_EVENTS, ZoneRestriction.CANNON, ZoneRestriction.FOLLOWERS, ZoneRestriction.TELEPORT, ZoneRestriction.OFF_MAP) + } + + override fun areaEnter(entity: Entity) { + if (entity is Player) { + sendMessage(entity, "Head for the center of the maze.") + entity.interfaceManager.removeTabs(0, 1, 2, 3, 4, 5, 6, 12) + openOverlay(entity, MAZE_TIMER_INTERFACE) + } + } + + override fun areaLeave(entity: Entity, logout: Boolean) { + if (entity is Player) { + entity.interfaceManager.restoreTabs() + closeOverlay(entity) + entity.unhook(this) + } + } + override fun entityStep(entity: Entity, location: Location, lastLocation: Location) { + if (entity is Player) { + entity.hook(Event.Tick, this) + } + } + +} \ No newline at end of file diff --git a/Server/src/main/content/global/ame/events/maze/MazeNPC.kt b/Server/src/main/content/global/ame/events/maze/MazeNPC.kt new file mode 100644 index 000000000..973f8423d --- /dev/null +++ b/Server/src/main/content/global/ame/events/maze/MazeNPC.kt @@ -0,0 +1,58 @@ +package content.global.ame.events.maze + +import content.global.ame.RandomEventNPC +import core.api.* +import core.api.utils.WeightBasedTable +import core.game.interaction.QueueStrength +import core.game.node.entity.npc.NPC +import core.game.system.timer.impl.AntiMacro +import core.game.world.map.Location +import core.game.world.map.build.DynamicRegion +import core.game.world.update.flag.context.Graphics +import org.rs09.consts.NPCs +import org.rs09.consts.Sounds + +class MazeNPC(var type: String = "", override var loot: WeightBasedTable? = null) : RandomEventNPC(NPCs.MYSTERIOUS_OLD_MAN_410) { + + override fun init() { + super.init() + sendChat("Aha, you'll do ${player.username}!") + face(player) + queueScript(player, 4, QueueStrength.SOFT) { stage: Int -> + when (stage) { + 0 -> { + lock(player, 6) + sendGraphics(Graphics(1576, 0, 0), player.location) + animate(player,8939) + playAudio(player, Sounds.TELEPORT_ALL_200) + return@queueScript delayScript(player, 3) + } + 1 -> { + if (getAttribute(player, MazeInterface.MAZE_ATTRIBUTE_RETURN_LOC, null) == null) { + setAttribute(player, MazeInterface.MAZE_ATTRIBUTE_RETURN_LOC, player.location) + } + MazeInterface.initMaze(player) + // Note: This event is NOT instanced: + // Sources: + // https://youtu.be/2gpzn9oNdy0 (2007) + // https://youtu.be/Tni1HURgnxg (2008) + // https://youtu.be/igdwDZOv9LU (2008) + // https://youtu.be/0oBCkLArUmc (2011 - even with personal Mysterious Old Man) - "Sorry, this is not the old man you are looking for." + // https://youtu.be/FMuKZm-Ikgs (2011) + // val region = DynamicRegion.create(11591) + teleport(player, MazeInterface.STARTING_POINTS.random()) // 10 random spots + AntiMacro.terminateEventNpc(player) + sendGraphics(Graphics(1577, 0, 0), player.location) + animate(player,8941) + removeAttribute(player, MazeInterface.MAZE_ATTRIBUTE_CHESTS_OPEN) + return@queueScript stopExecuting(player) + } + else -> return@queueScript stopExecuting(player) + } + } + } + + override fun talkTo(npc: NPC) { + // Do nothing. + } +} \ No newline at end of file diff --git a/Server/src/main/core/game/global/action/DoorActionHandler.java b/Server/src/main/core/game/global/action/DoorActionHandler.java index 028030824..02234e136 100644 --- a/Server/src/main/core/game/global/action/DoorActionHandler.java +++ b/Server/src/main/core/game/global/action/DoorActionHandler.java @@ -115,7 +115,9 @@ public final class DoorActionHandler { if (object.getCharge() == IN_USE_CHARGE) { return false; } - final Scenery second = (object.getId() == 3) ? null : getSecondDoor(object, entity); + // TODO: Maybe have this passed in as an optional parameter or overload handleAutowalkDoor? + boolean ignoreSecondDoor = (object.getId() == 3628 || object.getId() == 3629 || object.getId() == 3630 || object.getId() == 3631|| object.getId() == 3632); // Ignore second door for Maze Random + final Scenery second = (object.getId() == 3 || ignoreSecondDoor) ? null : getSecondDoor(object, entity); entity.lock(4); final Location loc = entity.getLocation(); if (entity instanceof Player) { diff --git a/Server/src/main/core/net/packet/PacketProcessor.kt b/Server/src/main/core/net/packet/PacketProcessor.kt index a171b9c63..d6028d648 100644 --- a/Server/src/main/core/net/packet/PacketProcessor.kt +++ b/Server/src/main/core/net/packet/PacketProcessor.kt @@ -15,6 +15,7 @@ import core.game.node.entity.player.info.Rights import core.game.node.entity.player.info.login.LoginConfiguration import core.game.node.entity.player.link.SpellBookManager import core.game.node.entity.combat.spell.MagicSpell +import content.global.ame.events.maze.MazeInterface import content.global.skill.summoning.familiar.FamiliarSpecial import core.game.node.item.GroundItemManager import core.game.node.item.Item @@ -670,6 +671,11 @@ object PacketProcessor { if (pkt.id == 6899) scenery = Scenery(6899, Location(3221, 9618)) + // Random Event Maze chests are overridden by the walls, which needs to be hacked in. + if ((scenery?.id == 3626 || scenery?.id == 3635) && (objId in 3635..3636)) { + scenery = MazeInterface.overrideScenery(scenery, objId) + } + // Family crest levers don't have varps associated with them, so their state is validated with attributes // instead, and they always appear as their down/odd variant in the server's map if (objId in 2421..2426 && objId % 2 == 0) {