From fecb24365024b8c2e63554172ca40d049d5cc1e3 Mon Sep 17 00:00:00 2001 From: oftheshire Date: Sun, 21 Jun 2026 08:03:39 +0000 Subject: [PATCH 01/42] Implemented swamp decay --- .../morytania/handlers/MorytaniaListeners.kt | 27 +++- .../handlers/MoyrtniaSwampPlugin.java | 30 ---- .../morytania/handlers/SwampDecayHandler.kt | 148 ++++++++++++++++++ 3 files changed, 174 insertions(+), 31 deletions(-) delete mode 100644 Server/src/main/content/region/morytania/handlers/MoyrtniaSwampPlugin.java create mode 100644 Server/src/main/content/region/morytania/handlers/SwampDecayHandler.kt diff --git a/Server/src/main/content/region/morytania/handlers/MorytaniaListeners.kt b/Server/src/main/content/region/morytania/handlers/MorytaniaListeners.kt index 0cd82feda..254bbd4a0 100644 --- a/Server/src/main/content/region/morytania/handlers/MorytaniaListeners.kt +++ b/Server/src/main/content/region/morytania/handlers/MorytaniaListeners.kt @@ -14,6 +14,8 @@ import core.game.interaction.InteractionListener import core.game.interaction.IntType import kotlin.random.Random import content.data.Quests +import core.game.interaction.InterfaceListener +import org.rs09.consts.Components /** * File to be used for anything Morytania related. @@ -40,12 +42,15 @@ class MorytaniaListeners : InteractionListener { on(SWAMP_GATES, IntType.SCENERY, "open"){ player, node -> if(player.location.y == 3457){ core.game.global.action.DoorActionHandler.handleAutowalkDoor(player, node.asScenery()) + sendMessage(player, "You skip gladly out of murky Mort Myre.") + removeTimer(player) GlobalScope.launch { findLocalNPC(player, NPCs.ULIZIUS_1054)?.sendChat("Oh my! You're still alive!", 2) } } else { if (player.questRepository.hasStarted(Quests.NATURE_SPIRIT)) { - core.game.global.action.DoorActionHandler.handleAutowalkDoor(player, node.asScenery()) + setAttribute(player, "swampgate", node) + openInterface(player, Components.CWS_WARNING_20_580) } else { sendNPCDialogue( player, @@ -97,4 +102,24 @@ class MorytaniaListeners : InteractionListener { return@on true } } +} + +class warningInterface : InterfaceListener { + override fun defineInterfaceListeners() { + on(Components.CWS_WARNING_20_580) { player, _, _, buttonID, _, _ -> + when(buttonID) { + 17 -> { + val gate = getAttribute(player, "swampgate", null) as? core.game.node.scenery.Scenery + if (gate != null) { + core.game.global.action.DoorActionHandler.handleAutowalkDoor(player, gate) + sendMessage(player, "You walk into the gloomy atmosphere of Mort Myre.") + } + closeInterface(player) + } + 18 -> closeInterface(player) + } + removeAttribute(player, "swampgate") + return@on true + } + } } \ No newline at end of file diff --git a/Server/src/main/content/region/morytania/handlers/MoyrtniaSwampPlugin.java b/Server/src/main/content/region/morytania/handlers/MoyrtniaSwampPlugin.java deleted file mode 100644 index 69e663f47..000000000 --- a/Server/src/main/content/region/morytania/handlers/MoyrtniaSwampPlugin.java +++ /dev/null @@ -1,30 +0,0 @@ -package content.region.morytania.handlers; - -import core.cache.def.impl.SceneryDefinition; -import core.game.interaction.OptionHandler; -import core.game.node.Node; -import core.game.node.entity.player.Player; -import core.plugin.Initializable; -import core.plugin.Plugin; - -/** - * Represents the mortynia swamp plugin. - * @author 'Vexia - * @version 1.0 - */ -@Initializable -public final class MoyrtniaSwampPlugin extends OptionHandler { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - SceneryDefinition.forId(3506).getHandlers().put("option:open", this); - return this; - } - - @Override - public boolean handle(Player player, Node node, String option) { - player.getDialogueInterpreter().sendDialogue("There's a message attached to this gate, it reads:~", "~ Mort Myre is a dangerous Ghast infested swamp. ~", "~ Do not enter if you value your life. ~", "~ All persons wishing to enter must see Drezel. ~"); - return true; - } - -} diff --git a/Server/src/main/content/region/morytania/handlers/SwampDecayHandler.kt b/Server/src/main/content/region/morytania/handlers/SwampDecayHandler.kt new file mode 100644 index 000000000..995294730 --- /dev/null +++ b/Server/src/main/content/region/morytania/handlers/SwampDecayHandler.kt @@ -0,0 +1,148 @@ +package content.region.morytania.handlers + +import core.api.* +import core.game.node.entity.Entity +import core.game.node.entity.player.Player +import core.game.system.timer.PersistTimer +import core.game.system.timer.RSTimer +import core.game.system.timer.TimerFlag +import core.game.world.map.zone.ZoneBorders +import org.rs09.consts.Items +import kotlin.random.Random + + +/** + * The area for swamp decay + */ +class SwampDecayArea : MapArea { + override fun defineAreaBorders(): Array { + return arrayOf( + ZoneBorders(3392, 3328, 3519, 3455) + ) + } + + /** + * Sources: + * General mechanic: https://runescape.wiki/w/Swamp_decay + * Historic damage: https://runescape.wiki/w/Mort_Myre_Swamp?oldid=1961846 + * Precise area: https://oldschool.runescape.wiki/w/Swamp_decay + * Video: https://www.youtube.com/watch?v=gkw959x8Q6s + * Video: https://www.youtube.com/watch?v=N86RywmEyEU + */ + + override fun areaEnter(entity: Entity) { + if (entity is Player) { + getOrStartTimer(entity) + } + super.areaEnter(entity) + } + +} + +/** + * The timer for swamp decay + */ +class SwampDecayTimer : PersistTimer (200, "swampdecay", isSoft = true, flags = arrayOf(TimerFlag.ClearOnDeath)) { + companion object { + // Silver sickle, Rod of Ivandis, or Ivandis Flail in equip or inv will protect the player. + private val PROTECTIVE_ITEMS = intArrayOf( + Items.SILVER_SICKLEB_2963, + Items.ROD_OF_IVANDIS10_7639, + Items.ROD_OF_IVANDIS9_7640, + Items.ROD_OF_IVANDIS8_7641, + Items.ROD_OF_IVANDIS7_7642, + Items.ROD_OF_IVANDIS6_7643, + Items.ROD_OF_IVANDIS5_7644, + Items.ROD_OF_IVANDIS4_7645, + Items.ROD_OF_IVANDIS3_7646, + Items.ROD_OF_IVANDIS2_7647, + Items.ROD_OF_IVANDIS1_7648, + Items.IVANDIS_FLAIL_30_13117, + Items.IVANDIS_FLAIL_29_13118, + Items.IVANDIS_FLAIL28_13119, + Items.IVANDIS_FLAIL_27_13120, + Items.IVANDIS_FLAIL_26_13121, + Items.IVANDIS_FLAIL_25_13122, + Items.IVANDIS_FLAIL_24_13123, + Items.IVANDIS_FLAIL_23_13124, + Items.IVANDIS_FLAIL_22_13125, + Items.IVANDIS_FLAIL_21_13126, + Items.IVANDIS_FLAIL_20_13127, + Items.IVANDIS_FLAIL_19_13128, + Items.IVANDIS_FLAIL_18_13129, + Items.IVANDIS_FLAIL_17_13130, + Items.IVANDIS_FLAIL_16_13131, + Items.IVANDIS_FLAIL_15_13132, + Items.IVANDIS_FLAIL_14_13133, + Items.IVANDIS_FLAIL_13_13134, + Items.IVANDIS_FLAIL_12_13135, + Items.IVANDIS_FLAIL_11_13136, + Items.IVANDIS_FLAIL_10_13137, + Items.IVANDIS_FLAIL_9_13138, + Items.IVANDIS_FLAIL_8_13139, + Items.IVANDIS_FLAIL_7_13140, + Items.IVANDIS_FLAIL_6_13141, + Items.IVANDIS_FLAIL_5_13142, + Items.IVANDIS_FLAIL_4_13143, + Items.IVANDIS_FLAIL_3_13144, + Items.IVANDIS_FLAIL_2_13145, + Items.IVANDIS_FLAIL_1_13146 + ) + } + + // returning false stops the timer. returning true keeps it running. + override fun run(entity: Entity): Boolean { + if (entity !is Player) return false + + // player is outside the zone + if (entity.location.x !in 3392..3519 || entity.location.y !in 3328..3455) { + sendMessage(entity, "The swamp decay effect is now over.") + return false + } + + // the nature spirit camp zone is safe + if (entity.location.x in 3435..3445 && entity.location.y in 3331..3442) { + sendMessage(entity, "The aura of Filliman's camp protects you from the swamp.") + return true + } + + // protected + if (hasProtection(entity)) { + return true + } + + // decayed (2-4 random damage) + sendMessage(entity, "The swamp decays you!") + impact(entity, Random.nextInt(2, 5)) + visualize(entity, -1, 255) + return true + } + + override fun onRegister(entity: Entity) { + if (entity !is Player) return + } + + override fun getTimer(vararg args: Any): RSTimer { + val t = SwampDecayTimer() + t.runInterval = args.getOrNull(0) as? Int ?: 200 + return t + } + + // always remember your protection + private fun hasProtection(player: Player): Boolean { + for (itemId in PROTECTIVE_ITEMS) { + if (inEquipmentOrInventory(player, itemId)) { + val prot = when(itemId) { + 2963 -> "blessed silver sickle" + in 7639..7648 -> "rod of Ivandis" + in 13117..13146 -> "Ivandis flail" + else -> "item" // fallback + } + sendMessage(player, "The power of the $prot prevents the swamp decay.") + visualize(player, -1, 259) + return true + } + } + return false + } +} \ No newline at end of file From 08e2e0b45e99f73f886e807a6948818049c8721e Mon Sep 17 00:00:00 2001 From: oftheshire Date: Sun, 21 Jun 2026 08:08:33 +0000 Subject: [PATCH 02/42] Added player and free text search to ::quest -p --- .../system/command/sets/QuestCommandSet.kt | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/Server/src/main/core/game/system/command/sets/QuestCommandSet.kt b/Server/src/main/core/game/system/command/sets/QuestCommandSet.kt index 18000ecf8..c8c414c38 100644 --- a/Server/src/main/core/game/system/command/sets/QuestCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/QuestCommandSet.kt @@ -29,20 +29,38 @@ class QuestCommandSet : CommandSet(Privilege.ADMIN){ /** * Displays the currently implemented quests with debug information */ - define("quest", usage = "::quest [player-name]", description = "Shows quest stages for you, or for [player-name] if provided."){player,args -> - if (args.size < 3) { - val lookupP = if (args.size == 1) { - player - } else if (Repository.getPlayerByName(args[1]) != null) { - Repository.getPlayerByName(args[1]) ?: return@define + define("quest", + usage = "::quest [-p username] [quest-filter]", + description = "Shows quest stages for you, or for [username] if -p is used. Add optional text to filter by quest name." + ) { player, args -> + + var target = player + var filter: String? = null + + if (args.size > 1) { + if (args[1].equals("-p", ignoreCase = true)) { + // need at least 3 arguments if -p is used: command, -p, username + if (args.size < 3) { + reject(player, "ERROR: Missing player name. Usage: ::quest -p [quest-filter]") + return@define + } + + val playerName = args[2] + target = Repository.getPlayerByName(playerName) ?: run { + reject(player, "ERROR: Username '$playerName' not found.") + return@define + } + + // arguments after the player name are the quest filter + if (args.size > 3) { + filter = args.drop(3).joinToString(" ") + } } else { - reject(player, "ERROR: Username not found. Usage: ::quest ") - return@define + // no -p, so the arguments are the quest filter + filter = args.drop(1).joinToString(" ") } - sendQuestsDebug(player, lookupP) - } else { - reject(player, "Usage: ::quest || ::quest ") } + sendQuestsDebug(player, target, filter) } /** @@ -96,24 +114,49 @@ class QuestCommandSet : CommandSet(Privilege.ADMIN){ /** * Sends the list of quests with debug information * @param admin the player. + * @param lookupUser the target player to view quests for. + * @param questFilter optional string to filter quests by name. */ - private fun sendQuestsDebug(admin: Player?, lookupUser: Player?) { + private fun sendQuestsDebug(admin: Player?, lookupUser: Player?, questFilter: String? = null) { admin!!.interfaceManager.open(Component(275)) + + // clear interface lines for (i in 0..310) { admin.packetDispatch.sendString("", 275, i) } + var lineId = 11 + + // title admin.packetDispatch.sendString("${lookupUser!!.username}'s Quest Debug", 275, 2) - for (q in QuestRepository.getQuests().values) { - // Add a space to beginning and end of string for the strikethrough + + // filtered quests + val questsToDisplay = QuestRepository.getQuests().values.filter { q -> + questFilter.isNullOrBlank() || q.quest.toString().contains(questFilter, ignoreCase = true) + } + + // no quests to filter + if (questsToDisplay.isEmpty()) { + admin.packetDispatch.sendString("No quests found matching: $questFilter", 275, lineId++) + return + } + + for (q in questsToDisplay) { + // can't display more than 308 lines + if (lineId >= 308) { + admin.packetDispatch.sendString("List truncated (too many results).", 275, lineId) + break + } + val stage = lookupUser.questRepository.getStage(q) val statusColor = when { stage >= 100 -> "80ff00" stage in 1..99 -> "ff8400" else -> "ff0000" } + admin.packetDispatch.sendString("${q.quest}", 275, lineId++) - admin.packetDispatch.sendString("Index: ${q.index} | Stage: ${lookupUser.questRepository.getStage(q)}", 275, lineId++) + admin.packetDispatch.sendString("Index: ${q.index} | Stage: ${stage}", 275, lineId++) admin.packetDispatch.sendString(" ", 275, lineId++) } } From 9bfeea1d186c61ab17ea59e85c7da9b0cf9c1759 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:06:52 -0400 Subject: [PATCH 03/42] Ring of dueling and games necklace teleports now use the dedicated jewellery teleport animation --- Server/src/main/content/data/EnchantedJewellery.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Server/src/main/content/data/EnchantedJewellery.kt b/Server/src/main/content/data/EnchantedJewellery.kt index 61373e8f3..242c9305f 100644 --- a/Server/src/main/content/data/EnchantedJewellery.kt +++ b/Server/src/main/content/data/EnchantedJewellery.kt @@ -246,7 +246,13 @@ enum class EnchantedJewellery( when (stage) { 0 -> { lock(player, 4) - visualize(player, ANIMATION, GRAPHICS) + // This is an intentional teleport animation & graphics difference as these two jewellery + // animations are authentic to 2009 while others are currently unknown. + if (this == RING_OF_DUELING || this == GAMES_NECKLACE) { + visualize(player, JEWELLERY_ANIMATION, JEWELLERY_GRAPHICS) + } else { + visualize(player, ANIMATION, GRAPHICS) + } playGlobalAudio(player.location, Sounds.TELEPORT_ALL_200) player.impactHandler.disabledTicks = 4 closeInterface(player) @@ -255,6 +261,9 @@ enum class EnchantedJewellery( 1 -> { teleport(player, location) resetAnimator(player) + if (this == RING_OF_DUELING || this == GAMES_NECKLACE) { + player.graphics(JEWELLERY_GRAPHICS) + } unlock(player) player.dispatch(TeleportEvent(TeleportManager.TeleportType.NORMAL, TeleportMethod.JEWELRY, item, location)) @@ -370,6 +379,8 @@ enum class EnchantedJewellery( companion object { private val ANIMATION = Animation(714) private val GRAPHICS = Graphics(308, 100, 50) + private val JEWELLERY_ANIMATION = Animation(9603) + private val JEWELLERY_GRAPHICS = Graphics(1684) val idMap = HashMap() init { From eb5af4a5a8aa9e3021a8767dfcc3751214b59095 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sun, 21 Jun 2026 12:12:42 +0000 Subject: [PATCH 04/42] Rewrote glider interface --- .../handlers/iface/GliderInterface.java | 37 ------------------- .../global/handlers/iface/GliderInterface.kt | 17 +++++++++ 2 files changed, 17 insertions(+), 37 deletions(-) delete mode 100644 Server/src/main/content/global/handlers/iface/GliderInterface.java create mode 100644 Server/src/main/content/global/handlers/iface/GliderInterface.kt diff --git a/Server/src/main/content/global/handlers/iface/GliderInterface.java b/Server/src/main/content/global/handlers/iface/GliderInterface.java deleted file mode 100644 index 08459cefe..000000000 --- a/Server/src/main/content/global/handlers/iface/GliderInterface.java +++ /dev/null @@ -1,37 +0,0 @@ -package content.global.handlers.iface; - -import static core.api.ContentAPIKt.*; -import core.game.component.Component; -import core.game.component.ComponentDefinition; -import core.game.component.ComponentPlugin; -import content.global.travel.glider.GliderPulse; -import content.global.travel.glider.Gliders; -import core.game.node.entity.player.Player; -import core.plugin.Initializable; -import core.plugin.Plugin; - -/** - * Represents the glider interface component. - * @author Emperor - * @author 'Vexia - * @version 1.0 - */ -@Initializable -public final class GliderInterface extends ComponentPlugin { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ComponentDefinition.put(138, this); - return this; - } - - @Override - public boolean handle(final Player player, Component component, int opcode, int button, int slot, int itemId) { - final Gliders glider = Gliders.forId(button); - if (glider == null) { - return true; - } - submitWorldPulse(new GliderPulse(1, player, glider)); - return true; - } -} diff --git a/Server/src/main/content/global/handlers/iface/GliderInterface.kt b/Server/src/main/content/global/handlers/iface/GliderInterface.kt new file mode 100644 index 000000000..cc43ede4a --- /dev/null +++ b/Server/src/main/content/global/handlers/iface/GliderInterface.kt @@ -0,0 +1,17 @@ +package content.global.handlers.iface + +import core.api.submitWorldPulse +import content.global.travel.glider.GliderPulse +import content.global.travel.glider.Gliders +import core.game.interaction.InterfaceListener +import org.rs09.consts.Components + +class GliderInterface : InterfaceListener { + override fun defineInterfaceListeners() { + on(Components.GLIDERMAP_138) { player, _, _, button, _, _ -> + val glider = Gliders.forId(button) ?: return@on true + submitWorldPulse(GliderPulse(1, player, glider)) + return@on true + } + } +} From a5d98df06d709cf7c57e608fbfb723ac5d209d84 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:18:03 -0400 Subject: [PATCH 05/42] Fixed sex placeholder in Mysterious Ghost dialogue --- .../desert/quest/curseofzaros/MysteriousGhostRennardDialogue.kt | 2 +- .../desert/quest/curseofzaros/MysteriousGhostViggoraDialogue.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostRennardDialogue.kt b/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostRennardDialogue.kt index 83f8383ef..6ac1468ad 100644 --- a/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostRennardDialogue.kt +++ b/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostRennardDialogue.kt @@ -74,7 +74,7 @@ class MysteriousGhostRennardDialogueFile : DialogueLabeller() { player("Why don't you tell me what happened? I might be able to help...") npc("Well, I was making me merry way along, having just pulled off a glorious jewellery heist from a bunch of stinking dwarves...") player("Hey, that's no way to talk about dwarves! Some of my best friends are short!") - npc("Ah, yer misunderstand me [lad/lass], I wasn't generalising about the whole dwarf species, I had just stolen a bundle of jewels from a very specific group of dwarves who happened to have an odious stench about them!") + npc("Ah, yer misunderstand me @g[lad,lass], I wasn't generalising about the whole dwarf species, I had just stolen a bundle of jewels from a very specific group of dwarves who happened to have an odious stench about them!") player("Oh. Well I guess that's okay then. Please continue.") npc("Well, as I headed on me merry way, hoping the foul odours that lingered in me nostrils would soon pass, I see in front of me this explorer fella, all decked out in in his fine clothing, and carrying some long package") npc("bundled in rags.") diff --git a/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostViggoraDialogue.kt b/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostViggoraDialogue.kt index d5464ee19..dae618e1d 100644 --- a/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostViggoraDialogue.kt +++ b/Server/src/main/content/region/desert/quest/curseofzaros/MysteriousGhostViggoraDialogue.kt @@ -175,7 +175,7 @@ class MysteriousGhostViggoraDialogueFile : DialogueLabeller() { npc("Hello yourself.") player("I really liked your little story. Can you tell me another one?") player("Preferably something with a big fight and lots of explosions!") - npc("...You are a very strange young @g[man/woman].") + npc("...You are a very strange young @g[man,woman].") label("lostghostlything") player(ChatAnim.SAD, "Can I have that cloak back?") From aa8c26f50c64b7d644048c7799cf5e7e7893df82 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:26:28 -0400 Subject: [PATCH 06/42] Fixed Edgar's cave exit teleporting to Saba's cave --- .../region/asgarnia/trollheim/handlers/TrollheimPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/region/asgarnia/trollheim/handlers/TrollheimPlugin.java b/Server/src/main/content/region/asgarnia/trollheim/handlers/TrollheimPlugin.java index b62009775..d6a2c1fc6 100644 --- a/Server/src/main/content/region/asgarnia/trollheim/handlers/TrollheimPlugin.java +++ b/Server/src/main/content/region/asgarnia/trollheim/handlers/TrollheimPlugin.java @@ -202,7 +202,7 @@ public final class TrollheimPlugin extends OptionHandler { switch (id) { case 32738: if (loc.equals(new Location(2892, 10072, 0))) { - player.getProperties().setTeleportLocation(LOCATIONS[0]); + player.getProperties().setTeleportLocation(LOCATIONS[9]); return true; } player.teleport(LOCATIONS[1]); From 0443230b8503f2f852435c16cfc29e4191ae009a Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:32:59 -0400 Subject: [PATCH 07/42] Black unicorn foals now drop bones --- Server/data/configs/drop_tables.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Server/data/configs/drop_tables.json b/Server/data/configs/drop_tables.json index 508b21b17..77028e6b3 100644 --- a/Server/data/configs/drop_tables.json +++ b/Server/data/configs/drop_tables.json @@ -8353,6 +8353,20 @@ "description": "", "main": [] }, + { + "default": [ + { + "minAmount": "1", + "weight": "100.0", + "id": "526", + "maxAmount": "1" + } + ], + "charm": [], + "ids": "1329", + "description": "", + "main": [] + }, { "default": [ { From 200dfc64f935aee0b517cf601a0405a9d4354e14 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:51:29 -0400 Subject: [PATCH 08/42] King Black Dragon breath effects now trigger on impact instead of attack launch --- .../handlers/KingBlackDragonNPC.java | 5 +- .../handlers/KBDBreathEffectTests.kt | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 Server/src/test/kotlin/content/region/wilderness/handlers/KBDBreathEffectTests.kt diff --git a/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java b/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java index ab5559616..51ec635a5 100644 --- a/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java @@ -14,6 +14,7 @@ import core.game.node.entity.impl.Animator.Priority; import core.game.node.entity.npc.AbstractNPC; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; +import core.game.world.GameWorld; import core.game.world.map.Location; import core.game.world.update.flag.context.Animation; import core.plugin.Initializable; @@ -127,7 +128,6 @@ public final class KingBlackDragonNPC extends AbstractNPC { @Override public void adjustBattleState(Entity entity, Entity victim, BattleState state) { if (style == CombatStyle.RANGE) { - fireType.getTask().exec(victim, entity); state.setStyle(null); DRAGONFIRE.adjustBattleState(entity, victim, state); state.setStyle(CombatStyle.RANGE); @@ -184,6 +184,9 @@ public final class KingBlackDragonNPC extends AbstractNPC { @Override public void impact(Entity entity, Entity victim, BattleState state) { + if (style != CombatStyle.MELEE && victim.getImpactHandler().getDisabledTicks() <= GameWorld.getTicks()) { + fireType.getTask().exec(victim, entity); + } style.getSwingHandler().impact(entity, victim, state); } diff --git a/Server/src/test/kotlin/content/region/wilderness/handlers/KBDBreathEffectTests.kt b/Server/src/test/kotlin/content/region/wilderness/handlers/KBDBreathEffectTests.kt new file mode 100644 index 000000000..d40bef56b --- /dev/null +++ b/Server/src/test/kotlin/content/region/wilderness/handlers/KBDBreathEffectTests.kt @@ -0,0 +1,56 @@ +package content.region.wilderness.handlers + +import TestUtils +import core.api.getTimer +import core.game.node.entity.combat.BattleState +import core.game.node.entity.combat.equipment.FireType +import core.game.system.timer.impl.Poison +import core.game.world.map.Location +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class KBDBreathEffectTests { + init { + TestUtils.preTestSetup() + } + + @Test fun toxicBreathShouldPoisonOnImpactNotOnLaunch() { + val kbd = KingBlackDragonNPC(50, Location.create(2273, 4698, 0)) + val handler = kbd.getSwingHandler(false) + // KBD rolls a random FireType per swing, force toxic breath for determinism + val fireTypeField = handler.javaClass.getDeclaredField("fireType") + fireTypeField.isAccessible = true + fireTypeField.set(handler, FireType.TOXIC_BREATH) + + TestUtils.getMockPlayer("kbdbreathtiming").use { player -> + val state = BattleState(kbd, player) + state.estimatedHit = 0 + + handler.adjustBattleState(kbd, player, state) + Assertions.assertNull(getTimer(player), "Breath effect must not apply on the launch tick.") + + handler.impact(kbd, player, state) + Assertions.assertNotNull(getTimer(player), "Breath effect must apply when the attack lands.") + } + } + + @Test fun toxicBreathShouldNotPoisonVictimWhoTeleportedMidFlight() { + val kbd = KingBlackDragonNPC(50, Location.create(2273, 4698, 0)) + val handler = kbd.getSwingHandler(false) + val fireTypeField = handler.javaClass.getDeclaredField("fireType") + fireTypeField.isAccessible = true + fireTypeField.set(handler, FireType.TOXIC_BREATH) + + TestUtils.getMockPlayer("kbdbreathteleport").use { player -> + val state = BattleState(kbd, player) + state.estimatedHit = 0 + + handler.adjustBattleState(kbd, player, state) + // Disable impacts like TeleportManager.send does for a real teleport + player.impactHandler.setDisabledTicks(12) + + handler.impact(kbd, player, state) + Assertions.assertNull(getTimer(player), "Breath effect must not apply to a victim whose impacts are disabled (teleported away).") + } + } +} From 16187ad211405b514277dbd7c9e800739c308f40 Mon Sep 17 00:00:00 2001 From: oftheshire Date: Sun, 21 Jun 2026 12:52:46 +0000 Subject: [PATCH 09/42] Hunter audit and implementation for geckos, raccoons, monkeys, and squirrels --- Server/data/configs/item_configs.json | 134 ++++++++++++----- Server/data/configs/npc_configs.json | 141 +++++++++++++----- .../global/skill/hunter/NetTrapNode.kt | 25 ++++ .../content/global/skill/hunter/Traps.java | 51 +++++-- 4 files changed, 261 insertions(+), 90 deletions(-) create mode 100644 Server/src/main/content/global/skill/hunter/NetTrapNode.kt diff --git a/Server/data/configs/item_configs.json b/Server/data/configs/item_configs.json index 8a7014647..a51c6805d 100644 --- a/Server/data/configs/item_configs.json +++ b/Server/data/configs/item_configs.json @@ -77818,6 +77818,16 @@ "durability": null, "weight": "1" }, + { + "id": "9721", + "name": "Scroll", + "examine": "A scroll with some writing on it." + }, + { + "id": "9722", + "name": "Key", + "examine": "Quite a small key." + }, { "id": "9723", "name": "Pipe", @@ -99252,7 +99262,7 @@ { "id": "12184", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, @@ -100617,42 +100627,42 @@ { "id": "12486", "name": "Baby raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "A stripy little baby raccoon.", "archery_ticket_price": "0", "durability": null }, { "id": "12487", "name": "Raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "He can run with us.", "archery_ticket_price": "0", "durability": null }, { "id": "12488", "name": "Baby gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "It's so adorably tiny!", "archery_ticket_price": "0", "durability": null }, { "id": "12489", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, { "id": "12490", "name": "Baby squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "A tiny nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12491", "name": "Squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "An experienced nut-thief.", "archery_ticket_price": "0", "durability": null }, @@ -100689,10 +100699,15 @@ { "id": "12496", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12497", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12498", "name": "Vulture chick", @@ -102382,66 +102397,111 @@ { "id": "12682", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12683", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12684", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12685", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12686", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12687", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12688", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12689", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12690", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12691", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12692", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12693", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12694", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12695", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12696", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12697", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12698", "name": "Baby monkey", - "examine": "(Baby) It's a tiny, cheeky monkey. (Adult) Hide the bananas!", + "examine": "It's a tiny, cheeky monkey.", "archery_ticket_price": "0", "durability": null }, + { + "id": "12699", + "name": "Monkey", + "examine": "Hide the bananas!" + }, { "id": "12700", "name": "Terrier puppy", @@ -102707,84 +102767,84 @@ { "id": "12734", "name": "Baby raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "A stripy little baby raccoon.", "archery_ticket_price": "0", "durability": null }, { "id": "12735", "name": "Raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "He can run with us.", "archery_ticket_price": "0", "durability": null }, { "id": "12736", "name": "Baby raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "A stripy little baby raccoon.", "archery_ticket_price": "0", "durability": null }, { "id": "12737", "name": "Raccoon", - "examine": "(Baby) A stripy little baby raccoon.(Adult) He can run with us.", + "examine": "He can run with us.", "archery_ticket_price": "0", "durability": null }, { "id": "12738", "name": "Baby gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "It's so adorably tiny!", "archery_ticket_price": "0", "durability": null }, { "id": "12739", "name": "Baby gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "It's so adorably tiny!", "archery_ticket_price": "0", "durability": null }, { "id": "12740", "name": "Baby gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "It's so adorably tiny!", "archery_ticket_price": "0", "durability": null }, { "id": "12741", "name": "Baby gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "It's so adorably tiny!", "archery_ticket_price": "0", "durability": null }, { "id": "12742", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, { "id": "12743", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, { "id": "12744", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, { "id": "12745", "name": "Gecko", - "examine": "(Baby) It's so adorably tiny! (Adult) How cute!", + "examine": "How cute!", "archery_ticket_price": "0", "durability": null }, @@ -102855,56 +102915,56 @@ { "id": "12754", "name": "Baby squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "A tiny nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12755", "name": "Squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "An experienced nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12756", "name": "Baby squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "A tiny nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12757", "name": "Squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "An experienced nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12758", "name": "Baby squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "A tiny nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12759", "name": "Squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "An experienced nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12760", "name": "Baby squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "A tiny nut-thief.", "archery_ticket_price": "0", "durability": null }, { "id": "12761", "name": "Squirrel", - "examine": "Adult: An experienced nut-thief.Baby: A tiny nut-thief.", + "examine": "An experienced nut-thief.", "archery_ticket_price": "0", "durability": null }, diff --git a/Server/data/configs/npc_configs.json b/Server/data/configs/npc_configs.json index 106281883..2f3dd9984 100644 --- a/Server/data/configs/npc_configs.json +++ b/Server/data/configs/npc_configs.json @@ -38057,11 +38057,13 @@ }, { "id": "3284", - "name": "Squirrel" + "name": "Squirrel", + "examine": "Bushy tail!" }, { "id": "3285", - "name": "Squirrel" + "name": "Squirrel", + "examine": "Bushy tail!" }, { "id": "3286", @@ -38081,7 +38083,8 @@ }, { "id": "3287", - "name": "Raccoon" + "name": "Raccoon", + "examine": "A raccoon." }, { "id": "3288", @@ -77156,20 +77159,23 @@ }, { "id": "6913", - "name": "Baby raccoon" + "name": "Baby raccoon", + "examine": "A stripy little baby raccoon." }, { "id": "6914", - "name": "Raccoon" + "name": "Raccoon", + "examine": "He can run with us." }, { "id": "6915", - "name": "Baby gecko" + "name": "Baby gecko", + "examine": "It's so adorably tiny!" }, { "id": "6916", "name": "Gecko", - "examine": "" + "examine": "How cute!" }, { "id": "6917", @@ -77194,11 +77200,13 @@ }, { "id": "6919", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "6920", - "name": "Squirrel" + "name": "Squirrel", + "examine": "An experienced nut-thief." }, { "id": "6921", @@ -77299,6 +77307,7 @@ { "id": "6942", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -77310,6 +77319,7 @@ { "id": "6943", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -77461,7 +77471,8 @@ }, { "id": "6970", - "name": "Pikkupstix" + "name": "Pikkupstix", + "examine": "He's a very important druid." }, { "id": "6971", @@ -77805,6 +77816,16 @@ }, { "id": "6988", + "name": "Pikkupstix", + "examine": "He's a very important druid." + }, + { + "id": "6989", + "name": "Pikkupstix", + "examine": "He's a very important druid." + }, + { + "id": "6990", "name": "Giant wolpertinger" }, { @@ -79666,6 +79687,7 @@ { "id": "7210", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79677,6 +79699,7 @@ { "id": "7211", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79688,6 +79711,7 @@ { "id": "7212", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79699,6 +79723,7 @@ { "id": "7213", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79710,6 +79735,7 @@ { "id": "7214", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79721,6 +79747,7 @@ { "id": "7215", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79732,6 +79759,7 @@ { "id": "7216", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79743,6 +79771,7 @@ { "id": "7217", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79754,6 +79783,7 @@ { "id": "7218", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79765,6 +79795,7 @@ { "id": "7219", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79776,6 +79807,7 @@ { "id": "7220", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79787,6 +79819,7 @@ { "id": "7221", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79798,6 +79831,7 @@ { "id": "7222", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79809,6 +79843,7 @@ { "id": "7223", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79820,6 +79855,7 @@ { "id": "7224", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79831,6 +79867,7 @@ { "id": "7225", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79842,6 +79879,7 @@ { "id": "7226", "name": "Baby monkey", + "examine": "It's a tiny, cheeky monkey.", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -79853,6 +79891,7 @@ { "id": "7227", "name": "Monkey", + "examine": "Hide the bananas!", "attack_level": "1", "combat_audio": "629,631,630", "defence_level": "1", @@ -80161,19 +80200,23 @@ }, { "id": "7271", - "name": "Baby raccoon" + "name": "Baby raccoon", + "examine": "A stripy little baby raccoon." }, { "id": "7272", - "name": "Raccoon" + "name": "Raccoon", + "examine": "He can run with us." }, { "id": "7273", - "name": "Baby raccoon" + "name": "Baby raccoon", + "examine": "A stripy little baby raccoon." }, { "id": "7274", - "name": "Raccoon" + "name": "Raccoon", + "examine": "He can run with us." }, { "id": "7275", @@ -80193,40 +80236,48 @@ }, { "id": "7276", - "name": "Baby raccoon" + "name": "Baby raccoon", + "examine": "A stripy little baby raccoon." }, { "id": "7277", - "name": "Baby gecko" + "name": "Baby gecko", + "examine": "It's so adorably tiny!" }, { "id": "7278", - "name": "Baby gecko" + "name": "Baby gecko", + "examine": "It's so adorably tiny!" }, { "id": "7279", - "name": "Baby gecko" + "name": "Baby gecko", + "examine": "It's so adorably tiny!" }, { "id": "7280", - "name": "Baby gecko" + "name": "Baby gecko", + "examine": "It's so adorably tiny!" }, { "id": "7281", "name": "Gecko", - "examine": "" + "examine": "How cute!" }, { "id": "7282", - "name": "Gecko" + "name": "Gecko", + "examine": "How cute!" }, { "id": "7283", - "name": "Gecko" + "name": "Gecko", + "examine": "How cute!" }, { "id": "7284", - "name": "Gecko" + "name": "Gecko", + "examine": "How cute!" }, { "id": "7285", @@ -80252,7 +80303,7 @@ { "id": "7287", "name": "Baby gecko", - "examine": "" + "examine": "It's so adorably tiny!" }, { "id": "7288", @@ -80261,7 +80312,8 @@ }, { "id": "7289", - "name": "Gecko" + "name": "Gecko", + "examine": "How cute!" }, { "id": "7290", @@ -80270,7 +80322,8 @@ }, { "id": "7291", - "name": "Gecko" + "name": "Gecko", + "examine": "How cute!" }, { "id": "7292", @@ -80311,51 +80364,63 @@ }, { "id": "7301", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7302", - "name": "Squirrel" + "name": "Squirrel", + "examine": "An experienced nut-thief." }, { "id": "7303", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7304", - "name": "Squirrel" + "name": "Squirrel", + "examine": "An experienced nut-thief." }, { "id": "7305", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7306", - "name": "Squirrel" + "name": "Squirrel", + "examine": "An experienced nut-thief." }, { "id": "7307", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7308", - "name": "Squirrel" + "name": "Squirrel", + "examine": "An experienced nut-thief." }, { "id": "7309", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7310", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7311", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7312", - "name": "Baby squirrel" + "name": "Baby squirrel", + "examine": "A tiny nut-thief." }, { "id": "7313", diff --git a/Server/src/main/content/global/skill/hunter/NetTrapNode.kt b/Server/src/main/content/global/skill/hunter/NetTrapNode.kt new file mode 100644 index 000000000..43b9fdad5 --- /dev/null +++ b/Server/src/main/content/global/skill/hunter/NetTrapNode.kt @@ -0,0 +1,25 @@ +package content.global.skill.hunter + +import core.api.getStatLevel +import core.game.node.entity.npc.NPC +import core.game.node.entity.skill.Skills +import core.game.node.item.Item + +/** + * Handles the net trap node. + */ +class NetTrapNode( + npcIds: IntArray, + level: Int, + experience: Double, + rewards: Array, + private val summoningLevel: Int +) : TrapNode(npcIds, level, experience, intArrayOf(), rewards) { + + override fun canCatch(wrapper: TrapWrapper, npc: NPC): Boolean { + if (getStatLevel(wrapper.player, Skills.SUMMONING) < summoningLevel) { + return false + } + return super.canCatch(wrapper, npc) + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/skill/hunter/Traps.java b/Server/src/main/content/global/skill/hunter/Traps.java index f2151d660..f0cec6d54 100644 --- a/Server/src/main/content/global/skill/hunter/Traps.java +++ b/Server/src/main/content/global/skill/hunter/Traps.java @@ -9,6 +9,8 @@ import core.tools.Log; import core.tools.SystemLogger; import core.game.world.map.Location; import core.game.world.update.flag.context.Animation; +import org.rs09.consts.Items; +import org.rs09.consts.NPCs; import java.util.ArrayList; import java.util.List; @@ -33,16 +35,30 @@ public enum Traps { } }), BOX_TRAP(new TrapSetting(10008, new int[] { 19187 }, new int[] { 1963, 12579, 1869, 9996, 5972, 12535 }, "lay", 19192, Animation.create(5208), new Animation(9726), 27), - new BoxTrapNode(new int[] { 5081 }, 27, 100, new Item[] { new Item(10092) }, 1), - new BoxTrapNode(new int[] { 6918, 7289, 7290, 7291, 7292 }, 27, 100, new Item[] { new Item(12184) }, 10), - new BoxTrapNode(new int[] { 1487 }, 27, 100, new Item[] { new Item(4033, 1) }, 95), - new BoxTrapNode(new int[] { 7021, 7022, 7023 }, 48, 150, new Item[] { new Item(12551, 1) }, 1), - new BoxTrapNode(new int[] { 5079 }, 53, 198, new Item[] { new Item(10033, 1) }, 1), - new BoxTrapNode(new int[] { 5428, 5430, 5449, 5450, 5451 }, 56, 150, new Item[] { new Item(12188) }, 1), - new BoxTrapNode(new int[] { 5080 }, 63, 265, new Item[] { new Item(10034, 1) }, 1), - new BoxTrapNode(new int[] { 7012, 7014 }, 66, 400, new Item[] { new Item(12535) }, 1), - new BoxTrapNode(new int[] { 8654 }, 73, 315, new Item[] { new Item(14861) }, 1), - new BoxTrapNode(new int[] { 7010, 7011 }, 77, 0, new Item[] { new Item(12539, 1) }, 1) { + new BoxTrapNode(new int[] { 5081 }, 27, 100, new Item[] { new Item(10092) }, 1), // Ferret + new BoxTrapNode(new int[] {NPCs.BABY_GECKO_6917 }, 27, 100, new Item[] { new Item(Items.BABY_GECKO_12488) }, 10), // Gecko Orange + new BoxTrapNode(new int[] {NPCs.BABY_GECKO_7285 }, 27, 100, new Item[] { new Item(Items.BABY_GECKO_12738) }, 10), // Gecko Speckled + new BoxTrapNode(new int[] {NPCs.BABY_GECKO_7286 }, 27, 100, new Item[] { new Item(Items.BABY_GECKO_12739) }, 10), // Gecko Green + new BoxTrapNode(new int[] {NPCs.BABY_GECKO_7287 }, 27, 100, new Item[] { new Item(Items.BABY_GECKO_12740) }, 10), // Gecko Blue + new BoxTrapNode(new int[] {NPCs.BABY_GECKO_7288 }, 27, 100, new Item[] { new Item(Items.BABY_GECKO_12741) }, 10), // Gecko Red + new BoxTrapNode(new int[] {NPCs.BABY_RACCOON_6997 }, 27, 100, new Item[] { new Item(Items.BABY_RACCOON_12486) }, 80), // Raccoon Gray + new BoxTrapNode(new int[] {NPCs.BABY_RACCOON_7275 }, 27, 100, new Item[] { new Item(Items.BABY_RACCOON_12734) }, 80), // Raccoon Tan + new BoxTrapNode(new int[] {NPCs.BABY_RACCOON_7276 }, 27, 100, new Item[] { new Item(Items.BABY_RACCOON_12736) }, 80), // Raccoon Red + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_6944 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12496) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7228 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12682) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7229 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12684) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7230 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12686) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7231 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12688) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7232 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12690) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7233 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12692) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7234 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12694) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7235 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12696) }, 95), // Monkey + new BoxTrapNode(new int[] { NPCs.BABY_MONKEY_7236 }, 27, 100, new Item[] { new Item(Items.BABY_MONKEY_12698) }, 95), // Monkey + new BoxTrapNode(new int[] { 7021, 7022, 7023 }, 48, 150, new Item[] { new Item(12551, 1) }, 1), // Platypus + new BoxTrapNode(new int[] { 5079 }, 53, 198, new Item[] { new Item(10033, 1) }, 1), // Chinchompa + new BoxTrapNode(new int[] { 5080 }, 63, 265, new Item[] { new Item(10034, 1) }, 1), // Red Chinchompa + new BoxTrapNode(new int[] { 7012, 7014 }, 66, 400, new Item[] { new Item(12535) }, 1), // Pawya + new BoxTrapNode(new int[] { 7010, 7011 }, 77, 0, new Item[] { new Item(12539, 1) }, 1) { // Grenwall @Override public boolean canCatch(TrapWrapper wrapper, final NPC npc) { //old xp: 726 @@ -59,11 +75,16 @@ public enum Traps { new TrapNode(new int[] { 5086 }, 37, 204, new int[] { 19208, 19208, 19217 }, new Item[] { new Item(10105), new Item(526) }), new TrapNode(new int[] { 7039 }, 44, 200, new int[] { 28939, 28940, 28941 }, new Item[] { new Item(12567), new Item(526) }), new TrapNode(new int[] { 5087 }, 51, 200, new int[] { 19209, 19210, 19216 }, new Item[] { new Item(10109), new Item(526) })), - NET_TRAP(new NetTrapSetting(), new TrapNode(new int[] { 5117 }, 29, 152, new int[] {}, new Item[] { new Item(10149) }), - new TrapNode(new int[] { 5114 }, 47, 224, new int[] {}, new Item[] { new Item(10146) }), - new TrapNode(new int[] { 6921 }, 29, 152, new int[] {}, new Item[] { new Item(12130) }), - new TrapNode(new int[] { 5115 }, 59, 272, new int[] {}, new Item[] { new Item(10147) }), - new TrapNode(new int[] { 5116 }, 67, 304, new int[] {}, new Item[] { new Item(10148) })); + NET_TRAP(new NetTrapSetting(), + new NetTrapNode(new int[] { 5117 }, 29, 152, new Item[] { new Item(10149) }, 1), // Swamp Lizard + new NetTrapNode(new int[] { 5114 }, 47, 224, new Item[] { new Item(10146) }, 1), // Orange Salamander + new NetTrapNode(new int[] { NPCs.BABY_SQUIRREL_6921 }, 29, 152, new Item[] { new Item(Items.BABY_SQUIRREL_12490) }, 60), // Squirrel Gray + new NetTrapNode(new int[] { NPCs.BABY_SQUIRREL_7309 }, 29, 152, new Item[] { new Item(Items.BABY_SQUIRREL_12754) }, 60), // Squirrel Tan + new NetTrapNode(new int[] { NPCs.BABY_SQUIRREL_7310 }, 29, 152, new Item[] { new Item(Items.BABY_SQUIRREL_12756) }, 60), // Squirrel White + new NetTrapNode(new int[] { NPCs.BABY_SQUIRREL_7311 }, 29, 152, new Item[] { new Item(Items.BABY_SQUIRREL_12758) }, 60), // Squirrel Grayish/Brown w/e + new NetTrapNode(new int[] { NPCs.BABY_SQUIRREL_7312 }, 29, 152, new Item[] { new Item(Items.BABY_SQUIRREL_12760) }, 60), // Squirrel Brown + new NetTrapNode(new int[] { 5115 }, 59, 272, new Item[] { new Item(10147) }, 1), // Red Salamander + new NetTrapNode(new int[] { 5116 }, 67, 304, new Item[] { new Item(10148) }, 1)); // Black Salamander /** * The location hooks for this node. From fd8f67272856a778ab14b0bc99858937c1684c3c Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 08:56:40 -0400 Subject: [PATCH 10/42] Borrokar now drops bones --- Server/data/configs/drop_tables.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Server/data/configs/drop_tables.json b/Server/data/configs/drop_tables.json index 77028e6b3..4a1af4001 100644 --- a/Server/data/configs/drop_tables.json +++ b/Server/data/configs/drop_tables.json @@ -19698,7 +19698,14 @@ ] }, { - "default": [], + "default": [ + { + "minAmount": "1", + "weight": "100.0", + "id": "526", + "maxAmount": "1" + } + ], "charm": [], "ids": "1307", "description": "", From 5670ac5e9d45c0849d1635bc26044564c9c5b0d8 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sun, 21 Jun 2026 12:57:31 +0000 Subject: [PATCH 11/42] Fixed some text display bugs on some interfaces --- .../DeathPlateauInteractionListener.kt | 15 ++++++++--- .../quest/deathplateau/IOUNoteDialogueFile.kt | 8 +++--- .../MudSkipperPointListeners.kt | 6 ++--- .../desert/quest/thegolem/TheGolemDialogue.kt | 24 ++++++++--------- .../desert/quest/thegolem/TheGolemQuest.kt | 18 +++---------- .../quest/plaguecity/PlagueCityListeners.kt | 7 ++--- .../quest/grandtree/GrandTreeListeners.kt | 27 +++++++------------ 7 files changed, 47 insertions(+), 58 deletions(-) diff --git a/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/DeathPlateauInteractionListener.kt b/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/DeathPlateauInteractionListener.kt index 01a253ad4..fd59d4697 100644 --- a/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/DeathPlateauInteractionListener.kt +++ b/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/DeathPlateauInteractionListener.kt @@ -1,11 +1,13 @@ package content.region.asgarnia.burthorpe.quest.deathplateau import content.data.Quests +import content.global.handlers.iface.ScrollInterface import core.api.* import core.game.global.action.DoorActionHandler import core.game.interaction.IntType import core.game.interaction.InteractionListener import core.game.node.item.GroundItemManager +import org.rs09.consts.Components import org.rs09.consts.Items import org.rs09.consts.Scenery @@ -23,6 +25,11 @@ class DeathPlateauInteractionListener : InteractionListener { Scenery.STONE_MECHANISM_3676, // 4 Outer stone plates Scenery.STONE_MECHANISM_3677 // 2 Inner stone plates ) + val combinationScroll = arrayOf( + "Red is North of Blue. Yellow is South of Purple.", + "Green is North of Purple. Blue is West of", + "Yellow. Purple is East of Red." + ) } override fun defineListeners() { on(Scenery.DOOR_3747, SCENERY, "open") { player, node -> @@ -48,10 +55,10 @@ class DeathPlateauInteractionListener : InteractionListener { } on(Items.COMBINATION_3102, ITEM, "read") { player, _ -> - openInterface(player, 220) - setInterfaceText(player, "Red is North of Blue. Yellow is South of Purple.", 220, 7) - setInterfaceText(player, "Green is North of Purple. Blue is West of", 220, 8) - setInterfaceText(player, "Yellow. Purple is East of Red.", 220, 9) + // https://youtu.be/C8n8Yi-J8wU?t=466 + openInterface(player, 222) + setInterfaceText(player, combinationScroll.joinToString("
"), 222, 5) + return@on true } diff --git a/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/IOUNoteDialogueFile.kt b/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/IOUNoteDialogueFile.kt index 4a4531422..29978f609 100644 --- a/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/IOUNoteDialogueFile.kt +++ b/Server/src/main/content/region/asgarnia/burthorpe/quest/deathplateau/IOUNoteDialogueFile.kt @@ -4,6 +4,9 @@ import content.data.Quests import core.api.* import core.game.dialogue.DialogueFile import core.game.dialogue.FacialExpression +import core.game.interaction.IntType +import core.game.interaction.InteractionListeners +import core.game.node.item.Item import core.tools.END_DIALOGUE import org.rs09.consts.Items @@ -26,10 +29,7 @@ class IOUNoteDialogueFile : DialogueFile() { 4 -> { end() stage = END_DIALOGUE - openInterface(player!!, 220) - setInterfaceText(player!!, "Red is North of Blue. Yellow is South of Purple.", 220, 7) - setInterfaceText(player!!, "Green is North of Purple. Blue is West of", 220, 8) - setInterfaceText(player!!, "Yellow. Purple is East of Red.", 220, 9) + InteractionListeners.run(Items.COMBINATION_3102, IntType.ITEM, "read", player!!, Item(Items.COMBINATION_3102)) } } } diff --git a/Server/src/main/content/region/asgarnia/mudskipperpoint/MudSkipperPointListeners.kt b/Server/src/main/content/region/asgarnia/mudskipperpoint/MudSkipperPointListeners.kt index eed56b4bf..6d31ea26b 100644 --- a/Server/src/main/content/region/asgarnia/mudskipperpoint/MudSkipperPointListeners.kt +++ b/Server/src/main/content/region/asgarnia/mudskipperpoint/MudSkipperPointListeners.kt @@ -14,6 +14,7 @@ import core.game.world.update.flag.context.Animation import core.game.world.update.flag.context.Graphics import core.plugin.Initializable import core.tools.RandomFunction +import core.tools.colorize import org.rs09.consts.Items import org.rs09.consts.NPCs import org.rs09.consts.Scenery @@ -119,9 +120,8 @@ class MudSkipperPointListeners : InteractionListener { } on(Scenery.SIGNPOST_10090, IntType.SCENERY, "read") { player, _ -> - setInterfaceText(player, "Mudskipper Point.", 220, 2) - setInterfaceText(player, "WARNING! BEWARE OF THE MUDSKIPPERS!", 220, 4) - openInterface(player, 220) + // https://runescape.wiki/w/Mudskipper_Point?oldid=969991 + sendDialogue(player, colorize("%RBEWARE OF THE MUDSKIPPERS!")) return@on true } diff --git a/Server/src/main/content/region/desert/quest/thegolem/TheGolemDialogue.kt b/Server/src/main/content/region/desert/quest/thegolem/TheGolemDialogue.kt index c1331e614..19af2735d 100644 --- a/Server/src/main/content/region/desert/quest/thegolem/TheGolemDialogue.kt +++ b/Server/src/main/content/region/desert/quest/thegolem/TheGolemDialogue.kt @@ -9,6 +9,7 @@ import org.rs09.consts.NPCs import core.game.dialogue.DialogueBuilder import core.game.dialogue.DialogueBuilderFile import content.data.Quests +import content.global.handlers.iface.ScrollLine @Initializable public final class ClayGolemDialoguePlugin(player: Player? = null) : DialoguePlugin(player) { @@ -107,19 +108,18 @@ class CuratorHaigHalenGolemDialogue : DialogueBuilderFile() { } } +// https://youtu.be/owPSLUB9E8k?t=261 val LETTER_LINES = arrayOf( - "", - "", - "Dearest Varmen,", - "I hope this finds you well. Here are the books you asked for", - "There has been an exciting development closer to home --", - "another city from the same period has been discovered east", - "of Varrock, and we are starting a huge excavation project", - "here. I don't know if the museum will be able to finance your", - "expedition as well as this one, so I fear your current trip will be", - "the last.", - "May Saradomin grant you a safe journey home", - "Your loving Elissa.", + ScrollLine("Dearest Varmen,", 3), + ScrollLine("I hope this finds you well. Here are the books you asked for", 4), + ScrollLine("There has been an exciting development closer to home --", 5), + ScrollLine("another city from the same period has been discovered east", 6), + ScrollLine("of Varrock, and we are starting a huge excavation project", 7), + ScrollLine("here. I don't know if the museum will be able to finance your", 8), + ScrollLine("expedition as well as this one, so I fear your current trip will be", 9), + ScrollLine("the last.", 10), + ScrollLine("May Saradomin grant you a safe journey home", 11), + ScrollLine("Your loving Elissa.", 12) ) val DISPLAY_CASE_TEXT = arrayOf("3rd age - yr 3000-4000", diff --git a/Server/src/main/content/region/desert/quest/thegolem/TheGolemQuest.kt b/Server/src/main/content/region/desert/quest/thegolem/TheGolemQuest.kt index 89d485f91..4d637b890 100644 --- a/Server/src/main/content/region/desert/quest/thegolem/TheGolemQuest.kt +++ b/Server/src/main/content/region/desert/quest/thegolem/TheGolemQuest.kt @@ -20,6 +20,8 @@ import core.game.interaction.InteractionListener import core.game.interaction.InterfaceListener import core.game.world.GameWorld import content.data.Quests +import content.global.handlers.iface.ScrollInterface +import org.rs09.consts.Components @Initializable class TheGolemQuest : Quest(Quests.THE_GOLEM, 70, 69, 1, 437, 0, 1, 10) { @@ -112,19 +114,6 @@ class ClayGolemNPC : AbstractNPC { } } -class LetterListener : InterfaceListener { - override fun defineInterfaceListeners() { - onOpen(220) { player, component -> - val lines: Array = player.getAttribute("ifaces:220:lines", arrayOf()) - for(i in 0 until Math.min(lines.size, 15)) { - setInterfaceText(player, lines[i], 220, i+1) - //setInterfaceText(player, "${i}", 220, i+1) - } - return@onOpen true - } - } -} - class DisplayCaseListener : InterfaceListener { override fun defineInterfaceListeners() { onOpen(534) { player, component -> @@ -372,9 +361,8 @@ class TheGolemListeners : InteractionListener { on(34978, IntType.SCENERY, "climb-down") { player, node -> core.game.global.action.ClimbActionHandler.climb(player, core.game.global.action.ClimbActionHandler.CLIMB_DOWN, core.game.global.action.SpecialLadders.getDestination(node.location)); return@on true } on(6372, IntType.SCENERY, "climb-up") { player, node -> core.game.global.action.ClimbActionHandler.climb(player, core.game.global.action.ClimbActionHandler.CLIMB_UP, core.game.global.action.SpecialLadders.getDestination(node.location)); return@on true } on(4615, IntType.ITEM, "read") { player, node -> - player.setAttribute("ifaces:220:lines", LETTER_LINES) player.setAttribute("/save:the-golem:read-elissa-letter", true) - openInterface(player, 220) + ScrollInterface.scrollSetup(player, Components.MESSAGESCROLL_220, LETTER_LINES) return@on true } on(35226, IntType.SCENERY, "search") { player, node -> diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt b/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt index 0c8ef60a0..9c5866a59 100644 --- a/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt +++ b/Server/src/main/content/region/kandarin/ardougne/quest/plaguecity/PlagueCityListeners.kt @@ -406,16 +406,17 @@ class PlagueCityListeners : InteractionListener { } } + // https://youtu.be/AG81LH51WME?t=302 private fun scruffyNote(player: Player) { val scruffynotes = arrayOf( "Got a bncket of nnilk", - "Tlen grind sorne lhoculate", + "Tlen qrind sorne lhoculate", "vnith a pestal and rnortar", "ald the grourd dlocolate to tho milt", - "finales add 5cme snape gras5", + "fnales add 5cme snape gras5", ) - setInterfaceText(player, scruffynotes.joinToString("
"), 222, 5) + setInterfaceText(player, scruffynotes.joinToString("
"), 222, 4) } override fun defineDestinationOverrides() { diff --git a/Server/src/main/content/region/kandarin/quest/grandtree/GrandTreeListeners.kt b/Server/src/main/content/region/kandarin/quest/grandtree/GrandTreeListeners.kt index 8548efe96..a0dd8b258 100644 --- a/Server/src/main/content/region/kandarin/quest/grandtree/GrandTreeListeners.kt +++ b/Server/src/main/content/region/kandarin/quest/grandtree/GrandTreeListeners.kt @@ -15,6 +15,9 @@ import org.rs09.consts.Items import org.rs09.consts.NPCs import org.rs09.consts.Sounds import content.data.Quests +import content.global.handlers.iface.ScrollInterface +import content.global.handlers.iface.ScrollLine +import org.rs09.consts.Components class GrandTreeListeners: InteractionListener { @@ -26,12 +29,11 @@ class GrandTreeListeners: InteractionListener { Location(2473,9897,0), ) - val hazelmerescroll = Items.HAZELMERES_SCROLL_786 - - val hazelmerescrollText = arrayOf( - "Es lemanto meso pro eis prit ta Cinqo mond.", - "Mi lovos ta lemanto Daconia arpos", - "et Daconia arpos eto meriz ta priw!", + // https://youtu.be/-jfueZpRhzU?t=184 + val hazelmereScrollText = arrayOf( + ScrollLine("Es lemanto meso pro eis prit ta Cinqo mond.", 6), + ScrollLine("Mi lovos ta lemanto Daconia arpos", 7), + ScrollLine("et Daconia arpos eto meriz ta priw!", 8) ) fun unlockTUZODoor(player: Player) { @@ -84,8 +86,8 @@ class GrandTreeListeners: InteractionListener { openDialogue(player, AnitaDialogue(), npc) return@on true } - on(hazelmerescroll, IntType.ITEM, "read") { player, node -> - hazelmereScroll(player, node.asItem()) + on(Items.HAZELMERES_SCROLL_786, IntType.ITEM, "read") { player, node -> + ScrollInterface.scrollSetup(player, Components.MESSAGESCROLL_220, hazelmereScrollText) return@on true } @@ -209,14 +211,5 @@ class GrandTreeListeners: InteractionListener { return@on true } } - - private fun hazelmereScroll(player: Player, item: Item) { - val id = item.id - openInterface(player, 222).also { - when (id) { - hazelmerescroll -> setInterfaceText(player, hazelmerescrollText.joinToString("
"), 222, 6) - } - } - } } From 0fdbf323d6911b068e57e97eb080ba6fca10df5f Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Sun, 21 Jun 2026 09:03:34 -0400 Subject: [PATCH 12/42] Fixed typos in Tribal Totem crate label dialogue --- .../region/karamja/quest/tribaltotem/TribalTotemListeners.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/region/karamja/quest/tribaltotem/TribalTotemListeners.kt b/Server/src/main/content/region/karamja/quest/tribaltotem/TribalTotemListeners.kt index 0fdbf616f..97a16f7d9 100644 --- a/Server/src/main/content/region/karamja/quest/tribaltotem/TribalTotemListeners.kt +++ b/Server/src/main/content/region/karamja/quest/tribaltotem/TribalTotemListeners.kt @@ -34,7 +34,7 @@ class TribalTotemListeners : InteractionListener { on(realCrate, IntType.SCENERY, "Investigate"){ player, node -> if(player.questRepository.getStage(Quests.TRIBAL_TOTEM) in 1..19 && !player.inventory.containsAtLeastOneItem(Items.ADDRESS_LABEL_1858)){ - sendDialogue(player,"There is a label on this crate. It says; To Lord Handelmort, Handelmort Mansion Ardogune.You carefully peel it off and take it.") + sendDialogue(player,"There is a label on this crate. It says; To Lord Handelmort, Handelmort Mansion, Ardougne.You carefully peel it off and take it.") addItemOrDrop(player,Items.ADDRESS_LABEL_1858,1) } else if(player.questRepository.getStage(Quests.TRIBAL_TOTEM) in 1..19 && player.inventory.containsAtLeastOneItem(Items.ADDRESS_LABEL_1858)){ From 18d5fb62cd059989a2d723cae060bff7311e52c4 Mon Sep 17 00:00:00 2001 From: Skal Fate <13443576-SkalFate@users.noreply.gitlab.com> Date: Fri, 3 Jul 2026 02:16:39 -0600 Subject: [PATCH 13/42] Small Bot Improvements and Fixes Fixed bot head item randomisation Fixed thief NPC bot stuck in bronze outfit Added outfits for poor/average/rich classes of bots Changed sex ratio of bots to 1:3 F:M from 1:5 (open to adjustment with era-authentic sources) --- .../main/content/global/bots/ManThiever.kt | 3 +- .../handlers/iface/CharacterDesign.java | 31 ++-- .../src/main/core/game/bots/AIPBuilder.java | 2 +- Server/src/main/core/game/bots/AIPlayer.java | 2 +- .../core/game/bots/SkillingBotAssembler.kt | 161 +++++++++++++++++- 5 files changed, 180 insertions(+), 19 deletions(-) diff --git a/Server/src/main/content/global/bots/ManThiever.kt b/Server/src/main/content/global/bots/ManThiever.kt index 25a2638c2..75e0a6aac 100644 --- a/Server/src/main/content/global/bots/ManThiever.kt +++ b/Server/src/main/content/global/bots/ManThiever.kt @@ -5,6 +5,7 @@ import core.game.bots.SkillingBotAssembler import core.game.bots.Script import core.game.interaction.IntType import core.game.interaction.InteractionListeners +import core.game.node.entity.skill.Skills import java.util.* class ManThiever : Script() { @@ -22,6 +23,6 @@ class ManThiever : Script() { } init { - equipment.addAll(Arrays.asList(Item(1103), Item(1139), Item(1265))) + skills[Skills.THIEVING] = 20 } } \ No newline at end of file diff --git a/Server/src/main/content/region/misc/tutisland/handlers/iface/CharacterDesign.java b/Server/src/main/content/region/misc/tutisland/handlers/iface/CharacterDesign.java index a40f65f84..c20c65f72 100644 --- a/Server/src/main/content/region/misc/tutisland/handlers/iface/CharacterDesign.java +++ b/Server/src/main/content/region/misc/tutisland/handlers/iface/CharacterDesign.java @@ -178,8 +178,8 @@ public final class CharacterDesign { case 40: player.getSettings().toggleMouseButton(); break; - case 92://hair style - case 93: + case 92://hair style left arrow + case 93://hair style right arrow changeLook(player, 0, buttonId == 93); break; case 97: @@ -348,18 +348,23 @@ public final class CharacterDesign { private static int getValue(Player player, String key, int index, int currentIndex, boolean increment) { int[] array = player.getAttribute("male", player.getAppearance().isMale()) ? MALE_LOOK_IDS[index] : FEMALE_LOOK_IDS[index]; int val = 0; - if (increment && currentIndex + 1 > array.length -1) { - val = (int) array[0]; - currentIndex = 0; - } else if (!increment && currentIndex -1 < 0) { - val = (int) array[array.length -1]; - currentIndex = array.length -1; - } else if (increment) { - val = (int) array[currentIndex + 1]; - currentIndex++; + if (player.isArtificial() && index <= 1) { + currentIndex = RandomFunction.random(array.length -1); + val = array[currentIndex]; } else { - val = (int) array[currentIndex - 1]; - currentIndex--; + if (increment && currentIndex + 1 > array.length -1) { + val = (int) array[0]; + currentIndex = 0; + } else if (!increment && currentIndex -1 < 0) { + val = (int) array[array.length -1]; + currentIndex = array.length -1; + } else if (increment) { + val = (int) array[currentIndex + 1]; + currentIndex++; + } else { + val = (int) array[currentIndex - 1]; + currentIndex--; + } } player.setAttribute(key + ":" + index, currentIndex); return val; diff --git a/Server/src/main/core/game/bots/AIPBuilder.java b/Server/src/main/core/game/bots/AIPBuilder.java index e79c95447..fb619f730 100644 --- a/Server/src/main/core/game/bots/AIPBuilder.java +++ b/Server/src/main/core/game/bots/AIPBuilder.java @@ -91,7 +91,7 @@ public final class AIPBuilder { //Varrock Ge bank RandomAfkPlayer(new Location(3189, 3439)); - //Home Bank + // Lunar Island Bank RandomAfkPlayer(new Location(2099, 3918)); } diff --git a/Server/src/main/core/game/bots/AIPlayer.java b/Server/src/main/core/game/bots/AIPlayer.java index 079875dec..e54fe732a 100644 --- a/Server/src/main/core/game/bots/AIPlayer.java +++ b/Server/src/main/core/game/bots/AIPlayer.java @@ -109,7 +109,7 @@ public class AIPlayer extends Player { * Generates bot stats/equipment/etc based on OSRScopyLine */ public void updateRandomValues() { - this.getAppearance().setGender(RandomFunction.random(5) == 1 ? Gender.FEMALE : Gender.MALE); + this.getAppearance().setGender(RandomFunction.random(3) == 1 ? Gender.FEMALE : Gender.MALE); int setTo = RandomFunction.random(0,10); CharacterDesign.randomize(this,true); this.setDirection(Direction.values()[new Random().nextInt(Direction.values().length)]); //Random facing dir diff --git a/Server/src/main/core/game/bots/SkillingBotAssembler.kt b/Server/src/main/core/game/bots/SkillingBotAssembler.kt index 4e47773fa..decb90a85 100644 --- a/Server/src/main/core/game/bots/SkillingBotAssembler.kt +++ b/Server/src/main/core/game/bots/SkillingBotAssembler.kt @@ -2,6 +2,7 @@ package core.game.bots import core.game.node.item.Item import core.game.world.map.Location +import org.rs09.consts.Items class SkillingBotAssembler { fun produce(type: Wealth, loc: Location): AIPlayer { @@ -39,7 +40,161 @@ class SkillingBotAssembler { RICH } - val POORSETS = arrayOf(listOf(542,544), listOf(581), listOf(6654,6655,6656), listOf(6654,6656), listOf(636,646), listOf(638,648), listOf(), listOf(), listOf()) - val AVGSETS = arrayOf(listOf(2649,342,344), listOf(2651,542,544), listOf(6654,6655,6656), listOf(6139,6141), listOf(9923,9924,9925), listOf(10400,10402,2649), listOf(10404,10406), listOf(12971,12978)) - val RICHSETS = arrayOf(listOf(10330,10332,2649), listOf(12873,12880,1046), listOf(13858,13861,13864), listOf(13887,13893), listOf(3481,3483), listOf(2653,2655), listOf(2661,2663), listOf(2591,2593), listOf(14490,14492)) + val POORSETS = arrayOf( + listOf(Items.BLACK_ROBE_581, Items.BLACK_SKIRT_1015, Items.STAFF_OF_AIR_1381), + listOf(Items.BLACK_ROBE_581, Items.BLACK_SKIRT_1015), + listOf(Items.BLACK_ROBE_581, Items.BLACK_SKIRT_1015, Items.WIZARD_HAT_1017), + listOf(), // No equipment + listOf(Items.WIZARD_HAT_579), + listOf(Items.WIZARD_HAT_579, Items.STAFF_OF_AIR_1381), + listOf(Items.WIZARD_HAT_579, Items.STAFF_1379), + listOf(Items.WIZARD_HAT_579, Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011), + listOf(Items.WIZARD_HAT_579, Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.TIARA_5525), + listOf(Items.WIZARD_HAT_579, Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.TEAM_1_CAPE_4315), + listOf(Items.WIZARD_HAT_579, Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.STAFF_OF_AIR_1381), + listOf(Items.WIZARD_HAT_579, Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.STAFF_OF_WATER_1383), + listOf(Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011), + listOf(Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.TIARA_5525), + listOf(Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.TEAM_1_CAPE_4315), + listOf(Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.STAFF_OF_AIR_1381), + listOf(Items.WIZARD_ROBE_577, Items.BLUE_SKIRT_1011, Items.STAFF_OF_WATER_1383), + listOf(), // No equipment + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_PLATEBODY_1119, Items.STEEL_KITESHIELD_1193, Items.STEEL_LONGSWORD_1295, Items.TEAM_29_CAPE_4371), + listOf(Items.STEEL_PLATESKIRT_1083, Items.STEEL_PLATEBODY_1119, Items.STEEL_KITESHIELD_1193, Items.STEEL_LONGSWORD_1295, Items.TEAM_30_CAPE_4373), + listOf(Items.STEEL_PLATESKIRT_1083, Items.STEEL_PLATEBODY_1119, Items.STEEL_FULL_HELM_1157, Items.TEAM_30_CAPE_4373), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.STEEL_FULL_HELM_1157, Items.TEAM_31_CAPE_4375), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_PLATEBODY_1119, Items.TEAM_29_CAPE_4371), + listOf(Items.STEEL_PLATESKIRT_1083, Items.STEEL_PLATEBODY_1119, Items.TEAM_30_CAPE_4373), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.TEAM_31_CAPE_4375), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.STEEL_2H_SWORD_1311, Items.TEAM_34_CAPE_4381), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.STEEL_SCIMITAR_1325, Items.TEAM_35_CAPE_4383), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.STEEL_BATTLEAXE_1365, Items.TEAM_35_CAPE_4383), + listOf(Items.STEEL_PLATELEGS_1069, Items.STEEL_CHAINBODY_1105, Items.STEEL_BATTLEAXE_1365, Items.STEEL_SQ_SHIELD_1177, Items.TEAM_35_CAPE_4383), + listOf(), // No equipment + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.IRON_KITESHIELD_1191, Items.IRON_LONGSWORD_1293, Items.TEAM_36_CAPE_4385), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.IRON_2H_SWORD_1309, Items.TEAM_37_CAPE_4387), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.IRON_SCIMITAR_1323, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.IRON_BATTLEAXE_1363, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_CHAINBODY_1101, Items.IRON_KITESHIELD_1191, Items.IRON_LONGSWORD_1293, Items.TEAM_33_CAPE_4379), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.TEAM_39_CAPE_4391), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.TEAM_37_CAPE_4387), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_PLATEBODY_1115, Items.IRON_SCIMITAR_1323, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_PLATEBODY_1115, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_CHAINBODY_1101, Items.TEAM_33_CAPE_4379), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_PLATEBODY_1115, Items.IRON_FULL_HELM_1153, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATESKIRT_1081, Items.IRON_CHAINBODY_1101, Items.IRON_FULL_HELM_1153, Items.TEAM_32_CAPE_4377), + listOf(Items.IRON_PLATELEGS_1067, Items.IRON_CHAINBODY_1101, Items.IRON_FULL_HELM_1153, Items.TEAM_33_CAPE_4379), + listOf(), // No equipment + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_PLATEBODY_1117,Items.BRONZE_FULL_HELM_1155,Items.BRONZE_KITESHIELD_1189,Items.BRONZE_LONGSWORD_1291,Items.TEAM_27_CAPE_4367), + listOf(Items.BRONZE_PLATESKIRT_1087,Items.BRONZE_PLATEBODY_1117,Items.BRONZE_FULL_HELM_1155,Items.BRONZE_KITESHIELD_1189,Items.BRONZE_LONGSWORD_1291,Items.TEAM_27_CAPE_4367), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_PLATEBODY_1117,Items.BRONZE_FULL_HELM_1155,Items.BRONZE_2H_SWORD_1307, Items.TEAM_28_CAPE_4369), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_CHAINBODY_1103), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_2H_SWORD_1307), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_SCIMITAR_1321), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_BATTLEAXE_1375), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_BATTLEAXE_1375,Items.BRONZE_SQ_SHIELD_1173), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_PLATEBODY_1117, Items.TEAM_27_CAPE_4367), + listOf(Items.BRONZE_PLATELEGS_1075,Items.BRONZE_PLATEBODY_1117,Items.BRONZE_FULL_HELM_1155, Items.TEAM_27_CAPE_4367), + listOf(Items.BRONZE_PLATESKIRT_1087,Items.BRONZE_CHAINBODY_1103), + listOf(Items.BRONZE_PLATESKIRT_1087,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_FULL_HELM_1155), + listOf(Items.BRONZE_PLATESKIRT_1087,Items.BRONZE_CHAINBODY_1103,Items.BRONZE_FULL_HELM_1155, Items.TEAM_28_CAPE_4369), + listOf(Items.BRONZE_PLATESKIRT_1087,Items.BRONZE_PLATEBODY_1117, Items.TEAM_28_CAPE_4369), + listOf(), // No equipment + listOf(Items.LEATHER_BODY_1129,Items.LEATHER_CHAPS_1095), + listOf(), // No equipment + listOf(Items.STUDDED_CHAPS_1097,Items.STUDDED_BODY_1133), + listOf(Items.MIME_LEGS_3059,Items.MIME_TOP_3058,Items.MIME_BOOTS_3061,Items.MIME_GLOVES_3060,Items.MIME_MASK_3057), + listOf(Items.MIME_LEGS_3059,Items.MIME_TOP_3058,Items.MIME_BOOTS_3061,Items.MIME_GLOVES_3060), + listOf(Items.MONKS_ROBE_542,Items.MONKS_ROBE_544), + listOf(Items.CHICKEN_LEGS_11022,Items.CHICKEN_WINGS_11020,Items.CHICKEN_HEAD_11021,Items.CHICKEN_FEET_11019), + listOf(Items.TOY_KITE_12844,Items.CHOCATRICE_CAPE_12634), + listOf(), // No equipment + listOf(Items.WHITE_APRON_1005, Items.CHEFS_HAT_1949), + listOf(Items.TEAM_10_CAPE_4333), + listOf(Items.TEAM_2_CAPE_4317), + listOf(), // No equipment + listOf(), // No equipment + listOf(), // No equipment + listOf()) + + val AVGSETS = arrayOf( + listOf(Items.SKELETAL_TOP_6139, Items.SKELETAL_BOTTOMS_6141), + listOf(Items.SKELETON_LEGGINGS_9923, Items.SKELETON_SHIRT_9924, Items.SKELETON_MASK_9925), + listOf(Items.BLACK_ELE_SHIRT_10400,Items.BLACK_ELE_LEGS_10402,Items.BROWN_HEADBAND_2649), + listOf(Items.RED_ELE_SHIRT_10404,Items.RED_ELE_LEGS_10406), + listOf(Items.COMBAT_ROBE_TOP_100_12971, Items.COMBAT_ROBE_BOTTOM_100_12978), + listOf(Items.COMBAT_ROBE_TOP_100_12971, Items.COMBAT_ROBE_BOTTOM_100_12978, Items.COMBAT_HOOD_100_12964), + listOf(Items.COMBAT_ROBE_TOP_100_12971, Items.COMBAT_ROBE_BOTTOM_100_12978, Items.RUNE_BERSERKER_SHIELD_100_12929), + listOf(Items.COMBAT_ROBE_TOP_100_12971, Items.COMBAT_ROBE_BOTTOM_100_12978, Items.ADAMANT_BERSERKER_SHIELD_100_12915), + listOf(Items.DRUIDIC_MAGE_TOP_100_12894, Items.DRUIDIC_MAGE_BOTTOM_100_12901, Items.DRUIDIC_MAGE_HOOD_100_12887), + listOf(), // No equipment + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_PLATEBODY_1121, Items.MITHRIL_KITESHIELD_1197, Items.MITHRIL_LONGSWORD_1299, Items.TEAM_29_CAPE_4371), + listOf(Items.MITHRIL_PLATESKIRT_1085, Items.MITHRIL_PLATEBODY_1121, Items.MITHRIL_KITESHIELD_1197, Items.MITHRIL_LONGSWORD_1299, Items.TEAM_30_CAPE_4373), + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_PLATEBODY_1121, Items.TEAM_29_CAPE_4371), + listOf(Items.MITHRIL_PLATESKIRT_1085, Items.MITHRIL_PLATEBODY_1121, Items.TEAM_30_CAPE_4373), + listOf(Items.MITHRIL_PLATESKIRT_1085, Items.MITHRIL_CHAINBODY_1109, Items.TEAM_30_CAPE_4373), + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_CHAINBODY_1109, Items.TEAM_31_CAPE_4375), + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_CHAINBODY_1109, Items.MITHRIL_2H_SWORD_1315, Items.TEAM_34_CAPE_4381), + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_CHAINBODY_1109, Items.MITHRIL_SCIMITAR_1329, Items.TEAM_35_CAPE_4383), + listOf(Items.MITHRIL_PLATELEGS_1071, Items.MITHRIL_CHAINBODY_1109, Items.MITHRIL_BATTLEAXE_1369, Items.TEAM_36_CAPE_4385), + listOf(), // No equipment + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_PLATEBODY_1124,Items.ADAMANT_KITESHIELD_1199,Items.ADAMANT_LONGSWORD_1301, Items.TEAM_29_CAPE_4371), + listOf(Items.ADAMANT_PLATESKIRT_1091,Items.ADAMANT_PLATEBODY_1124,Items.ADAMANT_KITESHIELD_1199,Items.ADAMANT_LONGSWORD_1301, Items.TEAM_30_CAPE_4373), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_PLATEBODY_1124, Items.TEAM_29_CAPE_4371), + listOf(Items.ADAMANT_PLATESKIRT_1091,Items.ADAMANT_PLATEBODY_1124, Items.TEAM_30_CAPE_4373), + listOf(Items.ADAMANT_PLATESKIRT_1091,Items.ADAMANT_CHAINBODY_1111, Items.TEAM_30_CAPE_4373), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_CHAINBODY_1111, Items.TEAM_31_CAPE_4375), + listOf(Items.ADAMANT_PLATESKIRT_1091,Items.ADAMANT_CHAINBODY_1111,Items.ADAMANT_FULL_HELM_1161, Items.TEAM_30_CAPE_4373), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_CHAINBODY_1111,Items.ADAMANT_FULL_HELM_1161, Items.TEAM_31_CAPE_4375), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_CHAINBODY_1111,Items.ADAMANT_2H_SWORD_1317, Items.TEAM_34_CAPE_4381), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_CHAINBODY_1111,Items.ADAMANT_SCIMITAR_1331, Items.TEAM_35_CAPE_4383), + listOf(Items.ADAMANT_PLATELEGS_1073,Items.ADAMANT_CHAINBODY_1111,Items.ADAMANT_BATTLEAXE_1371, Items.TEAM_35_CAPE_4383), + listOf(), // No equipment + listOf(Items.GREEN_DHIDE_BODY_1135,Items.GREEN_DHIDE_CHAPS_1099,Items.GREEN_DHIDE_VAMB_1065,Items.GREEN_DHIDE_COIF_100_12936,Items.AVAS_ACCUMULATOR_10499), + listOf(Items.GREEN_DHIDE_BODY_1135,Items.GREEN_DHIDE_CHAPS_1099,Items.GREEN_DHIDE_VAMB_1065,Items.GREEN_DHIDE_COIF_100_12936,Items.ADAMANT_CROSSBOW_9183,Items.AVAS_ACCUMULATOR_10499), + listOf(Items.TEAM_10_CAPE_4333), + listOf(Items.TEAM_1_CAPE_4315), + listOf(), // No equipment + listOf(), // No equipment + listOf()) + + val RICHSETS = arrayOf( + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_PLATEBODY_1127,Items.RUNE_KITESHIELD_1201,Items.RUNE_FULL_HELM_1163,Items.RUNE_LONGSWORD_1303,Items.TEAM_29_CAPE_4371), + listOf(Items.RUNE_PLATESKIRT_1093,Items.RUNE_PLATEBODY_1127,Items.RUNE_KITESHIELD_1201,Items.RUNE_FULL_HELM_1163,Items.RUNE_LONGSWORD_1303,Items.TEAM_30_CAPE_4373), + listOf(Items.RUNE_PLATESKIRT_1093,Items.RUNE_PLATEBODY_1127, Items.TEAM_29_CAPE_4371), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_PLATEBODY_1127,Items.RUNE_FULL_HELM_1163,Items.TEAM_29_CAPE_4371), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_PLATEBODY_1127,Items.RUNE_BERSERKER_SHIELD_100_12929,Items.TEAM_29_CAPE_4371), + listOf(Items.RUNE_PLATESKIRT_1093,Items.RUNE_PLATEBODY_1127,Items.TEAM_30_CAPE_4373), + listOf(Items.RUNE_PLATESKIRT_1093,Items.RUNE_CHAINBODY_1113,Items.TEAM_30_CAPE_4373), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_CHAINBODY_1113,Items.TEAM_31_CAPE_4375), + listOf(Items.RUNE_PLATESKIRT_1093,Items.RUNE_CHAINBODY_1113,Items.RUNE_FULL_HELM_1163,Items.TEAM_30_CAPE_4373), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_CHAINBODY_1113,Items.RUNE_FULL_HELM_1163,Items.TEAM_31_CAPE_4375), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_CHAINBODY_1113,Items.RUNE_2H_SWORD_1319,Items.TEAM_34_CAPE_4381), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_CHAINBODY_1113,Items.RUNE_SCIMITAR_1333,Items.TEAM_35_CAPE_4383), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_CHAINBODY_1113,Items.RUNE_BATTLEAXE_1373,Items.TEAM_35_CAPE_4383), + listOf(Items.RUNE_PLATELEGS_1079,Items.RUNE_PLATEBODY_1127,Items.ABYSSAL_WHIP_4151,Items.TEAM_29_CAPE_4371), + listOf(), // No equipment + listOf(Items.RUNE_PLATEBODY_G_2615,Items.RUNE_PLATELEGS_G_2618,Items.RUNE_KITESHIELD_G_2621,Items.RUNE_FULL_HELMG_2619, Items.TEAM_27_CAPE_4367), + listOf(Items.RUNE_PLATEBODY_T_2623,Items.RUNE_PLATELEGS_T_2625,Items.RUNE_KITESHIELD_T_2629,Items.RUNE_FULL_HELM_T_2627, Items.TEAM_27_CAPE_4367), + listOf(Items.GILDED_PLATEBODY_3481,Items.GILDED_PLATELEGS_3483,Items.CAPE_1030), + listOf(Items.ZAMORAK_PLATEBODY_2653,Items.ZAMORAK_PLATELEGS_2655,Items.ZAMORAK_FULL_HELM_2657), + listOf(Items.SARADOMIN_PLATEBODY_2661,Items.SARADOMIN_PLATELEGS_2663,Items.SARADOMIN_FULL_HELM_2665), + listOf(Items.GUTHIX_PLATEBODY_2669,Items.GUTHIX_PLATELEGS_2671,Items.GUTHIX_FULL_HELM_2673), + listOf(Items.ZAMORAK_PLATEBODY_2653,Items.ZAMORAK_PLATELEGS_2655,Items.ZAMORAK_FULL_HELM_2657,Items.ZAMORAK_CAPE_2414), + listOf(Items.SARADOMIN_PLATEBODY_2661,Items.SARADOMIN_PLATELEGS_2663,Items.SARADOMIN_FULL_HELM_2665,Items.SARADOMIN_CAPE_2412), + listOf(Items.GUTHIX_PLATEBODY_2669,Items.GUTHIX_PLATELEGS_2671,Items.GUTHIX_FULL_HELM_2673,Items.GUTHIX_CAPE_2413), + listOf(Items.THIRD_AGE_ROBE_10340,Items.THIRD_AGE_ROBE_TOP_10338,Items.SANTA_HAT_1050), + listOf(), // No equipment + listOf(Items.DRAGON_PLATELEGS_4087,Items.DRAGON_CHAINBODY_3140,Items.ABYSSAL_WHIP_4151,Items.DRAGON_FULL_HELM_11335), + listOf(Items.DRAGON_PLATELEGS_4087,Items.DRAGON_CHAINBODY_3140,Items.DRAGONFIRE_SHIELD_11283,Items.DRAGON_FULL_HELM_11335), + listOf(), // No equipment + listOf(Items.BLACK_DHIDE_BODY_2503,Items.BLACK_DHIDE_CHAPS_2497,Items.BLACK_DHIDE_VAMB_2491,Items.RUNE_CROSSBOW_9185,Items.AVAS_ACCUMULATOR_10499), + listOf(Items.BLACK_DHIDE_BODY_2503,Items.BLACK_DHIDE_CHAPS_2497,Items.BLACK_DHIDE_VAMB_2491,Items.BLACK_DHIDE_COIF_100_12957,Items.RUNE_CROSSBOW_9185,Items.RUNE_KITESHIELD_1201), + listOf(Items.ELITE_BLACK_PLATEBODY_14492,Items.ELITE_BLACK_PLATELEGS_14490, Items.ELITE_BLACK_FULL_HELM_14494), + listOf(Items.BATTLE_ROBE_BOTTOM_100_12880,Items.BATTLE_ROBE_TOP_100_12873,Items.PURPLE_PARTYHAT_1046), + listOf(Items.ZURIELS_ROBE_BOTTOM_13862,Items.ZURIELS_ROBE_TOP_13858,Items.ZURIELS_HOOD_13864,Items.ZURIELS_HOOD_13864,Items.ZURIELS_STAFF_13867), + listOf(Items.VESTAS_CHAINBODY_13887,Items.VESTAS_PLATESKIRT_13893), + listOf(), // No equipment + listOf(), // No equipment + listOf()) } From 7b55acaddc408f00103a6e08f91c42d3af45e5b2 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Fri, 3 Jul 2026 04:24:12 -0400 Subject: [PATCH 14/42] Added purple dye ground spawn near the Khazard battlefield --- Server/data/configs/ground_spawns.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Server/data/configs/ground_spawns.json b/Server/data/configs/ground_spawns.json index d0c035733..1893b6df3 100644 --- a/Server/data/configs/ground_spawns.json +++ b/Server/data/configs/ground_spawns.json @@ -415,6 +415,10 @@ "item_id": "1755", "loc_data": "{1,2935,3286,0,90}-" }, + { + "item_id": "1773", + "loc_data": "{1,2563,3261,0,100}-" + }, { "item_id": "1785", "loc_data": "{1,2822,3355,0,150}-" From 559bdb5a681d09e5319b415ba1aa13c230e81617 Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Fri, 3 Jul 2026 04:24:50 -0400 Subject: [PATCH 15/42] Abyssal walkers now drop two abyssal charms --- Server/data/configs/drop_tables.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/data/configs/drop_tables.json b/Server/data/configs/drop_tables.json index 4a1af4001..840ac4d07 100644 --- a/Server/data/configs/drop_tables.json +++ b/Server/data/configs/drop_tables.json @@ -29831,10 +29831,10 @@ "maxAmount": "1" }, { - "minAmount": "1", + "minAmount": "2", "weight": "100.0", "id": "12161", - "maxAmount": "1" + "maxAmount": "2" }, { "minAmount": "1", From 0d1dda90d822f3c6c76727fccb489d1a2ddbc02a Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Fri, 3 Jul 2026 04:35:07 -0400 Subject: [PATCH 16/42] Made Leaf-bladed spear alchemisable --- Server/data/configs/item_configs.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/data/configs/item_configs.json b/Server/data/configs/item_configs.json index a51c6805d..b928857e7 100644 --- a/Server/data/configs/item_configs.json +++ b/Server/data/configs/item_configs.json @@ -35942,6 +35942,7 @@ "id": "4158", "name": "Leaf-bladed spear", "examine": "A spear with a leaf-shaped point.", + "alchemizable": "true", "archery_ticket_price": "0", "attack_anims": "2080,2081,2082,2080", "attack_speed": "5", From db960154e678cc8f5395daaa54dc4184f4e4e47b Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Fri, 3 Jul 2026 08:35:56 +0000 Subject: [PATCH 17/42] Can now smelt jewellery at the clay forge on Netiznot --- .../global/skill/crafting/JewelleryCraftPlugin.java | 8 +++++++- .../skill/crafting/silver/SilverCraftingListener.kt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Server/src/main/content/global/skill/crafting/JewelleryCraftPlugin.java b/Server/src/main/content/global/skill/crafting/JewelleryCraftPlugin.java index 72d19ef46..30c382180 100644 --- a/Server/src/main/content/global/skill/crafting/JewelleryCraftPlugin.java +++ b/Server/src/main/content/global/skill/crafting/JewelleryCraftPlugin.java @@ -5,6 +5,7 @@ import content.global.skill.crafting.jewellery.JewelleryCrafting; import core.game.interaction.NodeUsageEvent; import core.game.interaction.UseWithHandler; import core.plugin.Plugin; +import org.rs09.consts.Scenery; /** * Represents the plugin used to craft jewellery. @@ -17,7 +18,12 @@ public final class JewelleryCraftPlugin extends UseWithHandler { /** * Represents the ids to use for this plugin. */ - private static final int[] IDS = new int[] { 4304, 6189, 11010, 11666, 12100, 12809, 18497, 26814, 30021, 30510, 36956, 37651 }; + private static final int[] IDS = new int[]{ + Scenery.FURNACE_4304, Scenery.FURNACE_6189, Scenery.FURNACE_11010, Scenery.FURNACE_11666, + Scenery.FURNACE_12100, Scenery.FURNACE_12809, Scenery.FURNACE_18497, Scenery.CLAY_FORGE_21303, + Scenery.FURNACE_26814, Scenery.FURNACE_30021, Scenery.FURNACE_30510, Scenery.FURNACE_36956, + Scenery.FURNACE_37651 + }; /** * Constructs a new {@code JewelleryCraftPlugin} {@code Object}. diff --git a/Server/src/main/content/global/skill/crafting/silver/SilverCraftingListener.kt b/Server/src/main/content/global/skill/crafting/silver/SilverCraftingListener.kt index 346b61b3e..6ab0e7a45 100644 --- a/Server/src/main/content/global/skill/crafting/silver/SilverCraftingListener.kt +++ b/Server/src/main/content/global/skill/crafting/silver/SilverCraftingListener.kt @@ -31,7 +31,7 @@ class SilverCraftingListener : InterfaceListener, InteractionListener { Scenery.FURNACE_12100, Scenery.FURNACE_12809, Scenery.FURNACE_18497, Scenery.FURNACE_18525, Scenery.FURNACE_18526, Scenery.FURNACE_21879, Scenery.FURNACE_22721, Scenery.FURNACE_26814, Scenery.FURNACE_28433, Scenery.FURNACE_28434, Scenery.FURNACE_30021, Scenery.FURNACE_30510, - Scenery.FURNACE_36956, Scenery.FURNACE_37651 + Scenery.FURNACE_36956, Scenery.FURNACE_37651, Scenery.CLAY_FORGE_21303 ) private const val ATTRIBUTE_FURNACE_ID = "crafting:silver:furnace_id" From 166ecbacbf13782928c314c7cfa7c2f9636d72aa Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Fri, 3 Jul 2026 04:36:22 -0400 Subject: [PATCH 18/42] Brutal green dragons now drop hard clue scrolls instead of easy --- Server/data/configs/drop_tables.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/data/configs/drop_tables.json b/Server/data/configs/drop_tables.json index 840ac4d07..2b5926e69 100644 --- a/Server/data/configs/drop_tables.json +++ b/Server/data/configs/drop_tables.json @@ -41196,7 +41196,7 @@ { "minAmount": "1", "weight": "5.0", - "id": "1", + "id": "12070", "maxAmount": "1" }, { From e84798c82cd498cb34f75f8ed3e73f4bb576e48e Mon Sep 17 00:00:00 2001 From: Edie Date: Fri, 3 Jul 2026 08:39:26 +0000 Subject: [PATCH 19/42] Changed the initial spawn location for new accounts --- Server/src/test/resources/test.conf | 2 +- Server/worldprops/default.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/src/test/resources/test.conf b/Server/src/test/resources/test.conf index b143db9e0..e789bae24 100644 --- a/Server/src/test/resources/test.conf +++ b/Server/src/test/resources/test.conf @@ -35,7 +35,7 @@ motw_identifier = "0" #text shown for message of the week - @name will be replaced with the name property set above. motw_text = "Welcome to @name!" #the coordinates new players spawn at -new_player_location = "2524,5002,0" +new_player_location = "3094,3107,0" #the location of home teleport home_location = "3222,3218,0" autostock_ge = false diff --git a/Server/worldprops/default.conf b/Server/worldprops/default.conf index ecf6da0bf..ed0a4fa62 100644 --- a/Server/worldprops/default.conf +++ b/Server/worldprops/default.conf @@ -66,7 +66,7 @@ motw_identifier = "0" #text shown for message of the week - @name will be replaced with the name property set above. motw_text = "Welcome to @name!" #the coordinates new players spawn at -new_player_location = "2524,5002,0" +new_player_location = "3094,3107,0" #the location of home teleport home_location = "3222,3218,0" autostock_ge = false From 5e6c5bdee167580e13e278ce255ba4939a917d3b Mon Sep 17 00:00:00 2001 From: Tooze Date: Fri, 3 Jul 2026 08:59:43 +0000 Subject: [PATCH 20/42] NPCs no longer favor the north east corner when walking --- Server/src/main/core/game/node/entity/npc/NPC.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index b8139e66a..d0e349d7c 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -767,7 +767,8 @@ public class NPC extends Entity { */ protected Location getMovementDestination() { if (!pathBoundMovement || movementPath == null || movementPath.length < 1) { - Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0); + int radius = getWalkRadius() / 2; + Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(RandomFunction.random(-radius, radius+1), RandomFunction.random(-radius, radius+1), 0); int dist = (int) Location.getDistance(location, returnToSpawnLocation); int pathLimit = 14; if (dist > pathLimit) { From 97f78b9058d67e48694a6ca20e7dfc70d2de7344 Mon Sep 17 00:00:00 2001 From: Edie Date: Fri, 3 Jul 2026 09:02:55 +0000 Subject: [PATCH 21/42] Fixed another soft lock in Dwarf Cannon quest --- .../content/minigame/barrows/BarrowsActivityPlugin.java | 1 - .../kandarin/quest/dwarfcannon/DwarfCannonPlugin.java | 8 ++++++++ .../region/kandarin/quest/dwarfcannon/NulodionsNotes.kt | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Server/src/main/content/minigame/barrows/BarrowsActivityPlugin.java b/Server/src/main/content/minigame/barrows/BarrowsActivityPlugin.java index fa8df174c..48b95f68f 100644 --- a/Server/src/main/content/minigame/barrows/BarrowsActivityPlugin.java +++ b/Server/src/main/content/minigame/barrows/BarrowsActivityPlugin.java @@ -151,7 +151,6 @@ public final class BarrowsActivityPlugin extends ActivityPlugin { Player player = (Player) e; PacketRepository.send(MinimapState.class, new MinimapStateContext(player, 2)); player.getInterfaceManager().openOverlay(OVERLAY); - setVarp(player, 0, 1); if (getVarp(player, 452) == 0) { shuffleCatacombs(player); } diff --git a/Server/src/main/content/region/kandarin/quest/dwarfcannon/DwarfCannonPlugin.java b/Server/src/main/content/region/kandarin/quest/dwarfcannon/DwarfCannonPlugin.java index 309895c76..876f9cfda 100644 --- a/Server/src/main/content/region/kandarin/quest/dwarfcannon/DwarfCannonPlugin.java +++ b/Server/src/main/content/region/kandarin/quest/dwarfcannon/DwarfCannonPlugin.java @@ -44,6 +44,7 @@ public class DwarfCannonPlugin extends OptionHandler { SceneryDefinition.forId(3).getHandlers().put("option:open", this); SceneryDefinition.forId(5).getHandlers().put("option:inspect", this); SceneryDefinition.forId(2).getHandlers().put("option:enter", this); + SceneryDefinition.forId(11).getHandlers().put("option:climb-up", this); SceneryDefinition.forId(13).getHandlers().put("option:climb-over", this); SceneryDefinition.forId(15601).getHandlers().put("option:inspect", this); for (int i = 15; i < 21; i++) { @@ -110,6 +111,13 @@ public class DwarfCannonPlugin extends OptionHandler { player.animate(Animation.create(845)); player.teleport(new Location(2619, 9797, 0), 2); break; + case 11: + if (node.getLocation().equals(new Location(2570, 3443, 1)) + && quest.getStage(player) == 20 + && !player.hasItem(DwarfCannon.DWARF_REMAINS)) { + setVarp(player, 0, 3, true); + } + return ClimbActionHandler.climbLadder(player, node.asScenery(), option); case 13: ClimbActionHandler.climb(player, new Animation(828), new Location(2623, 3391, 0)); break; diff --git a/Server/src/main/content/region/kandarin/quest/dwarfcannon/NulodionsNotes.kt b/Server/src/main/content/region/kandarin/quest/dwarfcannon/NulodionsNotes.kt index 9d9aea830..9923b7426 100644 --- a/Server/src/main/content/region/kandarin/quest/dwarfcannon/NulodionsNotes.kt +++ b/Server/src/main/content/region/kandarin/quest/dwarfcannon/NulodionsNotes.kt @@ -4,7 +4,6 @@ import content.global.handlers.iface.BookInterface import content.global.handlers.iface.BookLine import content.global.handlers.iface.Page import content.global.handlers.iface.PageSet -import core.api.setAttribute import core.game.interaction.IntType import core.game.interaction.InteractionListener import core.game.node.entity.player.Player From 09d9d1ec55595d934ccf979dd449067adf16d0fa Mon Sep 17 00:00:00 2001 From: Bishop Date: Fri, 3 Jul 2026 09:08:16 +0000 Subject: [PATCH 22/42] Rewrote boss kill counter Removed inauthentic slayer tasks and boss pets --- .../main/content/data/BossKillCounter.java | 172 ------------------ .../src/main/content/data/BossKillCounter.kt | 72 ++++++++ .../handlers/item/EnchantedGemListener.kt | 8 +- .../content/global/skill/slayer/Tasks.java | 12 -- .../content/minigame/barrows/RewardChest.kt | 2 +- .../falador/handlers/GiantMoleNPC.java | 2 +- .../handlers/gwd/GodwarsBossNPC.java | 2 +- .../desert/handlers/KalphiteQueenNPC.java | 4 +- .../waterbirth/handlers/DagannothKingNPC.java | 2 +- .../handlers/TzhaarFightCavesPlugin.java | 11 +- .../lumbridge/handlers/TormentedDemonNPC.java | 2 +- .../region/wilderness/handlers/BorkNPC.java | 2 +- .../handlers/ChaosElementalNPC.java | 2 +- .../handlers/CorporealBeastNPC.java | 2 +- .../handlers/KingBlackDragonNPC.java | 2 +- 15 files changed, 86 insertions(+), 211 deletions(-) delete mode 100644 Server/src/main/content/data/BossKillCounter.java create mode 100644 Server/src/main/content/data/BossKillCounter.kt diff --git a/Server/src/main/content/data/BossKillCounter.java b/Server/src/main/content/data/BossKillCounter.java deleted file mode 100644 index e6d865600..000000000 --- a/Server/src/main/content/data/BossKillCounter.java +++ /dev/null @@ -1,172 +0,0 @@ -package content.data; - -import core.game.node.entity.player.Player; - -/** - * The BossKillcounter keeps track of the amount of bosses the player has slain. - * addtoKillcount(player, npcId) should be added in the finalizeDeath() method of the combat handler for the boss. - * @author Splinter - */ -public enum BossKillCounter { - - - /* ORDINAL BOUND */ - KING_BLACK_DRAGON(new int[] { 50 }, "King Black Dragon", 14649), - BORK(new int[] { 7133, 7134 }, "Bork", -1), - DAGANNOTH_SUPREME(new int[] { 2881 }, "Dagannoth Supreme", 14639), - DAGANNOTH_PRIME(new int[] { 2882 }, "Dagannoth Prime", 14640), - DAGANNOTH_REX(new int[] { 2883 }, "Dagannoth Rex", 14641), - CHAOS_ELEMENTAL(new int[] { 3200 }, "Chaos Elemental", 14638), - GIANT_MOLE(new int[] { 3340 }, "Giant Mole", 14642), - SARADOMIN(new int[] { 6247 }, "Commander Zilyana", 14647), - ZAMORAK(new int[] { 6203 }, "K'ril Tsutsaroth", 14648), - BANDOS(new int[] { 6260 }, "General Graardor", 14646), - ARMADYL(new int[] { 6222 }, "Kree'arra", 14645), - JAD(new int[] { 2745 }, "Tz-Tok Jad", 14828), - KALPHITE_QUEEN(new int[] { 1160 }, "Kalphite Queen", 14650), - CORPOREAL_BEAST(new int[] { 8133 }, "Corporeal Beast", 14653), - TORMENTED_DEMONS(new int[] { - 8349, 8350, 8351, 8352, 8353, 8354, - 8355, 8356, 8357, 8358, 8359, 8360, - 8361, 8362, 8363, 8364, 8365, 8366, - }, "Tormented demon", -1), - - - ; - - /** - * The npcs that can increase the killcounter - */ - private final int[] npc; - - /** - * The name of the NPC, to be displayed as a sendMessage - */ - private final String name; - - /** - * The item ID of the pet relating to the boss. - */ - private final int petId; - - /** - * Constructs a new {@code BossKillCounter} {@code Object}. - * @param npc the npc. - * @param name the npc's string name - */ - BossKillCounter(final int[] npc, final String name, final int petId) { - this.npc = npc; - this.name = name; - this.petId = petId; - } - - /** - * Gets the npc. - * @return The npc. - */ - public int[] getNpc() { - return npc; - } - - /** - * Gets the NPC's name - * @return their name - */ - public String getName() { - return name; - } - - /** - * Gets the petId - * @return The petId - */ - public int getPetId() { - return petId; - } - - /** - * Gets the type for the npc. - * @param npc the npc. - * @return the BossKillcounter - */ - public static BossKillCounter forNPC(final int npc) { - for (BossKillCounter kc : BossKillCounter.values()) { - for (int i : kc.getNpc()) { - if (npc == i) { - return kc; - } - } - } - return null; - } - - /** - * Adds to the player's killcount for that particular boss. - * @param killer The player who killed the npc - * @param npcid the ID of the npc that just died - */ - public static void addtoKillcount(Player killer, int npcid) { - if (killer == null) { - return; - } - BossKillCounter boss = BossKillCounter.forNPC(npcid); - if (boss == null) { - return; - } - killer.getSavedData().getGlobalData().getBossCounters()[boss.ordinal()]++; - killer.getPacketDispatch().sendMessage("Your " + boss.getName() + " killcount is now: " + killer.getSavedData().getGlobalData().getBossCounters()[boss.ordinal()] + "."); -// addBossPet(killer, npcid, boss); - } - -// /** -// * Gives the player the pet if they killed a certain boss. -// * The chance by default is 1/5000. This rate lowers to 1/2200 if the for Boss Pets is active. -// * Note: Not all bosses have pet versions of themselves. -// */ -// private static void addBossPet(Player killer, int npcid, BossKillCounter boss){ -// if(boss.getPetId() == -1){ //The boss does not have a pet version. -// return; -// } -// int number = 5000; -// if (npcid == 2745) { -// number = 200; -// if (killer.getSlayer().getTask() == Tasks.JAD) { -// number = 100; -// } -// } else if (npcid == 3200) { -// number = 300; -// } -// int rand = number; -// if(rand == 10){ -// for (int i = 0; i < killer.getFamiliarManager().getInsuredPets().size(); i++) { -// if (killer.getFamiliarManager().getInsuredPets().get(i).getBabyItemId() == boss.getPetId()) { -// return; -// } -// } -// if(killer.getFamiliarManager().hasFamiliar() && killer.getInventory().freeSlots() < 1){ -// return; -// } -// if(!killer.getFamiliarManager().hasFamiliar()){ -// killer.getFamiliarManager().summon(new Item(boss.getPetId()), true); -// killer.sendNotificationMessage("You have a funny feeling like you're being followed."); -// } else if (killer.getInventory().freeSlots() > 0){ -// killer.getInventory().add(new Item(boss.getPetId(), 1)); -// killer.sendNotificationMessage("You feel something weird sneaking into your backpack."); -// } -// Repository.sendNews(killer.getUsername()+" now commands a miniature "+(boss.equals(CORPOREAL_BEAST) ? "Dark core" : boss.getName())+"!"); -// } -// } - - /** - * Increments the player's Barrows chest counter. - * @param player the player - */ - public static void addtoBarrowsCount(Player player) { - if (player == null) { - return; - } - player.getSavedData().getGlobalData().setBarrowsLoots(player.getSavedData().getGlobalData().getBarrowsLoots() + 1); - player.getPacketDispatch().sendMessage("Your Barrows chest count is: " + player.getSavedData().getGlobalData().getBarrowsLoots() + "."); - } - -} diff --git a/Server/src/main/content/data/BossKillCounter.kt b/Server/src/main/content/data/BossKillCounter.kt new file mode 100644 index 000000000..cea7c687d --- /dev/null +++ b/Server/src/main/content/data/BossKillCounter.kt @@ -0,0 +1,72 @@ +package content.data + +import core.api.sendMessage +import core.game.node.entity.player.Player +import org.rs09.consts.NPCs + +/** + * Enumerates the bosses that have their kill counts tracked in a player's statistics (::stats). + * addToBossKillCount(player, npcId) should be added in the finalizeDeath() method of the combat handler for the boss. + * ORDINAL BOUND + * @author Bishop + */ + +enum class BossKillCounter(val bossId: IntArray, val bossName: String) { + + KING_BLACK_DRAGON(intArrayOf(NPCs.KING_BLACK_DRAGON_50), "King Black Dragon"), + BORK(intArrayOf(NPCs.BORK_7133, NPCs.BORK_7134), "Bork"), + DAGANNOTH_SUPREME(intArrayOf(NPCs.DAGANNOTH_SUPREME_2881), "Dagannoth Supreme"), + DAGANNOTH_PRIME(intArrayOf(NPCs.DAGANNOTH_PRIME_2882), "Dagannoth Prime"), + DAGANNOTH_REX(intArrayOf(NPCs.DAGANNOTH_REX_2883), "Dagannoth Rex"), + CHAOS_ELEMENTAL(intArrayOf(NPCs.CHAOS_ELEMENTAL_3200), "Chaos Elemental"), + GIANT_MOLE(intArrayOf(NPCs.GIANT_MOLE_3340), "Giant Mole"), + SARADOMIN(intArrayOf(NPCs.COMMANDER_ZILYANA_6247), "Commander Zilyana"), + ZAMORAK(intArrayOf(NPCs.KRIL_TSUTSAROTH_6203), "K'ril Tsutsaroth"), + BANDOS(intArrayOf(NPCs.GENERAL_GRAARDOR_6260), "General Graardor"), + ARMADYL(intArrayOf(NPCs.KREEARRA_6222), "Kree'arra"), + JAD(intArrayOf(NPCs.TZTOK_JAD_2745), "Tz-Tok Jad"), + KALPHITE_QUEEN(intArrayOf(NPCs.KALPHITE_QUEEN_1160), "Kalphite Queen"), + CORPOREAL_BEAST(intArrayOf(NPCs.CORPOREAL_BEAST_8133), "Corporeal Beast"), + TORMENTED_DEMONS( + intArrayOf( + NPCs.TORMENTED_DEMON_8349, NPCs.TORMENTED_DEMON_8350, NPCs.TORMENTED_DEMON_8351, + NPCs.TORMENTED_DEMON_8352, NPCs.TORMENTED_DEMON_8353, NPCs.TORMENTED_DEMON_8354, + NPCs.TORMENTED_DEMON_8355, NPCs.TORMENTED_DEMON_8356, NPCs.TORMENTED_DEMON_8357, + NPCs.TORMENTED_DEMON_8358, NPCs.TORMENTED_DEMON_8359, NPCs.TORMENTED_DEMON_8360, + NPCs.TORMENTED_DEMON_8361, NPCs.TORMENTED_DEMON_8362, NPCs.TORMENTED_DEMON_8363, + NPCs.TORMENTED_DEMON_8364, NPCs.TORMENTED_DEMON_8365, NPCs.TORMENTED_DEMON_8366), + "Tormented demon" + ), + PENANCE_QUEEN(intArrayOf(NPCs.PENANCE_QUEEN_5247), "Penance Queen"); + + companion object { + private fun forBossId(npc: Int): BossKillCounter? { + for (kc in values()) { + for (i in kc.bossId) { + if (npc == i) { + return kc + } + } + } + return null + } + + @JvmStatic + fun addToBossKillCount(killer: Player?, bossId: Int) { + if (killer == null) { + return + } + val boss: BossKillCounter = forBossId(bossId) ?: return + killer.getSavedData().globalData.bossCounters[boss.ordinal]++ + sendMessage(killer, "Your ${boss.bossName} killcount is now: ${killer.getSavedData().globalData.bossCounters[boss.ordinal]}.") + } + + fun addToBarrowsChestCount(player: Player?) { + if (player == null) { + return + } + player.getSavedData().globalData.barrowsLoots++ + sendMessage(player, "Your Barrows chest count is: ${player.getSavedData().globalData.barrowsLoots}.") + } + } +} diff --git a/Server/src/main/content/global/handlers/item/EnchantedGemListener.kt b/Server/src/main/content/global/handlers/item/EnchantedGemListener.kt index 252dbe2ff..e376d2855 100644 --- a/Server/src/main/content/global/handlers/item/EnchantedGemListener.kt +++ b/Server/src/main/content/global/handlers/item/EnchantedGemListener.kt @@ -2,7 +2,6 @@ package content.global.handlers.item import content.global.skill.slayer.SlayerUtils import core.api.* -import content.global.skill.slayer.Tasks import org.rs09.consts.Items import core.game.dialogue.DialogueFile import core.game.dialogue.IfTopic @@ -39,12 +38,7 @@ class EnchantedGemDialogue() : DialogueFile() { if(!hasSlayerTask(player!!)) { npcl(core.game.dialogue.FacialExpression.HALF_THINKING, "You need something new to hunt. Come and see me when you can and I'll give you a new task.").also { stage = 1 } } else { - if(getSlayerTask(player!!) == Tasks.JAD) { - npcl(core.game.dialogue.FacialExpression.FRIENDLY, "You're currently assigned to kill TzTok-Jad!") - } else { - npcl(core.game.dialogue.FacialExpression.FRIENDLY, "You're currently assigned to kill ${SlayerUtils.pluralise( - getSlayerTaskName(player!!))}; only ${getSlayerTaskKillsRemaining(player!!)} more to go.") - } + npcl(core.game.dialogue.FacialExpression.FRIENDLY, "You're currently assigned to kill ${SlayerUtils.pluralise(getSlayerTaskName(player!!))}; only ${getSlayerTaskKillsRemaining(player!!)} more to go.") setVarp(player!!, 2502, getSlayerTaskFlags(player!!) shr 4) stage = 1 } diff --git a/Server/src/main/content/global/skill/slayer/Tasks.java b/Server/src/main/content/global/skill/slayer/Tasks.java index 81a195f5a..15b905dba 100644 --- a/Server/src/main/content/global/skill/slayer/Tasks.java +++ b/Server/src/main/content/global/skill/slayer/Tasks.java @@ -21,7 +21,6 @@ public enum Tasks { ABERRANT_SPECTRES(65, new int[] { 1604, 1605, 1606, 1607, 7801, 7802, 7803, 7804 }, new String[] { "Aberrant Spectres are fetid, vile ghosts. The very", "smell of them will paralyse and harm you. A nose peg", "will help ignore their stink." }, 60, true, false), ABYSSAL_DEMONS(85, new int[] { 1615 }, new String[] { "Abyssal Demons are nasty creatures to fight. They", "aren't really part of this realm, and are able to", "move very quickly to trap their prey."}, 85, false, false), ANKOU(40, new int[] { 4381, 4382, 4383 }, new String[] { "Ankou are undead skeletal ghosts. They'll fight you", "up close but make sure to take advantage out of their", "limited defence." }, 1, true, false), - AVIANSIES(60, new int[] { 6245, 6243, 6235, 6232, 6244, 6246, 6233, 6241, 6238, 6237, 6240, 6242, 6239, 6234 }, new String[] { "Aviansies are bird-like creatures found in the icy", "dungeons of the north. Melee weapons can't reach them,", "so use Magic or Ranged attacks." }, 1, false, false), BANSHEE(20, new int[] { 1612 }, new String[] { "Banshees use a piercing scream to shock their enemies.", "You'll need some earmuffs to protect yourself from them." }, 15, true, false), BASILISKS(40, new int[] { 1616, 1617 }, new String[] { "Basilisks, like Cockatrice, have a gaze which will", "paralyse and harm their prey. You'll need a Mirror", "Shield to protect you." }, 40, false, false), BATS(5, new int[] { 412, 78, 3711 }, new String[] { "Bats are rarely found on the ground, so you'll have", "to fight them while they're airborne, which won't be", "easy for melee." }, 1, false, false), @@ -107,12 +106,10 @@ public enum Tasks { SPIRTUAL_WARRIORS(60, new int[] { 6219, 6229, 6255, 6277, }, new String[] { "Spiritual warriors can be found in the icy caverns near", "Trollheim, supporting the cause of their chosen god." }, 68, Quests.DEATH_PLATEAU), STEEL_DRAGONS( 85,new int[] { 1592, 3590 }, new String[] { "Steel dragons are dangerous and metallic, with steel", "scales that are far thicker than normal steel armour. As", "you are an accomplished slayer, I am sure you'll be", "able to deal with them easily."}, 1, false, true), SUQAHS (65, new int[] { 4527, 4528, 4529, 4530, 4531, 4532, 4533 }, new String[] { "Suqahs can only be found on the mystical Lunar Isle.", "They are capable of melee and magic attacks and often", "drop hide, teeth and herbs!" }, 1, Quests.LUNAR_DIPLOMACY), - // No access to Lair of Tarn Razorlor but this should be added as a task when there is access TERROR_DOGS(1, new int[] { 5417, 5418 }, new String[] { "Terror dogs are the personal pets of Tarn Razorlor.", "Wherever you find him, you will find them. They are", "bad-tempered and generally unfriendly." }, 40, Quests.HAUNTED_MINE), TROLLS(60, new int[] { 72, 3584, 1098, 1096, 1097, 1095, 1101, 1105, 1102, 1103, 1104, 1130, 1131, 1132, 1133, 1134, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1138, 1560, 1561, 1562, 1563, 1564, 1565, 1566, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 3840, 3841, 3842, 3843, 3845, 1933, 1934, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 391, 392, 393, 394, 395, 396}, new String[] { "Trolls regenerate damage quickly but are still", "vulnerable to poisons, they usually use crushing", "weapons." }, 1, false, false), TUROTHS(60, new int[] { 1622, 1611, 1623, 1626, 1627, 1628, 1629, 1630, 7800}, new String[] { "Turoth are large vicious creatures with thick hides.", "You'll need a Leaf-Tipped Spear Sword or Battle-axe,", "Broad Arrows, or a Magic Dart to harm them." }, 55, false, false), VAMPIRES(35, new int[] { 1220, 1223, 1225, 6214 }, new String[] { "Vampires are extremely powerful beings. They feed on", "the blood of the living so watch out you don't", "get bitten." }, 1, false, false), - // Waiting for the wall beast movement bug to be fixed before adding this as a task WALL_BEASTS(1, new int[] { 7823 }, new String[] { "Wall Beasts are really much larger creatures but", "you'll only see their arms. You'll want something", "sharp on your head to stop them grabbing you." }, 35, false, false ), // No way to get to Poison Swamp Cave // If a grapple is implemented from W Castle Wars to E Posion Swamps @@ -124,15 +121,6 @@ public enum Tasks { WEREWOLVES(60, new int[] { 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6212, 6213, 6607, 6609, 6614, 6617, 6625, 6632, 6644, 6663, 6675, 6686, 6701, 6712, 6724, 6728, }, new String[] { "Werewolves are feral creatures, they are strong and", "tough with sharp claws and teeth." }, 1, false, false), WOLVES(20, new int[] { 95, 96, 97, 141, 142, 143, 839, 1198, 1330, 1558, 1559, 1951, 1952, 1953, 1954, 1955, 1956, 4413, 4414, 6046, 6047, 6048, 6049, 6050, 6051, 6052, 6829, 6830, 7005 }, new String[] { "Wolves are pack animals, so you'll always find them", "in groups. Watch out for their bite, it can be nasty." }, 1, false, false), ZOMBIES(10, new int[] { 73, 74, 75, 76, 2060, 2714, 2863, 2866, 2869, 2878, 3622, 4392, 4393, 4394, 5293, 5294, 5295, 5296, 5297, 5298, 5299, 5300, 5301, 5302, 5303, 5304, 5305, 5306, 5307, 5308, 5309, 5310, 5311, 5312, 5313, 5314, 5315, 5316, 5317, 5318, 5319, 5320, 5321, 5322, 5323, 5324, 5325, 5326, 5327, 5328, 5329, 5330, 5331, 5375, 5376, 5377, 5378, 5379, 5380, 5393, 5394, 5395, 5396, 5397, 5398, 5399, 5400, 5401, 5402, 5403, 5404, 5405, 5406, 5407, 5408, 5409, 5410, 6099, 6100, 6131, 8149, 8150, 8151, 8152, 8153, 8159, 8160, 8161, 8162, 8163, 8164, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053, 2054, 2055, 7641, 1465, 1466, 1467, 2837, 2838, 2839, 2840, 2841, 2842, 5629, 5630, 5631, 5632, 5633, 5634, 5635, 5636, 5637, 5638, 5639, 5640, 5641, 5642, 5643, 5644, 5645, 5646, 5647, 5648, 5649, 5650, 5651, 5652, 5653, 5654, 5655, 5656, 5657, 5658, 5659, 5660, 5661, 5662, 5663, 5664, 5665, 2843, 2844, 2845, 2846, 2847, 2848}, new String[] { "Zombies are undead so magic is your best bet against", "them, there is even a spell specially for", "fighting the undead." }, 1, true, false), - // Bosses need to be handled differently/have a source. They should probably be removed from here - JAD(90, new int[] { }, new String[] { "You must complete the entire fight cave, including", "TzTok-Jad. Beware, only those skilled in combat should", "attempt this and death will reset your task." }, 1, false, false), - CHAOS_ELEMENTAL(90, new int[] { 3200 }, new String[] { "The Chaos Elemental roams the deepest Wilderness. It", "can teleport you and make you unequip items randomly." }, 1, false, false), - GIANT_MOLE(75, new int[] { 3340 }, new String[] { "Dig on the mole-hills in Falador Park to enter the", "mole cave, and bring a lantern he can't extinguish." }, 1, false, false), - KING_BLACK_DRAGON(75, new int[] { 50 }, new String[] { "The King Black Dragon's cave is reached by pulling a", "lever in the Wilderness. An anti-fire shield is", "strongly recommended." }, 1, false, true), - COMMANDER_ZILYANA(90, new int[] { 6247 }, new String[] { "Commander Zilyana is in the God Wars Dungeon.", "She frequently uses magic attacks." }, 1, false, false), - GENERAL_GRARDOOR(90, new int[] { 6260 }, new String[] { "General Graardor is in the God Wars Dungeon.", "He uses melee and ranged attacks." }, 1, false, false), - KRIL_TSUTSAROTH(90, new int[] { 6203 }, new String[] { "K'ril Tsutsaroth is in the God Wars Dungeon.", "He's poisonous and he can hit through prayers." }, 1, false, false), - KREE_ARRA(90, new int[] { 6222 }, new String[] { "Kree'arra roosts in the God Wars Dungeon.", "Melee attacks don't work on flying creatures,", "and you'll need a grappling hook to get in." }, 1, false, false), ; static final HashMap taskMap = new HashMap<>(); diff --git a/Server/src/main/content/minigame/barrows/RewardChest.kt b/Server/src/main/content/minigame/barrows/RewardChest.kt index d12f48a1e..ccb57c985 100644 --- a/Server/src/main/content/minigame/barrows/RewardChest.kt +++ b/Server/src/main/content/minigame/barrows/RewardChest.kt @@ -78,7 +78,7 @@ object RewardChest { InterfaceContainer.generateItems(player, rewards.toTypedArray(), arrayOf("Examine"), 364, 4, 3, 4) player.interfaceManager.open(Component(Components.TRAIL_REWARD_364)) - BossKillCounter.addtoBarrowsCount(player) + BossKillCounter.addToBarrowsChestCount(player) for (item in rewards) { announceIfRare(player, item) if (!player.inventory.add(item)) { diff --git a/Server/src/main/content/region/asgarnia/falador/handlers/GiantMoleNPC.java b/Server/src/main/content/region/asgarnia/falador/handlers/GiantMoleNPC.java index 6564c96dd..20edbb3ac 100644 --- a/Server/src/main/content/region/asgarnia/falador/handlers/GiantMoleNPC.java +++ b/Server/src/main/content/region/asgarnia/falador/handlers/GiantMoleNPC.java @@ -299,7 +299,7 @@ public final class GiantMoleNPC extends AbstractNPC { super.finalizeDeath(killer); if (killer instanceof Player) { Player player = killer.asPlayer(); - BossKillCounter.addtoKillcount(player, this.getId()); + BossKillCounter.addToBossKillCount(player, this.getId()); } } diff --git a/Server/src/main/content/region/asgarnia/trollheim/handlers/gwd/GodwarsBossNPC.java b/Server/src/main/content/region/asgarnia/trollheim/handlers/gwd/GodwarsBossNPC.java index 4d62e0f41..ad04fa730 100644 --- a/Server/src/main/content/region/asgarnia/trollheim/handlers/gwd/GodwarsBossNPC.java +++ b/Server/src/main/content/region/asgarnia/trollheim/handlers/gwd/GodwarsBossNPC.java @@ -231,7 +231,7 @@ public final class GodwarsBossNPC extends AbstractNPC { public void finalizeDeath(Entity killer) { super.finalizeDeath(killer); if (getId() == 6222 || getId() == 6260 || getId() == 6247 || getId() == 6203) { - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } if (minions == null) { return; diff --git a/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java b/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java index be677c52d..39dbc9399 100644 --- a/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java +++ b/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java @@ -97,10 +97,10 @@ public final class KalphiteQueenNPC extends AbstractNPC { removeAttribute("disable:drop"); super.finalizeDeath(killer); reTransform(); - BossKillCounter.addtoKillcount((Player) killer, 1160); + BossKillCounter.addToBossKillCount((Player) killer, 1160); return; } - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); setAttribute("disable:drop", true); super.finalizeDeath(killer); super.setRespawnTick(-1); diff --git a/Server/src/main/content/region/fremennik/waterbirth/handlers/DagannothKingNPC.java b/Server/src/main/content/region/fremennik/waterbirth/handlers/DagannothKingNPC.java index e928e6179..65f5d4e43 100644 --- a/Server/src/main/content/region/fremennik/waterbirth/handlers/DagannothKingNPC.java +++ b/Server/src/main/content/region/fremennik/waterbirth/handlers/DagannothKingNPC.java @@ -97,7 +97,7 @@ public final class DagannothKingNPC extends AbstractNPC { public void finalizeDeath(Entity killer) { super.finalizeDeath(killer); if (getId() == 2881 || getId() == 2882 || getId() == 2883) { - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } } diff --git a/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java b/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java index 7559b9362..89a8d5e38 100644 --- a/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java +++ b/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java @@ -1,6 +1,5 @@ package content.region.karamja.tzhaar.handlers; -import content.global.skill.slayer.SlayerManager; import core.game.event.NPCKillEvent; import core.game.activity.ActivityPlugin; import content.data.BossKillCounter; @@ -10,8 +9,6 @@ import core.game.node.entity.Entity; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.diary.DiaryType; -import core.game.node.entity.skill.Skills; -import content.global.skill.slayer.Tasks; import core.game.node.item.GroundItemManager; import core.game.node.item.Item; import core.game.node.scenery.Scenery; @@ -23,6 +20,7 @@ import core.plugin.Initializable; import core.tools.RandomFunction; import core.game.world.GameWorld; import core.game.world.repository.Repository; +import org.rs09.consts.NPCs; import java.util.ArrayList; import java.util.List; @@ -167,12 +165,7 @@ public final class TzhaarFightCavesPlugin extends ActivityPlugin { } player.getPacketDispatch().sendMessage("You were victorious!"); if (!practice) { - BossKillCounter.addtoKillcount(player, 2745); - if (SlayerManager.getInstance(player).getTask() == Tasks.JAD) { - player.getSkills().addExperience(Skills.SLAYER, 25000); - SlayerManager.getInstance(player).clear(); - player.sendMessage("You receive 25,000 slayer experience for defeating TzTok-Jad."); - } + BossKillCounter.addToBossKillCount(player, NPCs.TZTOK_JAD_2745); player.getDialogueInterpreter().sendDialogues(2617, null, "You even defeated TzTok-Jad, I am most impressed!", "Please accept this gift as a reward."); Repository.sendNews(player.getUsername() + " has been victorious in defeating TzTok-Jad for a firecape!"); } else { diff --git a/Server/src/main/content/region/misthalin/lumbridge/handlers/TormentedDemonNPC.java b/Server/src/main/content/region/misthalin/lumbridge/handlers/TormentedDemonNPC.java index 87f5de168..17942de67 100644 --- a/Server/src/main/content/region/misthalin/lumbridge/handlers/TormentedDemonNPC.java +++ b/Server/src/main/content/region/misthalin/lumbridge/handlers/TormentedDemonNPC.java @@ -184,7 +184,7 @@ public class TormentedDemonNPC extends AbstractNPC { super.finalizeDeath(killer); reTransform(); fireShield = true; - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } @Override diff --git a/Server/src/main/content/region/wilderness/handlers/BorkNPC.java b/Server/src/main/content/region/wilderness/handlers/BorkNPC.java index c0a28f080..53bd862e5 100644 --- a/Server/src/main/content/region/wilderness/handlers/BorkNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/BorkNPC.java @@ -118,7 +118,7 @@ public class BorkNPC extends AbstractNPC { @Override public void finalizeDeath(Entity killer){ super.finalizeDeath(killer); - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } @Override diff --git a/Server/src/main/content/region/wilderness/handlers/ChaosElementalNPC.java b/Server/src/main/content/region/wilderness/handlers/ChaosElementalNPC.java index 4da67fe93..9e4f6f758 100644 --- a/Server/src/main/content/region/wilderness/handlers/ChaosElementalNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/ChaosElementalNPC.java @@ -82,7 +82,7 @@ public class ChaosElementalNPC extends AbstractNPC { @Override public void finalizeDeath(Entity killer) { super.finalizeDeath(killer); - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } /** diff --git a/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java b/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java index 67ad9c876..0b71e76e6 100644 --- a/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java @@ -91,7 +91,7 @@ public final class CorporealBeastNPC extends NPCBehavior { @Override public void onDeathFinished(NPC self, Entity killer) { - BossKillCounter.addtoKillcount((Player) killer, NPCs.CORPOREAL_BEAST_8133); + BossKillCounter.addToBossKillCount((Player) killer, NPCs.CORPOREAL_BEAST_8133); if (darkEnergyCore != null) { darkEnergyCore.clear(); darkEnergyCore = null; diff --git a/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java b/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java index 51ec635a5..75f8a94de 100644 --- a/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/KingBlackDragonNPC.java @@ -49,7 +49,7 @@ public final class KingBlackDragonNPC extends AbstractNPC { @Override public void finalizeDeath(Entity killer) { super.finalizeDeath(killer); - BossKillCounter.addtoKillcount((Player) killer, this.getId()); + BossKillCounter.addToBossKillCount((Player) killer, this.getId()); } /** From 1d383f0ff34451ede13ec33cfff4961f9da1b006 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Fri, 3 Jul 2026 09:23:43 +0000 Subject: [PATCH 23/42] Rewrote world map interface handling (search still not implemented) --- .../handlers/iface/WorldMapInterface.java | 39 ------------------- .../handlers/iface/WorldMapInterface.kt | 23 +++++++++++ 2 files changed, 23 insertions(+), 39 deletions(-) delete mode 100644 Server/src/main/content/global/handlers/iface/WorldMapInterface.java create mode 100644 Server/src/main/content/global/handlers/iface/WorldMapInterface.kt diff --git a/Server/src/main/content/global/handlers/iface/WorldMapInterface.java b/Server/src/main/content/global/handlers/iface/WorldMapInterface.java deleted file mode 100644 index 113c28bfa..000000000 --- a/Server/src/main/content/global/handlers/iface/WorldMapInterface.java +++ /dev/null @@ -1,39 +0,0 @@ -package content.global.handlers.iface; - -import core.game.component.Component; -import core.game.component.ComponentDefinition; -import core.game.component.ComponentPlugin; -import core.game.node.entity.player.Player; -import core.plugin.Initializable; -import core.plugin.Plugin; - -/** - * Handles the world map interface. - * @author Emperor - * - */ -@Initializable -public final class WorldMapInterface extends ComponentPlugin { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ComponentDefinition.forId(755).setPlugin(this); - return this; - } - - //Thanks snicker! - @Override - public boolean handle(Player player, Component component, int opcode, int button, int slot, int itemId) { - switch (button) { - case 3: - player.getInterfaceManager().openWindowsPane(new Component(player.getInterfaceManager().isResizable() ? 746 : 548), 2); - player.getPacketDispatch().sendRunScript(1187, "ii", 0, 0); - player.updateSceneGraph(true); - return true; - default: - //log(this.getClass(), Log.ERR, "World map: buttonid: " + button + ", opcode: " + opcode + ", slot: " + slot); - return true; - } - } - -} diff --git a/Server/src/main/content/global/handlers/iface/WorldMapInterface.kt b/Server/src/main/content/global/handlers/iface/WorldMapInterface.kt new file mode 100644 index 000000000..cd1fc427b --- /dev/null +++ b/Server/src/main/content/global/handlers/iface/WorldMapInterface.kt @@ -0,0 +1,23 @@ +package content.global.handlers.iface + +import core.game.component.Component +import core.game.interaction.InterfaceListener +import org.rs09.consts.Components + +class WorldMapInterface : InterfaceListener { + override fun defineInterfaceListeners() { + on(Components.WORLDMAP_755) { player, _, _, button, _, _ -> + if (button == 3) { + val paneId = if (player.interfaceManager.isResizable) { + Components.TOPLEVEL_FULLSCREEN_746 + } else { + Components.TOPLEVEL_548 + } + player.interfaceManager.openWindowsPane(Component(paneId), 2) + player.packetDispatch.sendRunScript(1187, "ii", 0, 0) + player.updateSceneGraph(true) + } + return@on true + } + } +} From 392b13697b71cbe84574848e3f1e52e2d7122029 Mon Sep 17 00:00:00 2001 From: GregF Date: Fri, 3 Jul 2026 09:30:48 +0000 Subject: [PATCH 24/42] Implemented HP carryover after NPC transform Fixed goblin cave miners not taking damage on the first hit --- Server/data/configs/npc_configs.json | 32 +++++++++---------- .../quest/thelosttribe/CaveGoblinMinerNPC.kt | 2 +- .../canifis/handlers/CanafisWerewolfNPC.kt | 5 ++- Server/src/main/core/api/ContentAPI.kt | 19 +++++++++++ .../main/core/game/node/entity/npc/NPC.java | 12 +++++++ 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/Server/data/configs/npc_configs.json b/Server/data/configs/npc_configs.json index 2f3dd9984..44c10bdcb 100644 --- a/Server/data/configs/npc_configs.json +++ b/Server/data/configs/npc_configs.json @@ -25592,7 +25592,7 @@ "death_animation": "6003", "defence_animation": "6008", "defence_level": "5", - "lifepoints": "7", + "lifepoints": "10", "magic_animation": "0", "melee_animation": "6007", "range_animation": "0", @@ -25610,7 +25610,7 @@ "death_animation": "6003", "defence_animation": "6008", "defence_level": "5", - "lifepoints": "7", + "lifepoints": "10", "magic_animation": "0", "melee_animation": "6007", "range_animation": "0", @@ -25628,7 +25628,7 @@ "death_animation": "6003", "defence_animation": "6008", "defence_level": "5", - "lifepoints": "7", + "lifepoints": "10", "magic_animation": "0", "melee_animation": "6007", "range_animation": "0", @@ -25646,7 +25646,7 @@ "death_animation": "6003", "defence_animation": "6008", "defence_level": "5", - "lifepoints": "7", + "lifepoints": "10", "magic_animation": "0", "melee_animation": "6007", "range_animation": "0", @@ -25697,61 +25697,61 @@ "id": "2075", "name": "Cave goblin miner", "examine": "He's working hard!", - "attack_level": "1", + "attack_level": "5", "combat_audio": "469,472,471", "death_animation": "6003", "defence_animation": "6008", - "defence_level": "1", + "defence_level": "5", "lifepoints": "10", "melee_animation": "6007", "range_level": "1", "safespot": null, - "strength_level": "1" + "strength_level": "5" }, { "id": "2076", "name": "Cave goblin miner", "examine": "He's working hard!", - "attack_level": "1", + "attack_level": "5", "combat_audio": "469,472,471", "death_animation": "6003", "defence_animation": "6008", - "defence_level": "1", + "defence_level": "5", "lifepoints": "10", "melee_animation": "6007", "range_level": "1", "safespot": null, - "strength_level": "1" + "strength_level": "5" }, { "id": "2077", "name": "Cave goblin miner", "examine": "He's working hard!", - "attack_level": "1", + "attack_level": "5", "combat_audio": "469,472,471", "death_animation": "6003", "defence_animation": "6008", - "defence_level": "1", + "defence_level": "5", "lifepoints": "10", "melee_animation": "6007", "range_level": "1", "safespot": null, - "strength_level": "1" + "strength_level": "5" }, { "id": "2078", "name": "Cave goblin miner", "examine": "He's working hard!", - "attack_level": "1", + "attack_level": "5", "combat_audio": "469,472,471", "death_animation": "6003", "defence_animation": "6008", - "defence_level": "1", + "defence_level": "5", "lifepoints": "10", "melee_animation": "6007", "range_level": "1", "safespot": null, - "strength_level": "1" + "strength_level": "5" }, { "id": "2079", diff --git a/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/CaveGoblinMinerNPC.kt b/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/CaveGoblinMinerNPC.kt index 6813ad0b9..4d6feec03 100644 --- a/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/CaveGoblinMinerNPC.kt +++ b/Server/src/main/content/region/misthalin/dorgeshuun/quest/thelosttribe/CaveGoblinMinerNPC.kt @@ -29,7 +29,7 @@ class CaveGoblinMinerNPC(id: Int = 0, location: Location? = null) : AbstractNPC( mining = (id > 2074) if(properties.combatPulse.isAttacking && mining){ - transform(id - 6) + transformWithHpCarryover(id - 6) this.isWalks = true this.walkRadius = 4 this.isNeverWalks = false diff --git a/Server/src/main/content/region/morytania/canifis/handlers/CanafisWerewolfNPC.kt b/Server/src/main/content/region/morytania/canifis/handlers/CanafisWerewolfNPC.kt index 51a82b834..369aa550f 100644 --- a/Server/src/main/content/region/morytania/canifis/handlers/CanafisWerewolfNPC.kt +++ b/Server/src/main/content/region/morytania/canifis/handlers/CanafisWerewolfNPC.kt @@ -3,6 +3,7 @@ package content.region.morytania.canifis.handlers import core.api.* import core.game.interaction.QueueStrength import core.game.node.entity.Entity +import core.game.node.entity.skill.Skills import core.game.node.entity.combat.BattleState import core.game.node.entity.combat.DeathTask import core.game.node.entity.npc.NPC @@ -38,7 +39,9 @@ class WerewolfBehavior : NPCBehavior(*HUMAN_NPCS) { return@queueScript delayScript(self, WEREWOLF_IN_ANIMATION.duration) } 1 -> { - transformNpc(self, WEREWOLF_NPCS[self.id - 6026], 200) + val humanMaxLp = self.getSkills().getMaximumLifepoints() + transformNpcWithHpCarryover(self, WEREWOLF_NPCS[self.id - 6026], 200) + self.getSkills().heal(self.getSkills().getMaximumLifepoints() - humanMaxLp) return@queueScript delayScript(self, 1) } 2 -> { diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index 2acac9bea..7423518a7 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -1342,6 +1342,25 @@ fun transformNpc(npc: NPC, transformTo: Int, restoreTicks: Int) { }) } +/** + * Transforms an NPC for the given number of ticks with hp carryover + * @param npc the NPC object to transform + * @param transformTo the ID of the NPC to turn into + * @param restoreTicks the number of ticks until the NPC returns to normal + */ +fun transformNpcWithHpCarryover(npc: NPC, transformTo: Int, restoreTicks: Int) { + npc.transformWithHpCarryover(transformTo) + queueScript(npc, 0, QueueStrength.SOFT) { stage: Int -> + when (stage) { + 0 -> return@queueScript delayScript(npc, restoreTicks) + else -> { + npc.reTransform() + return@queueScript stopExecuting(npc) + } + } + } +} + /** * Produces a Location object using the given x,y,z values */ diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index d0e349d7c..18334251e 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -751,6 +751,18 @@ public class NPC extends Entity { return this; } + /** + * Transforms this NPC with HP carried over from pre-transformed entity. + * HP adjustments (ie healing/scaling HP to new maximum) should be done manually per-NPC + * @param id The new NPC id. + */ + public NPC transformWithHpCarryover(int id) { + int lp = getSkills().getLifepoints(); + this.transform(id); + getSkills().setLifepoints(lp); + return this; + } + /** * Transforms this NPC back to original. */ From fd0976e5d5e265214288c08a7ceb9df44a839b2a Mon Sep 17 00:00:00 2001 From: Edie Date: Fri, 3 Jul 2026 10:31:12 +0000 Subject: [PATCH 25/42] The summoning orb no longer disappears from the minimap on logout/login --- .../summoning/familiar/FamiliarManager.java | 26 +++++++++++-------- .../asgarnia/taverley/quest/WolfWhistle.java | 6 +++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Server/src/main/content/global/skill/summoning/familiar/FamiliarManager.java b/Server/src/main/content/global/skill/summoning/familiar/FamiliarManager.java index 3e8e3b7e8..e65c36cf1 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/FamiliarManager.java +++ b/Server/src/main/content/global/skill/summoning/familiar/FamiliarManager.java @@ -1,5 +1,6 @@ package content.global.skill.summoning.familiar; +import content.data.Quests; import content.global.skill.summoning.pet.Pet; import content.global.skill.summoning.pet.Pets; import core.cache.def.impl.ItemDefinition; @@ -31,6 +32,15 @@ import static core.api.ContentAPIKt.*; */ public final class FamiliarManager { + /** + * These varbits are packed into varp 1160. Legacy code added 243269632 directly to the raw varp, + * which toggled the Summoning orb visibility on every login. + * They are now written as individual varbits, but two of them remain unknown. + */ + private static final int VARBIT_SUMMONING_ORB_VISIBILITY = 4280; + private static final int VARBIT_SUMMONING_UNKNOWN1 = 4281; + private static final int VARBIT_SUMMONING_UNKNOWN2 = 4282; + /** * The familiars mapping. */ @@ -158,7 +168,11 @@ public final class FamiliarManager { if (hasFamiliar()) { familiar.init(); } - player.getFamiliarManager().setConfig(243269632); + boolean unlocked = player.getQuestRepository().isComplete(Quests.WOLF_WHISTLE); + + setVarbit(player, VARBIT_SUMMONING_ORB_VISIBILITY, unlocked ? 1 : 0); + setVarbit(player, VARBIT_SUMMONING_UNKNOWN1, 0); + setVarbit(player, VARBIT_SUMMONING_UNKNOWN2, 7); } /** @@ -456,16 +470,6 @@ public final class FamiliarManager { return true; } - /** - * Sets a config value. - * @param value the value. - */ - public void setConfig(int value) { - int current = getVarp(player, 1160); - int newVal = current + value; - setVarp(player, 1160, newVal); - } - /** * Gets the familiar. * @return The familiar. diff --git a/Server/src/main/content/region/asgarnia/taverley/quest/WolfWhistle.java b/Server/src/main/content/region/asgarnia/taverley/quest/WolfWhistle.java index ec4661ccf..29160561d 100644 --- a/Server/src/main/content/region/asgarnia/taverley/quest/WolfWhistle.java +++ b/Server/src/main/content/region/asgarnia/taverley/quest/WolfWhistle.java @@ -23,6 +23,11 @@ public class WolfWhistle extends Quest { */ public static final Item WOLF_BONES = new Item(2859, 2); + /** + * Gates the visibility of the Summoning orb next to the minimap behind quest completion. + */ + private static final int VARBIT_SUMMONING_ORB_VISIBILITY = 4280; + /** * Constructs a new {@code WolfWhistle} {@code Object}. */ @@ -164,6 +169,7 @@ public class WolfWhistle extends Quest { player.getSkills().addExperience(Skills.SUMMONING, 276); player.getInventory().add(new Item(12158, 275), player); player.removeAttribute("searched-body"); + setVarbit(player, VARBIT_SUMMONING_ORB_VISIBILITY, 1); player.getQuestRepository().syncronizeTab(player); player.getInterfaceManager().openInfoBars(); } From 9a872f763d85f6ea27d72f9773b7b2565b946c47 Mon Sep 17 00:00:00 2001 From: Tooze Date: Fri, 3 Jul 2026 10:46:20 +0000 Subject: [PATCH 26/42] Fixed bug causing null or corrupt players/bots --- Server/src/main/core/game/world/update/PlayerRenderer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/src/main/core/game/world/update/PlayerRenderer.kt b/Server/src/main/core/game/world/update/PlayerRenderer.kt index 0fb1068bc..e192463d7 100644 --- a/Server/src/main/core/game/world/update/PlayerRenderer.kt +++ b/Server/src/main/core/game/world/update/PlayerRenderer.kt @@ -107,7 +107,7 @@ object PlayerRenderer { if (offsetX < 0) { offsetX += 32 } - val appearance = info.appearanceStamps[other.index and 0x800] != other.updateMasks.appearanceStamp + val appearance = info.appearanceStamps[other.index] != other.updateMasks.appearanceStamp val update = appearance || other.updateMasks.isUpdateRequired || other.updateMasks.hasSynced() buffer.putBits(1, if (update) 1 else 0) buffer.putBits(5, offsetX) @@ -117,7 +117,7 @@ object PlayerRenderer { info.localPlayers.add(other) if (update) { if (appearance) { - info.appearanceStamps[other.index and 0x800] = other.updateMasks.appearanceStamp + info.appearanceStamps[other.index] = other.updateMasks.appearanceStamp } writeMaskUpdates(player, other, flags, appearance, true) } From 5d82897215d614c4622a42d1574858e96410cab5 Mon Sep 17 00:00:00 2001 From: iampwn <24796277-iampwn@users.noreply.gitlab.com> Date: Wed, 24 Jun 2026 10:56:27 -0400 Subject: [PATCH 27/42] Made the Stronghold of Player Safety crevice shortcut persist between logins --- .../stronghold/playersafety/StrongHoldOfPlayerSafetyListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/region/misthalin/barbvillage/stronghold/playersafety/StrongHoldOfPlayerSafetyListener.kt b/Server/src/main/content/region/misthalin/barbvillage/stronghold/playersafety/StrongHoldOfPlayerSafetyListener.kt index 8c6a1ca73..68fd59034 100644 --- a/Server/src/main/content/region/misthalin/barbvillage/stronghold/playersafety/StrongHoldOfPlayerSafetyListener.kt +++ b/Server/src/main/content/region/misthalin/barbvillage/stronghold/playersafety/StrongHoldOfPlayerSafetyListener.kt @@ -31,7 +31,7 @@ package content.region.misthalin.barbvillage.stronghold.playersafety SceneryConst.JAIL_DOOR_29601 to 698, ) - private val creviceClimbedAttribute = "player_strong:crevice_climbed" + private val creviceClimbedAttribute = "/save:player_strong:crevice_climbed" } override fun defineListeners() { From 76523c01303a710632be3f06dc36f87451942b4b Mon Sep 17 00:00:00 2001 From: Ceikry Date: Fri, 3 Jul 2026 11:08:00 +0000 Subject: [PATCH 28/42] Item respawn time logic now handles unset min/max times --- Server/src/main/core/game/system/config/GroundSpawnLoader.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/src/main/core/game/system/config/GroundSpawnLoader.kt b/Server/src/main/core/game/system/config/GroundSpawnLoader.kt index cbf5dabc9..d57ac9031 100644 --- a/Server/src/main/core/game/system/config/GroundSpawnLoader.kt +++ b/Server/src/main/core/game/system/config/GroundSpawnLoader.kt @@ -67,6 +67,7 @@ class GroundSpawnLoader { * Method used to initialize this spawn. */ fun init(): GroundItem { + if (respawnRate shr 16 == 0) respawnRate = respawnRate or (respawnRate shl 16) return GroundItemManager.create(this) } From afeffc1b98a565572c39d0aaeda925fbba6ab2aa Mon Sep 17 00:00:00 2001 From: downthecrop Date: Fri, 3 Jul 2026 11:10:44 +0000 Subject: [PATCH 29/42] Added support for websocket connections (disabled by default) --- README.md | 35 +++++++++ Server/pom.xml | 5 ++ Server/src/main/core/Server.kt | 15 ++++ Server/src/main/core/ServerConstants.kt | 15 ++++ .../core/game/system/SystemTermination.java | 4 + .../game/system/config/ServerConfigParser.kt | 5 ++ Server/src/main/core/net/IoSession.java | 45 ++++++++++-- Server/src/main/core/net/NioReactor.java | 15 ++++ .../main/core/net/packet/PacketProcessor.kt | 27 +++++-- .../main/core/net/packet/PacketWriteQueue.kt | 23 ++++-- .../core/net/websocket/GameWebSocketServer.kt | 73 +++++++++++++++++++ .../core/net/websocket/WebSocketIoSession.kt | 41 +++++++++++ .../main/core/net/websocket/WebSocketTls.kt | 45 ++++++++++++ Server/worldprops/default.conf | 11 +++ 14 files changed, 340 insertions(+), 19 deletions(-) create mode 100644 Server/src/main/core/net/websocket/GameWebSocketServer.kt create mode 100644 Server/src/main/core/net/websocket/WebSocketIoSession.kt create mode 100644 Server/src/main/core/net/websocket/WebSocketTls.kt diff --git a/README.md b/README.md index e986e4311..60054bcf8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ * [Prerequisites](#prerequisites) * [Project Setup](#project-setup) * [Running the project](#running-the-project) + * [Browser WebSocket Transport](#browser-websocket-transport) * [License](#license) * [Contact](#contact) @@ -125,6 +126,40 @@ Start the game server with the included run script. Use `./run -h` for more info Start the game server with `run-server.bat` +#### Browser WebSocket Transport + +Enable the listener in `Server/worldprops/default.conf`: + +```properties +websocket_enabled = true +websocket_port = 0 +``` + +When `websocket_port = 0`, the listener uses `53594 + world_id`. For world `1`, that is `53595`. + +Both plain WebSocket (`ws://`) and secure WebSocket (`wss://`) are supported. Use `ws://` for local HTTP testing. PWA requires HTTPS/WSS. + +Enable WSS with a Java keystore: + +```properties +websocket_tls_enabled = true +websocket_tls_keystore_path = "certs/dev-wss.p12" +websocket_tls_keystore_password = "" +``` + +The keystore password is optional. Leave `websocket_tls_keystore_password` blank when using a PKCS12 file exported with an empty password: + +```bash +openssl pkcs12 -export \ + -in fullchain.pem \ + -inkey privkey.pem \ + -out certs/dev-wss.p12 \ + -name websocket \ + -passout pass: +``` + +For local WSS development, create a certificate for your hostname or LAN IP that the browser will use, then point the server at a PKCS12 keystore. For production, use a normal certificate from certbot. Plain WS does not require a certificate. + #### Docker Make sure [Docker Engine](https://docs.docker.com/engine/install/) & [Docker Compose](https://docs.docker.com/compose/install/) plugin are installed first: diff --git a/Server/pom.xml b/Server/pom.xml index a18581177..5c93c44d2 100644 --- a/Server/pom.xml +++ b/Server/pom.xml @@ -91,6 +91,11 @@ 2.9.0 compile + + org.java-websocket + Java-WebSocket + 1.5.7 + diff --git a/Server/src/main/core/Server.kt b/Server/src/main/core/Server.kt index 80f29a8cf..cf46e2823 100644 --- a/Server/src/main/core/Server.kt +++ b/Server/src/main/core/Server.kt @@ -6,6 +6,8 @@ import core.game.system.SystemState import core.game.system.config.ServerConfigParser import core.game.world.GameWorld import core.net.NioReactor +import core.net.websocket.GameWebSocketServer +import core.net.websocket.WebSocketTls import core.tools.Log import core.tools.NetworkReachability import core.tools.TimeStamp @@ -44,6 +46,9 @@ object Server { @JvmStatic var reactor: NioReactor? = null + @JvmStatic + var webSocketServer: GameWebSocketServer? = null + var networkReachability = NetworkReachability.Reachable /** @@ -70,6 +75,16 @@ object Server { try { reactor = NioReactor.configure(43594 + GameWorld.settings?.worldId!!) reactor!!.start() + if (ServerConstants.WEBSOCKET_ENABLED) { + val websocketPort = if (ServerConstants.WEBSOCKET_PORT > 0) { + ServerConstants.WEBSOCKET_PORT + } else { + 53594 + GameWorld.settings?.worldId!! + } + webSocketServer = GameWebSocketServer(websocketPort, 1) + WebSocketTls.configure(webSocketServer!!) + webSocketServer!!.start() + } } catch (e: BindException) { log(this::class.java, Log.ERR, "Port " + (43594 + GameWorld.settings?.worldId!!) + " is already in use!") throw e diff --git a/Server/src/main/core/ServerConstants.kt b/Server/src/main/core/ServerConstants.kt index 4e40457f4..8c5f493a4 100644 --- a/Server/src/main/core/ServerConstants.kt +++ b/Server/src/main/core/ServerConstants.kt @@ -374,5 +374,20 @@ class ServerConstants { @JvmField var CONNECTIVITY_TIMEOUT = 500 + + @JvmField + var WEBSOCKET_ENABLED = false + + @JvmField + var WEBSOCKET_PORT = 0 + + @JvmField + var WEBSOCKET_TLS_ENABLED = false + + @JvmField + var WEBSOCKET_TLS_KEYSTORE_PATH = "" + + @JvmField + var WEBSOCKET_TLS_KEYSTORE_PASSWORD = "" } } diff --git a/Server/src/main/core/game/system/SystemTermination.java b/Server/src/main/core/game/system/SystemTermination.java index a1557f381..813b16fe3 100644 --- a/Server/src/main/core/game/system/SystemTermination.java +++ b/Server/src/main/core/game/system/SystemTermination.java @@ -45,6 +45,10 @@ public final class SystemTermination { log(this.getClass(), Log.INFO, "Stopping all bots..."); AIRepository.clearAllBots(); Server.getReactor().terminate(); + if (Server.getWebSocketServer() != null) { + Server.getWebSocketServer().stop(1000, "Server shutting down"); + Server.setWebSocketServer(null); + } log(this.getClass(), Log.INFO, "Stopping all pulses..."); GameWorld.getMajorUpdateWorker().stop(); for (Iterator it = Repository.getPlayers().iterator(); it.hasNext();) { diff --git a/Server/src/main/core/game/system/config/ServerConfigParser.kt b/Server/src/main/core/game/system/config/ServerConfigParser.kt index ebacf36a1..58e3e6ebf 100644 --- a/Server/src/main/core/game/system/config/ServerConfigParser.kt +++ b/Server/src/main/core/game/system/config/ServerConfigParser.kt @@ -176,6 +176,11 @@ object ServerConfigParser { ServerConstants.BOOSTED_TRAWLER_REWARDS = data.getBoolean("world.boosted_trawler_rewards", false) ServerConstants.CONNECTIVITY_CHECK_URL = data.getString("server.connectivity_check_url", "https://google.com,https://2009scape.org") ServerConstants.CONNECTIVITY_TIMEOUT = data.getLong("server.connectivity_timeout", 500L).toInt() + ServerConstants.WEBSOCKET_ENABLED = data.getBoolean("server.websocket_enabled", false) + ServerConstants.WEBSOCKET_PORT = data.getLong("server.websocket_port", 0L).toInt() + ServerConstants.WEBSOCKET_TLS_ENABLED = data.getBoolean("server.websocket_tls_enabled", false) + ServerConstants.WEBSOCKET_TLS_KEYSTORE_PATH = data.getString("server.websocket_tls_keystore_path", "") + ServerConstants.WEBSOCKET_TLS_KEYSTORE_PASSWORD = data.getString("server.websocket_tls_keystore_password", "") val logLevel = data.getString("server.log_level", "VERBOSE").uppercase() ServerConstants.LOG_LEVEL = parseEnumEntry(logLevel) ?: LogLevel.VERBOSE diff --git a/Server/src/main/core/net/IoSession.java b/Server/src/main/core/net/IoSession.java index 7e61dc28c..956b1d9da 100644 --- a/Server/src/main/core/net/IoSession.java +++ b/Server/src/main/core/net/IoSession.java @@ -122,12 +122,45 @@ public class IoSession { * @param service The executor service. */ public IoSession(SelectionKey key, ExecutorService service) { + this(key, service, resolveRemoteAddress(key)); + } + + /** + * Constructs a new {@code IoSession} with an explicit remote address. + * @param key The selection key. + * @param service The executor service. + * @param remoteAddress The remote address. + */ + public IoSession(SelectionKey key, ExecutorService service, String remoteAddress) { this.key = key; this.service = service; - this.address = getRemoteAddress().replaceAll("/", "").split(":")[0]; + this.address = normalizeRemoteAddress(remoteAddress); this.js5Queue = new JS5Queue(this); } + private static String resolveRemoteAddress(SelectionKey key) { + if (key == null || !(key.channel() instanceof SocketChannel)) { + return "127.0.0.1"; + } + try { + return ((SocketChannel) key.channel()).getRemoteAddress().toString(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static String normalizeRemoteAddress(String remoteAddress) { + if (remoteAddress == null || remoteAddress.trim().isEmpty()) { + return "127.0.0.1"; + } + String normalized = remoteAddress.replace("/", ""); + int separator = normalized.lastIndexOf(':'); + if (separator > 0) { + return normalized.substring(0, separator); + } + return normalized; + } + /** * Fires a write event created using the current event producer. * @param context The event context. @@ -213,9 +246,11 @@ public class IoSession { return; } active = false; - key.cancel(); - SocketChannel channel = (SocketChannel) key.channel(); - channel.socket().close(); + if (key != null) { + key.cancel(); + SocketChannel channel = (SocketChannel) key.channel(); + channel.socket().close(); + } if (getPlayer() != null) { try { getPlayer().clear(); @@ -435,4 +470,4 @@ public class IoSession { this.clientInfo = clientInfo; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/NioReactor.java b/Server/src/main/core/net/NioReactor.java index 42fea5a1d..fb07b826d 100644 --- a/Server/src/main/core/net/NioReactor.java +++ b/Server/src/main/core/net/NioReactor.java @@ -143,6 +143,21 @@ public final class NioReactor implements Runnable { */ public void terminate() { running = false; + try { + if (channel != null) { + if (channel.getChannel() != null) { + channel.getChannel().close(); + } + if (channel.getSocket() != null) { + channel.getSocket().close(); + } + channel.getSelector().wakeup(); + channel.getSelector().close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + service.shutdownNow(); } } \ No newline at end of file diff --git a/Server/src/main/core/net/packet/PacketProcessor.kt b/Server/src/main/core/net/packet/PacketProcessor.kt index 6ec7d04ba..c05798490 100644 --- a/Server/src/main/core/net/packet/PacketProcessor.kt +++ b/Server/src/main/core/net/packet/PacketProcessor.kt @@ -10,7 +10,6 @@ import core.ServerConstants import core.api.getAttribute import core.api.log import core.api.sendMessage -import core.api.tryPop import core.api.utils.Vector import core.cache.def.impl.ItemDefinition import core.cache.def.impl.NPCDefinition @@ -62,21 +61,35 @@ import java.lang.Math.min import java.util.* object PacketProcessor { - val queue = LinkedList() + private var queue = LinkedList() + private var processingQueue = LinkedList() + private val queueLock = Any() @JvmStatic fun enqueue(pkt: Packet) { - queue.addLast(pkt) + synchronized(queueLock) { + queue.addLast(pkt) + } } @JvmStatic fun processQueue() { - var countThisCycle = queue.size + synchronized(queueLock) { + if (queue.isEmpty()) { + return + } + val queued = queue + queue = processingQueue + processingQueue = queued + } val sw = StringWriter() val pw = PrintWriter(sw) var pkt: Packet - while (countThisCycle-- > 0) { - pkt = queue.tryPop(Packet.NoProcess()) ?: return + while (processingQueue.isNotEmpty()) { + pkt = processingQueue.pollFirst() ?: return if (pkt is Packet.NoProcess) { - queue.clear() + synchronized(queueLock) { + queue.clear() + } + processingQueue.clear() return } try { diff --git a/Server/src/main/core/net/packet/PacketWriteQueue.kt b/Server/src/main/core/net/packet/PacketWriteQueue.kt index fad453300..ed3de3d43 100644 --- a/Server/src/main/core/net/packet/PacketWriteQueue.kt +++ b/Server/src/main/core/net/packet/PacketWriteQueue.kt @@ -1,18 +1,18 @@ package core.net.packet import core.api.log -import core.api.tryPop import core.net.packet.out.* import core.tools.Log import core.tools.SystemLogger import java.io.PrintWriter import java.io.StringWriter import java.util.* -import java.util.concurrent.locks.ReentrantLock class PacketWriteQueue { companion object { - private val packetsToWrite = LinkedList>() + private var packetsToWrite = LinkedList>() + private var packetsToFlush = LinkedList>() + private val queueLock = Any() @JvmStatic fun handle(packet: OutgoingPacket, context: T) { @@ -34,16 +34,25 @@ class PacketWriteQueue { log(this::class.java, Log.ERR, "${packet::class.java.simpleName} tried to queue with a null context!") return } - packetsToWrite.add(QueuedPacket(packet, context)) + synchronized(queueLock) { + packetsToWrite.add(QueuedPacket(packet, context)) + } } @JvmStatic fun flush() { - var countThisCycle = packetsToWrite.size + synchronized(queueLock) { + if (packetsToWrite.isEmpty()) { + return + } + val queued = packetsToWrite + packetsToWrite = packetsToFlush + packetsToFlush = queued + } val sw = StringWriter() val pw = PrintWriter(sw) - while (countThisCycle-- > 0) { - val pkt = packetsToWrite.tryPop(null) ?: continue + while (packetsToFlush.isNotEmpty()) { + val pkt = packetsToFlush.pollFirst() ?: continue try { write(pkt.out, pkt.context) } catch (e: Exception) { diff --git a/Server/src/main/core/net/websocket/GameWebSocketServer.kt b/Server/src/main/core/net/websocket/GameWebSocketServer.kt new file mode 100644 index 000000000..dcd7d3f3f --- /dev/null +++ b/Server/src/main/core/net/websocket/GameWebSocketServer.kt @@ -0,0 +1,73 @@ +package core.net.websocket + +import core.api.log +import core.net.IoSession +import core.tools.Log +import org.java_websocket.WebSocket +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class GameWebSocketServer(port: Int, poolSize: Int) : WebSocketServer(InetSocketAddress(port)) { + private val service: ExecutorService = Executors.newFixedThreadPool(poolSize) { runnable -> + Thread(runnable, "WebSocketIo").apply { + isDaemon = true + } + } + private val sessions = ConcurrentHashMap() + + init { + // This pool processes websocket frames; it is not a cap on connected webclients. + setTcpNoDelay(true) + setReuseAddr(true) + } + + override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { + sessions[conn] = WebSocketIoSession(conn, service) + } + + override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) { + val session = sessions.remove(conn) + if (session != null && session.isActive) { + session.disconnect() + } + } + + override fun onMessage(conn: WebSocket, message: String) { + log(javaClass, Log.WARN, "Closing websocket client ${conn.remoteSocketAddress}: text frames are unsupported.") + conn.close(1003, "Binary frames only") + } + + override fun onMessage(conn: WebSocket, message: ByteBuffer) { + val session: IoSession = sessions[conn] ?: run { + conn.close(1011, "Session not initialized") + return + } + val copy = ByteBuffer.allocate(message.remaining()) + copy.put(message) + copy.flip() + service.execute(session.producer.produceReader(session, copy)) + } + + override fun onError(conn: WebSocket?, ex: Exception?) { + if (ex != null) { + log(javaClass, Log.ERR, "WebSocket server error: ${ex.message}") + } + if (conn != null) { + sessions.remove(conn)?.disconnect() + } + } + + override fun onStart() { + log(javaClass, Log.INFO, "WebSocket listener started on port $port.") + } + + override fun stop(timeout: Int, closeMessage: String) { + super.stop(timeout, closeMessage) + service.shutdownNow() + } +} diff --git a/Server/src/main/core/net/websocket/WebSocketIoSession.kt b/Server/src/main/core/net/websocket/WebSocketIoSession.kt new file mode 100644 index 000000000..49693ec09 --- /dev/null +++ b/Server/src/main/core/net/websocket/WebSocketIoSession.kt @@ -0,0 +1,41 @@ +package core.net.websocket + +import core.net.IoSession +import org.java_websocket.WebSocket +import org.java_websocket.exceptions.WebsocketNotConnectedException +import java.nio.ByteBuffer +import java.util.concurrent.ExecutorService + +class WebSocketIoSession( + private val socket: WebSocket, + service: ExecutorService +) : IoSession(null, service, socket.remoteSocketAddress?.toString() ?: "127.0.0.1") { + + override fun queue(buffer: ByteBuffer?) { + if (buffer == null || !socket.isOpen) { + return + } + val copy = buffer.slice() + val data = ByteArray(copy.remaining()) + copy.get(data) + try { + socket.send(data) + } catch (e: WebsocketNotConnectedException) { + disconnect() + } + } + + override fun write() { + // WebSocket sessions bypass the SelectionKey-backed TCP write queue. + } + + override fun disconnect() { + if (!isActive) { + return + } + super.disconnect() + if (socket.isOpen) { + socket.close() + } + } +} diff --git a/Server/src/main/core/net/websocket/WebSocketTls.kt b/Server/src/main/core/net/websocket/WebSocketTls.kt new file mode 100644 index 000000000..981afe060 --- /dev/null +++ b/Server/src/main/core/net/websocket/WebSocketTls.kt @@ -0,0 +1,45 @@ +package core.net.websocket + +import core.ServerConstants +import core.api.log +import core.game.system.config.ServerConfigParser +import core.tools.Log +import org.java_websocket.server.DefaultSSLWebSocketServerFactory +import java.io.FileInputStream +import java.security.KeyStore +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext + +object WebSocketTls { + + private const val KEYSTORE_TYPE = "PKCS12" + + fun configure(server: GameWebSocketServer) { + if (!ServerConstants.WEBSOCKET_TLS_ENABLED) { + return + } + server.setWebSocketFactory(DefaultSSLWebSocketServerFactory(createContext())) + } + + private fun createContext(): SSLContext { + val keystorePath = ServerConstants.WEBSOCKET_TLS_KEYSTORE_PATH.trim() + if (keystorePath.isEmpty()) { + throw IllegalStateException("server.websocket_tls_enabled is true but websocket_tls_keystore_path is blank") + } + val resolvedKeystorePath = ServerConfigParser.parsePath(keystorePath) + val keystorePassword = ServerConstants.WEBSOCKET_TLS_KEYSTORE_PASSWORD.toCharArray() + + val keyStore = KeyStore.getInstance(KEYSTORE_TYPE) + FileInputStream(resolvedKeystorePath).use { input -> + keyStore.load(input, keystorePassword) + } + + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + keyManagerFactory.init(keyStore, keystorePassword) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(keyManagerFactory.keyManagers, null, null) + log(WebSocketTls::class.java, Log.INFO, "Configured secure websocket listener using keystore $resolvedKeystorePath.") + return sslContext + } +} diff --git a/Server/worldprops/default.conf b/Server/worldprops/default.conf index ed0a4fa62..308db7417 100644 --- a/Server/worldprops/default.conf +++ b/Server/worldprops/default.conf @@ -25,6 +25,17 @@ daily_accounts_per_ip = 3 watchdog_enabled = true connectivity_check_url = "https://google.com,https://2009scape.org" connectivity_timeout = 500 +# Enables a websocket listener for browser clients. The websocket stream carries the +# same raw binary protocol as TCP; each websocket binary frame is treated as a byte chunk. +websocket_enabled = false +# Leave at 0 to use the default websocket port of 53594 + world_id. +websocket_port = 0 +# Enables TLS on the websocket listener so browser clients can connect over wss://. +websocket_tls_enabled = false +# PKCS12 keystore path, e.g. certs/dev-wss.p12. +websocket_tls_keystore_path = "" +# Optional. Leave blank for a PKCS12 file exported with an empty password. +websocket_tls_keystore_password = "" [database] database_name = "global" From 5822b8df99e3dbbf738f2326aa9b5472f54affcb Mon Sep 17 00:00:00 2001 From: Oven Bread Date: Mon, 6 Jul 2026 13:08:28 +0000 Subject: [PATCH 30/42] Modernised some location/pathing architecture --- .../main/content/data/EnchantedJewellery.kt | 2 +- .../global/handlers/npc/SheepBehavior.kt | 6 +- .../lightsources/LightSourceLighter.kt | 2 +- .../gather/woodcutting/WoodcuttingListener.kt | 2 +- .../skill/skillcapeperks/SkillcapePerks.kt | 2 +- .../draynor/handlers/DraynorManorHouseZone.kt | 6 +- .../GardenerGhostDialogue.kt | 2 +- .../system/command/oldsys/VisualCommand.kt | 2 +- .../main/core/game/world/map/Location.java | 578 ------------------ .../src/main/core/game/world/map/Location.kt | 257 ++++++++ .../main/core/game/world/map/RegionManager.kt | 8 +- .../src/test/kotlin/core/PathfinderTests.kt | 4 +- 12 files changed, 275 insertions(+), 596 deletions(-) delete mode 100644 Server/src/main/core/game/world/map/Location.java create mode 100644 Server/src/main/core/game/world/map/Location.kt diff --git a/Server/src/main/content/data/EnchantedJewellery.kt b/Server/src/main/content/data/EnchantedJewellery.kt index 242c9305f..251bfa6c1 100644 --- a/Server/src/main/content/data/EnchantedJewellery.kt +++ b/Server/src/main/content/data/EnchantedJewellery.kt @@ -193,7 +193,7 @@ enum class EnchantedJewellery( ), RING_OF_LIFE(arrayOf(), arrayOf( - Location.create(ServerConstants.HOME_LOCATION) + Location.create(ServerConstants.HOME_LOCATION!!) ), true, Items.RING_OF_LIFE_2570 diff --git a/Server/src/main/content/global/handlers/npc/SheepBehavior.kt b/Server/src/main/content/global/handlers/npc/SheepBehavior.kt index fbd283e77..c6ed46374 100644 --- a/Server/src/main/content/global/handlers/npc/SheepBehavior.kt +++ b/Server/src/main/content/global/handlers/npc/SheepBehavior.kt @@ -129,9 +129,9 @@ class SheepBehavior : NPCBehavior(*sheepIds), InteractionListener { val sheepDirection = Direction.getDirection(sheepLocation, playerLocation) // Get direction sheep is facing, from the player's location val sheepOppositeDirection = sheepDirection.getOpposite() // Switch to opposite direction - val xWalkLocation = sheepLocation.getX() + (sheepOppositeDirection.getStepX() * 3) // Gets x location, if set, 3 steps away from player - val yWalkLocation = sheepLocation.getY() + (sheepOppositeDirection.getStepY() * 3) // Gets y location, if set, 3 steps away from player - val sheepWalkToLocation = Location(xWalkLocation, yWalkLocation, sheepLocation.getZ()); // New location for pathfinding + val xWalkLocation = sheepLocation.x + (sheepOppositeDirection.getStepX() * 3) // Gets x location, if set, 3 steps away from player + val yWalkLocation = sheepLocation.y + (sheepOppositeDirection.getStepY() * 3) // Gets y location, if set, 3 steps away from player + val sheepWalkToLocation = Location(xWalkLocation, yWalkLocation, sheepLocation.z); // New location for pathfinding sendMessage(player, messagePlayer) diff --git a/Server/src/main/content/global/skill/crafting/lightsources/LightSourceLighter.kt b/Server/src/main/content/global/skill/crafting/lightsources/LightSourceLighter.kt index d636ee588..b558ec38f 100644 --- a/Server/src/main/content/global/skill/crafting/lightsources/LightSourceLighter.kt +++ b/Server/src/main/content/global/skill/crafting/lightsources/LightSourceLighter.kt @@ -79,7 +79,7 @@ class LightSourceLighter : UseWithHandler(590,36,38){ // For Temple of Ikov - if you are in the dark basement and light a light source, switch to the light basement. // For the listener that covers the firemaking cape perk, see content.global.skill.skillcapeperks.SkillcapePerks.kt if(event.player.location.isInRegion(10648) && event.player.location.withinDistance(Location(2639,9738,0), 8)) { - teleport(event.player, Location.create(event.player.getLocation().getX(), event.player.getLocation().getY() + 23, event.player.getLocation().getZ())) + teleport(event.player, Location.create(event.player.getLocation().x, event.player.getLocation().y + 23, event.player.getLocation().z)) closeDialogue(event.player) // Dark basement is region 10648, min 2639 9738 0, max 2643 9744 0. Add 23 to the Y loc to tele to light basement } diff --git a/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt index 28cfc2c86..5a4a137b8 100644 --- a/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt +++ b/Server/src/main/content/global/skill/gather/woodcutting/WoodcuttingListener.kt @@ -270,7 +270,7 @@ class WoodcuttingListener : InteractionListener { var amount = amount var experience: Double = resource.getExperience() val reward = resource.reward - if (player.getLocation().getRegionId() == 10300) { + if (player.getLocation().regionId == 10300) { return 1.0 } diff --git a/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt b/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt index 7bb35b190..148696543 100644 --- a/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt +++ b/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt @@ -142,7 +142,7 @@ enum class SkillcapePerks(val attribute: String, val effect: ((Player) -> Unit)? // For Temple of Ikov - if you are in the dark basement and put on the firemaking cape with its 2009Scape light source perk, switch to the light basement. // For the listener that teleports if you light a normal light source, see content.global.skill.crafting.lightsources.LightSourceLighter.kt if(player.location.isInRegion(10648) && player.location.withinDistance(Location(2639,9738,0), 8)) { - teleport(player, Location.create(player.getLocation().getX(), player.getLocation().getY() + 23, player.getLocation().getZ())) + teleport(player, Location.create(player.getLocation().x, player.getLocation().y + 23, player.getLocation().z)) closeDialogue(player) } } diff --git a/Server/src/main/content/region/misthalin/draynor/handlers/DraynorManorHouseZone.kt b/Server/src/main/content/region/misthalin/draynor/handlers/DraynorManorHouseZone.kt index 9478c6656..529b497a6 100644 --- a/Server/src/main/content/region/misthalin/draynor/handlers/DraynorManorHouseZone.kt +++ b/Server/src/main/content/region/misthalin/draynor/handlers/DraynorManorHouseZone.kt @@ -31,10 +31,10 @@ class DraynorManorHouseZone : MapZone("Draynor Manor House", true), Plugin if (n.shouldPreventStacking(e)) { val s1 = e.size() val s2 = n.size() - val x = destination.getX() - val y = destination.getY() + val x = destination.x + val y = destination.y val l = n.getLocation() - if (Pathfinder.isStandingIn(x, y, s1, s1, l.getX(), l.getY(), s2, s2)) { + if (Pathfinder.isStandingIn(x, y, s1, s1, l.x, l.y, s2, s2)) { return false } } diff --git a/Server/src/main/content/region/morytania/quest/creatureoffenkenstrain/GardenerGhostDialogue.kt b/Server/src/main/content/region/morytania/quest/creatureoffenkenstrain/GardenerGhostDialogue.kt index 70387d2c8..6c7bd9830 100644 --- a/Server/src/main/content/region/morytania/quest/creatureoffenkenstrain/GardenerGhostDialogue.kt +++ b/Server/src/main/content/region/morytania/quest/creatureoffenkenstrain/GardenerGhostDialogue.kt @@ -18,7 +18,7 @@ class GardenerGhostDialogue (player: Player? = null) : DialoguePlugin(player) { if (gardenerGhost.location.withinDistance(gardenerGhost.graveLocation, 5)) { sendChat(gardenerGhost, "Here is the place where I met me' maker.") } else { - sendChat(gardenerGhost, "Go " + gardenerGhost.location.deriveDirection(gardenerGhost.graveLocation).name.lowercase(Locale.getDefault()).replace('_', '-') + ", mate") + sendChat(gardenerGhost, "Go " + gardenerGhost.location.deriveDirection(gardenerGhost.graveLocation)!!.name.lowercase(Locale.getDefault()).replace('_', '-') + ", mate") gardenerGhost.continueFollowing(player) } } else { diff --git a/Server/src/main/core/game/system/command/oldsys/VisualCommand.kt b/Server/src/main/core/game/system/command/oldsys/VisualCommand.kt index fc2a08c28..17d98c883 100644 --- a/Server/src/main/core/game/system/command/oldsys/VisualCommand.kt +++ b/Server/src/main/core/game/system/command/oldsys/VisualCommand.kt @@ -197,7 +197,7 @@ class VisualCommand : CommandPlugin() { return true } location = if (args.size > 2) Location.create(args[1]!!.toInt(), args[2]!!.toInt(), player!!.location.z) else player!!.location - `object` = RegionManager.getObject(location) + `object` = RegionManager.getObject(location!!) if (`object` == null) { player!!.debug("error: object not found in region cache.") return true diff --git a/Server/src/main/core/game/world/map/Location.java b/Server/src/main/core/game/world/map/Location.java deleted file mode 100644 index ac4b1b310..000000000 --- a/Server/src/main/core/game/world/map/Location.java +++ /dev/null @@ -1,578 +0,0 @@ -package core.game.world.map; - -import core.game.interaction.DestinationFlag; -import core.game.node.Node; -import core.game.world.map.path.Path; -import core.game.world.map.path.Pathfinder; -import core.tools.RandomFunction; -import org.jetbrains.annotations.NotNull; -import core.api.utils.Vector; - -import java.util.ArrayList; -import java.util.List; -import java.lang.Math; - -/** - * Represents a location on the world map. - * @author Emperor - */ -public final class Location extends Node { - - /** - * The x-coordinate. - */ - private int x; - - /** - * The y-coordinate. - */ - private int y; - - /** - * The plane. - */ - private int z; - - /** - * Constructs a new {@code Location} {@code Object}. - * @param x The x-coordinate. - * @param y The y-coordinate. - * @param z The z-coordinate. - */ - public Location(int x, int y, int z) { - super(null, null); - super.destinationFlag = DestinationFlag.LOCATION; - this.x = x; - this.y = y; - if (z < 0) { - z += 4; - } - this.z = z; - } - - /** - * Constructs a new {@code Location} {@code Object} - * @param x The x-coordinate. - * @param y The y-coordinate. - */ - public Location(int x, int y) { - this(x, y, 0); - } - - /** - * Constructs a new {@code Location} {@code Object}. - * @param x The x-coordinate. - * @param y The y coordinate. - * @param z The z-coordinate. - * @param randomizer The amount we should randomize the x and y coordinates - * with (x + random(randomizer), y + random(randomizer)). - */ - public Location(int x, int y, int z, int randomizer) { - this(x + RandomFunction.getRandom(randomizer), y + RandomFunction.getRandom(randomizer), z); - } - - /** - * Construct a new Location. - * @param x The x-coordinate. - * @param y The y-coordinate. - * @param z The z-coordinate. - * @return The constructed location. - */ - public static Location create(int x, int y, int z) { - return new Location(x, y, z); - } - - public static Location create(int x, int y) { - return new Location(x, y, 0); - } - - /** - * Creates a new instance of the given location. - * @param location The given location. - * @return The new instance. - */ - public static Location create(Location location) { - return create(location.getX(), location.getY(), location.getZ()); - } - - /** - * Creates a location instance with coordinates being the difference between - * {@code other} & {@code location}. - * @param location The first location. - * @param other The other location. - * @return The delta location. - */ - public static Location getDelta(Location location, Location other) { - return Location.create(other.x - location.x, other.y - location.y, other.z - location.z); - } - - /** - * Gets a random location near the main location. - * @param main The main location. - * @param radius The radius. - * @param reachable If the locations should be able to reach eachother. - * @return The location. - */ - public static Location getRandomLocation(Location main, int radius, boolean reachable) { - Location location = RegionManager.getTeleportLocation(main, radius); - if (!reachable) { - return location; - } - Path path = Pathfinder.find(main, location, false, Pathfinder.DUMB); - if (!path.isSuccessful()) { - location = main; - if (!path.getPoints().isEmpty()) { - Point p = path.getPoints().getLast(); - location = Location.create(p.getX(), p.getY(), main.getZ()); - } - } - return location; - } - - @Override - public Location getLocation() { - return this; - } - - /** - * Checks if this location is right next to the node (assuming the node is - * size 1x1). - * @param node The node to check. - * @return {@code True} if this location is 1 tile north, west, south or - * east of the node location. - */ - public boolean isNextTo(Node node) { - Location l = node.getLocation(); - if (l.getY() == y) { - return l.getX() - x == -1 || l.getX() - x == 1; - } - if (l.getX() == x) { - return l.getY() - y == -1 || l.getY() - y == 1; - } - return false; - } - - /** - * Gets the region id. - * @return The region id. - */ - public int getRegionId() { - return (x >> 6) << 8 | (y >> 6); - } - - /** - * Compares the users region with the one given - * @return True if user is in given region - */ - public boolean isInRegion(int region) { - return getRegionId() == region; - } - - /** - * Gets the location incremented by the given coordinates. - * @param dir The direction to transform this location. - * @return The location. - */ - public Location transform(Direction dir) { - return transform(dir, 1); - } - - /** - * Gets the location incremented by the given coordinates. - * @param dir The direction to transform this location. - * @param steps The amount of steps to move in this direction. - * @return The location. - */ - public Location transform(Direction dir, int steps) { - return new Location(x + (dir.getStepX() * steps), y + (dir.getStepY() * steps), this.z); - } - - /** - * Gets the location incremented by the given coordinates. - * @param l incremental location - * @return The location. - */ - public Location transform(Location l) { - return new Location(x + l.getX(), y + l.getY(), this.z + l.getZ()); - } - - /** - * Gets the location incremented by the given coordinates. - * @param diffX The x-difference. - * @param diffY The y-difference. - * @param z The height difference. - * @return The location. - */ - public Location transform(int diffX, int diffY, int z) { - return new Location(x + diffX, y + diffY, this.z + z); - } - - /** - * Checks if the other location is within viewing distance. - * @param other The other location. - * @return If you're within the other distance. - */ - public boolean withinDistance(Location other) { - return withinDistance(other, MapDistance.RENDERING.getDistance()); - } - - /** - * Returns if a player is within a specified distance. - * @param other The other location. - * @param dist The amount of distance. - * @return If you're within the other distance. - */ - public boolean withinDistance(Location other, int dist) { - if (other.z != z) { - return false; - } - - int a = (other.x - x); - int b = (other.y - y); - double product = Math.sqrt((a*a) + (b*b)); - return product <= dist; - } - - /** - * Returns if a player is within a specified distance using max norm distance. - * @param other The other location. - * @param dist The amount of distance. - * @return If you're within the other distance. - */ - public boolean withinMaxnormDistance(Location other, int dist) { - if (other.z != z) { - return false; - } - - int a = Math.abs(other.x - x); - int b = Math.abs(other.y - y); - double max = Math.max(a, b); - return max <= dist; - } - - /** - * Returns the distance between you and the other. - * @param other The other location. - * @return The amount of distance between you and other. - */ - public double getDistance(Location other) { - int xdiff = this.getX() - other.getX(); - int ydiff = this.getY() - other.getY(); - return Math.sqrt(xdiff * xdiff + ydiff * ydiff); - } - - /** - * Returns the distance between the first and the second specified distance. - * @param first The first location. - * @param second The other location. - * @return The amount of distance between first and other. - */ - public static double getDistance(Location first, Location second) { - int xdiff = first.getX() - second.getX(); - int ydiff = first.getY() - second.getY(); - return Math.sqrt(xdiff * xdiff + ydiff * ydiff); - } - - /** - * Gets the 8 tiles surrounding this location as an ArrayList - */ - public ArrayList getSurroundingTiles() { - ArrayList locs = new ArrayList<>(); - - locs.add(transform(-1,-1,0));//SW - locs.add(transform(0,-1,0)); //S - locs.add(transform(1,-1,0)); //SE - locs.add(transform(1,0,0)); //E - locs.add(transform(1,1,0)); //NE - locs.add(transform(0,1,0)); //N - locs.add(transform(-1,1,0));//NW - locs.add(transform(-1,0,0));//W - - return locs; - } - - public ArrayList getCardinalTiles() { - ArrayList locs = new ArrayList<>(); - - locs.add(transform(-1, 0, 0)); - locs.add(transform(0, -1, 0)); - locs.add(transform(1, 0, 0)); - locs.add(transform(0, 1, 0)); - return locs; - } - - /** - * Gets a square of 3 x 3 tiles as an ArrayList - */ - public ArrayList get3x3Tiles() { - ArrayList locs = new ArrayList<>(); - locs.add(transform(0,0,0)); //Center - locs.add(transform(0,1,0)); //N - locs.add(transform(1,1,0)); //NE - locs.add(transform(1,0,0)); //E - locs.add(transform(1,-1,0)); //SE - locs.add(transform(0,-1,0)); //S - locs.add(transform(-1,-1,0));//SW - locs.add(transform(-1,0,0));//W - locs.add(transform(-1,1,0));//NW - return locs; - } - - /** - * Gets the x position on the region chunk. - * @return The x position on the region chunk. - */ - public int getChunkOffsetX() { - int x = getLocalX(); - //return x - ((x / RegionChunk.SIZE) * RegionChunk.SIZE); - return x & 7; - } - - /** - * Gets the y position on the region chunk. - * @return The y position on the region chunk. - */ - public int getChunkOffsetY() { - int y = getLocalY(); - //return y - ((y / RegionChunk.SIZE) * RegionChunk.SIZE); - return y & 7; - } - - /** - * Gets the base location for the chunk this location is in. - * @return The base location. - */ - public Location getChunkBase() { - return create(getRegionX() << 3, getRegionY() << 3, z); - } - - /** - * Gets the region x-coordinate. - * @return The region x-coordinate. - */ - public int getRegionX() { - return x >> 3; - } - - /** - * Gets the region y-coordinate. - * @return The region y-coordinate. - */ - public int getRegionY() { - return y >> 3; - } - - /** - * Gets the local x-coordinate on the current region in [0, 64). - * @return The local x-coordinate. - */ - public int getLocalX() { - return x & 63; - } - - /** - * Gets the local y-coordinate on the current region in [0, 64). - * @return The local y-coordinate. - */ - public int getLocalY() { - return y & 63; - } - - /** - * Gets the scene x-coordinate in [48, 55] (note that 104/2 = 52). - * @return The local x-coordinate. - */ - public int getSceneX() { - return x - ((getRegionX() - 6) << 3); - } - - /** - * Gets the local y-coordinate in [48, 55] (note that 104/2 = 52). - * @return The local y-coordinate. - */ - public int getSceneY() { - return y - ((getRegionY() - 6) << 3); - } - - /** - * Gets the local x-coordinate. - * @param loc The location containing the regional coordinates. - * @return The local x-coordinate. - */ - public int getSceneX(Location loc) { - return x - ((loc.getRegionX() - 6) << 3); - } - - /** - * Gets the local y-coordinate. - * @param loc The location containing the regional coordinates. - * @return The local y-coordinate. - */ - public int getSceneY(Location loc) { - return y - ((loc.getRegionY() - 6) << 3); - } - - /** - * Gets the chunk's x-coordinate (0-7). - * @return The x in the (8x8) region. - */ - public int getChunkX() { - return getLocalX() >> 3; - } - - /** - * Gets the chunk's y-coordinate (0-7). - * @return The y in the (8x8) region. - */ - public int getChunkY() { - return getLocalY() >> 3; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Location)) { - return false; - } - Location loc = (Location) other; - return loc.x == x && loc.y == y && loc.z == z; - } - - /** - * Checks if these coordinates equal this location. - * @param x the x. - * @param y the y. - * @param z the x. - * @return {@code True} if so. - */ - public boolean equals(int x, int y, int z) { - return equals(new Location(x, y, z)); - } - - @Override - public String toString() { - return "[" + x + ", " + y + ", " + z + "]"; - } - - public static Location fromString(String locString) { - String trimmed = locString.replace("[", "").replace("]", ""); - String[] tokens = trimmed.split(","); - return Location.create( - Integer.parseInt(tokens[0].trim()), - Integer.parseInt(tokens[1].trim()), - Integer.parseInt(tokens[2].trim()) - ); - } - - @Override - public int hashCode() { - return z << 30 | x << 15 | y; - } - - /** - * Gets the x. - * @return The x. - */ - public int getX() { - return x; - } - - /** - * Sets the x. - * @param x The x to set. - */ - public void setX(int x) { - this.x = x; - } - - /** - * Gets the y. - * @return The y. - */ - public int getY() { - return y; - } - - /** - * Sets the y. - * @param y The y to set. - */ - public void setY(int y) { - this.y = y; - } - - /** - * Gets the z. - * @return The z. - */ - public int getZ() { - return z % 4; - } - - /** - * Sets the z. - * @param z The z to set. - */ - public void setZ(int z) { - this.z = z; - } - - @NotNull - public List getStepComponents(Direction dir) { - List output = new ArrayList<>(2); - int stepX = dir.getStepX(); - int stepY = dir.getStepY(); - - // alright ill break it down real simple - // - // we can move diagonally but theres no melee attacking diagonally. its gotta be from N, W, S or E. - // if we are moving diagonally, we then need to check if we can attack from one of the valid directions. - // in other words we need to check the two tiles that share a corner with our destination and make sure they can be attacked from - // - // picture this: you are at (0,0). im at (1,1). you want to strangle my head off so you gotta check - // 1. to the west of destination: (1,1) + (-1,0) = (0,1) or north from where you started - // 2. to the south of the destination: (1,1) + (0,-1) = (1,0) or east from where you started - // this function is used only by CombatSwingHandler.kt and assumes that we return the X tiles, west or east, FIRST - // and then the Y tiles South and North. - // - // idk what else to say, if youre reading this youre either crazy or crazy good at weird ass shit like this - // and dont even need letters to understand, theres just numbers and arrows and fking formulas or something in ur head (and a lot of work to do here) - - if (stepX != 0) output.add(transform(-stepX, 0, 0)); // Ive never dealt with coordinate systems at this level, but it feels like Im in a cold, damp, deep and dark cave with fking goblins etc - if (stepY != 0) output.add(transform(0, -stepY, 0)); // and minotaurs - return output; - } - - public Direction deriveDirection(Location location) { - int diffX = location.x - this.x; - int diffY = location.y - this.y; - - diffX = diffX >= 0 ? Math.min(diffX, 1) : -1; - diffY = diffY >= 0 ? Math.min(diffY, 1) : -1; - - StringBuilder sb = new StringBuilder(); - - if (diffY != 0) { - if (diffY > 0) { - sb.append("NORTH"); - } else { - sb.append("SOUTH"); - } - } - - if (diffX != 0) { - if (sb.length() > 0) sb.append("_"); - if (diffX > 0) { - sb.append("EAST"); - } else { - sb.append("WEST"); - } - } - - if (sb.length() == 0) return null; - return Direction.valueOf(sb.toString()); - } - - public Location transform (Vector vector) { - return Location.create(this.x + (int) Math.floor(vector.getX()), this.y + (int) Math.floor(vector.getY())); - } -} diff --git a/Server/src/main/core/game/world/map/Location.kt b/Server/src/main/core/game/world/map/Location.kt new file mode 100644 index 000000000..a202e4a49 --- /dev/null +++ b/Server/src/main/core/game/world/map/Location.kt @@ -0,0 +1,257 @@ +package core.game.world.map + +import core.api.utils.Vector +import core.game.interaction.DestinationFlag +import core.game.node.Node +import core.game.world.map.RegionManager.getTeleportLocation +import core.game.world.map.path.Pathfinder +import core.tools.RandomFunction +import kotlin.math.* + +/** + * Specify a position (x,y,z), implements [Node]. + * - Coordinates x and y can be negative for delta values. Values ±16,383. + * - Coordinate z (level) has a quirk, this class doesn't let you init with a negative z. Values 0 - 3. + */ +/* +Official Terms + - Zones: 8x8 tiles + - Map Squares: 64x64 tiles + - Build Area: 104x104 tiles (13 x 13 chunks centered around player's chunk) - client's loaded area + +Some Definitions: + - Cardinal: Four Base Direction (N E S W) + - Diagonal: Four Diagonal directions (NE SE SW NW) + - Compass: All 8 directions (For this game, compass in relation to 8-point Cardinal+Diagonal) + - Euclidean distance: Straight-line length of two points. Direct calculation. e.g. [2,3] distance=√13(≈3.61) + - Manhattan distance: Cardinal length of two points. Count of steps along grid lines. e.g. [2,3] distance=5 + - Chebyshev distance: Cardinal+Diagonal length of two points. Count of king moves to make. e.g. [2,3] distance=3 + */ +class Location(var hash: LocationHash) : Node(null, null) { + @JvmOverloads + constructor(x: Int, y: Int, z: Int = 0) : this( + LocationHash(x, y, (z % 4 + 4) % 4) // The previous z calc didn't make sense since they play around with negative numbers. + ) + constructor(location: Location) : this(location.hash) + constructor(x: Int, y: Int, z: Int, randomizer: Int) : + this(x + RandomFunction.getRandom(randomizer), y + RandomFunction.getRandom(randomizer), z) + init { super.destinationFlag = DestinationFlag.LOCATION } // This is to set the type of [Node]. + + // Map properties directly to the underlying hash for convenience + var x: Int + get() = hash.x + set(value) { hash = LocationHash(value, y, z) } + + var y: Int + get() = hash.y + set(value) { hash = LocationHash(x, value, z) } + + var z: Int + get() = hash.z + set(value) { hash = LocationHash(x, y, (value % 4 + 4) % 4) } + + companion object { + // Alternate static constructors + @JvmStatic @JvmOverloads fun create(x: Int, y: Int, z: Int = 0): Location = Location(x, y, z) + @JvmStatic fun create(location: Location): Location = Location(location) + /** @return new Location parsed from a string format "[x, y, z]". */ + fun fromString(locString: String): Location { + val coordinates = locString.removeSurrounding("[", "]").split(",").map { it.trim().toInt() } + return create(coordinates[0], coordinates[1], coordinates[2]) + } + + /** @return Straight-line(Euclidean) distance between two locations. */ + @JvmStatic fun getDistance(first: Location, second: Location): Double { + val xDiff: Int = first.x - second.x + val yDiff: Int = first.y - second.y + return sqrt((xDiff * xDiff + yDiff * yDiff).toDouble()) + } + + /** @return A location representing the delta between two locations. This is not a good function because it doesn't care about z. */ + @JvmStatic fun getDelta(location: Location, other: Location): Location = create(other.x - location.x, other.y - location.y, other.z - location.z) + + /** @return A random location within radius, optionally verifying reachability. */ + fun getRandomLocation(main: Location, radius: Int, reachable: Boolean): Location { + val loc = getTeleportLocation(main, radius) + if (!reachable) return loc + val path = Pathfinder.find(main, loc, false, Pathfinder.DUMB) + if (!path.isSuccessful && path.points.isNotEmpty()) { + return create(path.points.last.x, path.points.last.y, main.z) + } else if (path.isSuccessful) { + return loc + } else { + return main + } + } + } + + // Function overrides of Node class and Java class + override fun getLocation(): Location = this + override fun toString(): String = "[$x, $y, $z]" + override fun hashCode(): Int = hash.hash + override fun equals(other: Any?): Boolean { + return when (other) { + is Location -> other.hash == this.hash + is LocationHash -> other == this.hash + else -> false + } + } + fun equals(x: Int, y: Int, z: Int): Boolean = hash == LocationHash(x, y, z) + + // Derived variables. Please note the following isn't a 1-1 representation of what is officially used. + + // Region - (Officially: Map Square) 64x64 + /** Unique index "key" (Officially: region_uid) for a 64x64 area. Made of x and y joined bitwise together. Used in xteas.json. */ + val regionId: Int get() = x shr 6 shl 8 or (y shr 6) // Cuts off the end 6 bits of both x y and joins them together. E.g. 10392 + /** The chunk x index for this position (x/8). For sets of 8x8 area. THIS IS NAMED WRONG, THIS IS A CHUNK(8) NOT REGION(64). */ + val regionX: Int get() = x shr 3 // shr 3 is essentially divide by 8 + /** The chunk y index for this position (y/8). For sets of 8x8 area. THIS IS NAMED WRONG, THIS IS A CHUNK(8) NOT REGION(64). */ + val regionY: Int get() = y shr 3 // shr 3 is essentially divide by 8 + // Local - x,y position inside a region + /** The relative x position inside a region(64x64). */ + val localX: Int get() = x and 63 // and 63 is essentially modulo 64 + /** The relative y position inside a region(64x64). */ + val localY: Int get() = y and 63 // and 63 is essentially modulo 64 + /** @return Check if it is in the same region id. */ + fun isInRegion(region: Int): Boolean = regionId == region + + // Chunk - (Officially: Zones) 8x8 + val chunkBase: Location get() = create(regionX shl 3, regionY shl 3, z) + /** The chunk x index for this position RELATIVE to a region[localX](x/8). THIS IS NOT GLOBAL CHUNK X. */ + val chunkX: Int get() = localX shr 3 // shr 3 is essentially divide by 8 (note this is localX) + /** The chunk y index for this position RELATIVE to a region[localY](y/8). THIS IS NOT GLOBAL CHUNK Y. */ + val chunkY: Int get() = localY shr 3 // shr 3 is essentially divide by 8 (note this is localX) + /** The relative x position inside a chunk(8x8). */ + val chunkOffsetX: Int get() = localX and 7 // and 7 is essentially modulo 8 + /** The relative y position inside a chunk(8x8). */ + val chunkOffsetY: Int get() = localY and 7 // and 7 is essentially modulo 8 + + // Scene - (Officially: Build Area) 104x104 - Made of 13x13 chunks(8x8) + /** The relative x position inside a scene(104x104). Used for client viewport rendering. */ + val sceneX: Int get() = x - (regionX - 6 shl 3) // Centers the current region at [7chunk,7chunk] + /** The relative y position inside a scene(104x104). Used for client viewport rendering. */ + val sceneY: Int get() = y - (regionY - 6 shl 3) // Centers the current region at [7chunk,7chunk] + /** @return The local scene x-coordinate relative to another location's regionX. */ + fun getSceneX(loc: Location): Int = x - (loc.regionX - 6 shl 3) + /** @return The local scene y-coordinate relative to another location's regionY. */ + fun getSceneY(loc: Location): Int = y - (loc.regionY - 6 shl 3) + + + + /** @return The location incremented by another location's coordinates. (POTENTIAL Z PROBLEMS HERE) */ + // TODO: THE FOLLOWING TRANSFORM HAS SHIT Z HANDLING. + fun transform(l: Location): Location = Location(x + l.x, y + l.y, (z + l.z) % 4) + /** @return The location incremented by specific x, y, z differences. */ + fun transform(diffX: Int, diffY: Int, z: Int): Location = Location(x + diffX, y + diffY, (this.z + z) % 4) + /** @return The location incremented by direction and steps. */ + @JvmOverloads fun transform(dir: Direction, steps: Int = 1): Location = Location(x + dir.stepX * steps, y + dir.stepY * steps, z) + /** Location transformed by a Vector. Limit usage to more complicated functionality. */ + fun transform(vector: Vector): Location = create(x + floor(vector.x).toInt(), y + floor(vector.y).toInt()) + + + /** @return True if this location is 1 tile N, W, S, or E of the node. THIS CAN BE REFACTORED OUT. */ + fun isNextTo(node: Node): Boolean { + val l = node.location + return if (l.y == y) abs(l.x - x) == 1 else if (l.x == x) abs(l.y - y) == 1 else false + } + + /** @return True if this other location is within dist of this location. */ + @JvmOverloads fun withinDistance(other: Location, dist: Int = MapDistance.RENDERING.distance): Boolean { + if (other.z != z) return false + return getDistance(this, other) <= dist + } + + /** @return True if within specified distance using max norm (Chebyshev) distance. */ + fun withinMaxnormDistance(other: Location, dist: Int): Boolean = if (other.z != z) false else max(abs(other.x - x), abs(other.y - y)) <= dist + + /** @return The straight-line(Euclidean) distance between this and another location. */ + fun getDistance(other: Location): Double = getDistance(this, other) + + /** @returns ArrayList of the 8 cardinal and diagonal tiles. Order is important and follows [Direction] indexing **/ + val surroundingTiles: ArrayList get() { + val locations = ArrayList() + locations.add(transform(-1, -1, 0)) + locations.add(transform(0, -1, 0)) + locations.add(transform(1, -1, 0)) + locations.add(transform(1, 0, 0)) + locations.add(transform(1, 1, 0)) + locations.add(transform(0, 1, 0)) + locations.add(transform(-1, 1, 0)) + locations.add(transform(-1, 0, 0)) + return locations + } + + /** @returns ArrayList of the 4 cardinal direction tiles. Order is important and follows [Direction] indexing **/ + val cardinalTiles: ArrayList get() { + val locations = ArrayList() + locations.add(transform(-1, 0, 0)) + locations.add(transform(0, -1, 0)) + locations.add(transform(1, 0, 0)) + locations.add(transform(0, 1, 0)) + return locations + } + + + /** + * Gets the directional components of a movement step. + * TODO: would prefer this in the other place. This doesn't exactly belong here since this isn't a common function for other instances of Location + * + * we can move diagonally but there's no melee attacking diagonally. its gotta be from N, W, S or E. + * if we are moving diagonally, we then need to check if we can attack from one of the valid directions. + * in other words we need to check the two tiles that share a corner with our destination and make sure they can be attacked from + * + * picture this: you are at (0,0). im at (1,1). you want to strangle my head off, so you got to check + * 1. to the west of destination: (1,1) + (-1,0) = (0,1) or north from where you started + * 2. to the south of the destination: (1,1) + (0,-1) = (1,0) or east from where you started + * this function is used only by CombatSwingHandler.kt and assumes that we return the X tiles, west or east, FIRST + * and then the Y tiles South and North. + */ + fun getStepComponents(dir: Direction): List { + val output = ArrayList(2) + if (dir.stepX != 0) output.add(transform(-dir.stepX, 0, 0)) + if (dir.stepY != 0) output.add(transform(0, -dir.stepY, 0)) + return output + } + + /** @return the Direction of the another location relative to this one. */ + fun deriveDirection(location: Location): Direction? { + val dx = (location.x - x).let { if (it >= 0) min(it, 1) else -1 } + val dy = (location.y - y).let { if (it >= 0) min(it, 1) else -1 } + val sb = StringBuilder() + if (dy != 0) sb.append(if (dy > 0) "NORTH" else "SOUTH") + if (dx != 0) { if (sb.isNotEmpty()) sb.append("_"); sb.append(if (dx > 0) "EAST" else "WEST") } + return if (sb.isEmpty()) null else Direction.valueOf(sb.toString()) + } + + /** + * Locations as an Integer rather than a Location object. This is for space efficiency. + * Inner class, because this is only used here, and to avoid adding another standard until this replaces Location + * + * 32 bits for an int (z z x x x x x x x x x x x x x x x y y y y y y y y y y y y y y y) + * - Rightmost set of 15 bits is for y (can handle negative numbers) + * - Middle set of 15 bits is for x (can handle negative numbers) + * - Leftmost set of 2 bits is for z + * + * Since x and y occupy 15 bits (0 to 32,767) it becomes => 1 sign + 14 bits (e.g., 16,384) for range of ±16,383. + */ + @JvmInline + value class LocationHash(val hash: Int) { + constructor(x: Int, y: Int, z: Int = 0) : this( + ((z and 0x3) shl 30) or + ((x and 0x7FFF) shl 15) or + (y and 0x7FFF) + ) + val x: Int get() { + val raw = (hash ushr 15) and 0x7FFF + // If the 15th bit is set, it's a negative number in 15-bit range + return if (raw >= 0x4000) raw - 0x8000 else raw + } + + val y: Int get() { + val raw = hash and 0x7FFF + // If the 15th bit is set, it's a negative number + return if (raw >= 0x4000) raw - 0x8000 else raw + } + val z: Int get() = hash ushr 30 + } +} diff --git a/Server/src/main/core/game/world/map/RegionManager.kt b/Server/src/main/core/game/world/map/RegionManager.kt index 46022fd01..9fb7b848b 100644 --- a/Server/src/main/core/game/world/map/RegionManager.kt +++ b/Server/src/main/core/game/world/map/RegionManager.kt @@ -783,10 +783,10 @@ object RegionManager { for (regionX in ((l.regionX - 6) shr 3)..((l.regionX + 6) shr 3)) { for (regionY in ((l.regionY - 6) shr 3)..((l.regionY + 6) shr 3)) { for (player in forId((regionX shl 8) or regionY).planes[l.z].players) { - if (player.location.x >= l.getX() - xdist && - player.location.x <= l.getX() + xdist && - player.location.y >= l.getY() - ydist && - player.location.y <= l.getY() + ydist) { + if (player.location.x >= l.x - xdist && + player.location.x <= l.x + xdist && + player.location.y >= l.y - ydist && + player.location.y <= l.y + ydist) { players.add(player) } } diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index f38eecb68..84f6c640c 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -235,7 +235,7 @@ class PathfinderTests { TestUtils.advanceTicks(5, false) Assertions.assertEquals(true, npc.getAttribute("return-to-spawn", false)) TestUtils.advanceTicks(50, false) - Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION) <= 9) + Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 9) } } @@ -254,7 +254,7 @@ class PathfinderTests { RegionManager.forId(npc.location.regionId).flagInactive(true) TestUtils.advanceTicks(50, false) Assertions.assertEquals(false, npc.getAttribute("return-to-spawn", false)) - Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION) <= 5) + Assertions.assertEquals(true, npc.location.getDistance(ServerConstants.HOME_LOCATION!!) <= 5) } } } \ No newline at end of file From 9a4566cb0e75650872876f49398543987fc6a183 Mon Sep 17 00:00:00 2001 From: Tooze Date: Mon, 6 Jul 2026 13:10:26 +0000 Subject: [PATCH 31/42] Corrected kidnap location for Quiz Master --- .../main/content/global/ame/events/quizmaster/QuizMasterNPC.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/global/ame/events/quizmaster/QuizMasterNPC.kt b/Server/src/main/content/global/ame/events/quizmaster/QuizMasterNPC.kt index 444fc1f1b..cccc4d0eb 100644 --- a/Server/src/main/content/global/ame/events/quizmaster/QuizMasterNPC.kt +++ b/Server/src/main/content/global/ame/events/quizmaster/QuizMasterNPC.kt @@ -23,7 +23,7 @@ class QuizMasterNPC(var type: String = "", override var loot: WeightBasedTable? super.init() sendChat("Hey ${player.username}! It's your lucky day!") face(player) - kidnapPlayer(this, player, Location(1952, 4764, 0)) { player, _ -> + kidnapPlayer(this, player, Location(1952, 4764, 1)) { player, _ -> setAttribute(player, QuizMasterDialogueFile.QUIZMASTER_ATTRIBUTE_QUESTIONS_CORRECT, 0) sendMessage(player, "Answer four questions correctly in a row to be teleported back where you came from.") sendMessage(player, "You will need to relog in if you lose the quiz dialog.") // Inauthentic, but there to notify the player in case. From 6cccaa79de7ef1c31130c358e76f58af3f2767bc Mon Sep 17 00:00:00 2001 From: Tooze Date: Mon, 6 Jul 2026 13:15:21 +0000 Subject: [PATCH 32/42] Improved combat level brackets for hostile random events --- Server/src/main/content/global/ame/RandomEventNPC.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/src/main/content/global/ame/RandomEventNPC.kt b/Server/src/main/content/global/ame/RandomEventNPC.kt index b4da55bb3..e31b5f36d 100644 --- a/Server/src/main/content/global/ame/RandomEventNPC.kt +++ b/Server/src/main/content/global/ame/RandomEventNPC.kt @@ -39,6 +39,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { var finalized = false var timerPaused = false var ticksLeft = secondsToTicks(180) + private val combatBracket = intArrayOf(10, 20, 40, 60, 90) open fun create(player: Player, loot: WeightBasedTable? = null, type: String = ""): RandomEventNPC { val event = this::class.createInstance() @@ -144,7 +145,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { } fun idForCombatLevel(ids: List, player: Player): Int { - val index = min(ids.size, ceil(player.properties.currentCombatLevel / 20.0).toInt()) - 1 + val index = combatBracket.indexOfFirst { player.properties.currentCombatLevel <= it }.takeIf { it != -1 }?: ids.lastIndex return ids[index] } From ba372ef31fd19bd39d9d13324040c6e4682a48c4 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Mon, 6 Jul 2026 16:16:20 +0300 Subject: [PATCH 33/42] Corrected zamorak brew/mix behaviour --- .../content/data/consumables/Consumables.java | 4 +- .../consumables/effects/DamageEffect.java | 14 ++-- .../data/consumables/effects/PrayerEffect.kt | 6 +- .../consumables/effects/ZamorakMixEffect.java | 25 +++++++ .../test/kotlin/content/ZamorakBrewTests.kt | 67 +++++++++++++++++++ 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 Server/src/main/content/data/consumables/effects/ZamorakMixEffect.java create mode 100644 Server/src/test/kotlin/content/ZamorakBrewTests.kt diff --git a/Server/src/main/content/data/consumables/Consumables.java b/Server/src/main/content/data/consumables/Consumables.java index d02f9f118..67791684b 100644 --- a/Server/src/main/content/data/consumables/Consumables.java +++ b/Server/src/main/content/data/consumables/Consumables.java @@ -353,7 +353,7 @@ public enum Consumables { FISHING(new Potion(new int[] {2438, 151, 153, 155}, new SkillEffect(Skills.FISHING, 3, 0))), PRAYER(new Potion(new int[] {2434, 139, 141, 143}, new PrayerEffect(7, 0.25))), SUPER_RESTO(new Potion(new int[] {3024, 3026, 3028, 3030}, new RestoreEffect(8, 0.25, true))), - ZAMMY_BREW(new Potion(new int[] {2450, 189, 191, 193}, new MultiEffect(new DamageEffect(10, true), new SkillEffect(Skills.ATTACK, 0, 0.25), new SkillEffect(Skills.STRENGTH, 0, 0.15), new SkillEffect(Skills.DEFENCE, 0, -0.1)))), + ZAMMY_BREW(new Potion(new int[] {2450, 189, 191, 193}, new MultiEffect(new DamageEffect(10, true, 2), new SkillEffect(Skills.ATTACK, 2, 0.20), new SkillEffect(Skills.STRENGTH, 2, 0.12), new SkillEffect(Skills.DEFENCE, -2, -0.10), new PrayerEffect(0.0, 0.10, true)))), ANTIFIRE(new Potion(new int[] {2452, 2454, 2456, 2458}, new AddTimerEffect("dragonfire:immunity", 600, true))), GUTH_REST(new Potion(new int[] {4417, 4419, 4421, 4423}, new MultiEffect(new RemoveTimerEffect("poison"), new EnergyEffect(5), new HealingEffect(5)))), MAGIC_ESS(new Potion(new int[] {9021, 9022, 9023, 9024}, new SkillEffect(Skills.MAGIC,3,0))), @@ -363,7 +363,7 @@ public enum Consumables { /** Barbarian Mixes */ PRAYERMIX(new BarbarianMix(new int[] {11465, 11467}, new MultiEffect(new PrayerEffect(7, 0.25), new HealingEffect(6)))), - ZAMMY_MIX(new BarbarianMix(new int[] {11521, 11523}, new MultiEffect(new DamageEffect(10, true), new SkillEffect(Skills.ATTACK, 0, 0.15), new SkillEffect(Skills.STRENGTH, 0, 0.25), new SkillEffect(Skills.DEFENCE, 0, -0.1)))), + ZAMMY_MIX(new BarbarianMix(new int[]{11521, 11523}, new MultiEffect(new ZamorakMixEffect(), new SkillEffect(Skills.ATTACK, 2, 0.20), new SkillEffect(Skills.STRENGTH, 2, 0.12), new SkillEffect(Skills.DEFENCE, -2, -0.10)))), ATT_MIX(new BarbarianMix(new int[] {11429, 11431}, new MultiEffect(new SkillEffect(Skills.ATTACK, 3, 0.1), new HealingEffect(3)))), ANTIP_MIX(new BarbarianMix(new int[] {11433, 11435}, new MultiEffect(new AddTimerEffect("poison:immunity", secondsToTicks(90)), new HealingEffect(3)))), RELIC_MIX(new BarbarianMix(new int[] {11437, 11439}, new MultiEffect(new CureDiseaseEffect(), new HealingEffect(3)))), diff --git a/Server/src/main/content/data/consumables/effects/DamageEffect.java b/Server/src/main/content/data/consumables/effects/DamageEffect.java index c4573306b..1b5e4778c 100644 --- a/Server/src/main/content/data/consumables/effects/DamageEffect.java +++ b/Server/src/main/content/data/consumables/effects/DamageEffect.java @@ -7,15 +7,21 @@ import core.game.node.entity.player.Player; public class DamageEffect extends ConsumableEffect { final double amt; final boolean isPercent; + final int baseAmount; - public DamageEffect(double amt,boolean isPercent){ + public DamageEffect(double amt, boolean isPercent) { + this(amt, isPercent, 0); + } + + public DamageEffect(double amt, boolean isPercent, int baseAmount) { this.amt = amt; this.isPercent = isPercent; + this.baseAmount = baseAmount; } @Override public void activate(Player p) { - p.getImpactHandler().manualHit(p,-getHealthEffectValue(p), ImpactHandler.HitsplatType.NORMAL); + p.getImpactHandler().manualHit(p, -getHealthEffectValue(p), ImpactHandler.HitsplatType.NORMAL); } @Override @@ -23,8 +29,8 @@ public class DamageEffect extends ConsumableEffect { double amount = amt; if (isPercent) { amount /= 100; - return (int) -(amount * player.getSkills().getLifepoints()); + return (int) -(amount * player.getSkills().getLifepoints() + baseAmount); } - return (int) -amt; + return (int) -(amt + baseAmount); } } diff --git a/Server/src/main/content/data/consumables/effects/PrayerEffect.kt b/Server/src/main/content/data/consumables/effects/PrayerEffect.kt index 9f636bc27..5edaff7dc 100644 --- a/Server/src/main/content/data/consumables/effects/PrayerEffect.kt +++ b/Server/src/main/content/data/consumables/effects/PrayerEffect.kt @@ -5,16 +5,18 @@ import core.game.consumable.ConsumableEffect import core.game.node.entity.player.Player import core.game.node.entity.skill.Skills import org.rs09.consts.Items +import kotlin.math.ceil import kotlin.math.floor -class PrayerEffect(var base: Double, var bonus: Double) : ConsumableEffect() { +class PrayerEffect @JvmOverloads constructor(var base: Double, var bonus: Double, private val roundUp: Boolean = false) : ConsumableEffect() { override fun activate(player: Player?) { if(player == null) return val level = getStatLevel(player, Skills.PRAYER) var b = bonus if(inInventory(player, Items.HOLY_WRENCH_6714)) b += 0.02 // Leaving this in for futureproofing. Current update is for properly rounding down. - val amount = floor(base + (level * b)) + val rawAmount = base + (level * b) + val amount = if (roundUp) ceil(rawAmount) else floor(rawAmount) modPrayerPoints(player, amount) } } \ No newline at end of file diff --git a/Server/src/main/content/data/consumables/effects/ZamorakMixEffect.java b/Server/src/main/content/data/consumables/effects/ZamorakMixEffect.java new file mode 100644 index 000000000..688da0961 --- /dev/null +++ b/Server/src/main/content/data/consumables/effects/ZamorakMixEffect.java @@ -0,0 +1,25 @@ +package content.data.consumables.effects; + +import core.game.consumable.ConsumableEffect; +import core.game.node.entity.combat.ImpactHandler; +import core.game.node.entity.player.Player; + +/** + * The Zamorak mix shows the regular Zamorak brew hitsplat (10% of pre-drink lifepoints + 2, rounded down) + * but also heals 6 lifepoints. The heal must be applied before the hit so the net change can never kill + * the player, while the hitsplat still reflects the pre-heal lifepoints. + */ +public class ZamorakMixEffect extends ConsumableEffect { + + @Override + public void activate(Player p) { + int damage = (int) (p.getSkills().getLifepoints() * 0.10 + 2); + p.getSkills().heal(6); + p.getImpactHandler().manualHit(p, damage, ImpactHandler.HitsplatType.NORMAL); + } + + @Override + public int getHealthEffectValue(Player player) { + return 6 - (int) (player.getSkills().getLifepoints() * 0.10 + 2); + } +} diff --git a/Server/src/test/kotlin/content/ZamorakBrewTests.kt b/Server/src/test/kotlin/content/ZamorakBrewTests.kt new file mode 100644 index 000000000..42d6c099a --- /dev/null +++ b/Server/src/test/kotlin/content/ZamorakBrewTests.kt @@ -0,0 +1,67 @@ +package content + +import TestUtils +import content.data.consumables.Consumables +import core.game.node.entity.skill.Skills +import core.game.node.item.Item +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.rs09.consts.Items + +class ZamorakBrewTests { + companion object { + init { + TestUtils.preTestSetup() + } + } + + @Test + fun zamorakBrewShouldDamageTenPercentOfCurrentLifepointsPlusTwo() { + TestUtils.getMockPlayer("zammybrewdrinker").use { p -> + p.skills.setStaticLevel(Skills.HITPOINTS, 99) + p.skills.lifepoints = 6 + p.inventory.clear() + p.inventory.add(Item(Items.ZAMORAK_BREW4_2450)) + + Consumables.getConsumableById(Items.ZAMORAK_BREW4_2450) + .consumable + .consume(p.inventory.get(0), p) + + Assertions.assertEquals(4, p.skills.lifepoints) + } + } + + @Test + fun zamorakMixDamageShouldBeBasedOnLifepointsBeforeItHeals() { + TestUtils.getMockPlayer("zammymixdrinker").use { p -> + p.skills.setStaticLevel(Skills.HITPOINTS, 99) + p.skills.lifepoints = 6 + p.inventory.clear() + p.inventory.add(Item(Items.ZAMORAK_MIX2_11521)) + + Consumables.getConsumableById(Items.ZAMORAK_MIX2_11521) + .consumable + .consume(p.inventory.get(0), p) + + // Damage on pre-drink lifepoints (10% of 6 + 2 = 2), then heal 6: 6 - 2 + 6 = 10 + Assertions.assertEquals(10, p.skills.lifepoints) + } + } + + @Test + fun zamorakMixShouldNotKillAtLowLifepoints() { + TestUtils.getMockPlayer("zammymixlowhp").use { p -> + p.skills.setStaticLevel(Skills.HITPOINTS, 99) + p.skills.lifepoints = 2 + p.inventory.clear() + p.inventory.add(Item(Items.ZAMORAK_MIX2_11521)) + + Consumables.getConsumableById(Items.ZAMORAK_MIX2_11521) + .consumable + .consume(p.inventory.get(0), p) + + // Hitsplat shows 2 (10% of 2 + 2), but the heal lands first: 2 + 6 - 2 = 6, no death + Assertions.assertEquals(6, p.skills.lifepoints) + } + } +} From be744e73b3b1209026b9df60467f3bc3e692988c Mon Sep 17 00:00:00 2001 From: Pwn <24796277-iampwn@users.noreply.gitlab.com> Date: Mon, 6 Jul 2026 09:16:49 -0400 Subject: [PATCH 34/42] Implemented mounted cockatrice head dialogue --- .../skillhall/MountedCockatriceHead.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 Server/src/main/content/global/skill/construction/decoration/skillhall/MountedCockatriceHead.kt diff --git a/Server/src/main/content/global/skill/construction/decoration/skillhall/MountedCockatriceHead.kt b/Server/src/main/content/global/skill/construction/decoration/skillhall/MountedCockatriceHead.kt new file mode 100644 index 000000000..54472597e --- /dev/null +++ b/Server/src/main/content/global/skill/construction/decoration/skillhall/MountedCockatriceHead.kt @@ -0,0 +1,68 @@ +package content.global.skill.construction.decoration.skillhall + +import content.global.skill.construction.HouseManager +import core.api.openDialogue +import core.game.dialogue.DialogueFile +import core.game.dialogue.FacialExpression +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.world.repository.Repository +import core.tools.END_DIALOGUE +import org.rs09.consts.NPCs +import org.rs09.consts.Scenery as Sceneries + +class MountedCockatriceHead : InteractionListener { + override fun defineListeners() { + on(Sceneries.COCKATRICE_HEAD_13482, IntType.SCENERY, "talk-to") { player, _ -> + val house = player.getAttribute("poh_entry", player.houseManager) + if (house == player.houseManager) { + openDialogue(player, MountedCockatriceHeadOwnerDialogue()) + } else { + val ownerName = Repository.players.first { it.houseManager == house }.username.capitalize() + openDialogue(player, MountedCockatriceHeadGuestDialogue(ownerName)) + } + return@on true + } + } +} + +class MountedCockatriceHeadOwnerDialogue : DialogueFile() { + override fun handle(componentID: Int, buttonID: Int) { + when (stage) { + 0 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_NORMAL, "You deaded me!").also { stage++ } + 1 -> playerl(FacialExpression.NEUTRAL, "Well, yes.").also { stage++ } + 2 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_NORMAL, "What did you do that for?").also { stage++ } + 3 -> options("A Slayer Master told me to.", "So I could mount your head on my wall.", "I just wanted to.").also { stage++ } + 4 -> when (buttonID) { + 1 -> playerl(FacialExpression.NEUTRAL, "A Slayer Master told me to.").also { stage = 10 } + 2 -> playerl(FacialExpression.NEUTRAL, "So I could mount your head on my wall.").also { stage = 20 } + 3 -> playerl(FacialExpression.NEUTRAL, "I just wanted to.").also { stage = 30 } + } + 10 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_SAD, "Why do the Slayer Masters all pick on poor cockatrice?").also { stage++ } + 11 -> playerl(FacialExpression.NEUTRAL, "They pick on lots of other creatures too.").also { stage++ } + 12 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_SAD, "Then mount one of them on your wall and let poor cockatrice rest in peace!").also { stage = END_DIALOGUE } + 20 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_SAD, "Another cockatrice falls victim to the dreaded mirror shield!").also { stage++ } + 21 -> playerl(FacialExpression.NEUTRAL, "Don't take it personally. You look good on my wall.").also { stage++ } + 22 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_SAD, "I don't care! I think I looked better with a body!").also { stage = END_DIALOGUE } + 30 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "You dirty rotten swine, you!").also { stage++ } + 31 -> playerl(FacialExpression.AMAZED, "Steady on.").also { stage++ } + 32 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "I will kill you with my paralyzing-type magic eyes look!").also { stage++ } + 33 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "Dots appear in air between eyes and victim. Dot! Dot! Dotty!").also { stage++ } + 34 -> playerl(FacialExpression.NEUTRAL, "Er, nothing's happening...").also { stage++ } + 35 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "Concentrates mental power. Eyes narrow beak clenches veins on head stand out.").also { stage++ } + 36 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "Strain!").also { stage++ } + 37 -> playerl(FacialExpression.ASKING, "You're dead, cockatrice. Your eyes are glass beads. It won't work.").also { stage++ } + 38 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_ANGRY, "STRA-A-AIN!").also { stage++ } + 39 -> playerl(FacialExpression.ASKING, "I think I'll leave you to it.").also { stage = END_DIALOGUE } + } + } +} + +class MountedCockatriceHeadGuestDialogue(private val ownerName: String) : DialogueFile() { + override fun handle(componentID: Int, buttonID: Int) { + when (stage) { + 0 -> playerl(FacialExpression.HAPPY, "Hey, a cockatrice!").also { stage++ } + 1 -> npcl(NPCs.COCKATRICE_4227, FacialExpression.CHILD_SAD, "$ownerName deaded me! That wasn't very nice!").also { stage = END_DIALOGUE } + } + } +} From efc324883bb150e450d811a9d9a466c931c9dd89 Mon Sep 17 00:00:00 2001 From: Tooze Date: Sat, 18 Jul 2026 13:06:23 +0000 Subject: [PATCH 35/42] Implemented berserker necklace effect --- .../node/entity/combat/MeleeSwingHandler.kt | 11 +++++++++-- .../node/entity/combat/equipment/ArmourSet.java | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt index 91864ec53..7f9b668d8 100644 --- a/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt @@ -253,10 +253,14 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) return 1.0 + (e!!.skills.maximumLifepoints - e.skills.lifepoints) * 0.01 } + var multiplier = 1.0 if(e is Player && e.isWearingVoid(CombatStyle.MELEE) && (skillId == Skills.ATTACK || skillId == Skills.STRENGTH)) { - return 1.1 + multiplier += 0.1 } - return 1.0 + if (e is Player && e.properties.armourSet === ArmourSet.BERSERKER_NECKLACE_SETS && skillId == Skills.STRENGTH) { + multiplier += 0.2 + } + return multiplier } override fun getArmourSet(e: Entity?): ArmourSet? { @@ -269,6 +273,9 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) if (ArmourSet.VERAC.isUsing(e)) { return ArmourSet.VERAC } + if (ArmourSet.BERSERKER_NECKLACE_SETS.isUsing(e)) { + return ArmourSet.BERSERKER_NECKLACE_SETS + } return if (ArmourSet.TORAG.isUsing(e)) { ArmourSet.TORAG } else super.getArmourSet(e) diff --git a/Server/src/main/core/game/node/entity/combat/equipment/ArmourSet.java b/Server/src/main/core/game/node/entity/combat/equipment/ArmourSet.java index f311b95e6..5b7a7e3af 100644 --- a/Server/src/main/core/game/node/entity/combat/equipment/ArmourSet.java +++ b/Server/src/main/core/game/node/entity/combat/equipment/ArmourSet.java @@ -8,6 +8,7 @@ import core.game.node.entity.player.Player; import core.game.node.item.Item; import core.game.world.update.flag.context.Graphics; import core.tools.RandomFunction; +import org.rs09.consts.Items; /** * Represents the types of armour sets. @@ -129,6 +130,15 @@ public enum ArmourSet { } return super.isUsing(e); } + }, + /** + * Berserker Necklace sets + */ + BERSERKER_NECKLACE_SETS(null, new int[][] { { Items.BERSERKER_NECKLACE_11128 }, { Items.TOKTZ_XIL_AK_6523, Items.TOKTZ_XIL_EK_6525, Items.TZHAAR_KET_EM_6527, Items.TZHAAR_KET_OM_6528 } }) { + @Override + public boolean isUsing(Entity e) { + return super.isUsing(e); + } }; /** @@ -148,8 +158,9 @@ public enum ArmourSet { */ private ArmourSet(Graphics endGraphic, int[][] set) { this.endGraphic = endGraphic; - this.sets = new Item[4][5]; + this.sets = new Item[set.length][]; for (int i = 0; i < set.length; i++) { + sets[i] = new Item[set[i].length]; for (int k = 0; k < sets[i].length; k++) { sets[i][k] = new Item(set[i][k]); } @@ -197,7 +208,7 @@ public enum ArmourSet { } } } - return hits == 4; + return hits == sets.length; } -} \ No newline at end of file +} From 37b30d7561f4643f34df1e87cbb9d43d79554a6a Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sat, 18 Jul 2026 16:10:00 +0300 Subject: [PATCH 36/42] Corrected implementation of dragon halberd special attack --- .../game/node/entity/combat/BattleState.java | 22 +++++++++++++++++-- .../node/entity/combat/CombatSwingHandler.kt | 8 ++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Server/src/main/core/game/node/entity/combat/BattleState.java b/Server/src/main/core/game/node/entity/combat/BattleState.java index e75aac19d..4a12b5966 100644 --- a/Server/src/main/core/game/node/entity/combat/BattleState.java +++ b/Server/src/main/core/game/node/entity/combat/BattleState.java @@ -9,6 +9,7 @@ import core.game.node.entity.combat.spell.CombatSpell; /** * Represents an entity's current battle state. + * * @author Emperor */ public final class BattleState { @@ -99,6 +100,7 @@ public final class BattleState { /** * Constructs a new {@code BattleState} {@Code Object} + * * @param victim The victim entity. */ public BattleState(Entity entity, Entity victim) { @@ -134,6 +136,7 @@ public final class BattleState { /** * Gets the estimatedHit. + * * @return The estimatedHit. */ public int getEstimatedHit() { @@ -142,6 +145,7 @@ public final class BattleState { /** * Sets the estimated hit. + * * @param estimatedHit The estimated hit to set. */ public void setEstimatedHit(int estimatedHit) { @@ -164,6 +168,7 @@ public final class BattleState { /** * Gets the recoilDamage. + * * @return The recoilDamage. */ public int getRecoilDamage() { @@ -172,6 +177,7 @@ public final class BattleState { /** * Sets the recoilDamage. + * * @param recoilDamage The recoilDamage to set. */ public void setRecoilDamage(int recoilDamage) { @@ -236,6 +242,7 @@ public final class BattleState { /** * Gets the maximumHit. + * * @return The maximumHit. */ public int getMaximumHit() { @@ -244,6 +251,7 @@ public final class BattleState { /** * Sets the maximumHit. + * * @param maximumHit The maximumHit to set. */ public void setMaximumHit(int maximumHit) { @@ -252,6 +260,7 @@ public final class BattleState { /** * Gets the rangeWeapon. + * * @return The rangeWeapon. */ public RangeWeapon getRangeWeapon() { @@ -260,6 +269,7 @@ public final class BattleState { /** * Sets the rangeWeapon. + * * @param rangeWeapon The rangeWeapon to set. */ public void setRangeWeapon(RangeWeapon rangeWeapon) { @@ -268,6 +278,7 @@ public final class BattleState { /** * Gets the ammunition. + * * @return The ammunition. */ public Ammunition getAmmunition() { @@ -276,6 +287,7 @@ public final class BattleState { /** * Sets the ammunition. + * * @param ammunition The ammunition to set. */ public void setAmmunition(Ammunition ammunition) { @@ -284,6 +296,7 @@ public final class BattleState { /** * Gets the frozen. + * * @return The frozen. */ public boolean isFrozen() { @@ -292,6 +305,7 @@ public final class BattleState { /** * Sets the frozen. + * * @param frozen The frozen to set. */ public void setFrozen(boolean frozen) { @@ -300,6 +314,7 @@ public final class BattleState { /** * Gets the style. + * * @return The style. */ public CombatStyle getStyle() { @@ -308,6 +323,7 @@ public final class BattleState { /** * Sets the style. + * * @param style The style to set. */ public void setStyle(CombatStyle style) { @@ -316,6 +332,7 @@ public final class BattleState { /** * Gets the armourEffect. + * * @return The armourEffect. */ public ArmourSet getArmourEffect() { @@ -324,6 +341,7 @@ public final class BattleState { /** * Sets the armourEffect. + * * @param armourEffect The armourEffect to set. */ public void setArmourEffect(ArmourSet armourEffect) { @@ -332,6 +350,7 @@ public final class BattleState { /** * Gets the attacking entity. + * * @return The entity. */ public Entity getAttacker() { @@ -340,10 +359,9 @@ public final class BattleState { public int getTotalDamage() { int hit = Math.max(estimatedHit, 0) + Math.max(secondaryHit, 0); - if (targets != null) { for (BattleState s : targets) { - if (s != null) { + if (s != null && s != this) { hit += Math.max(s.getEstimatedHit(), 0) + Math.max(s.getSecondaryHit(), 0); } } diff --git a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt index 2b151ba47..d4324b233 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt @@ -254,9 +254,15 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { ?: return InteractionType.STILL_INTERACT //if we cannot derive a direction, it's because both tiles are the same, so hand off control to the main logic which already handles this case var next = closestEntityTile - while (next.getDistance(closestVictimTile) > 3) { //skip the initial gap in distance if it exists, because standard pathfinding would already stop us before this point if something was between us and the NPC or vice versa + //Skip the initial gap in distance if it exists, because standard pathfinding would already stop us before this point if something was between us and the NPC or vice versa. + //A fixed-direction walk can only arrive within its starting distance in steps; on oblique approaches it + //passes beside the target and never converges, so the walk is hard-capped at that many steps. + val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1 + for (i in 0 until maxSkipSteps) { + if (next.getDistance(closestVictimTile) <= 3) break next = next.transform(dir) } + if (next.getDistance(closestVictimTile) > 3) return InteractionType.STILL_INTERACT //never converged (oblique approach), so defer to the range checks instead var result: InteractionType = InteractionType.STILL_INTERACT val maxIterations = next.getDistance(closestVictimTile).toInt() From 592138d90448cbdad3919d950c5e7a97f9b1528f Mon Sep 17 00:00:00 2001 From: oftheshire Date: Sat, 18 Jul 2026 13:14:53 +0000 Subject: [PATCH 37/42] Rewrote ring of stone and easter ring, which also fixes a teleblock bug --- Server/data/configs/interface_configs.json | 6 + .../global/handlers/item/MorphItemListener.kt | 77 ++++++++++++ .../global/handlers/item/MorphItemPlugin.java | 113 ------------------ 3 files changed, 83 insertions(+), 113 deletions(-) create mode 100644 Server/src/main/content/global/handlers/item/MorphItemListener.kt delete mode 100644 Server/src/main/content/global/handlers/item/MorphItemPlugin.java diff --git a/Server/data/configs/interface_configs.json b/Server/data/configs/interface_configs.json index 95087003a..4377daff0 100644 --- a/Server/data/configs/interface_configs.json +++ b/Server/data/configs/interface_configs.json @@ -539,6 +539,12 @@ "walkable": "true", "tabIndex": "-1" }, + { + "id": "375", + "interfaceType": "3", + "walkable": "false", + "tabIndex": "-1" + }, { "id": "377", "interfaceType": "8", diff --git a/Server/src/main/content/global/handlers/item/MorphItemListener.kt b/Server/src/main/content/global/handlers/item/MorphItemListener.kt new file mode 100644 index 000000000..b0b2e3857 --- /dev/null +++ b/Server/src/main/content/global/handlers/item/MorphItemListener.kt @@ -0,0 +1,77 @@ +package content.global.handlers.item + +import core.api.* +import core.game.interaction.InteractionListener +import core.game.interaction.InterfaceListener +import core.game.node.entity.player.Player +import org.rs09.consts.Items +import org.rs09.consts.NPCs +import org.rs09.consts.Sounds + +/** + * Handles morphing with the Ring of Stone and Easter Ring. + */ + +class MorphItemListener : InteractionListener, InterfaceListener { + + // the morphing items + private val morphItems = intArrayOf(Items.RING_OF_STONE_6583, Items.EASTER_RING_7927) + + // the NPCs to morph into + private val easterEggs = intArrayOf( + NPCs.EGG_3689, + NPCs.EGG_3690, + NPCs.EGG_3691, + NPCs.EGG_3692, + NPCs.EGG_3693, + NPCs.EGG_3694 + ) + private val stone = intArrayOf(NPCs.ROCKS_2626) + + // the "unmorph" interface + private val morphIface = 375 + + // equipping the morph items + override fun defineListeners() { + onEquip(morphItems) { player, item -> + morph(player, item.id) + return@onEquip false + } + } + + // the interface that appears when you morph + override fun defineInterfaceListeners() { + + // hitting the 'unmorph' button + on(morphIface){ player, _, _, buttonID, _, _ -> + when(buttonID) { + 3 -> closeAllInterfaces(player) + } + return@on true + } + + // after the interface closes + onClose(morphIface) { player, _ -> + unmorph(player) + return@onClose true + } + } + + // morphs the player + private fun morph(player: Player, item: Int) { + val morphId = when (item) { + Items.RING_OF_STONE_6583 -> stone.random() + else -> easterEggs.random() + } + playAudio(player, Sounds.EASTER06_HUMAN_INTO_EGG_1520) + stopWalk(player) + closeAllInterfaces(player) + player.appearance.transformNPC(morphId) + openInterface(player, morphIface) + } + + // unmorphs the player + private fun unmorph(player: Player) { + player.appearance.transformNPC(-1) + } +} \ No newline at end of file diff --git a/Server/src/main/content/global/handlers/item/MorphItemPlugin.java b/Server/src/main/content/global/handlers/item/MorphItemPlugin.java deleted file mode 100644 index 672de389f..000000000 --- a/Server/src/main/content/global/handlers/item/MorphItemPlugin.java +++ /dev/null @@ -1,113 +0,0 @@ -package content.global.handlers.item; - -import core.cache.def.impl.ItemDefinition; -import core.game.component.CloseEvent; -import core.game.component.Component; -import core.game.component.ComponentDefinition; -import core.game.component.ComponentPlugin; -import core.game.node.entity.player.Player; -import core.game.node.item.Item; -import core.game.world.GameWorld; -import core.plugin.Initializable; -import core.plugin.Plugin; -import core.plugin.ClassScanner; -import core.tools.RandomFunction; -import org.rs09.consts.Sounds; - -import static core.api.ContentAPIKt.playAudio; - -/** - * Handles a morph item. - * @author Vexia - */ -@Initializable -public class MorphItemPlugin implements Plugin { - - /** - * The easter egg ids. - */ - protected static final int[] EASTER_EGG_IDS = new int[] { 3689, 3690, 3691, 3692, 3693, 3694 }; - - /** - * The morph component. - */ - private static final Component COMPONENT = new Component(375).setCloseEvent(new CloseEvent() { - - @Override - public boolean close(Player player, Component c) { - unmorph(player); - return true; - } - - }); - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ItemDefinition.forId(7927).getHandlers().put("equipment", this); - ItemDefinition.forId(6583).getHandlers().put("equipment", this); - ClassScanner.definePlugin(new MorphInterfacePlugin()); - return this; - } - - @Override - public Object fireEvent(String identifier, Object... args) { - final Player player = (Player) args[0]; - final Item item = (Item) args[1]; - switch (identifier) { - case "equip": - morph(player, item); - return false; - } - return true; - } - - /** - * Morphs the player. - * @param player the player. - * @param item the item. - */ - private void morph(Player player, Item item) { - int morphId = item.getId() == 6583 ? 2626 : EASTER_EGG_IDS[RandomFunction.random(EASTER_EGG_IDS.length)]; - playAudio(player, 1520); - player.getInterfaceManager().close(); - player.getAppearance().transformNPC(morphId); - player.getInterfaceManager().removeTabs(0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); - player.getLocks().lockMovement(GameWorld.getTicks() + 900000000); - player.getLocks().lockInteractions(GameWorld.getTicks() + 90000000); - player.getLocks().lockTeleport(GameWorld.getTicks() + 900000000); - player.getInterfaceManager().openSingleTab(COMPONENT); - player.getAppearance().sync(); - player.getWalkingQueue().reset(); - } - - /** - * Unmorphs the player. - * @param player the player. - */ - private static void unmorph(Player player) { - player.getAppearance().transformNPC(-1); - player.unlock(); - player.getInterfaceManager().restoreTabs(); - } - - /** - * Handles the morph interface plugin. - * @author Vexia - */ - public class MorphInterfacePlugin extends ComponentPlugin { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ComponentDefinition.forId(375).setPlugin(this); - return this; - } - - @Override - public boolean handle(Player player, Component component, int opcode, int button, int slot, int itemId) { - player.getInterfaceManager().closeSingleTab(); - return true; - } - - } - -} From 5a37f2f8daab54676508b92fe60d4e4a0c6b3303 Mon Sep 17 00:00:00 2001 From: Tooze Date: Sat, 18 Jul 2026 13:15:40 +0000 Subject: [PATCH 38/42] Fixed mithril grappling tip smithing UI binding --- Server/src/main/content/global/skill/smithing/SmithingType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/global/skill/smithing/SmithingType.java b/Server/src/main/content/global/skill/smithing/SmithingType.java index fd38a6209..94465077e 100644 --- a/Server/src/main/content/global/skill/smithing/SmithingType.java +++ b/Server/src/main/content/global/skill/smithing/SmithingType.java @@ -106,7 +106,7 @@ public enum SmithingType { /** * Grapple Tips */ - TYPE_GRAPPLE_TIP(1, 170, 171, new int[] { 175, 176, 175, 174, 173 }, 1), + TYPE_GRAPPLE_TIP(1, 170, 171, new int[] { 176, 175, 174, 173 }, 1), /** * The studs type. From d5527c268fe9c30a9b1e6a81b2196ed63a78d6d2 Mon Sep 17 00:00:00 2001 From: Bishop Date: Sun, 26 Jul 2026 11:59:12 +0000 Subject: [PATCH 39/42] Removed inauthentic fight caves heal --- .../karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java b/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java index 89a8d5e38..5a4cdfd11 100644 --- a/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java +++ b/Server/src/main/content/region/karamja/tzhaar/handlers/TzhaarFightCavesPlugin.java @@ -145,11 +145,6 @@ public final class TzhaarFightCavesPlugin extends ActivityPlugin { public void leave(Player player, int wave) { activeNPCs.clear(); player.getProperties().setTeleportLocation(getSpawnLocation()); - player.getSkills().restore(); - player.timers.removeTimer("poison"); - player.timers.removeTimer("disease"); - player.timers.removeTimer("teleblock"); - player.getSettings().updateRunEnergy(-100); boolean practice = player.getAttribute("fc_practice_jad", false); if (wave == 63) { if (!practice) { From e611b1780bdf0ec3949d8ffca28b9613f887e580 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sun, 26 Jul 2026 15:18:14 +0300 Subject: [PATCH 40/42] Fixed dream spell animation loop --- .../skill/magic/lunar/LunarListeners.kt | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt b/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt index 8d70f99cb..fd8dc5ccd 100644 --- a/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt +++ b/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt @@ -37,6 +37,10 @@ import org.rs09.consts.Items import org.rs09.consts.Sounds import kotlin.math.floor +private const val DREAM_HEAL_INTERVAL_TICKS = 50 +private const val DREAM_HARD_RESET_INTERVAL_TICKS = 90 +private const val DREAM_HARD_RESET_DELAY_TICKS = 0 + class LunarListeners : SpellListener("lunar"), Commands { override fun defineListeners() { @@ -592,32 +596,46 @@ class LunarListeners : SpellListener("lunar"), Commands { // Level 79 private fun dream(player: Player) { - if(player.skills.lifepoints >= getStatLevel(player, Skills.HITPOINTS)) { + if (player.skills.lifepoints >= getStatLevel(player, Skills.HITPOINTS)) { sendMessage(player, "You have no need to cast this spell since your hitpoints are already full.") throw IllegalStateException() } - // https://runescape.wiki/w/Dream?oldid=880976 claims Dream makes you heal 1 hp every 20 seconds // https://oldschool.runescape.wiki/w/Dream claims that Dream has its own timer, so the Dream heals don't need // to align with the natural heals val timer = getOrStartTimer(player) + val dreamStartAnimationTicks = animationDuration(Animation(Animations.LUNAR_SPELLBOOK_DREAM_START_6295)) + var nextHealTick = getWorldTicks() + dreamStartAnimationTicks + DREAM_HEAL_INTERVAL_TICKS + var nextHardResetTick = -1 + var waitingForHardReset = false animate(player, Animations.LUNAR_SPELLBOOK_DREAM_START_6295) removeRunes(player, true) addXP(player, 82.0) - delayEntity(player, 4) - queueScript(player, 4, QueueStrength.WEAK) { stage: Int -> + delayEntity(player, dreamStartAnimationTicks) + queueScript(player, dreamStartAnimationTicks, QueueStrength.WEAK) { stage: Int -> if (stage == 0) { sendGraphics(Graphics.LUNAR_SPELLBOOK_DREAM_1056, player.location) playAudio(player, Sounds.LUNAR_SLEEP_3619) return@queueScript delayScript(player, 5) } + if (waitingForHardReset) { + waitingForHardReset = false + nextHardResetTick = getWorldTicks() + DREAM_HARD_RESET_INTERVAL_TICKS + } else if (nextHardResetTick != -1 && getWorldTicks() >= nextHardResetTick) { + // The 530 client eventually drops Dream back to idle if it only sees repeated 6296 updates + // Sending an explicit reset makes the next 6296 a full restart instead of a same-sequence refresh + resetAnimator(player) + waitingForHardReset = true + return@queueScript delayScript(player, DREAM_HARD_RESET_DELAY_TICKS) + } else if (nextHardResetTick == -1) { + nextHardResetTick = getWorldTicks() + DREAM_HARD_RESET_INTERVAL_TICKS + } animate(player, Animations.LUNAR_SPELLBOOK_DREAM_MID_6296) sendGraphics(Graphics.LUNAR_SPELLBOOK_DREAM_1056, player.location) - // This heals 2 HP every min. Naturally you heal 1 for a total of 3 - // The script steps every 5 ticks and we want 50 ticks before a heal - if (stage.mod(10) == 0) { + if (getWorldTicks() >= nextHealTick) { val amt = timer.getHealAmount(player) //accounts for regen brace heal(player, amt) + nextHealTick += DREAM_HEAL_INTERVAL_TICKS if (player.skills.lifepoints >= getStatLevel(player, Skills.HITPOINTS)) { animate(player, Animations.LUNAR_SPELLBOOK_DREAM_END_6297) return@queueScript stopExecuting(player) @@ -874,4 +892,3 @@ class LunarListeners : SpellListener("lunar"), Commands { } - From a643299b0c2a87e70a1c9bcd64289c1cb5d71a90 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sun, 26 Jul 2026 15:29:51 +0300 Subject: [PATCH 41/42] Added admin command ::infinitespecialmove [true|false] for infinite summoning familiar special move energy Providing no argument to admin commands ::infinitespecial and ::allowaggro now toggles the value instead of rejecting --- .../skill/summoning/familiar/Familiar.java | 6 +- .../system/command/sets/MiscCommandSet.kt | 82 ++++++++++++++----- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Server/src/main/content/global/skill/summoning/familiar/Familiar.java b/Server/src/main/content/global/skill/summoning/familiar/Familiar.java index b561a5b30..089db9352 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/Familiar.java +++ b/Server/src/main/content/global/skill/summoning/familiar/Familiar.java @@ -82,6 +82,8 @@ public abstract class Familiar extends NPC implements Plugin { * The amount of special points left. */ protected int specialPoints = 60; + + public static final String INFINITE_SPECIAL_MOVE_ATTRIBUTE = "infinite-special-move"; /** * The pouch id. @@ -494,7 +496,9 @@ public abstract class Familiar extends NPC implements Plugin { owner.getInventory().remove(new Item(scroll.getItemId())); playAudio(owner, Sounds.SPELL_4161); visualizeSpecialMove(); - updateSpecialPoints(specialCost); + if (!owner.getAttribute(INFINITE_SPECIAL_MOVE_ATTRIBUTE, false)) { + updateSpecialPoints(specialCost); + } owner.getSkills().addExperience(Skills.SUMMONING, scroll.getExperience(), true); } return true; diff --git a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt index 281491ae2..0727f65db 100644 --- a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt @@ -1,6 +1,7 @@ package core.game.system.command.sets import content.global.handlers.iface.* +import content.global.skill.summoning.familiar.Familiar import content.global.skill.cooking.fermenting.BrewGrowth import content.global.skill.farming.timers.* import content.minigame.fishingtrawler.TrawlerLoot @@ -40,6 +41,27 @@ import java.awt.HeadlessException import java.awt.Toolkit import java.awt.datatransfer.StringSelection +private const val INFINITE_SPECIAL_ATTRIBUTE = "infinite-special" +private const val ALLOW_ADMIN_AGGRESSION_ATTRIBUTE = "/save:allow_admin_aggression" + +private fun resolveBooleanToggle(player: Player, args: Array, attribute: String): Boolean? { + return when { + args.size < 2 -> !player.getAttribute(attribute, false) + args[1].equals("true", ignoreCase = true) -> true + args[1].equals("false", ignoreCase = true) -> false + else -> null + } +} + +private fun setBooleanAttribute(player: Player, attribute: String, enabled: Boolean, label: String) { + if (enabled) { + setAttribute(player, attribute, true) + } else { + removeAttribute(player, attribute) + } + sendMessage(player, "$label is now ${if (enabled) "enabled" else "disabled"}.") +} + @Initializable class MiscCommandSet : CommandSet(Privilege.ADMIN){ override fun defineCommands() { @@ -662,29 +684,49 @@ class MiscCommandSet : CommandSet(Privilege.ADMIN){ notify(player,"No parent NPC found.") } } - define("infinitespecial", Privilege.ADMIN, "::infinitespecial ", "Turns the infinite special-attack flag on or off for your player."){ player, args -> - val usageStr = "Usage: ::infinitespecial true|false" - if(args.size < 2){ - reject(player, usageStr) - } - when(args[1]){ - "true" -> player.setAttribute("infinite-special", true) - "false" -> player.removeAttribute("infinite-special") - else -> reject(player, usageStr) - } - } - define("allowaggro", Privilege.ADMIN, "allowaggro true | false", "Toggle NPCs aggroing on you") { player, args -> - val usageStr = "Usage: ::allowaggro true | false" - if(args.size < 2) { - notify(player, "Allow admin aggression is currently ${player.getAttribute("/save:allow_admin_aggression", false)}") + define( + "infinitespecial", + Privilege.ADMIN, + "::infinitespecial [true|false]", + "Toggles infinite special attack energy for your player (special-attack flag)." + ) { player, args -> + val enabled = resolveBooleanToggle(player, args, INFINITE_SPECIAL_ATTRIBUTE) + if (enabled == null) { + reject(player, "Usage: ::infinitespecial [true|false]") return@define } - when(args[1]) { - "true" -> player.setAttribute("/save:allow_admin_aggression", true) - "false" -> player.setAttribute("/save:allow_admin_aggression", false) - else -> reject(player, usageStr) + setBooleanAttribute(player, INFINITE_SPECIAL_ATTRIBUTE, enabled, "Infinite special attack") + } + define( + "infinitespecialmove", + Privilege.ADMIN, + "::infinitespecialmove [true|false]", + "Toggles infinite Summoning familiar special move points for your player." + ) { player, args -> + val enabled = resolveBooleanToggle(player, args, Familiar.INFINITE_SPECIAL_MOVE_ATTRIBUTE) + if (enabled == null) { + reject(player, "Usage: ::infinitespecialmove [true|false]") + return@define } - notify(player, "Setting aggro ${args[1]}") + setBooleanAttribute( + player, + Familiar.INFINITE_SPECIAL_MOVE_ATTRIBUTE, + enabled, + "Infinite familiar special move energy" + ) + } + define( + "allowaggro", + Privilege.ADMIN, + "::allowaggro [true|false]", + "Toggles NPCs aggroing on you as an admin." + ) { player, args -> + val enabled = resolveBooleanToggle(player, args, ALLOW_ADMIN_AGGRESSION_ATTRIBUTE) + if (enabled == null) { + reject(player, "Usage: ::allowaggro [true|false]") + return@define + } + setBooleanAttribute(player, ALLOW_ADMIN_AGGRESSION_ATTRIBUTE, enabled, "Admin NPC aggression") } define("rules", Privilege.STANDARD, "", "Shows the rules."){ player, _ -> From a6699243dbd6222a84b97c4da651d4d2fb20772e Mon Sep 17 00:00:00 2001 From: Tooze Date: Sun, 26 Jul 2026 12:37:41 +0000 Subject: [PATCH 42/42] Agility arena fixes Fixed looping when a player attempted dart trap, spinning blades, floor spikes and pressure pads Fixed direction player faces when traversing hand holds Fixed animation for walking across planks --- .../content/global/skill/agility/AgilityHandler.java | 10 ++++++++++ .../global/skill/agility/brimhaven/BladeTrap.java | 7 +++++++ .../skill/agility/brimhaven/BrimhavenCourse.java | 11 +++++++---- .../global/skill/agility/brimhaven/DartTrap.java | 5 +++++ .../global/skill/agility/brimhaven/FloorSpikes.java | 6 ++++++ .../global/skill/agility/brimhaven/PressurePad.java | 7 +++++++ .../skill/agility/brimhaven/SpinningBlades.java | 8 +++++++- 7 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Server/src/main/content/global/skill/agility/AgilityHandler.java b/Server/src/main/content/global/skill/agility/AgilityHandler.java index 722de4afe..c12b7d4fc 100644 --- a/Server/src/main/content/global/skill/agility/AgilityHandler.java +++ b/Server/src/main/content/global/skill/agility/AgilityHandler.java @@ -139,6 +139,13 @@ public final class AgilityHandler { * @return The force movement instance, if force movement is used. */ public static ForceMovement forceWalk(final Player player, final int courseIndex, Location start, Location end, Animation animation, int speed, final double experience, final String message) { + return forceWalk(player, courseIndex, start, end, animation, speed, experience, message, null); + } + + /** + * Walks across an obstacle while using an explicit force-movement facing direction. + */ + public static ForceMovement forceWalk(final Player player, final int courseIndex, Location start, Location end, Animation animation, int speed, final double experience, final String message, Direction direction) { player.logoutListeners.put("forcewalk", p -> { p.setLocation(player.getLocation().transform(0,0,0)); return Unit.INSTANCE; @@ -158,6 +165,9 @@ public final class AgilityHandler { player.logoutListeners.remove("forcewalk"); } }; + if (direction != null) { + movement.setDirection(direction); + } movement.start(); GameWorld.getPulser().submit(movement); return movement; diff --git a/Server/src/main/content/global/skill/agility/brimhaven/BladeTrap.java b/Server/src/main/content/global/skill/agility/brimhaven/BladeTrap.java index 773cd9907..5303bde7d 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/BladeTrap.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/BladeTrap.java @@ -5,6 +5,9 @@ import core.game.node.entity.Entity; import core.game.node.entity.player.Player; import core.game.system.task.MovementHook; import core.game.system.task.Pulse; +import core.net.packet.PacketRepository; +import core.net.packet.context.PlayerContext; +import core.net.packet.out.ClearMinimapFlag; import kotlin.Unit; import core.game.world.GameWorld; import core.game.world.map.Direction; @@ -24,6 +27,10 @@ public final class BladeTrap implements MovementHook { final Direction dir = e.getDirection(); final Player player = (Player) e; final Location start = l.transform(-dir.getStepX(), -dir.getStepY(), 0); + player.getPulseManager().clear(); + player.getWalkingQueue().reset(); + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext(player)); + e.lock(5); if(e.isPlayer()) { diff --git a/Server/src/main/content/global/skill/agility/brimhaven/BrimhavenCourse.java b/Server/src/main/content/global/skill/agility/brimhaven/BrimhavenCourse.java index 4fb8bdb52..adb8f14fd 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/BrimhavenCourse.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/BrimhavenCourse.java @@ -119,6 +119,8 @@ public final class BrimhavenCourse extends OptionHandler { final Direction dir = direction; final Direction faceDirection = Direction.get(object.getDirection().toInteger() - 1 & 3); final Location start = player.getLocation(); + player.setDirection(faceDirection); + player.faceLocation(start.transform(faceDirection)); player.getAppearance().setAnimations(Animation.create(1118 + m)); player.getAppearance().sync(); AgilityHandler.climb(player, -1, new Animation(1117 + m), start.transform(dir), 0.0, null); @@ -143,12 +145,12 @@ public final class BrimhavenCourse extends OptionHandler { } else if (count == 6) { player.getAppearance().setAnimations(); player.getAppearance().sync(); - AgilityHandler.forceWalk(player, -1, last, last.transform(dir), Animation.create(1120 + m), 5, getExp(player, 22.0), null).setDirection(faceDirection); + AgilityHandler.forceWalk(player, -1, last, last.transform(dir), Animation.create(1120 + m), 5, getExp(player, 22.0), null, faceDirection); player.logoutListeners.remove("brimcourse"); return true; } player.logoutListeners.remove("brimcourse"); - AgilityHandler.forceWalk(player, -1, last, last = last.transform(dir), Animation.create(1118 + m), 5, 0.0, null).setDirection(faceDirection); + AgilityHandler.forceWalk(player, -1, last, last = last.transform(dir), Animation.create(1118 + m), 5, 0.0, null, faceDirection); return false; } }); @@ -272,7 +274,7 @@ public final class BrimhavenCourse extends OptionHandler { public boolean pulse() { if (!finish) { setDelay(2); - AgilityHandler.fail(player, 1, player.getLocation().transform(0, 0, -3), Animation.create(189), getHitAmount(player), "You stepped on a broken piece of plank!"); + AgilityHandler.fail(player, 1, player.getLocation().transform(0, 0, -3), Animation.create(768), getHitAmount(player), "You stepped on a broken piece of plank!"); finish = true; return false; } @@ -282,7 +284,7 @@ public final class BrimhavenCourse extends OptionHandler { }); return; } - AgilityHandler.walk(player, -1, start, end, ForceMovement.WALK_ANIMATION, getExp(player, 6.0), null); + AgilityHandler.walk(player, -1, start, end, Animation.create(1426), getExp(player, 6.0), null); } /** @@ -389,6 +391,7 @@ public final class BrimhavenCourse extends OptionHandler { } break; case 3551: + case 3583: return n.getLocation(); } return null; diff --git a/Server/src/main/content/global/skill/agility/brimhaven/DartTrap.java b/Server/src/main/content/global/skill/agility/brimhaven/DartTrap.java index 086a59522..3ac7da4a3 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/DartTrap.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/DartTrap.java @@ -7,6 +7,8 @@ import core.game.node.entity.impl.Projectile; import core.game.node.entity.player.Player; import core.game.system.task.MovementHook; import core.game.system.task.Pulse; +import core.net.packet.context.PlayerContext; +import core.net.packet.out.ClearMinimapFlag; import kotlin.Unit; import core.game.world.GameWorld; import core.game.world.map.Direction; @@ -29,6 +31,9 @@ public final class DartTrap implements MovementHook { final Direction dir = e.getDirection(); final Player player = (Player) e; final Location start = l.transform(-dir.getStepX(), -dir.getStepY(), 0); + player.getPulseManager().clear(); + player.getWalkingQueue().reset(); + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext(player)); e.lock(6); if(e.isPlayer()) { diff --git a/Server/src/main/content/global/skill/agility/brimhaven/FloorSpikes.java b/Server/src/main/content/global/skill/agility/brimhaven/FloorSpikes.java index 69111c6bf..2885a7f6a 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/FloorSpikes.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/FloorSpikes.java @@ -6,6 +6,9 @@ import core.game.node.entity.Entity; import core.game.node.entity.player.Player; import core.game.system.task.MovementHook; import core.game.system.task.Pulse; +import core.net.packet.PacketRepository; +import core.net.packet.context.PlayerContext; +import core.net.packet.out.ClearMinimapFlag; import kotlin.Unit; import core.game.world.GameWorld; import core.game.world.map.Direction; @@ -24,6 +27,9 @@ public final class FloorSpikes implements MovementHook { final Direction dir = e.getDirection(); final Player player = (Player) e; final Location start = l.transform(-dir.getStepX(), -dir.getStepY(), 0); + player.getPulseManager().clear(); + player.getWalkingQueue().reset(); + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext(player)); e.lock(5); if(e.isPlayer()) { diff --git a/Server/src/main/content/global/skill/agility/brimhaven/PressurePad.java b/Server/src/main/content/global/skill/agility/brimhaven/PressurePad.java index a2bc07cb2..d27a76bcf 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/PressurePad.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/PressurePad.java @@ -6,6 +6,9 @@ import core.game.node.entity.Entity; import core.game.node.entity.player.Player; import core.game.system.task.MovementHook; import core.game.system.task.Pulse; +import core.net.packet.PacketRepository; +import core.net.packet.context.PlayerContext; +import core.net.packet.out.ClearMinimapFlag; import kotlin.Unit; import core.game.world.GameWorld; import core.game.world.map.Direction; @@ -24,6 +27,10 @@ public final class PressurePad implements MovementHook { final Direction dir = e.getDirection(); final Player player = (Player) e; final Location start = dest.transform(-dir.getStepX(), -dir.getStepY(), 0); + player.getPulseManager().clear(); + player.getWalkingQueue().reset(); + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext(player)); + e.lock(5); if(e.isPlayer()) { diff --git a/Server/src/main/content/global/skill/agility/brimhaven/SpinningBlades.java b/Server/src/main/content/global/skill/agility/brimhaven/SpinningBlades.java index a13815767..3d2a9c1e2 100644 --- a/Server/src/main/content/global/skill/agility/brimhaven/SpinningBlades.java +++ b/Server/src/main/content/global/skill/agility/brimhaven/SpinningBlades.java @@ -6,6 +6,9 @@ import core.game.node.entity.Entity; import core.game.node.entity.player.Player; import core.game.system.task.MovementHook; import core.game.system.task.Pulse; +import core.net.packet.PacketRepository; +import core.net.packet.context.PlayerContext; +import core.net.packet.out.ClearMinimapFlag; import kotlin.Unit; import core.game.world.GameWorld; import core.game.world.map.Direction; @@ -24,6 +27,9 @@ public final class SpinningBlades implements MovementHook { final Direction dir = e.getDirection(); final Player player = (Player) e; final Location start = l.transform(-dir.getStepX(), -dir.getStepY(), 0); + player.getPulseManager().clear(); + player.getWalkingQueue().reset(); + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext(player)); e.lock(5); if(e.isPlayer()) { @@ -70,4 +76,4 @@ public final class SpinningBlades implements MovementHook { } player.getPacketDispatch().sendSceneryAnimation(RegionManager.getObject(l), Animation.create(1107)); } -} \ No newline at end of file +}