From 96412c8ea99ece4811193e53e54de87d87cb1996 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Fri, 10 Apr 2026 06:16:43 -0700 Subject: [PATCH 01/17] Fix: NPCs spawned under a target couldn't attack --- .../entity/character/mode/interact/Interact.kt | 16 ++++++++++++++++ .../content/entity/combat/CombatMovementTest.kt | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt index 51d4d0bd7a..6ef1d067be 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt @@ -8,10 +8,14 @@ import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.move.Movement import world.gregs.voidps.engine.entity.character.mode.move.target.TargetStrategy +import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.cantReach import world.gregs.voidps.engine.entity.character.player.chat.noInterest +import world.gregs.voidps.engine.map.Overlap import world.gregs.voidps.engine.suspend.resumeSuspension +import world.gregs.voidps.type.Direction +import world.gregs.voidps.type.random /** * Moves a player within interact distance of [target] @@ -86,6 +90,18 @@ open class Interact( return } updateRange = false + val target = target + val npc = character as? NPC + if (npc != null && !npc.def["allowed_under", false] && target is Character && + Overlap.isUnder(npc.tile, npc.size, target.tile, target.size)) { + clearSteps() + val direction = Direction.cardinal.random(random) + if (canStep(direction.delta.x, direction.delta.y)) { + character.steps.queueStep(npc.tile.add(direction)) + } + super.tick() + return + } calculate() val interacted = processInteraction() if (interacted && interactionFinished()) { diff --git a/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt b/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt index f7eab10371..a911d14b8b 100644 --- a/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt +++ b/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt @@ -129,6 +129,16 @@ internal class CombatMovementTest : WorldTest() { assertTrue(npc.mode is CombatMovement) } + @Test + fun `Npc spawned under player steps out to attack`() { + val player = createPlayer(emptyTile) + val npc = createNPC("guard_falador", emptyTile) + npc.interactPlayer(player, "Attack") + tick(2) + assertTrue(npc.tile != emptyTile) + assertTrue(npc.mode is CombatMovement) + } + companion object { private const val MAX_EXP = 14000000.0 } From 15f8f32439191bc3824b9f338aee39cbc27988c8 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Fri, 10 Apr 2026 06:16:43 -0700 Subject: [PATCH 02/17] Fix: NPCs spawned under a target couldn't attack --- .../character/mode/combat/CombatMovement.kt | 16 ++-------- .../entity/character/mode/move/Movement.kt | 31 ++++++++++++++++++- .../entity/combat/CombatMovementTest.kt | 10 ++++++ 3 files changed, 42 insertions(+), 15 deletions(-) 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 7961124222..7a922d6fd1 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 @@ -16,9 +16,7 @@ import world.gregs.voidps.engine.entity.character.player.chat.cantReach import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.get import world.gregs.voidps.engine.map.Overlap -import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile -import world.gregs.voidps.type.random import kotlin.math.abs /** @@ -60,7 +58,6 @@ class CombatMovement( if (!attack()) { var skip: Boolean if (Overlap.isUnder(character.tile, character.size, target.tile, target.size)) { - stepOut() skip = true } else { val wasEmpty = character.steps.isEmpty() @@ -81,17 +78,8 @@ class CombatMovement( } } - private fun stepOut() { - clearSteps() - if (target.mode is CombatMovement || target.mode is Interact) { - return - } - val direction = Direction.cardinal.random(random) - if (!canStep(direction.delta.x, direction.delta.y)) { - return - } - character.steps.queueStep(strategy.tile.add(direction)) - } + override fun shouldQueueStepOut(): Boolean = + target.mode !is CombatMovement && target.mode !is Interact private fun attack(): Boolean { val attackRange = attackRange() diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt index f6e66dc622..c2e61a3e57 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt @@ -26,6 +26,7 @@ import world.gregs.voidps.type.Delta import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile import world.gregs.voidps.type.equals +import world.gregs.voidps.type.random import kotlin.math.sign open class Movement( @@ -53,6 +54,32 @@ open class Movement( needsCalculation = false } + /** + * Clears steps and queues a random cardinal step when an NPC overlaps its character target and isn't permitted to stand there. + */ + protected open fun stepOut(): Boolean { + val strategy = strategy ?: return false + if (strategy.shape != -2) return false + val npc = character as? NPC ?: return false + if (npc.def["allowed_under", false]) return false + if (!Overlap.isUnder(npc.tile, npc.size, npc.size, strategy.tile, strategy.width, strategy.height)) return false + clearSteps() + if (shouldQueueStepOut()) { + val direction = Direction.cardinal.random(random) + if (canStep(direction.delta.x, direction.delta.y)) { + character.steps.queueStep(npc.tile.add(direction)) + } + } + return true + } + + /** + * Whether a random step should be queued after clearing, or just the clear itself is sufficient. + * Overridden by [world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement] + * to skip queuing when the target is already moving toward the NPC. + */ + protected open fun shouldQueueStepOut(): Boolean = true + override fun tick() { val character = character if (character is Player && character.viewport?.loaded == false) { @@ -61,7 +88,9 @@ open class Movement( if (hasDelay() && !canMove() && !character.steps.destination.noCollision) { return } - calculate() + if (!stepOut()) { + calculate() + } if (step(runStep = false) && character.running) { if (character.steps.isNotEmpty()) { step(runStep = true) diff --git a/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt b/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt index f7eab10371..a911d14b8b 100644 --- a/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt +++ b/game/src/test/kotlin/content/entity/combat/CombatMovementTest.kt @@ -129,6 +129,16 @@ internal class CombatMovementTest : WorldTest() { assertTrue(npc.mode is CombatMovement) } + @Test + fun `Npc spawned under player steps out to attack`() { + val player = createPlayer(emptyTile) + val npc = createNPC("guard_falador", emptyTile) + npc.interactPlayer(player, "Attack") + tick(2) + assertTrue(npc.tile != emptyTile) + assertTrue(npc.mode is CombatMovement) + } + companion object { private const val MAX_EXP = 14000000.0 } From 1bf5b1415afcaa75ad1c869e4ad2a3bb6dc49094 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Fri, 10 Apr 2026 22:02:59 -0700 Subject: [PATCH 03/17] Fix: NPCs spawned under a target couldn't attack --- .../voidps/engine/entity/character/mode/move/Movement.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt index c2e61a3e57..9fb88c5a86 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt @@ -74,9 +74,7 @@ open class Movement( } /** - * Whether a random step should be queued after clearing, or just the clear itself is sufficient. - * Overridden by [world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement] - * to skip queuing when the target is already moving toward the NPC. + * Whether [stepOut] should queue a random step after clearing, or let normal recalculation handle repositioning. */ protected open fun shouldQueueStepOut(): Boolean = true From 635e183c8e2e8d23808e1db441034dce89fe2594 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sat, 11 Apr 2026 11:52:52 -0700 Subject: [PATCH 04/17] Fix: character creation screen skipped for new accounts --- .../kotlin/world/gregs/voidps/engine/data/AccountManager.kt | 1 - .../kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt | 2 -- 2 files changed, 3 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt index 4353a05984..541917bf13 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt @@ -41,7 +41,6 @@ class AccountManager( get() = Tile(Settings["world.home.x", 0], Settings["world.home.y", 0], Settings["world.home.level", 0]) fun create(name: String, passwordHash: String): Player = Player(tile = homeTile, accountName = name, passwordHash = passwordHash).apply { - this["creation"] = System.currentTimeMillis() this["new_player"] = true } diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt index 813b11793a..b356cb7d1b 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt @@ -85,9 +85,7 @@ class AccountManagerTest : KoinMock() { @Test fun `Create a new player`() { - val start = System.currentTimeMillis() val player = manager.create("name", "hash") - assertTrue(player["creation", 0L] >= start) assertTrue(player["new_player", false]) assertEquals(Tile(1234, 5432), player.tile) } From d15e006c556ded0b33fa590ef37a3dc552a11e80 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sat, 11 Apr 2026 19:04:20 -0700 Subject: [PATCH 05/17] Fix: game loop crash when dying to TzTok-Jad in Fight Cave --- .../kotlin/content/area/karamja/tzhaar_city/TzhaarFightCave.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/game/src/main/kotlin/content/area/karamja/tzhaar_city/TzhaarFightCave.kt b/game/src/main/kotlin/content/area/karamja/tzhaar_city/TzhaarFightCave.kt index 749cffc664..94c708c687 100644 --- a/game/src/main/kotlin/content/area/karamja/tzhaar_city/TzhaarFightCave.kt +++ b/game/src/main/kotlin/content/area/karamja/tzhaar_city/TzhaarFightCave.kt @@ -123,6 +123,9 @@ class TzhaarFightCave( return@npcDespawn } val wave = killer.wave + if (wave == -1) { + return@npcDespawn + } if (wave == 63 && id == "tztok_jad") { killer.leave(wave, true) } else if (wave < 63) { From 8bc6b1be5b418b2ae39298c52c312c77c01340b2 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sat, 11 Apr 2026 19:05:04 -0700 Subject: [PATCH 06/17] chore: add missing dragon arrow items to ammo_groups.toml --- data/skill/range/ammo_groups.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/skill/range/ammo_groups.toml b/data/skill/range/ammo_groups.toml index b6761d588d..d653f14d0b 100644 --- a/data/skill/range/ammo_groups.toml +++ b/data/skill/range/ammo_groups.toml @@ -72,6 +72,10 @@ items = [ "rune_arrow", "rune_fire_arrows_lit", "rune_fire_arrows_unlit", + "dragon_arrow", + "dragon_arrow_p", + "dragon_arrow_p+", + "dragon_arrow_p++", "ice_arrows", "broad_arrows", "saradomin_arrows", From 2e8369f805ff43eb8f3096c2fa2fba03c1e2a7f9 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sat, 11 Apr 2026 19:58:38 -0700 Subject: [PATCH 07/17] Fix: dragon claws special attack damage and hit timing --- .../skill/melee/weapon/special/DragonClaws.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt index e4f69d4e24..5e53cef007 100644 --- a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt +++ b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt @@ -5,6 +5,7 @@ import content.entity.combat.hit.Hit import content.entity.combat.hit.hit import content.skill.melee.weapon.weapon import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.queue.strongQueue import world.gregs.voidps.type.random class DragonClaws : Script { @@ -14,32 +15,39 @@ class DragonClaws : Script { anim("${id}_special") gfx("${id}_special") + val source = this val weapon = weapon var (hit1, hit2, hit3, hit4) = intArrayOf(0, 0, 0, 0) val maxHit = Damage.maximum(this, target, "melee", weapon) if (Hit.success(this, target, "melee", weapon, special = true)) { - hit1 = random.nextInt(maxHit / 2, maxHit - 10) + hit1 = random.nextInt(maxHit / 2, maxHit) hit2 = hit1 / 2 hit3 = hit2 / 2 - hit4 = hit3 + if (random.nextBoolean()) 10 else 0 + hit4 = hit3 + 1 } else if (Hit.success(this, target, "melee", weapon, special = true)) { hit2 = random.nextDouble(maxHit * 0.375, maxHit * 0.875).toInt() hit3 = hit2 / 2 - hit4 = hit3 + if (random.nextBoolean()) 10 else 0 + hit4 = hit3 + 1 } else if (Hit.success(this, target, "melee", weapon, special = true)) { hit3 = random.nextDouble(maxHit * 0.25, maxHit * 0.75).toInt() - hit4 = hit3 + if (random.nextBoolean()) 10 else 0 + hit4 = hit3 + 1 } else if (Hit.success(this, target, "melee", weapon, special = true)) { hit4 = random.nextDouble(maxHit * 0.25, maxHit * 1.25).toInt() } else { - hit3 = if (random.nextBoolean()) 10 else 0 - hit4 = if (random.nextBoolean()) 10 else 0 + // ~2/3 chance of dealing 2 total damage split between hits 3 and 4 + if (random.nextDouble() < 2.0 / 3.0) { + val split = random.nextInt(0, 3) + hit3 = split + hit4 = 2 - split + } } hit(target, damage = hit1) hit(target, damage = hit2) - hit(target, damage = hit3, delay = 30) - hit(target, damage = hit4, delay = 30) + target.strongQueue("claws_second_pair", 0) { + source.hit(target, weapon = weapon, damage = hit3) + source.hit(target, weapon = weapon, damage = hit4) + } } } } From 2b13373f4a5f0cb403ee9366d8f7b6a2c5fa1309 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sun, 12 Apr 2026 11:12:14 -0700 Subject: [PATCH 08/17] Fix: dragon claws special attack damage and hit timing --- data/area/wilderness/wilderness.areas.toml | 5 + .../wilderness/wilderness.npc-spawns.toml | 5 - .../modal/chat_box/warning.varbits.toml | 2 +- .../modal/chat_box/warnings.ifaces.toml | 13 +++ data/minigame/clan_wars/clan_wars.areas.toml | 13 +++ data/minigame/clan_wars/clan_wars.ifaces.toml | 4 +- data/minigame/clan_wars/clan_wars.objs.toml | 7 ++ .../mode/move/target/TargetStrategy.kt | 2 + .../clan_wars/ClanWarsFreeForAllSafe.kt | 97 +++++++++++++++++++ 9 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 data/minigame/clan_wars/clan_wars.objs.toml create mode 100644 game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt diff --git a/data/area/wilderness/wilderness.areas.toml b/data/area/wilderness/wilderness.areas.toml index f7cecc58a0..563f2ccb15 100644 --- a/data/area/wilderness/wilderness.areas.toml +++ b/data/area/wilderness/wilderness.areas.toml @@ -89,3 +89,8 @@ tags = ["teleport", "obelisk"] [wilderness_fishing_area] x = [3347, 3368] y = [3793, 3817] + +[clan_wars] +x = [3264, 3279] +y = [3672, 3695] +tags = ["safe_zone"] diff --git a/data/area/wilderness/wilderness.npc-spawns.toml b/data/area/wilderness/wilderness.npc-spawns.toml index b77d197eac..0a96aaf1f3 100644 --- a/data/area/wilderness/wilderness.npc-spawns.toml +++ b/data/area/wilderness/wilderness.npc-spawns.toml @@ -295,10 +295,6 @@ spawns = [ { id = "black_salamander", x = 3321, y = 3661 }, { id = "red_dragon", x = 3212, y = 3820 }, { id = "red_dragon", x = 3222, y = 3829 }, - { id = "skeleton_axe_shield", x = 3275, y = 3686 }, - { id = "skeleton_sword_shield", x = 3270, y = 3681 }, - { id = "skeleton_longsword", x = 3275, y = 3676 }, - { id = "skeleton_flail", x = 3269, y = 3672 }, { id = "waterfiend", x = 3362, y = 3642, members = true }, { id = "waterfiend", x = 3363, y = 3647, members = true }, { id = "waterfiend", x = 3365, y = 3639, members = true }, @@ -375,7 +371,6 @@ spawns = [ { id = "giant_rat_wilderness_2", x = 3248, y = 3553, members = true }, { id = "giant_rat_wilderness_2", x = 3256, y = 3541, members = true }, { id = "goblin_musician", x = 3140, y = 3642 }, - { id = "skeleton_mace_shield", x = 3268, y = 3688 }, { id = "green_dragon", x = 2982, y = 3618 }, { id = "green_dragon", x = 3118, y = 3820 }, { id = "green_dragon", x = 3338, y = 3676 }, diff --git a/data/entity/player/modal/chat_box/warning.varbits.toml b/data/entity/player/modal/chat_box/warning.varbits.toml index 5b30d06012..6a81e9c937 100644 --- a/data/entity/player/modal/chat_box/warning.varbits.toml +++ b/data/entity/player/modal/chat_box/warning.varbits.toml @@ -101,7 +101,7 @@ format = "int" [warning_clan_wars_ffa_safe] id = 5294 persist = true -format = "boolean" +format = "int" [warning_ranging_guild_tower] id = 3871 diff --git a/data/entity/player/modal/chat_box/warnings.ifaces.toml b/data/entity/player/modal/chat_box/warnings.ifaces.toml index 3c2aefaa52..dc26471e9a 100644 --- a/data/entity/player/modal/chat_box/warnings.ifaces.toml +++ b/data/entity/player/modal/chat_box/warnings.ifaces.toml @@ -441,6 +441,19 @@ id = 82 [.living_rock_caverns] id = 83 +[warning_clan_wars_ffa_safe] +id = 793 +type = "main_screen" + +[.yes] +id = 15 + +[.no] +id = 14 + +[.dont_ask] +id = 9 + [warning_godwars_wilderness_agility_route] id = 600 type = "main_screen" diff --git a/data/minigame/clan_wars/clan_wars.areas.toml b/data/minigame/clan_wars/clan_wars.areas.toml index 22f5f9b388..f658e55f9c 100644 --- a/data/minigame/clan_wars/clan_wars.areas.toml +++ b/data/minigame/clan_wars/clan_wars.areas.toml @@ -2,3 +2,16 @@ x = [3266, 3270] y = [3679, 3682] tags = ["teleport"] + +[clan_wars_ffa] +x = [2740, 2890] +y = [5490, 5640] + +[clan_wars_ffa_safe_arena] +x = [2756, 2878] +y = [5512, 5630] + +[clan_wars_ffa_safe_multi] +x = [2756, 2878] +y = [5571, 5630] +tags = ["multi_combat"] diff --git a/data/minigame/clan_wars/clan_wars.ifaces.toml b/data/minigame/clan_wars/clan_wars.ifaces.toml index 88026e5d00..0871554253 100644 --- a/data/minigame/clan_wars/clan_wars.ifaces.toml +++ b/data/minigame/clan_wars/clan_wars.ifaces.toml @@ -1,5 +1,6 @@ [clan_wars] id = 789 +type = "overlay" [clan_wars_defeat] id = 790 @@ -10,6 +11,3 @@ id = 791 [clan_wars_overview] id = 792 -[clan_wars_respawn] -id = 793 - diff --git a/data/minigame/clan_wars/clan_wars.objs.toml b/data/minigame/clan_wars/clan_wars.objs.toml new file mode 100644 index 0000000000..f663ba4a67 --- /dev/null +++ b/data/minigame/clan_wars/clan_wars.objs.toml @@ -0,0 +1,7 @@ +[clan_wars_portal_ffa_safe] +id = 38698 +examine = "A portal to a safe free-for-all fighting area." + +[clan_wars_portal_ffa_safe_exit] +id = 38700 +examine = "Leave the free-for-all safe area." diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt index bdcdf1eeaa..4bbadb42ad 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt @@ -62,6 +62,8 @@ interface TargetStrategy { "gnome_obstacle_pipe_east", "gnome_obstacle_pipe_west" -> TileTargetStrategy(entity.tile.addY(-1)) "lumbridge_church_bell" -> TileTargetStrategy(entity.tile.addY(-1)) "wall_pipe" -> TileTargetStrategy(entity.tile.addY(-1)) + "clan_wars_portal_ffa_safe" -> TileTargetStrategy(entity.tile.addY(-1)) + "clan_wars_portal_ffa_safe_exit" -> TileTargetStrategy(entity.tile.addY(entity.height)) else -> ObjectTargetStrategy(entity) } is FloorItem -> FloorItemTargetStrategy(entity) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt new file mode 100644 index 0000000000..1c823757fe --- /dev/null +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt @@ -0,0 +1,97 @@ +package content.minigame.clan_wars + +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.close +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.player.combatLevel +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.type.Tile + +class ClanWarsFreeForAllSafe : Script { + + val outside = Tile(3272, 3692, 0) + val arena = Tile(2815, 5511, 0) + + init { + // Entry portal — skip warning if player has dismissed it via doomsayer, otherwise show it + objectApproach("Enter", "clan_wars_portal_ffa_safe") { + approachRange(1) + if (combatLevel < 30) { + message("You need a combat level of at least 30 to enter this portal.") + return@objectApproach + } + if (get("warning_clan_wars_ffa_safe", 0) == 7) { + tele(arena) + return@objectApproach + } + open("warning_clan_wars_ffa_safe") + } + + // "Go in" button (component 15). Warning.kt handles the "dont_ask" toggle (component 9) + // and Doomsayer.kt increments the view count via interfaceOpened("warning_*"). + interfaceOption("Go in", "warning_clan_wars_ffa_safe:yes") { + close("warning_clan_wars_ffa_safe") + tele(arena) + } + + // "Cancel" button — close without entering + interfaceOption("Cancel", "warning_clan_wars_ffa_safe:no") { + close("warning_clan_wars_ffa_safe") + } + + // "Don't show again" checkbox + interfaceOption("Toggle warning", "warning_clan_wars_ffa_safe:dont_ask") { + set("warning_clan_wars_ffa_safe", if (get("warning_clan_wars_ffa_safe", 0) == 7) 0 else 7) + } + + // Exit portal + objectApproach("Leave", "clan_wars_portal_ffa_safe_exit") { + approachRange(1) + tele(outside) + message("You have left the Clan Wars Free-For-All (Safe).", ChatType.Filter) + } + + // Overlay: covers the full zone including the lobby at y=5511 + entered("clan_wars_ffa") { + open("clan_wars") + } + + exited("clan_wars_ffa") { + close("clan_wars") + } + + // PvP: only inside the fighting area (y >= 5512) + entered("clan_wars_ffa_safe_arena") { + set("in_pvp", true) + options.set(1, "Attack") + } + + exited("clan_wars_ffa_safe_arena") { + clear("in_pvp") + options.remove("Attack") + } + + // On death: keep items, respawn outside + playerDeath { + if (!get("in_pvp", false) && tile !in Areas["clan_wars_ffa"]) { + return@playerDeath + } + it.dropItems = false + it.teleport = outside + } + + // On login: restore overlay and pvp state if still inside + playerSpawn { + if (tile in Areas["clan_wars_ffa"]) { + open("clan_wars") + if (tile in Areas["clan_wars_ffa_safe_arena"]) { + set("in_pvp", true) + options.set(1, "Attack") + } + } + } + } +} From 116b520ed9c430a42446ca69159aa55a607aff18 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Sun, 12 Apr 2026 14:20:29 -0700 Subject: [PATCH 09/17] feat: add clan wars safe and dangerous free-for-all portals. --- .../modal/chat_box/warning.varbits.toml | 2 +- data/minigame/clan_wars/clan_wars.areas.toml | 11 +- data/minigame/clan_wars/clan_wars.objs.toml | 10 +- .../minigame/clan_wars/clan_wars.varbits.toml | 3 + .../mode/move/target/TargetStrategy.kt | 2 - .../minigame/clan_wars/ClanWarsFreeForAll.kt | 131 ++++++++++++++++++ .../clan_wars/ClanWarsFreeForAllSafe.kt | 97 ------------- 7 files changed, 154 insertions(+), 102 deletions(-) create mode 100644 data/minigame/clan_wars/clan_wars.varbits.toml create mode 100644 game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt delete mode 100644 game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt diff --git a/data/entity/player/modal/chat_box/warning.varbits.toml b/data/entity/player/modal/chat_box/warning.varbits.toml index 6a81e9c937..d5a8f322d5 100644 --- a/data/entity/player/modal/chat_box/warning.varbits.toml +++ b/data/entity/player/modal/chat_box/warning.varbits.toml @@ -146,7 +146,7 @@ format = "int" [warning_clan_wars_ffa_dangerous] id = 5295 persist = true -format = "boolean" +format = "int" [warning_living_rock_caverns] id = 6500 diff --git a/data/minigame/clan_wars/clan_wars.areas.toml b/data/minigame/clan_wars/clan_wars.areas.toml index f658e55f9c..8caa4a15b9 100644 --- a/data/minigame/clan_wars/clan_wars.areas.toml +++ b/data/minigame/clan_wars/clan_wars.areas.toml @@ -4,7 +4,7 @@ y = [3679, 3682] tags = ["teleport"] [clan_wars_ffa] -x = [2740, 2890] +x = [2740, 3090] y = [5490, 5640] [clan_wars_ffa_safe_arena] @@ -15,3 +15,12 @@ y = [5512, 5630] x = [2756, 2878] y = [5571, 5630] tags = ["multi_combat"] + +[clan_wars_ffa_dangerous_arena] +x = [2948, 3071] +y = [5512, 5631] + +[clan_wars_ffa_dangerous_multi] +x = [2948, 3071] +y = [5571, 5631] +tags = ["multi_combat"] diff --git a/data/minigame/clan_wars/clan_wars.objs.toml b/data/minigame/clan_wars/clan_wars.objs.toml index f663ba4a67..a6d475b8bb 100644 --- a/data/minigame/clan_wars/clan_wars.objs.toml +++ b/data/minigame/clan_wars/clan_wars.objs.toml @@ -2,6 +2,14 @@ id = 38698 examine = "A portal to a safe free-for-all fighting area." +[clan_wars_portal_ffa_dangerous] +id = 38699 +examine = "A portal to a dangerous free-for-all fighting area." + +[clan_wars_challenge_portal] +id = 28213 +examine = "A portal to the Clan Wars arena." + [clan_wars_portal_ffa_safe_exit] id = 38700 -examine = "Leave the free-for-all safe area." +examine = "Leave the free-for-all fighting area." diff --git a/data/minigame/clan_wars/clan_wars.varbits.toml b/data/minigame/clan_wars/clan_wars.varbits.toml new file mode 100644 index 0000000000..76fe0026e3 --- /dev/null +++ b/data/minigame/clan_wars/clan_wars.varbits.toml @@ -0,0 +1,3 @@ +[clan_wars_ffa_portal] +id = 5279 +format = "int" diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt index 4bbadb42ad..bdcdf1eeaa 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/target/TargetStrategy.kt @@ -62,8 +62,6 @@ interface TargetStrategy { "gnome_obstacle_pipe_east", "gnome_obstacle_pipe_west" -> TileTargetStrategy(entity.tile.addY(-1)) "lumbridge_church_bell" -> TileTargetStrategy(entity.tile.addY(-1)) "wall_pipe" -> TileTargetStrategy(entity.tile.addY(-1)) - "clan_wars_portal_ffa_safe" -> TileTargetStrategy(entity.tile.addY(-1)) - "clan_wars_portal_ffa_safe_exit" -> TileTargetStrategy(entity.tile.addY(entity.height)) else -> ObjectTargetStrategy(entity) } is FloorItem -> FloorItemTargetStrategy(entity) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt new file mode 100644 index 0000000000..1378af0eff --- /dev/null +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt @@ -0,0 +1,131 @@ +package content.minigame.clan_wars + +import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.message +import world.gregs.voidps.engine.client.ui.close +import world.gregs.voidps.engine.client.ui.open +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.move.tele +import world.gregs.voidps.engine.entity.character.player.combatLevel +import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.type.Tile + +class ClanWarsFreeForAll : Script { + + val outside = Tile(3272, 3692, 0) + val safeArena = Tile(2815, 5511, 0) + val dangerousArena = Tile(3007, 5511, 0) + + init { + // Entry portals - varbit 5279 selects safe (0) or dangerous (1) so the client shows the correct text + objectOperate("Enter", "clan_wars_portal_ffa_safe") { + if (combatLevel < 30) { + message("You need a combat level of at least 30 to enter this portal.") + return@objectOperate + } + set("clan_wars_ffa_portal", 0) + if (get("warning_clan_wars_ffa_safe", 0) == 7) { + tele(safeArena) + return@objectOperate + } + open("warning_clan_wars_ffa_safe") + } + + objectOperate("Enter", "clan_wars_portal_ffa_dangerous") { + if (combatLevel < 30) { + message("You need a combat level of at least 30 to enter this portal.") + return@objectOperate + } + set("clan_wars_ffa_portal", 1) + if (get("warning_clan_wars_ffa_dangerous", 0) == 7) { + tele(dangerousArena) + return@objectOperate + } + open("warning_clan_wars_ffa_safe") + } + + // "Go in" button - reads varbit 5279 to pick the arena + interfaceOption("Go in", "warning_clan_wars_ffa_safe:yes") { + close("warning_clan_wars_ffa_safe") + tele(if (get("clan_wars_ffa_portal", 0) == 1) dangerousArena else safeArena) + } + + // "Cancel" button + interfaceOption("Cancel", "warning_clan_wars_ffa_safe:no") { + close("warning_clan_wars_ffa_safe") + } + + // "Don't show again" checkbox - toggles the varbit for whichever portal opened the dialog + interfaceOption("Toggle warning", "warning_clan_wars_ffa_safe:dont_ask") { + val key = if (get("clan_wars_ffa_portal", 0) == 1) "warning_clan_wars_ffa_dangerous" else "warning_clan_wars_ffa_safe" + set(key, if (get(key, 0) == 7) 0 else 7) + } + + // Clan Wars challenge portal - not yet implemented + objectOperate("Enter", "clan_wars_challenge_portal") { + message("Clan Wars is still under construction.") + } + + // Exit portal - reads varbit 5279 to determine which arena the player entered from + objectOperate("Leave", "clan_wars_portal_ffa_safe_exit") { + tele(outside) + val type = if (get("clan_wars_ffa_portal", 0) == 1) "Dangerous" else "Safe" + message("You have left the Clan Wars Free-For-All ($type).", ChatType.Filter) + } + + // Overlay: covers both arenas including the lobby at y=5511 + entered("clan_wars_ffa") { + open("clan_wars") + } + + exited("clan_wars_ffa") { + close("clan_wars") + } + + // PvP - safe arena + entered("clan_wars_ffa_safe_arena") { + set("in_pvp", true) + options.set(1, "Attack") + } + + exited("clan_wars_ffa_safe_arena") { + clear("in_pvp") + options.remove("Attack") + } + + // PvP - dangerous arena + entered("clan_wars_ffa_dangerous_arena") { + set("in_pvp", true) + options.set(1, "Attack") + } + + exited("clan_wars_ffa_dangerous_arena") { + clear("in_pvp") + options.remove("Attack") + } + + // On death in safe arena: keep items, respawn outside + playerDeath { + if (tile !in Areas["clan_wars_ffa_safe_arena"]) return@playerDeath + it.dropItems = false + it.teleport = outside + } + + // On death in dangerous arena: drop items, respawn outside + playerDeath { + if (tile !in Areas["clan_wars_ffa_dangerous_arena"]) return@playerDeath + it.teleport = outside + } + + // On login: restore overlay and pvp state if still inside either arena + playerSpawn { + if (tile in Areas["clan_wars_ffa"]) { + open("clan_wars") + if (tile in Areas["clan_wars_ffa_safe_arena"] || tile in Areas["clan_wars_ffa_dangerous_arena"]) { + set("in_pvp", true) + options.set(1, "Attack") + } + } + } + } +} diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt deleted file mode 100644 index 1c823757fe..0000000000 --- a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAllSafe.kt +++ /dev/null @@ -1,97 +0,0 @@ -package content.minigame.clan_wars - -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.message -import world.gregs.voidps.engine.client.ui.close -import world.gregs.voidps.engine.client.ui.open -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.move.tele -import world.gregs.voidps.engine.entity.character.player.combatLevel -import world.gregs.voidps.engine.entity.character.player.chat.ChatType -import world.gregs.voidps.type.Tile - -class ClanWarsFreeForAllSafe : Script { - - val outside = Tile(3272, 3692, 0) - val arena = Tile(2815, 5511, 0) - - init { - // Entry portal — skip warning if player has dismissed it via doomsayer, otherwise show it - objectApproach("Enter", "clan_wars_portal_ffa_safe") { - approachRange(1) - if (combatLevel < 30) { - message("You need a combat level of at least 30 to enter this portal.") - return@objectApproach - } - if (get("warning_clan_wars_ffa_safe", 0) == 7) { - tele(arena) - return@objectApproach - } - open("warning_clan_wars_ffa_safe") - } - - // "Go in" button (component 15). Warning.kt handles the "dont_ask" toggle (component 9) - // and Doomsayer.kt increments the view count via interfaceOpened("warning_*"). - interfaceOption("Go in", "warning_clan_wars_ffa_safe:yes") { - close("warning_clan_wars_ffa_safe") - tele(arena) - } - - // "Cancel" button — close without entering - interfaceOption("Cancel", "warning_clan_wars_ffa_safe:no") { - close("warning_clan_wars_ffa_safe") - } - - // "Don't show again" checkbox - interfaceOption("Toggle warning", "warning_clan_wars_ffa_safe:dont_ask") { - set("warning_clan_wars_ffa_safe", if (get("warning_clan_wars_ffa_safe", 0) == 7) 0 else 7) - } - - // Exit portal - objectApproach("Leave", "clan_wars_portal_ffa_safe_exit") { - approachRange(1) - tele(outside) - message("You have left the Clan Wars Free-For-All (Safe).", ChatType.Filter) - } - - // Overlay: covers the full zone including the lobby at y=5511 - entered("clan_wars_ffa") { - open("clan_wars") - } - - exited("clan_wars_ffa") { - close("clan_wars") - } - - // PvP: only inside the fighting area (y >= 5512) - entered("clan_wars_ffa_safe_arena") { - set("in_pvp", true) - options.set(1, "Attack") - } - - exited("clan_wars_ffa_safe_arena") { - clear("in_pvp") - options.remove("Attack") - } - - // On death: keep items, respawn outside - playerDeath { - if (!get("in_pvp", false) && tile !in Areas["clan_wars_ffa"]) { - return@playerDeath - } - it.dropItems = false - it.teleport = outside - } - - // On login: restore overlay and pvp state if still inside - playerSpawn { - if (tile in Areas["clan_wars_ffa"]) { - open("clan_wars") - if (tile in Areas["clan_wars_ffa_safe_arena"]) { - set("in_pvp", true) - options.set(1, "Attack") - } - } - } - } -} From 78ff78bcf4451bcdba7f89c3054a05f112bf2b27 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Mon, 13 Apr 2026 05:48:20 -0700 Subject: [PATCH 10/17] Fix: bank tab creation and collapse --- .../content/entity/player/bank/BankTabCollapse.kt | 13 ++++++++++--- .../kotlin/content/entity/player/bank/BankTabs.kt | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/game/src/main/kotlin/content/entity/player/bank/BankTabCollapse.kt b/game/src/main/kotlin/content/entity/player/bank/BankTabCollapse.kt index 40ffe3b45b..a1895519d0 100644 --- a/game/src/main/kotlin/content/entity/player/bank/BankTabCollapse.kt +++ b/game/src/main/kotlin/content/entity/player/bank/BankTabCollapse.kt @@ -2,7 +2,6 @@ package content.entity.player.bank import content.entity.player.bank.Bank.tabIndex import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.inv.transact.operation.ShiftItem.shiftToFreeIndex class BankTabCollapse : Script { @@ -11,9 +10,17 @@ class BankTabCollapse : Script { val tab = it.component.removePrefix("tab_").toInt() - 1 val tabIndex = tabIndex(this, tab) val count: Int = get("bank_tab_$tab", 0) + val lastIndex = bank.count - 1 val collapsed = bank.transaction { - repeat(count) { - shiftToFreeIndex(tabIndex) + // Save the tab items being collapsed + val tabItems = (0 until count).map { inventory[tabIndex + it] } + // Shift all items after the tab left by count to close the gap + for (i in tabIndex until lastIndex - count + 1) { + set(i, inventory[i + count]) + } + // Place the collapsed tab items at the end + for (i in tabItems.indices) { + set(lastIndex - count + 1 + i, tabItems[i]) } } if (collapsed) { diff --git a/game/src/main/kotlin/content/entity/player/bank/BankTabs.kt b/game/src/main/kotlin/content/entity/player/bank/BankTabs.kt index 2a4dec67c0..daad8f40e8 100644 --- a/game/src/main/kotlin/content/entity/player/bank/BankTabs.kt +++ b/game/src/main/kotlin/content/entity/player/bank/BankTabs.kt @@ -38,7 +38,7 @@ class BankTabs : Script { set("bank_item_mode", if (value == "insert") "swap" else "insert") } - interfaceSwap("bank:tab_#") { _, toId, fromSlot, _ -> + interfaceSwap("bank:inventory", "bank:tab_#") { _, toId, fromSlot, _ -> val fromTab = Bank.getTab(this, fromSlot) val toTab = toId.substringAfter(":").removePrefix("tab_").toInt() - 1 val toIndex = if (toTab == Bank.MAIN_TAB) bank.freeIndex() else Bank.tabIndex(this, toTab + 1) From 7b9f6ef6467ceea27e7547f1b5f12baea86f21f0 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Mon, 13 Apr 2026 16:10:17 -0700 Subject: [PATCH 11/17] chore: replaced redundant code with stepOut function. --- .../entity/character/mode/interact/Interact.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt index 6ef1d067be..e2dc9308d3 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/interact/Interact.kt @@ -8,14 +8,10 @@ import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.move.Movement import world.gregs.voidps.engine.entity.character.mode.move.target.TargetStrategy -import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.cantReach import world.gregs.voidps.engine.entity.character.player.chat.noInterest -import world.gregs.voidps.engine.map.Overlap import world.gregs.voidps.engine.suspend.resumeSuspension -import world.gregs.voidps.type.Direction -import world.gregs.voidps.type.random /** * Moves a player within interact distance of [target] @@ -91,14 +87,7 @@ open class Interact( } updateRange = false val target = target - val npc = character as? NPC - if (npc != null && !npc.def["allowed_under", false] && target is Character && - Overlap.isUnder(npc.tile, npc.size, target.tile, target.size)) { - clearSteps() - val direction = Direction.cardinal.random(random) - if (canStep(direction.delta.x, direction.delta.y)) { - character.steps.queueStep(npc.tile.add(direction)) - } + if (stepOut()) { super.tick() return } From 1fd24cd412c34bcfee757b35fc750b9c28cb02c5 Mon Sep 17 00:00:00 2001 From: Harley Gilpin <75695035+HarleyGilpin@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:50:41 -0700 Subject: [PATCH 12/17] Update game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt Co-authored-by: Greg --- .../kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt index 1378af0eff..68189d48e7 100644 --- a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt @@ -24,7 +24,7 @@ class ClanWarsFreeForAll : Script { return@objectOperate } set("clan_wars_ffa_portal", 0) - if (get("warning_clan_wars_ffa_safe", 0) == 7) { + if (warning("clan_wars_ffa_safe")) { tele(safeArena) return@objectOperate } From 3869de982b5314e5123e88d2389fb515a45e2be7 Mon Sep 17 00:00:00 2001 From: Harley Gilpin <75695035+HarleyGilpin@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:51:02 -0700 Subject: [PATCH 13/17] Update game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt Co-authored-by: Greg --- .../content/minigame/clan_wars/ClanWarsFreeForAll.kt | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt index 68189d48e7..2e733f9b2b 100644 --- a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt @@ -117,15 +117,4 @@ class ClanWarsFreeForAll : Script { it.teleport = outside } - // On login: restore overlay and pvp state if still inside either arena - playerSpawn { - if (tile in Areas["clan_wars_ffa"]) { - open("clan_wars") - if (tile in Areas["clan_wars_ffa_safe_arena"] || tile in Areas["clan_wars_ffa_dangerous_arena"]) { - set("in_pvp", true) - options.set(1, "Attack") - } - } - } - } } From 889b863032783170720c99aa897e6b6546691632 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Mon, 13 Apr 2026 17:53:34 -0700 Subject: [PATCH 14/17] Fix: Clan Wars FFA "Don't show gain" toggle --- .../minigame/clan_wars/ClanWarsFreeForAll.kt | 8 ++--- .../skill/melee/weapon/special/DragonClaws.kt | 33 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt index 2e733f9b2b..2e16958ac7 100644 --- a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt @@ -24,7 +24,7 @@ class ClanWarsFreeForAll : Script { return@objectOperate } set("clan_wars_ffa_portal", 0) - if (warning("clan_wars_ffa_safe")) { + if (get("warning_clan_wars_ffa_safe", 0) == 1) { tele(safeArena) return@objectOperate } @@ -37,7 +37,7 @@ class ClanWarsFreeForAll : Script { return@objectOperate } set("clan_wars_ffa_portal", 1) - if (get("warning_clan_wars_ffa_dangerous", 0) == 7) { + if (get("warning_clan_wars_ffa_dangerous", 0) == 1) { tele(dangerousArena) return@objectOperate } @@ -58,7 +58,7 @@ class ClanWarsFreeForAll : Script { // "Don't show again" checkbox - toggles the varbit for whichever portal opened the dialog interfaceOption("Toggle warning", "warning_clan_wars_ffa_safe:dont_ask") { val key = if (get("clan_wars_ffa_portal", 0) == 1) "warning_clan_wars_ffa_dangerous" else "warning_clan_wars_ffa_safe" - set(key, if (get(key, 0) == 7) 0 else 7) + set(key, if (get(key, 0) == 1) 0 else 1) } // Clan Wars challenge portal - not yet implemented @@ -116,5 +116,5 @@ class ClanWarsFreeForAll : Script { if (tile !in Areas["clan_wars_ffa_dangerous_arena"]) return@playerDeath it.teleport = outside } - + } } diff --git a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt index 5e53cef007..d64fb6b71c 100644 --- a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt +++ b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt @@ -20,26 +20,31 @@ class DragonClaws : Script { var (hit1, hit2, hit3, hit4) = intArrayOf(0, 0, 0, 0) val maxHit = Damage.maximum(this, target, "melee", weapon) if (Hit.success(this, target, "melee", weapon, special = true)) { - hit1 = random.nextInt(maxHit / 2, maxHit) + // First hit lands: high damage, half, then two hits adding up to the second. + // e.g. 300-150-70-80 + hit1 = random.nextInt(maxHit / 2, maxHit + 1) hit2 = hit1 / 2 - hit3 = hit2 / 2 - hit4 = hit3 + 1 + hit3 = random.nextInt(0, hit2 + 1) + hit4 = hit2 - hit3 } else if (Hit.success(this, target, "melee", weapon, special = true)) { - hit2 = random.nextDouble(maxHit * 0.375, maxHit * 0.875).toInt() + // First misses, second hits: 3rd and 4th each deal half of the 2nd. + // e.g. 0-300-150-150 + hit2 = random.nextInt(maxHit / 2, maxHit + 1) hit3 = hit2 / 2 - hit4 = hit3 + 1 + hit4 = hit2 - hit3 } else if (Hit.success(this, target, "melee", weapon, special = true)) { - hit3 = random.nextDouble(maxHit * 0.25, maxHit * 0.75).toInt() - hit4 = hit3 + 1 + // First two miss: 3rd and 4th are regular hits, capped at 75% max. + // e.g. 0-0-300-300 + val cappedMax = (maxHit * 3) / 4 + hit3 = random.nextInt(cappedMax + 1) + hit4 = random.nextInt(cappedMax + 1) } else if (Hit.success(this, target, "melee", weapon, special = true)) { - hit4 = random.nextDouble(maxHit * 0.25, maxHit * 1.25).toInt() + // First three miss, fourth hits with 50% damage boost. + // e.g. 0-0-0-450 + hit4 = random.nextInt((maxHit * 3) / 2 + 1) } else { - // ~2/3 chance of dealing 2 total damage split between hits 3 and 4 - if (random.nextDouble() < 2.0 / 3.0) { - val split = random.nextInt(0, 3) - hit3 = split - hit4 = 2 - split - } + // All four miss: fourth hit almost always lands between 1 and 7. + hit4 = random.nextInt(1, 8) } hit(target, damage = hit1) From 99d61bc0c8f227905224d2249a63feefc4b93cc8 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Tue, 14 Apr 2026 04:28:28 -0700 Subject: [PATCH 15/17] bug: fixed the dragon claws delayed special attack. --- .../content/skill/melee/weapon/special/DragonClaws.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt index d64fb6b71c..2c070bcb5c 100644 --- a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt +++ b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt @@ -6,6 +6,8 @@ import content.entity.combat.hit.hit import content.skill.melee.weapon.weapon import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.queue.strongQueue +import world.gregs.voidps.engine.timer.CLIENT_TICKS +import world.gregs.voidps.engine.timer.TICKS import world.gregs.voidps.type.random class DragonClaws : Script { @@ -14,8 +16,6 @@ class DragonClaws : Script { specialAttack("slice_and_dice") { target, id -> anim("${id}_special") gfx("${id}_special") - - val source = this val weapon = weapon var (hit1, hit2, hit3, hit4) = intArrayOf(0, 0, 0, 0) val maxHit = Damage.maximum(this, target, "melee", weapon) @@ -46,13 +46,10 @@ class DragonClaws : Script { // All four miss: fourth hit almost always lands between 1 and 7. hit4 = random.nextInt(1, 8) } - hit(target, damage = hit1) hit(target, damage = hit2) - target.strongQueue("claws_second_pair", 0) { - source.hit(target, weapon = weapon, damage = hit3) - source.hit(target, weapon = weapon, damage = hit4) - } + hit(target, damage = hit3, delay = TICKS.toClientTicks(2)) + hit(target, damage = hit4 ,delay = TICKS.toClientTicks(2)) } } } From 9f113240afaf4caaa46cf2c32ff0b8901db05128 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Tue, 14 Apr 2026 05:02:08 -0700 Subject: [PATCH 16/17] chore: spotlessApply ran --- .../kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt | 2 +- .../kotlin/content/skill/melee/weapon/special/DragonClaws.kt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt index 2e16958ac7..f8b7be9028 100644 --- a/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt +++ b/game/src/main/kotlin/content/minigame/clan_wars/ClanWarsFreeForAll.kt @@ -6,8 +6,8 @@ import world.gregs.voidps.engine.client.ui.close import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.move.tele -import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.type.Tile class ClanWarsFreeForAll : Script { diff --git a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt index 2c070bcb5c..4bca735489 100644 --- a/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt +++ b/game/src/main/kotlin/content/skill/melee/weapon/special/DragonClaws.kt @@ -5,8 +5,6 @@ import content.entity.combat.hit.Hit import content.entity.combat.hit.hit import content.skill.melee.weapon.weapon import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.queue.strongQueue -import world.gregs.voidps.engine.timer.CLIENT_TICKS import world.gregs.voidps.engine.timer.TICKS import world.gregs.voidps.type.random @@ -49,7 +47,7 @@ class DragonClaws : Script { hit(target, damage = hit1) hit(target, damage = hit2) hit(target, damage = hit3, delay = TICKS.toClientTicks(2)) - hit(target, damage = hit4 ,delay = TICKS.toClientTicks(2)) + hit(target, damage = hit4, delay = TICKS.toClientTicks(2)) } } } From 27697efe743c2c8dcd7e6da448c2b52c943ae446 Mon Sep 17 00:00:00 2001 From: Harley Gilpin Date: Tue, 14 Apr 2026 05:35:57 -0700 Subject: [PATCH 17/17] Fix: Iterate cardinal directions in random order and queue the first walkable one, so a single blocked direction no longer traps the NPC. --- .../voidps/engine/entity/character/mode/move/Movement.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt index 9fb88c5a86..ef447a78c6 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt @@ -65,9 +65,11 @@ open class Movement( if (!Overlap.isUnder(npc.tile, npc.size, npc.size, strategy.tile, strategy.width, strategy.height)) return false clearSteps() if (shouldQueueStepOut()) { - val direction = Direction.cardinal.random(random) - if (canStep(direction.delta.x, direction.delta.y)) { - character.steps.queueStep(npc.tile.add(direction)) + for (direction in Direction.cardinal.shuffled(random)) { + if (canStep(direction.delta.x, direction.delta.y)) { + character.steps.queueStep(npc.tile.add(direction)) + break + } } } return true