From 4f18b5f97bbec115337eafa0135edd8137fdd645 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 1 Aug 2026 11:32:22 +0100 Subject: [PATCH 01/34] Close interfaces on hitsplat closes #1098 --- game/src/main/kotlin/content/entity/combat/hit/Hit.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/game/src/main/kotlin/content/entity/combat/hit/Hit.kt b/game/src/main/kotlin/content/entity/combat/hit/Hit.kt index b58b23c1bc..a592a220d5 100644 --- a/game/src/main/kotlin/content/entity/combat/hit/Hit.kt +++ b/game/src/main/kotlin/content/entity/combat/hit/Hit.kt @@ -11,6 +11,7 @@ import content.skill.melee.weapon.weapon import content.skill.prayer.Prayer import content.skill.summoning.familiarDefenceMultiplier import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.closeInterfaces import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.mode.combat.CombatApi import world.gregs.voidps.engine.entity.character.mode.combat.CombatAttack @@ -164,6 +165,9 @@ fun Character.hit( ): Int { val actualDamage = Damage.modify(this, target, offensiveType, damage, weapon, spell, special) .coerceAtMost(target.levels.get(Skill.Constitution)) + if (target is Player) { + target.closeInterfaces() + } if (this is Player) { CombatApi.attack(this, CombatAttack(target, actualDamage, offensiveType, weapon, spell, special, delay)) } else if (this is NPC) { @@ -194,6 +198,7 @@ fun Character.directHit(source: Character, damage: Int, type: String = "damage", return } if (this is Player) { + closeInterfaces() CombatApi.damage(this, CombatDamage(source, type, damage, weapon, spell, special)) } else if (this is NPC) { CombatApi.damage(this, CombatDamage(source, type, damage, weapon, spell, special)) From 6b0c8abae5754c454d13e10fc9c016f71aad7b4c Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 1 Aug 2026 12:10:40 +0100 Subject: [PATCH 02/34] Use summoning combat level on members worlds, add combat level up messages --- .../entity/player/combat/CombatLevel.kt | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt index 13a31e9991..aa26512eba 100644 --- a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt +++ b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt @@ -1,6 +1,7 @@ package content.entity.player.combat import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.World import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel @@ -13,8 +14,8 @@ class CombatLevel : Script { init { playerSpawn { - combatLevel = calculateCombatLevel(levels) - summoningCombatLevel = calculateCombatLevel(levels, true) + combatLevel = calculateCombatLevel(levels, summoning = World.members) + summoningCombatLevel = calculateCombatLevel(levels, summoning = true) } for (skill in Skill.entries.filter { it.ordinal <= 6 || it.ordinal == 23 }.toTypedArray()) { @@ -23,8 +24,37 @@ class CombatLevel : Script { } fun recalculate(player: Player, skill: Skill, from: Int, to: Int) { - player.combatLevel = calculateCombatLevel(player.levels) - player.summoningCombatLevel = calculateCombatLevel(player.levels, true) + val previous = player.summoningCombatLevel + val level = calculateCombatLevel(player.levels, summoning = World.members) + player.combatLevel = level + player.summoningCombatLevel = calculateCombatLevel(player.levels, summoning = true) + if (level > previous) { + when (level) { + 90, 100, 110, 120, 130 -> { + // 90 - https://youtu.be/Jjn4-f9eSMM?t=47 + // 100 - https://youtu.be/sk72rWo2VPI?t=219 + // 130 - https://youtu.be/bHnKZatcA2s?t=10 + player.message("Well done! You've reached the Combat level $level milestone!") + } + 126 -> { + // f2p - https://youtu.be/31_wDe_HJsU?t=64 + // mem - https://youtu.be/uKJpOSzeTpg?t=53 + player.message("Congratulations! Your Combat level is now 126 - the highest possible Combat level on free worlds!") + } + 138 -> { + // https://youtu.be/o6Ga2FGG1Dc?t=329 + player.message("Congratulations! Your Combat level is now 138! You've achieved the highest Combat level possible!") + } + else -> { + // 30 - https://youtu.be/b4vTaGjIk8Q?t=11 + // 80 - https://youtu.be/GKKIH2hca0E?t=68 + // 112 - https://youtu.be/3GG4KkmPUrU?t=70 + // 125 - https://youtu.be/lFY_jQn3pfE?t=54 + // 128 - https://youtu.be/tBB914-pLEw?t=28 + player.message("Congratulations! You've just reached Combat level ${level}!") + } + } + } } fun calculateCombatLevel(levels: Levels, summoning: Boolean = false): Int { From 63ed00ee222d82054451fffa7c4ec32ff736067f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 1 Aug 2026 12:49:43 +0100 Subject: [PATCH 03/34] Add proper skill level up messages and skip dialogue command --- data/skill/skill.vars.toml | 3 +++ .../content/entity/player/command/SkillCommands.kt | 4 ++++ .../content/entity/player/dialogue/type/LevelUp.kt | 9 +++++++++ 3 files changed, 16 insertions(+) create mode 100644 data/skill/skill.vars.toml diff --git a/data/skill/skill.vars.toml b/data/skill/skill.vars.toml new file mode 100644 index 0000000000..ece85785e0 --- /dev/null +++ b/data/skill/skill.vars.toml @@ -0,0 +1,3 @@ +[skip_level_up_dialogues] +format = "boolean" +persist = true \ No newline at end of file diff --git a/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt b/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt index 721eedbb90..2258e114e2 100644 --- a/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt @@ -45,6 +45,10 @@ class SkillCommands( adminCommands("level", self, other) commandSuggestion("level", "set_level") adminCommand("reset", stringArg("player-name", "target player (default self)", optional = true, autofill = accounts.displayNames.keys), desc = "Set all skills to level 1", handler = ::reset) + + playerCommand("level_dialogues", desc = "Toggle level up pop-up dialogues.") { + message("Level up pop-ups are now ${if (toggle("skip_level_up_dialogues")) "enabled" else "disabled"}.") + } } fun set(player: Player, args: List) { diff --git a/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt b/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt index 443655d118..c5a228b4a3 100644 --- a/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt +++ b/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt @@ -66,6 +66,11 @@ class LevelUp : Script { jingle("level_up_${skill.name.lowercase()}${if (unlock) "_unlock" else ""}", 0.5) addVarbit("skill_stat_flash", skill.name.lowercase()) val level = if (skill == Constitution) to / 10 else to + // https://youtu.be/o6Ga2FGG1Dc?t=326 + message("You've just advanced${skill.name.an()} ${skill.name} level! You have reached level $level.") + if (get("skip_level_up_dialogues", false)) { + return@maxLevelChanged + } engineQueue("level_up") { levelUp( this, @@ -75,6 +80,10 @@ class LevelUp : Script { You have now reached level $level! """, ) + if (skill == Dungeoneering && level == 120 || level == Level.MAX_LEVEL) { + // https://youtu.be/o6Ga2FGG1Dc?t=64 + message("Well done! You've achieved the highest possible level in this skill!") + } } } } From bb65041491c406e655cdfafa5054bbdd53c2a706 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 1 Aug 2026 12:50:03 +0100 Subject: [PATCH 04/34] Skip level up dialogues and messages when using admin commands --- .../main/kotlin/content/entity/player/combat/CombatLevel.kt | 3 +++ .../kotlin/content/entity/player/command/SkillCommands.kt | 4 ++++ .../kotlin/content/entity/player/dialogue/type/LevelUp.kt | 2 ++ 3 files changed, 9 insertions(+) diff --git a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt index aa26512eba..32b988a3db 100644 --- a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt +++ b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt @@ -28,6 +28,9 @@ class CombatLevel : Script { val level = calculateCombatLevel(player.levels, summoning = World.members) player.combatLevel = level player.summoningCombatLevel = calculateCombatLevel(player.levels, summoning = true) + if (player["skip_level_up", false]) { + return + } if (level > previous) { when (level) { 90, 100, 110, 120, 130 -> { diff --git a/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt b/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt index 2258e114e2..6350532dc2 100644 --- a/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/SkillCommands.kt @@ -59,21 +59,25 @@ class SkillCommands( } val skill = Skill.valueOf(string) val level = args[1].toInt() + player["skip_level_up"] = true target.experience.set(skill, Level.experience(skill, level)) target.levels.set(skill, level) target.queue("flash_reset", 1) { target.removeVarbit("skill_stat_flash", skill.name.lowercase()) + player["skip_level_up"] = false } } fun master(player: Player, args: List) { val target = Players.find(player, args.getOrNull(0)) ?: return + player["skip_level_up"] = true for (skill in Skill.all) { target.experience.set(skill, 14000000.0) target.levels.restore(skill, 1000) } target.queue("clear_flash", 1) { player.clear("skill_stat_flash") + player["skip_level_up"] = false } } diff --git a/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt b/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt index c5a228b4a3..6ae873a941 100644 --- a/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt +++ b/game/src/main/kotlin/content/entity/player/dialogue/type/LevelUp.kt @@ -1,6 +1,7 @@ package content.entity.player.dialogue.type import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.chat.an import world.gregs.voidps.engine.client.ui.close import world.gregs.voidps.engine.client.ui.open @@ -10,6 +11,7 @@ import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.Skill.* import world.gregs.voidps.engine.entity.character.player.skill.Skills import world.gregs.voidps.engine.entity.character.player.skill.exp.Experience +import world.gregs.voidps.engine.entity.character.player.skill.level.Level import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.queue.engineQueue import world.gregs.voidps.engine.suspend.pauseButton From 8150278d74ca4b9493bbc5bce431601c7bac8a0f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 00:06:29 +0100 Subject: [PATCH 05/34] Fix FixItems.kt not correctly encoding params --- .../gregs/voidps/cache/definition/Category.kt | 40 +++++++++++++++++++ .../voidps/cache/definition/Parameterized.kt | 12 +++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Category.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Category.kt index 1506e2b3f9..7ba468cce0 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Category.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Category.kt @@ -78,4 +78,44 @@ object Category { else -> error("Category $id not found") } + fun id(name: String) = when (name) { + "throwable" -> THROWABLE + "arrow" -> ARROW + "bolt" -> BOLT + "construction" -> CONSTRUCTION + "furniture" -> FURNITURE + "uncooked_food" -> UNCOOKED_FOOD + "construction_storable_clothes" -> CONSTRUCTION_STORABLE_CLOTHES + "crafting" -> CRAFTING + "summoning_pouches" -> SUMMONING_POUCHES + "construction_plant" -> CONSTRUCTION_PLANT + "fletching" -> FLETCHING + "edible" -> EDIBLE + "herblore" -> HERBLORE + "hunter_required_item" -> HUNTER_REQUIRED_ITEM + "hunter_reward" -> HUNTER_REWARD + "jewellery" -> JEWELLERY + "magic_armour" -> MAGIC_ARMOUR + "magic_weapon" -> MAGIC_WEAPON + "melee_armour_low" -> MELEE_ARMOUR_LOW + "melee_armour_mid" -> MELEE_ARMOUR_MID + "melee_armour_high" -> MELEE_ARMOUR_HIGH + "melee_weapon_low" -> MELEE_WEAPON_LOW + "melee_weapon_mid" -> MELEE_WEAPON_MID + "melee_weapon_high" -> MELEE_WEAPON_HIGH + "mining_smelting" -> MINING_SMELTING + "potion" -> POTION + "prayer_armour" -> PRAYER_ARMOUR + "prayer_consumable" -> PRAYER_CONSUMABLE + "range_armour" -> RANGE_ARMOUR + "range_weapon" -> RANGE_WEAPON + "runecrafting" -> RUNECRAFTING + "teleport" -> TELEPORT + "seed" -> SEED + "summoning_scroll" -> SUMMONING_SCROLL + "item_on_item" -> ITEM_ON_ITEM + "log" -> LOG + else -> error("Category $name not found") + } + } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Parameterized.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Parameterized.kt index c1002ee14a..5fd1bf009b 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Parameterized.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Parameterized.kt @@ -9,6 +9,7 @@ import world.gregs.voidps.cache.definition.Params.CATEGORY import world.gregs.voidps.cache.definition.Params.MAGIC_STRENGTH import world.gregs.voidps.cache.definition.Params.RANGED_STRENGTH import world.gregs.voidps.cache.definition.Params.STRENGTH +import kotlin.collections.set @Suppress("UNCHECKED_CAST") interface Parameterized { @@ -67,11 +68,12 @@ interface Parameterized { writer.writeByte(params.size) params.forEach { (id, value) -> writer.writeByte(value is String) - writer.writeMedium(id) - if (value is String) { - writer.writeString(value) - } else if (value is Int) { - writer.writeInt(value) + writer.writeMedium(if (value is Set<*>) CATEGORY else id) + when (value) { + is String -> writer.writeString(value) + is Int -> writer.writeInt(value) + is Double -> writer.writeInt((value * 10.0).toInt()) + is Set<*> -> writer.writeInt(Category.id((value as Set).first())) } } } From 33cbce2b4b0e04a2b4e485b1d3ca9fe0a65f82d4 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 00:09:46 +0100 Subject: [PATCH 06/34] Fix wolf combat and drop tables --- data/entity/npc/animal/wolf/wolf.npcs.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/entity/npc/animal/wolf/wolf.npcs.toml b/data/entity/npc/animal/wolf/wolf.npcs.toml index 3ac39ae509..bbf48dc24c 100644 --- a/data/entity/npc/animal/wolf/wolf.npcs.toml +++ b/data/entity/npc/animal/wolf/wolf.npcs.toml @@ -26,6 +26,7 @@ slayer_xp = 34.0 hunt_mode = "aggressive" respawn_delay = 90 categories = ["wolves"] +drop_table = "wolf_bones" examine = "A vicious mountain wolf." [white_wolf_level_38] @@ -41,6 +42,7 @@ slayer_xp = 44.0 hunt_mode = "aggressive" respawn_delay = 90 categories = ["wolves"] +drop_table = "wolf_bones" examine = "A vicious mountain wolf." [big_wolf] From 7a49f00323aac5bfb65dd5c2dd4bdc30188f1660 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 00:45:41 +0100 Subject: [PATCH 07/34] Add velrak the explorer to taverley dungeon --- .../black_knight.drops.toml | 6 +++ .../black_knight.npcs.toml | 5 ++ .../dungeon/black_knight_base.npc-spawns.toml | 1 + .../area/asgarnia/taverley/DustyKey.kt | 10 ++++ .../asgarnia/taverley/VelrakTheExplorer.kt | 46 +++++++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 data/area/asgarnia/black_knights_fortress/black_knight.drops.toml create mode 100644 game/src/main/kotlin/content/area/asgarnia/taverley/VelrakTheExplorer.kt diff --git a/data/area/asgarnia/black_knights_fortress/black_knight.drops.toml b/data/area/asgarnia/black_knights_fortress/black_knight.drops.toml new file mode 100644 index 0000000000..fbe3d84850 --- /dev/null +++ b/data/area/asgarnia/black_knights_fortress/black_knight.drops.toml @@ -0,0 +1,6 @@ +[jailer_black_knights_base_drop_table] +type = "all" +drops = [ + { id = "bones" }, + { id = "jail_key" }, +] \ No newline at end of file diff --git a/data/area/asgarnia/black_knights_fortress/black_knight.npcs.toml b/data/area/asgarnia/black_knights_fortress/black_knight.npcs.toml index 2035626b1c..91cd7b1800 100644 --- a/data/area/asgarnia/black_knights_fortress/black_knight.npcs.toml +++ b/data/area/asgarnia/black_knights_fortress/black_knight.npcs.toml @@ -6,12 +6,17 @@ examine = "One of the Black Knights." id = 200 examine = "Wears a stylish suit of armour." +[velrak_the_explorer] +id = 798 +examine = "He looks cold and hungry." + [jailer_black_knights_base] id = 201 hitpoints = 470 att = 40 str = 40 def = 40 +drop_table = "jailer_black_knights_base" combat_def = "man" max_hit_crush = 50 respawn_delay = 50 diff --git a/data/area/asgarnia/taverley/dungeon/black_knight_base.npc-spawns.toml b/data/area/asgarnia/taverley/dungeon/black_knight_base.npc-spawns.toml index b86a8a64b3..30ffa6b7ab 100644 --- a/data/area/asgarnia/taverley/dungeon/black_knight_base.npc-spawns.toml +++ b/data/area/asgarnia/taverley/dungeon/black_knight_base.npc-spawns.toml @@ -46,5 +46,6 @@ spawns = [ { id = "jailer_black_knights_base", x = 2933, y = 9693 }, { id = "captain_gilroy", x = 2915, y = 9674, members = true }, { id = "lord_daquarius", x = 2892, y = 9680, members = true }, + { id = "velrak_the_explorer", x = 2931, y = 9686, members = true }, { id = "black_knight_void_stares_back", x = 2913, y = 9674, members = true }, ] \ No newline at end of file diff --git a/game/src/main/kotlin/content/area/asgarnia/taverley/DustyKey.kt b/game/src/main/kotlin/content/area/asgarnia/taverley/DustyKey.kt index 4a7a30ead0..68b2386f83 100644 --- a/game/src/main/kotlin/content/area/asgarnia/taverley/DustyKey.kt +++ b/game/src/main/kotlin/content/area/asgarnia/taverley/DustyKey.kt @@ -18,5 +18,15 @@ class DustyKey : Script { message("The gate is locked.") } } + + objectOperate("Open", "door_547_closed") { (target) -> + if (inventory.contains("jail_key")) { + sound("unlock") + enterDoor(target) + } else { + sound("locked") + message("This door is locked.") + } + } } } diff --git a/game/src/main/kotlin/content/area/asgarnia/taverley/VelrakTheExplorer.kt b/game/src/main/kotlin/content/area/asgarnia/taverley/VelrakTheExplorer.kt new file mode 100644 index 0000000000..b3e278fd76 --- /dev/null +++ b/game/src/main/kotlin/content/area/asgarnia/taverley/VelrakTheExplorer.kt @@ -0,0 +1,46 @@ +package content.area.asgarnia.taverley + +import content.entity.player.bank.ownsItem +import content.entity.player.dialogue.Confused +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.item +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.entity.player.inv.item.addOrDrop +import world.gregs.voidps.engine.Script + +class VelrakTheExplorer : Script { + init { + npcOperate("Talk-to", "velrak_the_explorer") { + if (ownsItem("dusty_key")) { + player("Are you still here?") + npc("Yes... I'm still plucking up the courage to run out past those Black Knights..") + return@npcOperate + } + npc("Thank you for rescuing me! It isn't very comfy in this cell!") + choice { + option("So... do you know anywhere good to explore?") { + npc("Well, this dungeon was quite good to explore ...until I got captured, anyway. I was given a key to an inner part of this dungeon by a mysterious cloaked stranger!") + npc("It's rather tough for me to get that far into the dungeon however... I just keep getting captured! Would you like to give it a go?") + choice { + option("Yes please!") { + addOrDrop("dusty_key") + item("dusty_key", "Velrak reaches somewhere mysterious and passes you a key.") + } + option("No, it's too dangerous for me too.") { + npc("I don't blame you!") + } + } + } + option("Do I get a reward?") { + player("Do I get a reward? For freeing you and all...") + npc("Well... not really... The Black Knights took all of my stuff before throwing me in here to rot!") + } + } + } + } +} From 3ed87680503e2d5af836f8e8055a7054fff32ded Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 01:13:53 +0100 Subject: [PATCH 08/34] Allow combat regardless of being attacked by death spawns --- game/src/main/kotlin/content/entity/combat/Combat.kt | 4 +++- game/src/main/kotlin/content/entity/combat/Target.kt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/entity/combat/Combat.kt b/game/src/main/kotlin/content/entity/combat/Combat.kt index e9619c4828..c7098b97c4 100644 --- a/game/src/main/kotlin/content/entity/combat/Combat.kt +++ b/game/src/main/kotlin/content/entity/combat/Combat.kt @@ -218,7 +218,9 @@ class Combat(val combatDefinitions: CombatDefinitions) : CombatApi.start(character, target) } } - target.start("under_attack", 8) + if (target !is Player || !Target.isDeathSpawn(character)) { + target.start("under_attack", 8) + } target.start("hunted", 8) // FIXME Temp #1119 if (character is NPC) { CombatApi.swing(character, target, character.fightStyle) diff --git a/game/src/main/kotlin/content/entity/combat/Target.kt b/game/src/main/kotlin/content/entity/combat/Target.kt index 7d5c6a9ec0..60586793d3 100644 --- a/game/src/main/kotlin/content/entity/combat/Target.kt +++ b/game/src/main/kotlin/content/entity/combat/Target.kt @@ -98,7 +98,7 @@ object Target { } } } - if (source is NPC && source.id == "death_spawn") { + if (isDeathSpawn(target) || isDeathSpawn(source) || isDeathSpawn(target.attacker) || isDeathSpawn(source.attacker)) { return true } // If the target I'm trying to attack is already in combat and I am not the attacker @@ -121,6 +121,8 @@ object Target { return true } + fun isDeathSpawn(target: Character?) = target is NPC && target.id == "death_spawn" + fun isDemon(target: Character) = target is NPC && target.categories.contains("demons") fun isVampyre(target: Character) = target is NPC && target.categories.contains("vampyres") From a4a121c24bbff603fb1b84932b48af7d05895192 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 01:14:09 +0100 Subject: [PATCH 09/34] Fix killing death spawns not respawning --- .../content/area/morytania/slayer_tower/Nechryael.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt b/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt index 8435032cc9..198f5edecf 100644 --- a/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt +++ b/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt @@ -1,5 +1,6 @@ package content.area.morytania.slayer_tower +import content.entity.combat.killer import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.instruction.handle.interactPlayer import world.gregs.voidps.engine.entity.character.npc.NPCs @@ -39,5 +40,11 @@ class Nechryael : Script { target.inc("death_spawns") } } + + npcDeath("death_spawn") { + val killer = killer as? Player ?: return@npcDeath + killer.dec("death_spawns") + } + } } From 6db1d408a5f6896c0341293dad6889d6cc03de56 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 01:34:26 +0100 Subject: [PATCH 10/34] Fix in combat with nechryaels and death spawns --- .../main/kotlin/world/gregs/voidps/cache/definition/Params.kt | 2 ++ .../area/morytania/slayer_tower/nechryael/nechryael.npcs.toml | 2 ++ .../kotlin/world/gregs/voidps/engine/client/update/NPCTask.kt | 2 +- .../engine/entity/character/mode/combat/CombatMovement.kt | 3 ++- game/src/main/kotlin/content/entity/combat/Target.kt | 4 +++- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt index d1ea3f8d7e..fcc786ae12 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/definition/Params.kt @@ -262,8 +262,10 @@ object Params { const val VARIABLES = 5263 const val SUMMONING_BEAST_OF_BURDEN_ESSENCE = 5264 const val SUMMONING_SPECIAL_COST = 5265 + const val DISENGAGE = 5266 private fun custom(name: String) = when (name) { + "disengage" -> DISENGAGE "summoning_beast_of_burden_essence" -> SUMMONING_BEAST_OF_BURDEN_ESSENCE "summoning_special_cost" -> SUMMONING_SPECIAL_COST "variables" -> VARIABLES diff --git a/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml b/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml index dbb554775b..fb51ad165d 100644 --- a/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml +++ b/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml @@ -13,6 +13,7 @@ slayer_xp = 105.0 slayer_level = 80 drop_table = "nechryael" categories = ["nechryael"] +disengage = false examine = "An evil death demon." [death_spawn] @@ -26,4 +27,5 @@ hunt_mode = "cowardly" style = "slash" slayer_level = 80 drop_table = "death_spawn" +disengage = false examine = "An evil death spawn." diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/update/NPCTask.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/update/NPCTask.kt index 658a8e0b2e..cf302b2b5a 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/update/NPCTask.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/update/NPCTask.kt @@ -29,7 +29,7 @@ class NPCTask( // owner rather than wandering or standing still after a fight ends. val ownerIndex = character["owner_index", -1] val owner = if (ownerIndex != -1) Players.indexed(ownerIndex) else null - if (owner != null && owner.get("follower_index", -1) == character.index) { + if (owner != null && owner["follower_index", -1] == character.index) { character.mode = Follow(character, owner) } else if (wanders(character)) { character.mode = Wander(character) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/combat/CombatMovement.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/combat/CombatMovement.kt index e6a6ab043a..bc1ce912b6 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/combat/CombatMovement.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/combat/CombatMovement.kt @@ -1,5 +1,6 @@ package world.gregs.voidps.engine.entity.character.mode.combat +import world.gregs.voidps.cache.definition.Params import world.gregs.voidps.engine.client.ui.dialogue import world.gregs.voidps.engine.client.variable.hasClock import world.gregs.voidps.engine.data.config.CombatDefinition @@ -93,7 +94,7 @@ class CombatMovement( } // Disengage from targets that are already in combat val attacker = target.get("attacker") - if (character is NPC && attacker != null && attacker != character && !target.contains("in_multi_combat")) { + if (character is NPC && character.def[Params.DISENGAGE, true] && attacker != null && attacker != character && !target.contains("in_multi_combat")) { character.mode = EmptyMode return } diff --git a/game/src/main/kotlin/content/entity/combat/Target.kt b/game/src/main/kotlin/content/entity/combat/Target.kt index 60586793d3..ef18dabf43 100644 --- a/game/src/main/kotlin/content/entity/combat/Target.kt +++ b/game/src/main/kotlin/content/entity/combat/Target.kt @@ -98,7 +98,7 @@ object Target { } } } - if (isDeathSpawn(target) || isDeathSpawn(source) || isDeathSpawn(target.attacker) || isDeathSpawn(source.attacker)) { + if ((isNechryael(target.attacker) && target.attacker == this) || isDeathSpawn(source) || isDeathSpawn(target.attacker) || isDeathSpawn(source.attacker)) { return true } // If the target I'm trying to attack is already in combat and I am not the attacker @@ -123,6 +123,8 @@ object Target { fun isDeathSpawn(target: Character?) = target is NPC && target.id == "death_spawn" + fun isNechryael(target: Character?) = target is NPC && target.id == "nechryael" + fun isDemon(target: Character) = target is NPC && target.categories.contains("demons") fun isVampyre(target: Character) = target is NPC && target.categories.contains("vampyres") From a57eed542fa45fb08f5974e24637c4d1a1a7a289 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 01:34:44 +0100 Subject: [PATCH 11/34] Make nechs not aggressive --- data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml b/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml index fb51ad165d..b75e82cf40 100644 --- a/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml +++ b/data/area/morytania/slayer_tower/nechryael/nechryael.npcs.toml @@ -6,7 +6,6 @@ str = 97 def = 105 attack_speed = 4 combat_def = "nechryael" -hunt_mode = "cowardly" respawn_delay = 30 wander_range = 4 slayer_xp = 105.0 From 86bdae732433ee74091db1b228b7f50646f3aa05 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 01:35:12 +0100 Subject: [PATCH 12/34] Formatting --- .../kotlin/content/area/morytania/slayer_tower/Nechryael.kt | 1 - .../src/main/kotlin/content/entity/player/combat/CombatLevel.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt b/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt index 198f5edecf..8617b26591 100644 --- a/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt +++ b/game/src/main/kotlin/content/area/morytania/slayer_tower/Nechryael.kt @@ -45,6 +45,5 @@ class Nechryael : Script { val killer = killer as? Player ?: return@npcDeath killer.dec("death_spawns") } - } } diff --git a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt index 32b988a3db..bb984e5b50 100644 --- a/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt +++ b/game/src/main/kotlin/content/entity/player/combat/CombatLevel.kt @@ -54,7 +54,7 @@ class CombatLevel : Script { // 112 - https://youtu.be/3GG4KkmPUrU?t=70 // 125 - https://youtu.be/lFY_jQn3pfE?t=54 // 128 - https://youtu.be/tBB914-pLEw?t=28 - player.message("Congratulations! You've just reached Combat level ${level}!") + player.message("Congratulations! You've just reached Combat level $level!") } } } From edc269de3ef6054e62b8ad5eed9ce85699e3c1b2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 11:41:36 +0100 Subject: [PATCH 13/34] Fix mods being able to shift click teleport --- .../engine/client/instruction/handle/ExecuteCommandHandler.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt index c5bed6d08f..1382974b49 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt @@ -5,6 +5,8 @@ import world.gregs.voidps.engine.client.instruction.InstructionHandler import world.gregs.voidps.engine.client.command.Commands import world.gregs.voidps.engine.entity.character.move.tele import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.PlayerRights +import world.gregs.voidps.engine.entity.character.player.hasRights import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.network.client.instruction.ExecuteCommand @@ -18,7 +20,7 @@ class ExecuteCommandHandler : InstructionHandler() { val parts = instruction.command.split(" ") val prefix = parts[0] val content = instruction.command.removePrefix(prefix).trim() - if (instruction.automatic) { + if (instruction.automatic && player.hasRights(PlayerRights.Admin)) { val params = content.split(",") val level = params[0].toInt() val x = params[1].toInt() shl 6 or params[3].toInt() From 91deda1d024b05976a7d48c2da5e8fa6ad64d05f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 11:55:21 +0100 Subject: [PATCH 14/34] Fix light sources and extinguishing --- .../kotlin/content/skill/firemaking/Light.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/game/src/main/kotlin/content/skill/firemaking/Light.kt b/game/src/main/kotlin/content/skill/firemaking/Light.kt index cfe8219bf5..a01b6aab9f 100644 --- a/game/src/main/kotlin/content/skill/firemaking/Light.kt +++ b/game/src/main/kotlin/content/skill/firemaking/Light.kt @@ -1,28 +1,38 @@ package content.skill.firemaking +import world.gregs.voidps.engine.data.definition.Rows import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import kotlin.collections.any object Light { fun hasLightSource(player: Player): Boolean { - if (player.inventory.items.any { it.id.endsWith("lantern_lit") || it.id.endsWith("candle_lit") || it.id == "firemaking_cape_t" || it.id == "firemaking_cape" }) { + if (player.inventory.items.any { it.isNotEmpty() && (isFiremakingCape(it.id) || Rows.getOrNull("extinguish.${it.id}") != null) }) { return true } - - val cape = player.equipped(EquipSlot.Cape).id - return cape == "firemaking_cape" || cape == "firemaking_cape_t" + if (player.equipped(EquipSlot.Shield).id == "lit_bug_lantern") { + return true + } + return isFiremakingCape(player.equipped(EquipSlot.Cape).id) } + private fun isFiremakingCape(item: String) = item == "firemaking_cape" || item == "firemaking_cape_t" + fun extinguish(player: Player) { player.inventory.transaction { for (index in inventory.indices) { val item = inventory[index] - if (item.id.endsWith("candle_lit")) { - replace(index, item.id, item.id.removeSuffix("_lit")) + val row = Rows.getOrNull("extinguish.${item.id}") ?: continue + val type = row.string("type") + if (type != "candle" && type != "torch") { + // Don't extinguish lanterns + continue } + val unlit = row.item(".unlit") + replace(index, item.id, unlit) } } } From 81380983d5c45953864d44ee2bfdfb0e7f63bdb2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 11:57:20 +0100 Subject: [PATCH 15/34] Fix redberry pie recipe --- data/skill/cooking/cooking.recipes.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/skill/cooking/cooking.recipes.toml b/data/skill/cooking/cooking.recipes.toml index 0f54baf733..0a0f5e49ed 100644 --- a/data/skill/cooking/cooking.recipes.toml +++ b/data/skill/cooking/cooking.recipes.toml @@ -254,7 +254,7 @@ message = "You fill the pie with apple." skill = "cooking" level = 20 remove = ["redberries", "pie_shell"] -add = ["uncooked_meat_pie"] +add = ["uncooked_berry_pie"] ticks = 2 message = "You fill the pie with meat." From 70ab74f7f5df9a270a645f11c33ca86b92abf57d Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 12:06:24 +0100 Subject: [PATCH 16/34] Add opening all item packs --- .../player/inventory/item_pack.tables.toml | 35 +++++++++++++++++++ .../content/entity/player/inv/ItemPacks.kt | 33 +++++++++++++++++ .../content/skill/summoning/ShardSwapping.kt | 20 ----------- 3 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 data/entity/player/inventory/item_pack.tables.toml create mode 100644 game/src/main/kotlin/content/entity/player/inv/ItemPacks.kt diff --git a/data/entity/player/inventory/item_pack.tables.toml b/data/entity/player/inventory/item_pack.tables.toml new file mode 100644 index 0000000000..5e1b148f05 --- /dev/null +++ b/data/entity/player/inventory/item_pack.tables.toml @@ -0,0 +1,35 @@ +[packs] +unpacked = "item" +amount = "int" + +[.spirit_shard_pack] +unpacked = "spirit_shards" +amount = 5000 + +[.juju_vial_pack] +unpacked = "juju_vial_of_water_noted" +amount = 50 + +[.vial_pack] +unpacked = "vial_noted" +amount = 50 + +[.vial_of_water_pack] +unpacked = "vial_of_water_noted" +amount = 50 + +[.eye_of_newt_pack] +unpacked = "eye_of_newt_noted" +amount = 50 + +[.raw_bird_meat_pack] +unpacked = "raw_bird_meat_noted" +amount = 50 + +[.basket_pack] +unpacked = "basket_noted" +amount = 50 + +[.empty_sack_pack] +unpacked = "empty_sack_noted" +amount = 50 diff --git a/game/src/main/kotlin/content/entity/player/inv/ItemPacks.kt b/game/src/main/kotlin/content/entity/player/inv/ItemPacks.kt new file mode 100644 index 0000000000..9b7533f08e --- /dev/null +++ b/game/src/main/kotlin/content/entity/player/inv/ItemPacks.kt @@ -0,0 +1,33 @@ +package content.entity.player.inv + +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.data.definition.Rows +import world.gregs.voidps.engine.entity.character.player.chat.inventoryFull +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.transact.TransactionError +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove +import world.gregs.voidps.engine.inv.transact.operation.RemoveItemLimit.removeToLimit + +class ItemPacks : Script { + init { + itemOption("Open", "*_pack") { (item) -> + val row = Rows.getOrNull("packs.${item.id}") ?: return@itemOption + inventory.transaction { + remove(item.id) + add(row.item("unpacked"), row.int("amount")) + } + } + + itemOption("Open-All", "*_pack") { (item) -> + val row = Rows.getOrNull("packs.${item.id}") ?: return@itemOption + inventory.transaction { + val removed = removeToLimit(item.id, item.amount) + add(row.item("unpacked"), removed * row.int("amount")) + } + if (inventory.transaction.error is TransactionError.Full) { + inventoryFull() + } + } + } +} diff --git a/game/src/main/kotlin/content/skill/summoning/ShardSwapping.kt b/game/src/main/kotlin/content/skill/summoning/ShardSwapping.kt index 7fe9c3816c..79aabf7b8f 100644 --- a/game/src/main/kotlin/content/skill/summoning/ShardSwapping.kt +++ b/game/src/main/kotlin/content/skill/summoning/ShardSwapping.kt @@ -10,13 +10,10 @@ import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.data.definition.EnumDefinitions import world.gregs.voidps.engine.data.definition.ItemDefinitions import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.chat.inventoryFull import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.inv.transact.TransactionError import world.gregs.voidps.engine.inv.transact.operation.AddItem.add import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove -import world.gregs.voidps.engine.inv.transact.operation.RemoveItemLimit.removeToLimit import kotlin.math.min class ShardSwapping : Script { @@ -70,23 +67,6 @@ class ShardSwapping : Script { "Trade-All" -> swapForShards(this, actualItem, Int.MAX_VALUE) } } - - itemOption("Open", "spirit_shard_pack") { - inventory.transaction { - remove(it.item.id) - add("spirit_shards", 5000) - } - } - - itemOption("Open-All", "spirit_shard_pack") { - inventory.transaction { - val removed = removeToLimit(it.item.id, it.item.amount) - add("spirit_shards", removed * 5000) - } - if (inventory.transaction.error is TransactionError.Full) { - inventoryFull() - } - } } /** From ab5afd497f14ddd696e1bd9c57ac6ecb69500867 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 12:07:49 +0100 Subject: [PATCH 17/34] Fix attack potion herblore experience --- data/skill/herblore/potion.recipes.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/skill/herblore/potion.recipes.toml b/data/skill/herblore/potion.recipes.toml index f5aa6feaa6..5f63f88210 100644 --- a/data/skill/herblore/potion.recipes.toml +++ b/data/skill/herblore/potion.recipes.toml @@ -1,7 +1,7 @@ [attack_potion_finished] skill = "herblore" level = 3 -xp = 1.0 +xp = 25.0 ticks = 2 remove = ["guam_potion_unf", "eye_of_newt"] add = ["attack_potion_3"] From 7c151936f321f059dbc0340f64f49dbfed566bca Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 12:49:31 +0100 Subject: [PATCH 18/34] Add aleck and leon in yannile --- data/area/kandarin/yanille/yanille.npcs.toml | 1 + .../content/area/kandarin/yanille/Aleck.kt | 33 ++++ .../content/area/kandarin/yanille/Leon.kt | 173 ++++++++++++++++++ .../entity/player/command/TeleportCommands.kt | 3 +- 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 game/src/main/kotlin/content/area/kandarin/yanille/Aleck.kt create mode 100644 game/src/main/kotlin/content/area/kandarin/yanille/Leon.kt diff --git a/data/area/kandarin/yanille/yanille.npcs.toml b/data/area/kandarin/yanille/yanille.npcs.toml index 70cd486a80..d7774f79c4 100644 --- a/data/area/kandarin/yanille/yanille.npcs.toml +++ b/data/area/kandarin/yanille/yanille.npcs.toml @@ -56,6 +56,7 @@ examine = "A Supplier of Magical items." [aleck] id = 5110 +shop = "alecks_hunter_emporium" examine = "He acts as if he owns the place." [leon] diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/Aleck.kt b/game/src/main/kotlin/content/area/kandarin/yanille/Aleck.kt new file mode 100644 index 0000000000..584ebbd616 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/yanille/Aleck.kt @@ -0,0 +1,33 @@ +package content.area.kandarin.yanille + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Confused +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import world.gregs.voidps.engine.Script + +class Aleck : Script { + init { + npcOperate("Talk-to", "aleck") { (target) -> + player("Hello.") + npc("Hello, hello, and a most warm welcome to my Hunter Emporium. We have everything the discerning Hunter could need.") + npc("Would you like me to show you our range of equipment? Or was there something specific you were after?") + choice { + option("Ok, let's see what you've got!") { + openShop(target.def["shop"]) + } + option("I'm not interested, thanks.") { + npc("Well, if you do ever find yourself in need of the finest Hunter equipment available, then you know where to come.") + } + option("Who's that guy over there?") { + npc("Him? I think he might be crazy. Either that or he's seeking attention.") + npc("He keeps trying to sell me these barmy looking weapons he's invented. I can't see them working, personally.") + } + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/Leon.kt b/game/src/main/kotlin/content/area/kandarin/yanille/Leon.kt new file mode 100644 index 0000000000..313bc83bd6 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/yanille/Leon.kt @@ -0,0 +1,173 @@ +package content.area.kandarin.yanille + +import com.github.michaelbull.logging.InlineLogger +import content.entity.player.dialogue.* +import content.entity.player.dialogue.type.* +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.ui.chat.plural +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.transact.TransactionError +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove + +class Leon : Script { + + val logger = InlineLogger() + + init { + npcOperate("Talk-to", "leon") { + item("hunters_crossbow", "Leon is gazing intently at the crossbow in his hands.") + choice { + place() + infoCrossbow() + infoAmmo() + bye() + } + } + + npcOperate("Trade", "leon") { + choice { + option("Could I have a go with your crossbow?") { + buy() + } + makeAmmo() + bye() + } + } + } + + private fun ChoiceOption.place() = option("What is this place?") { + npc("This is Aleck's Hunter Emporium. Basically, it's just a shop with a fancy name; you can buy various weapons and traps here.") + choice { + shop() + infoCrossbow() + infoAmmo() + bye() + } + } + + private fun ChoiceOption.shop() = option("Can I buy some equipment from the shop then?") { + npc("Oh, this isn't my shop; the owner is Aleck over there behind the counter.") + npc("I experiment with weapon designs. I'm here because I've been trying to convince people to back my research and maybe sell some of my products - like this one I'm holding - in their shops.") + npc("Aleck doesn't seem to be interested, though.") + choice { + infoCrossbow() + infoAmmo() + bye() + } + } + + private fun ChoiceOption.infoCrossbow(): Unit = option("Can you tell me about your crossbow?") { + npc("It's good, isn't it? I designed it to incorporate the bones of various animals in its construction.") + npc("It's a fair bit faster than an ordinary crossbow too; it'll take you far less time to reload between shots.") + choice { + crazy() + trade() + infoAmmo() + bye() + } + } + + private fun ChoiceOption.crazy(): Unit = option("That's crazy, it'll never work!") { + npc("That's what they said about my wind-powered mouse traps, too.") + player("And did they work?") + npc("Well, they only ran into problems because people kept insisting on trying to use them indoors.") + npc("Anyway, I think my crossbow invention is showing a lot more promise.") + choice { + trade() + infoAmmo() + bye() + } + } + + private fun ChoiceOption.trade() { + option("That sounds good. Let's trade.") { + buy() + } + } + + private suspend fun Player.buy() { + npc("I'm afraid with it being a prototype, I've only got a few for my own testing purposes.") + npc("Of course, for the right price, it might be worth my while to make another prototype.") + player("How much?") + npc("To you, 1300 gold pieces.") + choice { + option("Ok, here you go.") { + inventory.transaction { + remove("coins", 1300) + add("hunters_crossbow") + } + when (inventory.transaction.error) { + is TransactionError.Deficient -> player("Oh, actually I don't have enough money with me.") + is TransactionError.Full -> npc("Well, you look a little overburdened there at the moment. Maybe you should free up some space to carry it in first.") + TransactionError.None -> { + item("hunters_crossbow", "You hand over the money and the weapon designer presents you with a prototype crossbow.") + npc("Here, you might as well take this one. Aleck over there doesn't seem interested and it'll save you a wait.") + } + else -> logger.warn { "Error buying hunters crossbow ${inventory.transaction.error}" } + } + } + option("I'll give it a miss, thanks.") { + npc("Your loss; they'll probably become highly valuable and sought after once I convince someone to market the things.") + } + } + } + + private fun ChoiceOption.infoAmmo() = option("Tell me about the ammo for your crossbow.") { + npc("Ah, I admit as a result of its... er... unique construction, it won't take just any old bolts.") + npc("If you can supply the materials and a token fee, I'd be happy to make some for you.") + npc("I need kebbit spikes, lots of 'em. Not all kebbits have spikes, mind you. You'll be wanting prickly kebbits or, even better, razor-backed kebbits to get material hard enough.") + choice { + option("Can't I just make my own?") { + npc("Yes, I suppose you could, although you'll need a steady hand with a knife and a chisel.") + npc("The bolts have an unusual diameter, but basically you'll just need to be able to carve kebbit spikes into straight shafts.") + npc("Meanwhile, since you're here, I can make some for you if you have the materials.") + choice { + makeAmmo() + infoCrossbow() + bye() + } + } + makeAmmo() + infoCrossbow() + bye() + } + } + + private fun ChoiceOption.bye() = option("I'll be off now, excuse me.") { + npc("Well, if you ever find yourself in need of that innovative edge, you can always find me here.") + player("... thanks.") + } + + private fun ChoiceOption.makeAmmo() = option("Okay, can you make ammo for me?") { + val (item, amount) = makeAmount(text = "How many batches would you like?", items = listOf("kebbit_bolts", "long_kebbit_bolts"), type = "Make", maximum = 27) + when (item) { + "kebbit_bolts" -> { + inventory.transaction { + remove("kebbit_spike", amount) + remove("coins", 20 * amount) + add("kebbit_bolts", 12 * amount) + } + when (inventory.transaction.error) { + is TransactionError.Deficient -> items("kebbit_spike", "coins_4", "You need $amount kebbit ${"spike".plural(amount)} and ${amount * 20} coins to make ${12 * amount} kebbit bolts.") + TransactionError.None -> item("kebbit_bolts", "You hand the weapon designer $amount ${"spike".plural(amount)} and ${amount * 20} coins. In return he presents you with ${12 * amount} bolts.") + else -> logger.warn { "Error buying kebbit bolts ${inventory.transaction.error}." } + } + } + "long_kebbit_bolts" -> { + inventory.transaction { + remove("long_kebbit_spike", amount) + remove("coins", 40 * amount) + add("long_kebbit_bolts", 12 * amount) + } + when (inventory.transaction.error) { + is TransactionError.Deficient -> items("long_kebbit_spike", "coins_4", "You need $amount long kebbit ${"spike".plural(amount)} and ${amount * 40} coins to make ${12 * amount} long kebbit bolts.") + TransactionError.None -> item("long_kebbit_bolts", "You hand the weapon designer $amount long ${"spike".plural(amount)} and ${amount * 40} coins. In return he presents you with ${12 * amount} long bolts.") + else -> logger.warn { "Error buying long kebbit bolts ${inventory.transaction.error}." } + } + } + else -> logger.warn { "Error making kebbit bolts." } + } + } +} diff --git a/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt b/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt index 743c3a5563..698db52d34 100644 --- a/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt @@ -17,7 +17,6 @@ import world.gregs.voidps.engine.entity.character.move.tele import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.Players import world.gregs.voidps.engine.entity.character.player.chat.ChatType -import world.gregs.voidps.engine.entity.character.player.name import world.gregs.voidps.type.Region import world.gregs.voidps.type.Tile @@ -29,7 +28,7 @@ class TeleportCommands( private val places = mapOf( "draynor" to Tile(3086, 3248), "varrock" to Tile(3212, 3429), - "varrock" to Tile(3212, 3429), + "yanille" to Tile(2605, 3097), "lumbridge" to Tile(3222, 3219), "burthorpe" to Tile(2899, 3546), "falador" to Tile(2966, 3379), From e393c70ed5a5b61c3ab2948531232734929ce222 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 12:50:04 +0100 Subject: [PATCH 19/34] Fix make amount dialogue double closing dialogues after it --- .../main/kotlin/content/entity/player/dialogue/DialogueInput.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt index 6d722e3631..619464d762 100644 --- a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt +++ b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt @@ -63,7 +63,6 @@ class DialogueInput : Script { continueDialogue("dialogue_skill_creation:choice*") { val choice = it.substringAfter(":choice").toIntOrNull() ?: 0 (suspension as? Suspension.IntEntry)?.resume(choice - 1) - closeDialogue() } } } From 67192dab512d8f42285f6f70fa13950f1fc0510a Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 12:52:17 +0100 Subject: [PATCH 20/34] Add frenita's shop in yannile --- data/area/kandarin/yanille/yanille.npcs.toml | 1 + .../content/area/kandarin/yanille/Frenita.kt | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt diff --git a/data/area/kandarin/yanille/yanille.npcs.toml b/data/area/kandarin/yanille/yanille.npcs.toml index d7774f79c4..8c284ec92e 100644 --- a/data/area/kandarin/yanille/yanille.npcs.toml +++ b/data/area/kandarin/yanille/yanille.npcs.toml @@ -77,6 +77,7 @@ examine = "A wizard." [frenita] id = 593 +shop = "frenitas_cookery_shop" examine = "A recipe for success- in Cooking." [pet_shop_owner_yanille] diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt b/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt new file mode 100644 index 0000000000..9a1d0827f2 --- /dev/null +++ b/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt @@ -0,0 +1,22 @@ +package content.area.kandarin.yanille + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import world.gregs.voidps.engine.Script + +class Frenita : Script { + init { + npcOperate("Talk-to", "frenita") { (target) -> + npc("Would you like to buy some cooking equipment?") + choice { + option("Yes please.") { + openShop(target.def["shop"]) + } + option("No thank you.") + } + } + } +} \ No newline at end of file From cbbb49bd9e2233cf7234ca9e673bfd4e3ee4b097 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 13:22:32 +0100 Subject: [PATCH 21/34] Add Nardah shops and npcs --- .../nardah/nardah.npc-spawns.toml | 2 +- .../kharidian_desert/nardah/nardah.npcs.toml | 4 ++ .../kharidian_desert/nardah/nardah.vars.toml | 3 + .../area/kharidian_desert/nardah/Artimeus.kt | 28 +++++++++ .../area/kharidian_desert/nardah/Kazemde.kt | 22 +++++++ .../area/kharidian_desert/nardah/Nkuku.kt | 21 +++++++ .../area/kharidian_desert/nardah/Rokuh.kt | 32 ++++++++++ .../area/kharidian_desert/nardah/Seddu.kt | 22 +++++++ .../area/kharidian_desert/nardah/Zahur.kt | 60 +++++++++++++++++++ .../entity/player/command/TeleportCommands.kt | 1 + 10 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 data/area/kharidian_desert/nardah/nardah.vars.toml create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Artimeus.kt create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Kazemde.kt create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Nkuku.kt create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Rokuh.kt create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Seddu.kt create mode 100644 game/src/main/kotlin/content/area/kharidian_desert/nardah/Zahur.kt diff --git a/data/area/kharidian_desert/nardah/nardah.npc-spawns.toml b/data/area/kharidian_desert/nardah/nardah.npc-spawns.toml index 3ec15e0103..4b3f880ab5 100644 --- a/data/area/kharidian_desert/nardah/nardah.npc-spawns.toml +++ b/data/area/kharidian_desert/nardah/nardah.npc-spawns.toml @@ -10,7 +10,7 @@ spawns = [ { id = "meskhenet", x = 3427, y = 2900 }, { id = "zahra", x = 3448, y = 2898 }, { id = "zahur", x = 3442, y = 2886 }, - { id = "seddu", x = 3407, y = 2926 }, + { id = "seddu", x = 3406, y = 2922 }, { id = "kazemde", x = 3414, y = 2906 }, { id = "awusah_the_mayor", x = 3442, y = 2912, members = true }, { id = "tarik_nardah", x = 3438, y = 2917 }, diff --git a/data/area/kharidian_desert/nardah/nardah.npcs.toml b/data/area/kharidian_desert/nardah/nardah.npcs.toml index cf4c3b5518..67fcd2b1d7 100644 --- a/data/area/kharidian_desert/nardah/nardah.npcs.toml +++ b/data/area/kharidian_desert/nardah/nardah.npcs.toml @@ -1,5 +1,6 @@ [rokuh] id = 13181 +shop = "roks_chocs_box" [ghaslor_the_elder] id = 3029 @@ -35,10 +36,12 @@ examine = "A herbalist... sells herbs." [seddu] id = 3038 +shop = "seddus_adventurers_store" examine = "An armour merchant." [kazemde] id = 3039 +shop = "nardah_general_store" examine = "A seller of many things." [awusah_the_mayor] @@ -106,6 +109,7 @@ examine = "They'll eat anything!" [artimeus] id = 5109 +shop = "nardah_hunter_shop" examine = "He looks like he knows what he's doing." [ali_the_wise] diff --git a/data/area/kharidian_desert/nardah/nardah.vars.toml b/data/area/kharidian_desert/nardah/nardah.vars.toml new file mode 100644 index 0000000000..e90d0948be --- /dev/null +++ b/data/area/kharidian_desert/nardah/nardah.vars.toml @@ -0,0 +1,3 @@ +[spoke_nardah_herblist] +format = "boolean" +persist = true \ No newline at end of file diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Artimeus.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Artimeus.kt new file mode 100644 index 0000000000..4f34eedf10 --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Artimeus.kt @@ -0,0 +1,28 @@ +package content.area.kharidian_desert.nardah + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import world.gregs.voidps.engine.Script + +class Artimeus : Script { + init { + npcOperate("Talk-to", "artimeus") { (target) -> + npc("Greetings, friend; my business here deals with Hunter related items. Is there anything in which I can interest you?") + choice { + option("What kinds of items do you stock?") { + npc("Take a look for yourself.") + openShop(target.def["shop"]) + } + option("I'm not in the market for Hunter equipment right now, thanks.") { + player("I'm not in the market for Hunter equipment right now, thanks.") + npc("Maybe another time, then.") + } + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Kazemde.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Kazemde.kt new file mode 100644 index 0000000000..e4ba0fba4a --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Kazemde.kt @@ -0,0 +1,22 @@ +package content.area.kharidian_desert.nardah + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import world.gregs.voidps.engine.Script + +class Kazemde : Script { + init { + npcOperate("Talk-to", "kazemde") { (target) -> + npc("Can I help you at all?") + choice { + option("Yes please. What are you selling?") { + openShop(target.def["shop"]) + } + option("No thank you.") + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Nkuku.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Nkuku.kt new file mode 100644 index 0000000000..566ac636f3 --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Nkuku.kt @@ -0,0 +1,21 @@ +package content.area.kharidian_desert.nardah + +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.questCompleted +import world.gregs.voidps.engine.Script + +class Nkuku : Script { + init { + npcOperate("Talk-to", "nkuku") { (target) -> + if (questCompleted("spirits_of_the_elid")) { + npc("It still shocks me that the spirits of the river would put such a nasty curse on us for something so small.") + player("Well strange are the ways of ancient beings such as them.") + } else { + player("Good day to you.") + npc("May Saradomin be with you.") + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Rokuh.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Rokuh.kt new file mode 100644 index 0000000000..02fc6813c6 --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Rokuh.kt @@ -0,0 +1,32 @@ +package content.area.kharidian_desert.nardah + +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player +import content.quest.questCompleted +import world.gregs.voidps.engine.Script + +class Rokuh : Script { + init { + npcOperate("Talk-to", "rokuh*") { (target) -> + npc("Come one, come all, buy my amazing choc ice invention here! Chocolate on the outside. Iced cream on the inside. Oh I also have some chocolate left over from making choc ices which I'm selling too.") + choice { + option("Cool I'd like to buy some.") + option("No thanks, I'm not interested.") + option("How do you stop your icy snacks melting?") { + player("How do you stop your icy snacks melting? The middle of the desert is the last place I'd expect to see ice.") + npc("It's quite a surprise, isn't it, my friend. I actually have this special magic box of ice, which I bought for a princely sum, from a strange man while adventuring in lands far away. He said it is imbued with some sort of") + npc("powerful ice magic which keeps it cold all the time. I had never seen any ice magic before, it's clearly very rare and powerful, so I felt I had to buy it.") + } + if (questCompleted("spirits_of_the_elid")) { + option("How's business, now that the curse has gone?") { + npc("Ohh as good as ever. The people of Nardah seem to have taken well to the taste of chocolate and iced cream combined and, with the curse gone, all my friends are much happier too, so it's all good.") + } + } + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Seddu.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Seddu.kt new file mode 100644 index 0000000000..a795056243 --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Seddu.kt @@ -0,0 +1,22 @@ +package content.area.kharidian_desert.nardah + +import content.entity.npc.shop.openShop +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import world.gregs.voidps.engine.Script + +class Seddu : Script { + init { + npcOperate("Talk-to", "seddu") { (target) -> + npc("I buy and sell adventurer's equipment, do you want to trade?") + choice { + option("Yes please.") { + openShop(target.def["shop"]) + } + option("No thank you.") + } + } + } +} diff --git a/game/src/main/kotlin/content/area/kharidian_desert/nardah/Zahur.kt b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Zahur.kt new file mode 100644 index 0000000000..33b2b9b282 --- /dev/null +++ b/game/src/main/kotlin/content/area/kharidian_desert/nardah/Zahur.kt @@ -0,0 +1,60 @@ +package content.area.kharidian_desert.nardah + +import content.entity.player.dialogue.Happy +import content.entity.player.dialogue.Quiz +import content.entity.player.dialogue.Sad +import content.entity.player.dialogue.Shifty +import content.entity.player.dialogue.type.ChoiceOption +import content.entity.player.dialogue.type.choice +import content.entity.player.dialogue.type.npc +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message + +class Zahur : Script { + init { + npcOperate("Talk-to", "zahur") { (target) -> + if (get("spoke_nardah_herblist", false)) { + npc("Hello again. Do you need anything?") + } else { + npc("Hi there. Do you need anything? I can clean herbs for a fee of 200 coins per herb. I can clean all of your herbs at once, or I can do individual herbs if you show them to me.") + set("spoke_nardah_herblist", true) + npc("I can also decant your potion vials to into different doses. That service is free so long as you have the empty vials needed. If not, it's just a few coins.") + } + choice { + cleanHerbs() + decantPotions() + whyPay() + anotherTime() + } + } + } + + private fun ChoiceOption.whyPay() { + option("Why would I pay for you to clean herbs?") { + npc("Many of my customers wish to sell the herbs which they can't clean to other adventurers. They might hope for a better price by doing this.") + choice { + cleanHerbs() + decantPotions() + anotherTime() + } + } + } + + private fun ChoiceOption.anotherTime() { + option("Maybe another time.") + } + + private fun ChoiceOption.decantPotions() { + option("Please decant my potions.") { + npc("I didn't find anything that I could decant.") + message("Not yet implemented.") // TODO + } + } + + private fun ChoiceOption.cleanHerbs() { + option("Please clean all my herbs.") { + npc("I didn't find anything that I could clean.") + message("Not yet implemented.") // TODO + } + } +} diff --git a/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt b/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt index 698db52d34..36b4a008b7 100644 --- a/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/TeleportCommands.kt @@ -65,6 +65,7 @@ class TeleportCommands( "uzer" to Tile(3484, 3092), "pollnivneach" to Tile(3357, 2968), "daemonheim" to Tile(3450, 3710), + "naradh" to Tile(3432, 2916), ) init { From 846916ee6ee8c3f53193d8483edbc283e13fb767 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 13:34:44 +0100 Subject: [PATCH 22/34] Fix waterskin cactus refilling --- .../area/kharidian_desert/DesertHeat.kt | 2 +- .../area/kharidian_desert/DesertHeatTest.kt | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 game/src/test/kotlin/content/area/kharidian_desert/DesertHeatTest.kt diff --git a/game/src/main/kotlin/content/area/kharidian_desert/DesertHeat.kt b/game/src/main/kotlin/content/area/kharidian_desert/DesertHeat.kt index 6bd5f0730d..6bf87b399e 100644 --- a/game/src/main/kotlin/content/area/kharidian_desert/DesertHeat.kt +++ b/game/src/main/kotlin/content/area/kharidian_desert/DesertHeat.kt @@ -72,7 +72,7 @@ class DesertHeat : Script { message("You fail to cut the cactus correctly and it gives no water this time.") return@objectOperate } - if (!inventory.replace("waterskin_0", "waterkin_1") && !inventory.replace("waterskin_1", "waterkin_2") && !inventory.replace("waterskin_2", "waterkin_3") && !inventory.replace("waterskin_3", "waterkin_4")) { + if (!inventory.replace("waterskin_0", "waterskin_1") && !inventory.replace("waterskin_1", "waterskin_2") && !inventory.replace("waterskin_2", "waterskin_3") && !inventory.replace("waterskin_3", "waterskin_4")) { message("You fail to cut the cactus correctly and it gives no water this time.") return@objectOperate } diff --git a/game/src/test/kotlin/content/area/kharidian_desert/DesertHeatTest.kt b/game/src/test/kotlin/content/area/kharidian_desert/DesertHeatTest.kt new file mode 100644 index 0000000000..d9b9a4b9fd --- /dev/null +++ b/game/src/test/kotlin/content/area/kharidian_desert/DesertHeatTest.kt @@ -0,0 +1,61 @@ +package content.area.kharidian_desert + +import FakeRandom +import WorldTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.client.instruction.handle.interactObject +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.setRandom + +class DesertHeatTest : WorldTest() { + + @Test + fun `Desert heat kills over time`() { + val player = createPlayer(Tile(3383, 2872)) + tick(1) + + assertTrue(player.timers.contains("desert_heat")) + tick(150) + + assertEquals(90, player.levels.get(Skill.Constitution)) + } + + @Test + fun `Desert heat usees waterskins`() { + val player = createPlayer(Tile(3383, 2872)) + player.inventory.add("waterskin_2") + tick(1) + + assertTrue(player.timers.contains("desert_heat")) + tick(150) + + assertTrue(player.inventory.contains("waterskin_1")) + assertFalse(player.inventory.contains("waterskin_2")) + assertEquals(100, player.levels.get(Skill.Constitution)) + } + + @Test + fun `Refill waterskin with cactus juice`() { + setRandom(object : FakeRandom() { + override fun nextInt(until: Int) = until - 1 + }) + // It'll quench ya! + val player = createPlayer(Tile(3383, 2872)) + player.inventory.add("waterskin_0") + player.inventory.add("knife") + tick(1) + + val cactus = GameObjects.find(Tile(3382, 2872), "desert_cactus_full") + + player.interactObject(cactus, "Cut") + tick(2) + + assertTrue(player.inventory.contains("waterskin_1")) + assertFalse(player.inventory.contains("waterskin_0")) + } +} From 5f7b867f3893b9df17b058ee8e3feff47fab0b83 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 13:35:40 +0100 Subject: [PATCH 23/34] Formatting --- game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt b/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt index 9a1d0827f2..3a29447995 100644 --- a/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt +++ b/game/src/main/kotlin/content/area/kandarin/yanille/Frenita.kt @@ -19,4 +19,4 @@ class Frenita : Script { } } } -} \ No newline at end of file +} From ae6fa7f76c2033021f53c3888f286c67257a6cce Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 14:11:42 +0100 Subject: [PATCH 24/34] Add crystal chest opening --- .../asgarnia/taverley/taverley.drops.toml | 80 +++++++++++++++++++ .../area/asgarnia/taverley/taverley.objs.toml | 7 ++ .../asgarnia/taverley/taverley.recipes.toml | 4 + .../big_chompy_bird_hunting.anims.toml | 3 - .../area/asgarnia/taverley/CrystalChest.kt | 37 +++++++++ .../content/skill/thieving/TrapChests.kt | 2 +- 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 data/area/asgarnia/taverley/taverley.objs.toml create mode 100644 data/area/asgarnia/taverley/taverley.recipes.toml create mode 100644 game/src/main/kotlin/content/area/asgarnia/taverley/CrystalChest.kt diff --git a/data/area/asgarnia/taverley/taverley.drops.toml b/data/area/asgarnia/taverley/taverley.drops.toml index d41b32a686..9c421d05ef 100644 --- a/data/area/asgarnia/taverley/taverley.drops.toml +++ b/data/area/asgarnia/taverley/taverley.drops.toml @@ -28,3 +28,83 @@ drops = [ { id = "limpwurt_root", chance = 3 }, { id = "antipoison_3" }, ] + +[crystal_chest_drop_table] +type = "all" +drops = [ + { id = "uncut_dragonstone" }, + { table = "crystal_chest_secondary" }, +] + +[crystal_chest_secondary] +roll = 128 +drops = [ + { table = "crystal_chest_1", chance = 34 }, + { table = "crystal_chest_2", chance = 12 }, + { table = "crystal_chest_3", chance = 12 }, + { id = "rune_bar", amount = 3, chance = 12 }, + { table = "crystal_chest_5", chance = 10 }, + { id = "iron_ore_noted", amount = 150, chance = 10 }, + { id = "coal_noted", amount = 100, chance = 10 }, + { table = "crystal_chest_8", chance = 8 }, + { id = "adamant_sq_shield", chance = 2 }, + { table = "crystal_chest_10", chance = 1 }, +] + +[crystal_chest_1] +type = "all" +drops = [ + { id = "spinach_roll" }, + { id = "coins", amount = 2000 }, +] + +[crystal_chest_2] +type = "all" +drops = [ + { id = "air_rune", amount = 50 }, + { id = "water_rune", amount = 50 }, + { id = "earth_rune", amount = 50 }, + { id = "fire_rune", amount = 50 }, + { id = "body_rune", amount = 50 }, + { id = "mind_rune", amount = 50 }, + { id = "chaos_rune", amount = 10 }, + { id = "death_rune", amount = 10 }, + { id = "cosmic_rune", amount = 10 }, + { id = "nature_rune", amount = 10 }, + { id = "law_rune", amount = 10 }, +] + +[crystal_chest_3] +type = "all" +drops = [ + { id = "ruby", amount = 2 }, + { id = "diamond", amount = 2 }, +] + +[crystal_chest_5] +type = "all" +drops = [ + { id = "coins", amount = 750 }, + { table = "crystal_key" }, +] + +[crystal_key] +roll = 10 +drops = [ + { id = "loop_half_of_a_key", chance = 5 }, + { id = "tooth_half_of_a_key", chance = 5 }, +] + +[crystal_chest_8] +type = "all" +drops = [ + { id = "raw_swordfish", amount = 5 }, + { id = "coins", amount = 1000 }, +] + +[crystal_chest_10] +roll = 2 +drops = [ + { id = "rune_platelegs" }, + { id = "rune_plateskirt" }, +] \ No newline at end of file diff --git a/data/area/asgarnia/taverley/taverley.objs.toml b/data/area/asgarnia/taverley/taverley.objs.toml new file mode 100644 index 0000000000..266ca31e7c --- /dev/null +++ b/data/area/asgarnia/taverley/taverley.objs.toml @@ -0,0 +1,7 @@ +[crystal_chest] +id = 172 +examine = "I wonder what's inside." + +[crystal_chest_open] +id = 173 +examine = "Perhaps I should search it." diff --git a/data/area/asgarnia/taverley/taverley.recipes.toml b/data/area/asgarnia/taverley/taverley.recipes.toml new file mode 100644 index 0000000000..92da27cf3f --- /dev/null +++ b/data/area/asgarnia/taverley/taverley.recipes.toml @@ -0,0 +1,4 @@ +[crystal_key] +remove = ["tooth_half_of_a_key", "loop_half_of_a_key"] +add = ["crystal_key"] +message = "You join the two halves of the key together." diff --git a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml index 310ce63418..24f8d4cd51 100644 --- a/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml +++ b/data/quest/members/big_chompy_bird_hunting/big_chompy_bird_hunting.anims.toml @@ -40,6 +40,3 @@ id = 4433 [human_castcurse_walkmerge] id = 11428 - -[human_openchest] -id = 536 \ No newline at end of file diff --git a/game/src/main/kotlin/content/area/asgarnia/taverley/CrystalChest.kt b/game/src/main/kotlin/content/area/asgarnia/taverley/CrystalChest.kt new file mode 100644 index 0000000000..30f197c424 --- /dev/null +++ b/game/src/main/kotlin/content/area/asgarnia/taverley/CrystalChest.kt @@ -0,0 +1,37 @@ +package content.area.asgarnia.taverley + +import content.entity.player.inv.item.addOrDrop +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.sound +import world.gregs.voidps.engine.entity.item.drop.DropTables +import world.gregs.voidps.engine.entity.obj.replace +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.remove + +class CrystalChest(val dropTables: DropTables) : Script { + init { + objectOperate("Open", "crystal_chest") { (target) -> + val table = dropTables.get("crystal_chest_drop_table") ?: return@objectOperate + if (!inventory.contains("crystal_key")) { + message("This chest is securely locked shut.") + sound("locked") + return@objectOperate + } + anim("open_chest") + sound("locked") + message("You unlock the chest with your key.", ChatType.Filter) + target.replace("crystal_chest_open", ticks = 2) + delay(1) + if (inventory.remove("crystal_key")) { + val drops = table.roll() + for (drop in drops) { + val item = drop.toItem() + addOrDrop(item.id, item.amount) + } + message("You find some treasure in the chest!") + } + } + } +} diff --git a/game/src/main/kotlin/content/skill/thieving/TrapChests.kt b/game/src/main/kotlin/content/skill/thieving/TrapChests.kt index 7c4cb10247..2b2b51e075 100644 --- a/game/src/main/kotlin/content/skill/thieving/TrapChests.kt +++ b/game/src/main/kotlin/content/skill/thieving/TrapChests.kt @@ -19,7 +19,7 @@ import world.gregs.voidps.engine.inv.transact.operation.AddItem.add class TrapChests(val tables: DropTables) : Script { init { - objectOperate("Open", "*_chest") { (target) -> + objectOperate("Open", "dorgesh_kaan_chest,nature_chest,barrows_chest,culinaromancers_chest,blood_rune_chest,dorgesh_kaan_gem_chest,shantay_chest,capn_arnav_chest,coin_chest,ardougne_castle_chest,steel_arrows_chest,maze_chest,lumbridge_closed_chest,big_coin_chest,edgeville_chest") { (target) -> when (target.id) { "coin_chest" -> damage(20 + levels.get(Skill.Constitution) / 10) "big_coin_chest" -> damage(40 + (levels.get(Skill.Constitution) * 0.15).toInt()) From 6a68cf0d3e56f54f31d6bd19df11a2bbd8ee5638 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 14:25:07 +0100 Subject: [PATCH 25/34] Fix items kept on death type defaults causing incorrect priorities --- .../world/gregs/voidps/engine/entity/item/ItemKept.kt | 6 ++---- .../kotlin/content/entity/player/kept/ItemsKeptOnDeath.kt | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/item/ItemKept.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/item/ItemKept.kt index 5cda6ada15..3155707cf5 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/item/ItemKept.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/item/ItemKept.kt @@ -27,9 +27,7 @@ enum class ItemKept { /** * Disappears regardless of if kept on death */ - Vanish, - - ; + Vanish; companion object { private val map = mapOf( @@ -40,6 +38,6 @@ enum class ItemKept { "Vanish" to Vanish, ) - fun by(name: String): ItemKept = map[name] ?: Never + fun by(name: String): ItemKept = map[name] ?: Wilderness } } diff --git a/game/src/main/kotlin/content/entity/player/kept/ItemsKeptOnDeath.kt b/game/src/main/kotlin/content/entity/player/kept/ItemsKeptOnDeath.kt index fd1b186540..c7da11717c 100644 --- a/game/src/main/kotlin/content/entity/player/kept/ItemsKeptOnDeath.kt +++ b/game/src/main/kotlin/content/entity/player/kept/ItemsKeptOnDeath.kt @@ -39,7 +39,7 @@ object ItemsKeptOnDeath { queue.pop() continue } - when (val type = item.def["kept", ItemKept.Never]) { + when (val type = item.def["kept", ItemKept.Wilderness]) { ItemKept.Never, ItemKept.Vanish -> { queue.pop() continue From 6cb17739f85793b9754ad7072d994310f917ce1c Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 14:27:40 +0100 Subject: [PATCH 26/34] Fix mining stardust not giving exp after collecting over 200 --- game/src/main/kotlin/content/skill/mining/Mining.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/skill/mining/Mining.kt b/game/src/main/kotlin/content/skill/mining/Mining.kt index 331b4c2580..a208b86b67 100644 --- a/game/src/main/kotlin/content/skill/mining/Mining.kt +++ b/game/src/main/kotlin/content/skill/mining/Mining.kt @@ -120,7 +120,7 @@ class Mining : Script { val xp = ore.int("xp") / 10.0 ShootingStarHandler.extraOreHandler(this, item, xp) val added = addOre(this, item, target) - if (added > 0) { + if (added > 0 || item == "stardust") { exp(Skill.Mining, xp * added) } if (added < 1 || deplete(target, ore)) { From 1e1c7640d4e666898b990df84e20683518f999ff Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 14:58:17 +0100 Subject: [PATCH 27/34] Fix bobs item repairing --- .../lumbridge/bobs_item_repair.tables.toml | 392 ++++++++++++++++++ .../content/area/misthalin/lumbridge/Bob.kt | 24 +- 2 files changed, 403 insertions(+), 13 deletions(-) create mode 100644 data/area/misthalin/lumbridge/bobs_item_repair.tables.toml diff --git a/data/area/misthalin/lumbridge/bobs_item_repair.tables.toml b/data/area/misthalin/lumbridge/bobs_item_repair.tables.toml new file mode 100644 index 0000000000..0437abe481 --- /dev/null +++ b/data/area/misthalin/lumbridge/bobs_item_repair.tables.toml @@ -0,0 +1,392 @@ +[repairable_items] +repaired = "item" + +[.ahrims_hood_100] +repaired = "ahrims_hood" + +[.ahrims_hood_75] +repaired = "ahrims_hood" + +[.ahrims_hood_50] +repaired = "ahrims_hood" + +[.ahrims_hood_25] +repaired = "ahrims_hood" + +[.ahrims_hood_broken] +repaired = "ahrims_hood" + +[.ahrims_staff_100] +repaired = "ahrims_staff" + +[.ahrims_staff_75] +repaired = "ahrims_staff" + +[.ahrims_staff_50] +repaired = "ahrims_staff" + +[.ahrims_staff_25] +repaired = "ahrims_staff" + +[.ahrims_staff_broken] +repaired = "ahrims_staff" + +[.ahrims_robe_top_100] +repaired = "ahrims_robe_top" + +[.ahrims_robe_top_75] +repaired = "ahrims_robe_top" + +[.ahrims_robe_top_50] +repaired = "ahrims_robe_top" + +[.ahrims_robe_top_25] +repaired = "ahrims_robe_top" + +[.ahrims_robe_top_broken] +repaired = "ahrims_robe_top" + +[.ahrims_robe_skirt_100] +repaired = "ahrims_robe_skirt" + +[.ahrims_robe_skirt_75] +repaired = "ahrims_robe_skirt" + +[.ahrims_robe_skirt_50] +repaired = "ahrims_robe_skirt" + +[.ahrims_robe_skirt_25] +repaired = "ahrims_robe_skirt" + +[.ahrims_robe_skirt_broken] +repaired = "ahrims_robe_skirt" + +[.dharoks_helm_100] +repaired = "dharoks_helm" + +[.dharoks_helm_75] +repaired = "dharoks_helm" + +[.dharoks_helm_50] +repaired = "dharoks_helm" + +[.dharoks_helm_25] +repaired = "dharoks_helm" + +[.dharoks_helm_broken] +repaired = "dharoks_helm" + +[.dharoks_greataxe_100] +repaired = "dharoks_greataxe" + +[.dharoks_greataxe_75] +repaired = "dharoks_greataxe" + +[.dharoks_greataxe_50] +repaired = "dharoks_greataxe" + +[.dharoks_greataxe_25] +repaired = "dharoks_greataxe" + +[.dharoks_greataxe_broken] +repaired = "dharoks_greataxe" + +[.dharoks_platebody_100] +repaired = "dharoks_platebody" + +[.dharoks_platebody_75] +repaired = "dharoks_platebody" + +[.dharoks_platebody_50] +repaired = "dharoks_platebody" + +[.dharoks_platebody_25] +repaired = "dharoks_platebody" + +[.dharoks_platebody_broken] +repaired = "dharoks_platebody" + +[.dharoks_platelegs_100] +repaired = "dharoks_platelegs" + +[.dharoks_platelegs_75] +repaired = "dharoks_platelegs" + +[.dharoks_platelegs_50] +repaired = "dharoks_platelegs" + +[.dharoks_platelegs_25] +repaired = "dharoks_platelegs" + +[.dharoks_platelegs_broken] +repaired = "dharoks_platelegs" + +[.guthans_helm_100] +repaired = "guthans_helm" + +[.guthans_helm_75] +repaired = "guthans_helm" + +[.guthans_helm_50] +repaired = "guthans_helm" + +[.guthans_helm_25] +repaired = "guthans_helm" + +[.guthans_helm_broken] +repaired = "guthans_helm" + +[.guthans_warspear_100] +repaired = "guthans_warspear" + +[.guthans_warspear_75] +repaired = "guthans_warspear" + +[.guthans_warspear_50] +repaired = "guthans_warspear" + +[.guthans_warspear_25] +repaired = "guthans_warspear" + +[.guthans_warspear_broken] +repaired = "guthans_warspear" + +[.guthans_platebody_100] +repaired = "guthans_platebody" + +[.guthans_platebody_75] +repaired = "guthans_platebody" + +[.guthans_platebody_50] +repaired = "guthans_platebody" + +[.guthans_platebody_25] +repaired = "guthans_platebody" + +[.guthans_platebody_broken] +repaired = "guthans_platebody" + +[.guthans_chainskirt_100] +repaired = "guthans_chainskirt" + +[.guthans_chainskirt_75] +repaired = "guthans_chainskirt" + +[.guthans_chainskirt_50] +repaired = "guthans_chainskirt" + +[.guthans_chainskirt_25] +repaired = "guthans_chainskirt" + +[.guthans_chainskirt_broken] +repaired = "guthans_chainskirt" + +[.karils_coif_100] +repaired = "karils_coif" + +[.karils_coif_75] +repaired = "karils_coif" + +[.karils_coif_50] +repaired = "karils_coif" + +[.karils_coif_25] +repaired = "karils_coif" + +[.karils_coif_broken] +repaired = "karils_coif" + +[.karils_crossbow_100] +repaired = "karils_crossbow" + +[.karils_crossbow_75] +repaired = "karils_crossbow" + +[.karils_crossbow_50] +repaired = "karils_crossbow" + +[.karils_crossbow_25] +repaired = "karils_crossbow" + +[.karils_crossbow_broken] +repaired = "karils_crossbow" + +[.karils_top_100] +repaired = "karils_top" + +[.karils_top_75] +repaired = "karils_top" + +[.karils_top_50] +repaired = "karils_top" + +[.karils_top_25] +repaired = "karils_top" + +[.karils_top_broken] +repaired = "karils_top" + +[.karils_skirt_100] +repaired = "karils_skirt" + +[.karils_skirt_75] +repaired = "karils_skirt" + +[.karils_skirt_50] +repaired = "karils_skirt" + +[.karils_skirt_25] +repaired = "karils_skirt" + +[.karils_skirt_broken] +repaired = "karils_skirt" + +[.torags_helm_100] +repaired = "torags_helm" + +[.torags_helm_75] +repaired = "torags_helm" + +[.torags_helm_50] +repaired = "torags_helm" + +[.torags_helm_25] +repaired = "torags_helm" + +[.torags_helm_broken] +repaired = "torags_helm" + +[.torags_hammers_100] +repaired = "torags_hammers" + +[.torags_hammers_75] +repaired = "torags_hammers" + +[.torags_hammers_50] +repaired = "torags_hammers" + +[.torags_hammers_25] +repaired = "torags_hammers" + +[.torags_hammers_broken] +repaired = "torags_hammers" + +[.torags_platebody_100] +repaired = "torags_platebody" + +[.torags_platebody_75] +repaired = "torags_platebody" + +[.torags_platebody_50] +repaired = "torags_platebody" + +[.torags_platebody_25] +repaired = "torags_platebody" + +[.torags_platebody_broken] +repaired = "torags_platebody" + +[.torags_platelegs_100] +repaired = "torags_platelegs" + +[.torags_platelegs_75] +repaired = "torags_platelegs" + +[.torags_platelegs_50] +repaired = "torags_platelegs" + +[.torags_platelegs_25] +repaired = "torags_platelegs" + +[.torags_platelegs_broken] +repaired = "torags_platelegs" + +[.veracs_helm_100] +repaired = "veracs_helm" + +[.veracs_helm_75] +repaired = "veracs_helm" + +[.veracs_helm_50] +repaired = "veracs_helm" + +[.veracs_helm_25] +repaired = "veracs_helm" + +[.veracs_helm_broken] +repaired = "veracs_helm" + +[.veracs_flail_100] +repaired = "veracs_flail" + +[.veracs_flail_75] +repaired = "veracs_flail" + +[.veracs_flail_50] +repaired = "veracs_flail" + +[.veracs_flail_25] +repaired = "veracs_flail" + +[.veracs_flail_broken] +repaired = "veracs_flail" + +[.veracs_brassard_100] +repaired = "veracs_brassard" + +[.veracs_brassard_75] +repaired = "veracs_brassard" + +[.veracs_brassard_50] +repaired = "veracs_brassard" + +[.veracs_brassard_25] +repaired = "veracs_brassard" + +[.veracs_brassard_broken] +repaired = "veracs_brassard" + +[.veracs_plateskirt_100] +repaired = "veracs_plateskirt" + +[.veracs_plateskirt_75] +repaired = "veracs_plateskirt" + +[.veracs_plateskirt_50] +repaired = "veracs_plateskirt" + +[.veracs_plateskirt_25] +repaired = "veracs_plateskirt" + +[.veracs_plateskirt_broken] +repaired = "veracs_plateskirt" + +[.torva_full_helm_broken] +repaired = "torva_full_helm" + +[.torva_platebody_broken] +repaired = "torva_platebody" + +[.torva_platelegs_broken] +repaired = "torva_platelegs" + +[.pernix_cowl_broken] +repaired = "pernix_cowl" + +[.pernix_body_broken] +repaired = "pernix_body" + +[.pernix_chaps_broken] +repaired = "pernix_chaps" + +[.virtus_mask_broken] +repaired = "virtus_mask" + +[.virtus_robe_top_broken] +repaired = "virtus_robe_top" + +[.virtus_robe_legs_broken] +repaired = "virtus_robe_legs" + +[.zaryte_bow_broken] +repaired = "zaryte_bow" diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/Bob.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/Bob.kt index 636e052f4a..1d909d39f4 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/Bob.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/Bob.kt @@ -7,14 +7,17 @@ import content.entity.player.dialogue.Quiz import content.entity.player.dialogue.Sad import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc +import content.entity.player.dialogue.type.player import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.data.definition.Tables import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.level.Interpolation.interpolate import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.transact.operation.AddItem.add +import world.gregs.voidps.engine.inv.transact.operation.ClearItem.clear import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove -import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace class Bob : Script { @@ -34,8 +37,9 @@ class Bob : Script { } } - itemOnNPCOperate("*", "bob") { (_, item) -> - if (!repairable(item.id)) { + itemOnNPCOperate("*", "bob") { (_, item, slot) -> + val repaired = Tables.itemOrNull("repairable_items.${item.id}.repaired") + if (repaired == null) { npc("Sorry friend, but I can't do anything with that.") return@itemOnNPCOperate } @@ -45,10 +49,13 @@ class Bob : Script { option("Yes I'm sure!") { val repaired = inventory.transaction { remove("coins", cost) - replace(item.id, repaired(item.id)) + clear(slot) + add(repaired) } if (repaired) { npc("There you go. It's a pleasure doing business with you!") + } else { + player("Oh dear, I don't seem to have enough money.") } } option("On second thoughts, no thanks.") @@ -56,15 +63,6 @@ class Bob : Script { } } - fun repairable(item: String) = item.endsWith("_100") || item.endsWith("_75") || item.endsWith("_50") || item.endsWith("_25") || item.endsWith("_broken") - - fun repaired(item: String) = item - .replace("_100", "_new") - .replace("_75", "_new") - .replace("_50", "_new") - .replace("_25", "_new") - .replace("_broken", "_new") - fun repairCost(player: Player, item: Item): Int { val cost = item.def.cost val max = (cost * 0.6).toInt() From 07ec0c9f991edaaba1b5ca0e9405052199f5e632 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:00:31 +0100 Subject: [PATCH 28/34] Fix nechryael test --- .../content/area/morytania/slayer_tower/NechryaelTest.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt b/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt index a1c0058167..9bb9c1c4f0 100644 --- a/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt +++ b/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt @@ -4,7 +4,10 @@ import FakeRandom import WorldTest import npcOption import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.client.instruction.handle.interactPlayer +import world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.type.setRandom @@ -45,11 +48,13 @@ class NechryaelTest : WorldTest() { val nechryael = NPCs.add("nechryael", emptyTile.addY(1)) tick() - player.npcOption(nechryael, "Attack") + val spawn = NPCs.add("death_spawn", emptyTile.addY(1)) + spawn.interactPlayer(player, "Attack") + tick(2) - assertEquals(1, player["death_spawns", 0]) + assertTrue(spawn.mode is CombatMovement) } } From 4fa45e744754d3986f42ca65162f80d7aa2981df Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:12:50 +0100 Subject: [PATCH 29/34] Add freaky forester exit portal --- .../freaky_forester/freaky_forester.objs.toml | 3 +++ .../event/random/RandomEventKidnap.kt | 2 +- .../event/random/RandomEventTrigger.kt | 2 +- .../activity/event/random/RandomEvents.kt | 3 +++ .../random/freaky_forester/FreakyForester.kt | 24 ++++++++++++++++--- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/data/activity/event/random/freaky_forester/freaky_forester.objs.toml b/data/activity/event/random/freaky_forester/freaky_forester.objs.toml index ca30e8a7c6..04694cf1ad 100644 --- a/data/activity/event/random/freaky_forester/freaky_forester.objs.toml +++ b/data/activity/event/random/freaky_forester/freaky_forester.objs.toml @@ -1,3 +1,6 @@ [bank_deposit_box_freaky_forester] id = 32931 examine = "Lets you put items into your bank." + +[exit_portal_freaky_forester] +id = 15645 \ No newline at end of file diff --git a/game/src/main/kotlin/content/activity/event/random/RandomEventKidnap.kt b/game/src/main/kotlin/content/activity/event/random/RandomEventKidnap.kt index 06a187866a..11dd1b40b6 100644 --- a/game/src/main/kotlin/content/activity/event/random/RandomEventKidnap.kt +++ b/game/src/main/kotlin/content/activity/event/random/RandomEventKidnap.kt @@ -114,7 +114,7 @@ suspend fun Player.returnHome(vararg rewards: String) { finishReturn(*rewards) } -private suspend fun Player.finishReturn(vararg rewards: String) { +private fun Player.finishReturn(vararg rewards: String) { walkTrigger = null RandomEvents.complete(this, *rewards) anim("teleport_land_modern") diff --git a/game/src/main/kotlin/content/activity/event/random/RandomEventTrigger.kt b/game/src/main/kotlin/content/activity/event/random/RandomEventTrigger.kt index 3c9c3bd213..1855019656 100644 --- a/game/src/main/kotlin/content/activity/event/random/RandomEventTrigger.kt +++ b/game/src/main/kotlin/content/activity/event/random/RandomEventTrigger.kt @@ -47,7 +47,7 @@ class RandomEventTrigger : Script { } } - adminCommand("random_event", stringArg("event", optional = true), desc = "Start a random event") { args -> + adminCommand("random_event", stringArg("event", optional = true, autofill = RandomEvents.keys), desc = "Start a random event") { args -> val event = args.getOrNull(0) ?: RandomEvents.pick() if (event == null || !RandomEvents.start(this, event)) { message("No random event found${if (args.isEmpty()) "" else " for '${args[0]}'"}.") diff --git a/game/src/main/kotlin/content/activity/event/random/RandomEvents.kt b/game/src/main/kotlin/content/activity/event/random/RandomEvents.kt index e0aaaf2b0f..4731ec6951 100644 --- a/game/src/main/kotlin/content/activity/event/random/RandomEvents.kt +++ b/game/src/main/kotlin/content/activity/event/random/RandomEvents.kt @@ -40,6 +40,9 @@ object RandomEvents : AutoCloseable { private val events = mutableMapOf Unit>() + val keys: Set + get() = events.keys + fun register(id: String, launcher: suspend Player.() -> Unit) { Script.checkLoading() events[id] = launcher diff --git a/game/src/main/kotlin/content/activity/event/random/freaky_forester/FreakyForester.kt b/game/src/main/kotlin/content/activity/event/random/freaky_forester/FreakyForester.kt index e801f7f7ee..c02db6b15c 100644 --- a/game/src/main/kotlin/content/activity/event/random/freaky_forester/FreakyForester.kt +++ b/game/src/main/kotlin/content/activity/event/random/freaky_forester/FreakyForester.kt @@ -8,6 +8,7 @@ import content.activity.event.random.rewardCostumePoint import content.entity.combat.killer import content.entity.player.dialogue.Happy import content.entity.player.dialogue.Neutral +import content.entity.player.dialogue.type.choice import content.entity.player.dialogue.type.npc import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message @@ -18,6 +19,7 @@ import world.gregs.voidps.engine.entity.character.player.name import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove +import world.gregs.voidps.engine.inv.removeToLimit import world.gregs.voidps.type.Tile import world.gregs.voidps.type.random @@ -57,10 +59,21 @@ class FreakyForester : Script { } death.dropItems = false // The pheasant drops the raw bird for the killer to pick up and hand to the forester. - val correct = tailCount(id) == killer.get("freaky_forester_task", 0) + val correct = tailCount(id) == killer["freaky_forester_task", 0] val bird = if (correct) "raw_pheasant" else "raw_pheasant_incorrect" FloorItems.add(tile, bird, revealTicks = FloorItems.NEVER, disappearTicks = 300, owner = killer) } + + objectOperate("Enter", "exit_portal_freaky_forester") { + choice("Are you sure you wish to leave?") { + // TODO proper message + option("Yes I'm sure.") { + clearState() + returnHome() + } + option("No, I'll stick around.") + } + } } private suspend fun Player.startEvent() { @@ -101,11 +114,16 @@ class FreakyForester : Script { private suspend fun Player.leaveForest() { rewardCostumePoint("lederhosen") - inventory.remove("raw_pheasant") - clear("freaky_forester_task") + clearState() returnHome("random_event_gift") } + private fun Player.clearState() { + inventory.removeToLimit("raw_pheasant", 28) + inventory.removeToLimit("raw_pheasant_incorrect", 28) + clear("freaky_forester_task") + } + private fun Player.carriesRawPheasant() = inventory.contains("raw_pheasant") || inventory.contains("raw_pheasant_incorrect") private fun tailCount(pheasantId: String) = pheasantId.removePrefix("pheasant_").substringBefore("_").toIntOrNull() ?: 0 From cdeef4015c15e288bd9b189392dba228ac636fc4 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:19:05 +0100 Subject: [PATCH 30/34] Fix nechryael test --- .../kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt b/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt index 9bb9c1c4f0..d368c1c929 100644 --- a/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt +++ b/game/src/test/kotlin/content/area/morytania/slayer_tower/NechryaelTest.kt @@ -30,7 +30,7 @@ class NechryaelTest : WorldTest() { player.npcOption(nechryael, "Attack") - tick(2) + tick(3) assertEquals(1, player["death_spawns", 0]) } From 398a6ab6e6b560568768d3f791cd79c1833a29b2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:31:55 +0100 Subject: [PATCH 31/34] Fix mime event soft lock --- .../activity/event/random/mime_theatre/Mime.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/activity/event/random/mime_theatre/Mime.kt b/game/src/main/kotlin/content/activity/event/random/mime_theatre/Mime.kt index 26d1705d5e..372147d292 100644 --- a/game/src/main/kotlin/content/activity/event/random/mime_theatre/Mime.kt +++ b/game/src/main/kotlin/content/activity/event/random/mime_theatre/Mime.kt @@ -16,6 +16,7 @@ import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.queue.strongQueue import world.gregs.voidps.engine.suspend.Suspension import world.gregs.voidps.engine.suspend.pauseString import world.gregs.voidps.type.Tile @@ -49,11 +50,19 @@ class Mime : Script { set("mime_correct", 0) } mysteriousOldMan() + walkTrigger { + strongQueue("start") { + start() + } + } kidnap(SPAWN) // The old man stays behind at the kidnap origin, so he narrates by id rather than talkWith. npc("mysterious_old_man", "Here's a little challenge for you:
Copy the Mime's performance, then you'll be released.") - walkOverDelay(WATCH) + start() + } + private suspend fun Player.start() { + walkOverDelay(WATCH) val mime = NPCs.firstOrNull(MIME_TILE) { it.id == "mime" } ?: NPCs.add("mime", MIME_TILE, ticks = -1, owner = this) runMime(mime) } From 6e44dbb98cb28642fff30d5b02801c84b5482bf7 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:32:12 +0100 Subject: [PATCH 32/34] Add talking to mysterious old man inside pinball --- .../activity/event/random/MysteriousOldMan.kt | 15 ++++++++++++ .../activity/event/random/pinball/Pinball.kt | 23 +++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 game/src/main/kotlin/content/activity/event/random/MysteriousOldMan.kt diff --git a/game/src/main/kotlin/content/activity/event/random/MysteriousOldMan.kt b/game/src/main/kotlin/content/activity/event/random/MysteriousOldMan.kt new file mode 100644 index 0000000000..4b287abe00 --- /dev/null +++ b/game/src/main/kotlin/content/activity/event/random/MysteriousOldMan.kt @@ -0,0 +1,15 @@ +package content.activity.event.random + +import content.activity.event.random.pinball.Pinball +import world.gregs.voidps.engine.Script + +class MysteriousOldMan : Script { + init { + npcOperate("Talk-to", "mysterious_old_man") { (target) -> + when (get("random_event", "")) { + "pinball" -> Pinball.explainPinball(this, target) + else -> return@npcOperate + } + } + } +} diff --git a/game/src/main/kotlin/content/activity/event/random/pinball/Pinball.kt b/game/src/main/kotlin/content/activity/event/random/pinball/Pinball.kt index f462262310..6a281c21b8 100644 --- a/game/src/main/kotlin/content/activity/event/random/pinball/Pinball.kt +++ b/game/src/main/kotlin/content/activity/event/random/pinball/Pinball.kt @@ -23,6 +23,7 @@ import world.gregs.voidps.engine.client.minimap import world.gregs.voidps.engine.client.ui.close import world.gregs.voidps.engine.client.ui.dialogue.talkWith import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.obj.GameObject @@ -118,15 +119,7 @@ class Pinball : Script { oldMan.face(this) face(oldMan) talkWith(oldMan) - npc("The rules of the game are quite simple. You have to score 10 points by tagging the flashing pillars.") - npc("Don't tag the ones that do not have rings around the base, as that will reset your points, and don't try and get past those trolls until you are done!") - npc("See you later!") - oldMan.anim("emote_wave") - delay(4) - oldMan.gfx("imp_puff") - delay(1) - NPCs.remove(oldMan) - statement("Tag the post with the $FLASHING_RINGS.", clickToContinue = false) + explainPinball(this, oldMan) } private suspend fun Player.tagPost(post: GameObject) { @@ -214,6 +207,18 @@ class Pinball : Script { private data class Post(val id: String, val tile: Tile) companion object { + suspend fun explainPinball(player: Player, oldMan: NPC) { + player.npc("The rules of the game are quite simple. You have to score 10 points by tagging the flashing pillars.") + player.npc("Don't tag the ones that do not have rings around the base, as that will reset your points, and don't try and get past those trolls until you are done!") + player.npc("See you later!") + oldMan.anim("emote_wave") + player.delay(4) + oldMan.gfx("imp_puff") + player.delay(1) + NPCs.remove(oldMan) + player.statement("Tag the post with the $FLASHING_RINGS.", clickToContinue = false) + } + private const val REQUIRED = 10 private const val FLASHING_RINGS = "flashing rings" private val REGION = Region(30, 78) From 31febaae910b37f87eea06cf763e1b20a39c14a3 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 15:48:15 +0100 Subject: [PATCH 33/34] Fix abyssal demon test --- .../kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt b/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt index 2c519b6d50..345fb98a74 100644 --- a/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt +++ b/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt @@ -3,11 +3,11 @@ package content.area.wilderness.abyss import FakeRandom import WorldTest import npcOption +import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.type.setRandom -import kotlin.test.assertNotEquals class AbyssalDemonTest : WorldTest() { From 3d562d3bfd8958d115a152bfd6a2e95695580804 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 2 Aug 2026 16:08:08 +0100 Subject: [PATCH 34/34] Tweak abyssal demon test --- .../kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt b/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt index 345fb98a74..0995f937aa 100644 --- a/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt +++ b/game/src/test/kotlin/content/area/wilderness/abyss/AbyssalDemonTest.kt @@ -34,6 +34,7 @@ class AbyssalDemonTest : WorldTest() { fun `Abyssal demon teleports around in combat`() { setRandom(object : FakeRandom() { override fun nextInt(until: Int) = if (until == 6) 6 else 0 + override fun nextInt(from: Int, until: Int) = 0 }) val player = createPlayer(emptyTile) player.levels.set(Skill.Constitution, 50)