From 7c8aa93ce010b5415ec9f22085e6957e744ef2b5 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 1 Apr 2026 21:56:18 +0300 Subject: [PATCH] Climbing rope fixes Same-tile interaction still fails --- .../castlewars/CastleWarsGameState.kt | 55 +++++++++++--- .../castlewars/CastleWarsListeners.kt | 76 +++++++++++++------ 2 files changed, 96 insertions(+), 35 deletions(-) diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt index 780f0d916..f78d1f5bd 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt @@ -16,6 +16,7 @@ import core.game.node.scenery.Scenery import core.game.node.scenery.SceneryBuilder import core.game.world.map.Location import core.game.world.map.RegionManager +import core.game.world.update.flag.chunk.ObjectUpdateFlag import core.game.world.map.zone.ZoneBorders import core.tools.ticksPerMinute import org.rs09.consts.Items @@ -61,6 +62,11 @@ enum class BoulderState { COLLAPSED, WEAKENED, CLEARED } +data class ActiveClimbingRope( + val rope: Scenery, + val originalWall: Scenery +) + /** * Manages the state of a Castle Wars game including scores, flags, and game lifecycle. * This is a singleton that tracks the current game state. @@ -91,6 +97,8 @@ object CastleWarsGameState { var zamorakDroppedFlagScenery: Scenery? = null private set + private val activeClimbingRopes: MutableMap = mutableMapOf() + // Tick when each flag was dropped (for auto-return timer) private var saradominFlagDroppedTick: Int = 0 private var zamorakFlagDroppedTick: Int = 0 @@ -263,22 +271,47 @@ object CastleWarsGameState { resetBattlefield() } + fun registerClimbingRope(rope: Scenery, originalWall: Scenery) { + val key = Location.create(rope.location) + val chunk = RegionManager.getRegionChunk(rope.location) + + rope.isActive = true + rope.isRenderable = true + activeClimbingRopes[key] = ActiveClimbingRope(rope, Scenery(originalWall)) + chunk.objects[rope.location.chunkOffsetX][rope.location.chunkOffsetY] = rope + } + + fun getClimbingRope(location: Location): Scenery? { + return activeClimbingRopes[Location.create(location)]?.rope?.takeIf { it.isActive } + } + + fun removeClimbingRope(location: Location, sendUpdate: Boolean = true): Boolean { + val activeRope = activeClimbingRopes.remove(Location.create(location)) ?: return false + val chunk = RegionManager.getRegionChunk(activeRope.rope.location) + + activeRope.rope.isActive = false + chunk.objects[activeRope.rope.location.chunkOffsetX][activeRope.rope.location.chunkOffsetY] = + activeRope.originalWall + + if (sendUpdate) { + chunk.flag(ObjectUpdateFlag(activeRope.rope, true)) + } + return true + } + + fun clearClimbingRopes(sendUpdate: Boolean = true) { + val ropeLocations = activeClimbingRopes.keys.map { Location.create(it) } + for (location in ropeLocations) { + removeClimbingRope(location, sendUpdate) + } + } + /** * Reset the battlefield by removing climbing ropes, restoring modified battlements, * and closing any open doors. Called at the end of each match. */ private fun resetBattlefield() { - for (x in 2368..2431) { - for (y in 3072..3135) { - for (z in 0..3) { - val loc = Location.create(x, y, z) - val obj = RegionManager.getObject(loc) - if (obj != null && obj.id == CastleWars.cwCastleClimbingRope) { - SceneryBuilder.remove(obj) - } - } - } - } + clearClimbingRopes() // Reset modified battlements back to original state // 36313 -> 4446, 36314 -> 4447 for (x in 2368..2431) { diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt index bb319bf01..1849ae6ca 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt @@ -38,6 +38,11 @@ data class BarrierData( val insideTile: Location ) +data class ClimbingRopeTraversal( + val topOfWallTile: Location, + val baseOfWallTile: Location +) + private val barrierCoordinates: Map> = mapOf( CastleWars.zamorakEnergyBarrier to listOf( BarrierData( @@ -111,19 +116,49 @@ internal fun getBarrierPassTiles(playerLoc: Location, barrierData: BarrierData): } } +internal fun getClimbingRopeTraversal(rope: Scenery): ClimbingRopeTraversal { + val ropeLoc = rope.location + val wallDirection = rope.direction.opposite + val topOfWallTile = Location(wallDirection.stepY + ropeLoc.x, -wallDirection.stepX + ropeLoc.y, ropeLoc.z) + val baseOfWallTile = Location(-wallDirection.stepY + ropeLoc.x, wallDirection.stepX + ropeLoc.y, ropeLoc.z) + return ClimbingRopeTraversal(topOfWallTile, baseOfWallTile) +} + +internal fun isOnClimbingRopeBaseSide(playerLoc: Location, rope: Scenery): Boolean { + val traversal = getClimbingRopeTraversal(rope) + if (playerLoc == traversal.baseOfWallTile) { + return true + } + + val ropeLoc = rope.location + val baseVectorX = traversal.baseOfWallTile.x - ropeLoc.x + val baseVectorY = traversal.baseOfWallTile.y - ropeLoc.y + val playerVectorX = playerLoc.x - ropeLoc.x + val playerVectorY = playerLoc.y - ropeLoc.y + + return playerVectorX * baseVectorX + playerVectorY * baseVectorY > 0 +} + +internal fun getClimbingRopeInteractionDestination(playerLoc: Location, rope: Scenery): Location { + val traversal = getClimbingRopeTraversal(rope) + return if (isOnClimbingRopeBaseSide(playerLoc, rope)) { + traversal.baseOfWallTile + } else { + traversal.topOfWallTile + } +} + +internal fun canClimbUpClimbingRope(playerLoc: Location, rope: Scenery): Boolean { + return playerLoc == getClimbingRopeTraversal(rope).baseOfWallTile +} + class CastleWarsListeners : InteractionListener { private val pickaxeIds = intArrayOf(1265, 1267, 1269, 1271, 1273, 1275) override fun defineDestinationOverrides() { setDest(IntType.SCENERY, intArrayOf(CastleWars.cwCastleClimbingRope), "climb") { entity, node -> - val rope = node.asScenery() - val playerLoc = entity.location - val ropeLoc = rope.location - if (playerLoc.getDistance(ropeLoc) <= 2.0) { - return@setDest playerLoc - } - return@setDest ropeLoc + return@setDest getClimbingRopeInteractionDestination(entity.location, node.asScenery()) } setDest( @@ -242,17 +277,20 @@ class CastleWarsListeners : InteractionListener { CastleWars.cwClimbingRope, *CastleWars.cwCastleBattlementsMap.keys.toIntArray() ) { player, rope, wall -> + if (CastleWarsGameState.getClimbingRope(wall.location) != null) { + sendDialogue(player, "There is already a rope tied to the battlements.") + return@onUseWith true + } removeItem(player, rope) val wallScenery = wall.asScenery() val ropeWall = wallScenery.transform(CastleWars.cwCastleBattlementsMap.getValue(wall.id)) SceneryBuilder.replace(wallScenery, ropeWall, CastleWars.ropeAliveTicks, false) val hangingRope = Scenery(CastleWars.cwCastleClimbingRope, wall.location, 4, wall.direction.toInteger()) + CastleWarsGameState.registerClimbingRope(hangingRope, wallScenery) RegionManager.getRegionChunk(hangingRope.location).flag(ObjectUpdateFlag(hangingRope, false)) - hangingRope.isActive = true GameWorld.Pulser.submit(object : Pulse(CastleWars.ropeAliveTicks) { override fun pulse(): Boolean { - RegionManager.getRegionChunk(hangingRope.location).flag(ObjectUpdateFlag(hangingRope, true)) - hangingRope.isActive = false + CastleWarsGameState.removeClimbingRope(hangingRope.location) return true } }) @@ -260,21 +298,11 @@ class CastleWarsListeners : InteractionListener { } on(CastleWars.cwCastleClimbingRope, SCENERY, "climb") { player, rope -> - val ropeLoc = rope.asScenery().location - val dir = rope.asScenery().direction.opposite + val ropeScenery = rope.asScenery() + val traversal = getClimbingRopeTraversal(ropeScenery) - val topOfWallLoc = Location(dir.stepY + ropeLoc.x, -dir.stepX + ropeLoc.y, ropeLoc.z) - - val baseOfWallLoc = Location(-dir.stepY + ropeLoc.x, dir.stepX + ropeLoc.y, ropeLoc.z) - - val distToTop = player.location.getDistance(topOfWallLoc) - val distToBase = player.location.getDistance(baseOfWallLoc) - - val onRopeTile = - player.location.x == ropeLoc.x && player.location.y == ropeLoc.y && player.location.z == ropeLoc.z - - if (onRopeTile || distToBase <= distToTop) { - teleport(player, topOfWallLoc, TeleportManager.TeleportType.INSTANT) + if (canClimbUpClimbingRope(player.location, ropeScenery)) { + teleport(player, traversal.topOfWallTile, TeleportManager.TeleportType.INSTANT) } else { sendMessage(player, "You can't climb down the rope.") }