Implemented Maze random event

This commit is contained in:
Oven Bread
2025-02-11 13:03:30 +00:00
committed by Ryan
parent b5784c5782
commit 612ccc6776
5 changed files with 348 additions and 1 deletions
@@ -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()),
@@ -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<TickEvent>, 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<Int>(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<ZoneBorders> {
return arrayOf(getRegionBorders(11591))
}
override fun getRestrictions(): Array<ZoneRestriction> {
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)
}
}
}
@@ -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<Location?>(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.
}
}
@@ -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) {
@@ -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) {