From 4ba22ba1a7d6b433e936f705343f10ac5f4e629b Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Thu, 23 Jul 2026 14:28:52 +0300 Subject: [PATCH] Big fix batch, return to non-MobyDick2 --- Server/data/configs/item_configs.json | 2 + Server/data/configs/shops.json | 11 +- .../castlewars/CastleWarsBarricadeNPC.kt | 6 +- .../castlewars/CastleWarsCatapultInterface.kt | 14 +- .../castlewars/CastleWarsDoorAttackPulse.kt | 2 +- .../CastleWarsEquipmentRestrictions.kt | 22 +-- .../castlewars/CastleWarsFlagHandler.kt | 10 +- .../castlewars/CastleWarsGameState.kt | 139 ++++++------------ .../minigame/castlewars/CastleWarsGameZone.kt | 34 +---- .../castlewars/CastleWarsItemHandler.kt | 36 ++--- .../castlewars/CastleWarsListeners.kt | 106 ++++++------- .../castlewars/CastleWarsShutdownHandler.kt | 26 +++- .../CastleWarsSubstitutionDialogue.kt | 7 +- .../castlewars/CastleWarsTunnelMiningPulse.kt | 25 +++- .../castlewars/CatapultFiringHandler.kt | 10 +- .../castlewars/CatapultInterfaceRenderer.kt | 22 +-- .../content/minigame/castlewars/Lanthus.kt | 11 +- .../castlewars/areas/CastleWarsArea.kt | 20 ++- .../castlewars/areas/CastleWarsGameArea.kt | 102 +++++++------ .../castlewars/areas/CastleWarsRespawnArea.kt | 33 ++++- .../castlewars/areas/CastleWarsWaitingArea.kt | 5 +- Server/src/main/core/api/MapArea.kt | 15 +- .../container/impl/EquipmentContainer.java | 13 +- .../core/game/global/action/EquipHandler.kt | 26 ++-- .../node/entity/combat/CombatSwingHandler.kt | 3 +- .../game/node/entity/combat/DeathContext.kt | 13 +- .../core/game/node/entity/player/Player.java | 17 ++- .../game/system/config/ItemConfigParser.kt | 9 +- .../core/game/world/map/zone/MapZone.java | 18 ++- .../core/game/world/map/zone/ZoneMonitor.java | 28 +++- Server/src/main/core/plugin/ClassScanner.kt | 20 ++- .../minigame/castlewars/CastleWarsTests.kt | 87 +++++++++++ 32 files changed, 504 insertions(+), 388 deletions(-) diff --git a/Server/data/configs/item_configs.json b/Server/data/configs/item_configs.json index bfdd22535..37003e975 100644 --- a/Server/data/configs/item_configs.json +++ b/Server/data/configs/item_configs.json @@ -34820,6 +34820,7 @@ "defence_anim": "420", "durability": null, "equipment_slot": "3", + "unequip_without_inventory_space": "true", "render_anim": "131", "run_anim": "1427", "stand_anim": "1421", @@ -34850,6 +34851,7 @@ "defence_anim": "420", "durability": null, "equipment_slot": "3", + "unequip_without_inventory_space": "true", "render_anim": "131", "run_anim": "1427", "stand_anim": "1421", diff --git a/Server/data/configs/shops.json b/Server/data/configs/shops.json index 126acfb0d..f4186de26 100644 --- a/Server/data/configs/shops.json +++ b/Server/data/configs/shops.json @@ -2178,6 +2178,15 @@ "title": "Castle Wars Ticket Exchange", "stock": "{4068,1,100}-{4069,1,100}-{4070,1,100}-{4071,1,100}-{4072,1,100}-{4503,1,100}-{4504,1,100}-{4505,1,100}-{4506,1,100}-{4507,1,100}-{4508,1,100}-{4509,1,100}-{4510,1,100}-{4511,1,100}-{4512,1,100}-{4513,1,100}-{4514,1,100}-{4515,1,100}-{4516,1,100}" }, + { + "npcs": "5111", + "high_alch": "0", + "currency": "995", + "general_store": "false", + "id": "256", + "title": "Leon's Prototype Crossbow", + "stock": "{10156,2,100}" + }, { "npcs": "2039", "high_alch": "0", @@ -2187,4 +2196,4 @@ "title": "Uglug's Stuffsies", "stock": "{4844,100,10}-{10927,0,100}-{2862,100,100}-{1777,10,200}-{2876,0,500}-{2878,10,100}-{4850,0,100}-{946,5,100}-{4773,0,1000}-{4778,0,1000}-{4783,0,1000}-{4788,0,1500}-{4793,0,2000}-{4798,0,3000}-{4803,0,4000}-{4827,0,2000}" } -] \ No newline at end of file +] diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsBarricadeNPC.kt b/Server/src/main/content/minigame/castlewars/CastleWarsBarricadeNPC.kt index 3510dad56..842b798f4 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsBarricadeNPC.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsBarricadeNPC.kt @@ -11,7 +11,7 @@ import org.rs09.consts.NPCs * Custom NPC class for Castle Wars barricades. * * - Block all entity movement through them (like Pest Control Brawlers) - * - Never retaliate when attacked (this was scary man they squared up) + * - Never retaliate when attacked * - Never turn to face attackers or any direction * - Cannot attack or move */ @@ -46,9 +46,7 @@ class CastleWarsBarricadeNPC : AbstractNPC { return false } - override fun attack(node: Node?) { - return - } + override fun attack(node: Node?) {} override fun isPoisonImmune(): Boolean { return true diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt b/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt index 830d38650..104101237 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt @@ -39,7 +39,7 @@ class CastleWarsCatapultInterface : InterfaceListener { } private fun handleButtonClick(player: Player, buttonId: Int): Boolean { - val team = getPlayerTeam(player) ?: return closeAndReturn(player, false) + val team = CastleWarsGameState.getActiveGameTeam(player) ?: return closeAndReturn(player, true) val currentCoord = getCurrentCoordinate(player) return when (val button = CatapultButton.fromId(buttonId)) { @@ -89,6 +89,10 @@ class CastleWarsCatapultInterface : InterfaceListener { catapultTeam: CastleWarsTeam, catapultLocation: Location ): Boolean { + if (!CastleWarsGameState.isGameActive || CastleWarsGameState.getActiveGameTeam(player) != playerTeam) { + sendMessage(player, "The Castle Wars game is no longer active.") + return false + } if (playerTeam != catapultTeam) { sendMessage(player, "You can't use the enemy's catapult!") return false @@ -99,6 +103,10 @@ class CastleWarsCatapultInterface : InterfaceListener { sendMessage(player, "The catapult is no longer there.") return false } + if (player.location.z != catapultLocation.z || player.location.getDistance(catapultLocation) > 2.0) { + sendMessage(player, "You have moved too far away from the catapult.") + return false + } if (currentCatapult.id in CastleWars.damagedCatapultIds || !CastleWarsGameState.isCatapultOperational( catapultTeam ) @@ -143,10 +151,6 @@ class CastleWarsCatapultInterface : InterfaceListener { renderer.renderCoordinate(player, getCurrentCoordinate(player)) } - private fun getPlayerTeam(player: Player): CastleWarsTeam? { - return CastleWarsGameState.getPlayerTeam(player) - } - private fun getCatapultLocation(player: Player): Location? { return getAttribute(player, ATTR_LOCATION, null) } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt b/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt index 3883e6d6d..539cb1fc4 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt @@ -25,7 +25,7 @@ class CastleWarsDoorAttackPulse( private var ticksUntilNextHit = 0 override fun pulse(): Boolean { - if (!player.isActive || CastleWarsGameState.isDoorDestroyed(doorTeam) || CastleWarsGameState.getPlayerTeam( + if (!player.isActive || CastleWarsGameState.isDoorDestroyed(doorTeam) || CastleWarsGameState.getActiveGameTeam( player ) == null ) { diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt b/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt index 5692fdee4..ec16fb0bf 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt @@ -16,8 +16,15 @@ object CastleWarsEquipmentRestrictions { CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak ) + private val castleWarsBorders by lazy { + CastleWarsGameArea.areaBorders.toList() + CastleWarsWaitingArea.areaBorders.toList() + listOf( + CastleWarsRespawnArea.saradominRespawnRoom, + CastleWarsRespawnArea.zamorakRespawnRoom + ) + } + fun blocksEquip(player: Player, equipmentSlot: Int): Boolean { - if (equipmentSlot !in restrictedSlots || !isInCastleWarsArea(player)) { + if (equipmentSlot !in restrictedSlots) { return false } @@ -34,6 +41,10 @@ object CastleWarsEquipmentRestrictions { } } + fun isInCastleWarsArea(player: Player): Boolean { + return castleWarsBorders.any { it.insideBorder(player.location) } + } + private fun removeEquipment(player: Player, slot: Int) { val item = player.equipment[slot] ?: return player.equipment.remove(item) @@ -41,13 +52,4 @@ object CastleWarsEquipmentRestrictions { GroundItemManager.create(item.copy(), player.location, player) } } - - private fun isInCastleWarsArea(player: Player): Boolean { - val allCastleWarsBorders = - CastleWarsGameArea.areaBorders.toList() + CastleWarsWaitingArea.areaBorders.toList() + listOf( - CastleWarsRespawnArea.saradominRespawnRoom, - CastleWarsRespawnArea.zamorakRespawnRoom - ) - return allCastleWarsBorders.any { it.insideBorder(player.location) } - } } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt index e820a863e..b1fcc00a4 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt @@ -43,7 +43,7 @@ class CastleWarsFlagHandler : InteractionListener { sendMessage(player, "There is no game in progress.") return@on true } - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { sendMessage(player, "You are not in a Castle Wars game.") return@on true @@ -129,7 +129,7 @@ class CastleWarsFlagHandler : InteractionListener { sendMessage(player, "There is no game in progress.") return@on true } - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { sendMessage(player, "You are not in a Castle Wars game.") return@on true @@ -149,11 +149,9 @@ class CastleWarsFlagHandler : InteractionListener { return@on true } if (flagTeam == playerTeam) { - if (CastleWarsGameState.pickUpOwnFlag(player, flagTeam)) { // Message already sent by pickUpOwnFlag - } + CastleWarsGameState.pickUpOwnFlag(player, flagTeam) } else { - if (CastleWarsGameState.pickUpFlag(player, flagTeam)) { // Message already sent by pickUpFlag - } + CastleWarsGameState.pickUpFlag(player, flagTeam) } return@on true diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt index 98cbed706..5e9806ae7 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt @@ -19,7 +19,6 @@ import core.game.node.scenery.SceneryBuilder import core.game.world.map.Location import core.game.world.map.RegionManager import core.game.world.map.zone.ZoneBorders -import core.game.world.update.flag.chunk.ObjectUpdateFlag import org.rs09.consts.Items /** @@ -64,7 +63,9 @@ enum class BoulderState { } data class ActiveClimbingRope( - val rope: Scenery, val originalWall: Scenery + val rope: Scenery, + val originalWall: Scenery, + val ropeWall: Scenery ) /** @@ -275,12 +276,12 @@ object CastleWarsGameState { resetBattlefield() } - fun registerClimbingRope(rope: Scenery, originalWall: Scenery) { + fun registerClimbingRope(rope: Scenery, originalWall: Scenery, ropeWall: Scenery) { val key = Location.create(rope.location) rope.isActive = true rope.isRenderable = true - activeClimbingRopes[key] = ActiveClimbingRope(rope, Scenery(originalWall)) + activeClimbingRopes[key] = ActiveClimbingRope(rope, Scenery(originalWall), Scenery(ropeWall)) SceneryBuilder.add(rope) } @@ -288,18 +289,21 @@ object CastleWarsGameState { 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 + fun removeClimbingRope(rope: Scenery): Boolean { + val key = Location.create(rope.location) + val activeRope = activeClimbingRopes[key] ?: return false + if (activeRope.rope !== rope) return false + activeClimbingRopes.remove(key) - activeRope.rope.isActive = false - SceneryBuilder.replace(activeRope.rope, activeRope.originalWall) + SceneryBuilder.remove(activeRope.rope) + SceneryBuilder.replace(activeRope.ropeWall, activeRope.originalWall, false, false) return true } - fun clearClimbingRopes(sendUpdate: Boolean = true) { - val ropeLocations = activeClimbingRopes.keys.map { Location.create(it) } - for (location in ropeLocations) { - removeClimbingRope(location, sendUpdate) + fun clearClimbingRopes() { + val ropes = activeClimbingRopes.values.map { it.rope } + for (rope in ropes) { + removeClimbingRope(rope) } } @@ -385,33 +389,9 @@ object CastleWarsGameState { CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder(location) } - /** - * Get the team a player is on. - * Includes a self-healing mechanism if the player is in the area but not in the sets. - */ - fun getPlayerTeam(player: Player): CastleWarsTeam? { + fun getActiveGameTeam(player: Player): CastleWarsTeam? { if (CastleWarsGameArea.saradominPlayers.contains(player)) return CastleWarsTeam.SARADOMIN if (CastleWarsGameArea.zamorakPlayers.contains(player)) return CastleWarsTeam.ZAMORAK - if (CastleWarsWaitingArea.waitingSaradominPlayers.contains(player)) return CastleWarsTeam.SARADOMIN - if (CastleWarsWaitingArea.waitingZamorakPlayers.contains(player)) return CastleWarsTeam.ZAMORAK - if (isGameActive) { - val inCWArea = - CastleWarsGameArea.areaBorders.any { it.insideBorder(player.location) } || CastleWarsRespawnArea.saradominRespawnRoom.insideBorder( - player.location - ) || CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder( - player.location - ) - if (inCWArea) { - val capeId = player.equipment.get(EquipmentContainer.SLOT_CAPE)?.id ?: -1 - if (capeId == CastleWars.saradominTeamHoodedCloak) { - CastleWarsGameArea.saradominPlayers.add(player) - return CastleWarsTeam.SARADOMIN - } else if (capeId == CastleWars.zamorakTeamHoodedCloak) { - CastleWarsGameArea.zamorakPlayers.add(player) - return CastleWarsTeam.ZAMORAK - } - } - } return null } @@ -422,7 +402,7 @@ object CastleWarsGameState { */ fun checkFlagCarriers() { saradominFlagCarrier?.let { player -> - if (!player.isActive || getPlayerTeam(player) == null) { + if (!player.isActive || getActiveGameTeam(player) == null) { dropFlag(player) } else { val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON) @@ -432,7 +412,7 @@ object CastleWarsGameState { } } zamorakFlagCarrier?.let { player -> - if (!player.isActive || getPlayerTeam(player) == null) { + if (!player.isActive || getActiveGameTeam(player) == null) { dropFlag(player) } else { val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON) @@ -442,7 +422,7 @@ object CastleWarsGameState { } } saradominOwnFlagCarrier?.let { player -> - if (!player.isActive || getPlayerTeam(player) == null) { + if (!player.isActive || getActiveGameTeam(player) == null) { dropOwnFlag(player) } else { val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON) @@ -454,7 +434,7 @@ object CastleWarsGameState { } } zamorakOwnFlagCarrier?.let { player -> - if (!player.isActive || getPlayerTeam(player) == null) { + if (!player.isActive || getActiveGameTeam(player) == null) { dropOwnFlag(player) } else { val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON) @@ -465,7 +445,6 @@ object CastleWarsGameState { } } } - updateAllHintIcons() } /** @@ -539,7 +518,7 @@ object CastleWarsGameState { * @return true if the flag was successfully picked up */ fun pickUpFlag(player: Player, flagTeam: CastleWarsTeam): Boolean { - val playerTeam = getPlayerTeam(player) ?: return false + val playerTeam = getActiveGameTeam(player) ?: return false if (playerTeam == flagTeam) { sendMessage(player, "You can't pick up your own team's flag!") return false @@ -572,7 +551,7 @@ object CastleWarsGameState { if (currentWeapon != null) { if (!player.inventory.add(currentWeapon)) { GroundItemManager.create(currentWeapon, player.location, player) - sendMessage(player, "Your weapon was dropped because your inventory is full.") // TODO: nah? + sendMessage(player, "Your weapon was dropped because your inventory is full.") } else { sendMessage(player, "Your weapon has been unequipped.") } @@ -582,7 +561,7 @@ object CastleWarsGameState { player.equipment.remove(shield) if (!player.inventory.add(shield)) { GroundItemManager.create(shield, player.location, player) - sendMessage(player, "Your shield was dropped because your inventory is full.") // TODO: nah? + sendMessage(player, "Your shield was dropped because your inventory is full.") } else { sendMessage(player, "Your shield has been unequipped.") } @@ -776,7 +755,7 @@ object CastleWarsGameState { * @return true if the flag was successfully picked up */ fun pickUpOwnFlag(player: Player, flagTeam: CastleWarsTeam): Boolean { - val playerTeam = getPlayerTeam(player) ?: return false // Must be your own team's flag + val playerTeam = getActiveGameTeam(player) ?: return false // Must be your own team's flag if (playerTeam != flagTeam) { sendMessage(player, "That's not your team's flag!") return false @@ -842,7 +821,7 @@ object CastleWarsGameState { * A grace period prevents instant return if the flag is picked up while already inside the castle. */ private fun checkOwnFlagReturn(player: Player) { - val playerTeam = getPlayerTeam(player) ?: return + val playerTeam = getActiveGameTeam(player) ?: return val carriedFlag = getOwnCarriedFlag(player) ?: return if (carriedFlag != playerTeam) return val pickupTick = when (playerTeam) { @@ -924,7 +903,7 @@ object CastleWarsGameState { * @return true if the flag was successfully captured */ fun captureFlag(player: Player): Boolean { - val playerTeam = getPlayerTeam(player) ?: return false + val playerTeam = getActiveGameTeam(player) ?: return false val carriedFlag = getCarriedFlag(player) ?: return false if (carriedFlag == playerTeam) { return false @@ -957,6 +936,7 @@ object CastleWarsGameState { announceToAllPlayers("${playerTeam.teamName} has captured the ${carriedFlag.teamName} flag!") announceToAllPlayers("Score: Saradomin $saradominScore - $zamorakScore Zamorak") updateAllOverlays() + updateAllHintIcons() return true } @@ -1034,7 +1014,7 @@ object CastleWarsGameState { * Update the hint icon for a single player */ private fun updateHintIconForPlayer(player: Player) { - val playerTeam = getPlayerTeam(player) ?: return + val playerTeam = getActiveGameTeam(player) ?: return val enemyTeam = playerTeam.opposite() playerHintIconSlots[player]?.let { slot -> HintIconManager.removeHintIcon(player, slot) @@ -1046,8 +1026,7 @@ object CastleWarsGameState { val carrier = if (enemyTeam == CastleWarsTeam.SARADOMIN) saradominFlagCarrier ?: saradominOwnFlagCarrier else zamorakFlagCarrier ?: zamorakOwnFlagCarrier carrier?.let { - if (it == player) { - } else { + if (it != player) { val slot = HintIconManager.registerHintIcon(player, it) if (slot >= 0) { playerHintIconSlots[player] = slot @@ -1123,7 +1102,7 @@ object CastleWarsGameState { fun endGame(extraRewardRecipients: Map = emptyMap()) { val rewardRecipients = linkedMapOf() for (player in getAllGamePlayers()) { - val team = getPlayerTeam(player) ?: continue + val team = getActiveGameTeam(player) ?: continue rewardRecipients[player] = team } for ((player, team) in extraRewardRecipients) { @@ -1190,6 +1169,7 @@ object CastleWarsGameState { * @param damage Amount of damage (0-100) */ fun damageDoor(team: CastleWarsTeam, damage: Int) { + require(damage >= 0) { "Door damage cannot be negative" } val previousHealth = getDoorHealth(team) when (team) { CastleWarsTeam.SARADOMIN -> { @@ -1252,6 +1232,7 @@ object CastleWarsGameState { * @param repair Amount to repair (0-100) */ fun repairDoor(team: CastleWarsTeam, repair: Int) { + require(repair >= 0) { "Door repair amount cannot be negative" } when (team) { CastleWarsTeam.SARADOMIN -> { saradominDoorHealth = (saradominDoorHealth + repair).coerceAtMost(100) @@ -1314,7 +1295,7 @@ object CastleWarsGameState { "saradomin2" -> saradominBoulder2State "zamorak1" -> zamorakBoulder1State "zamorak2" -> zamorakBoulder2State - else -> BoulderState.COLLAPSED + else -> throw IllegalArgumentException("Unknown Castle Wars boulder key: $boulderKey") } } @@ -1327,6 +1308,7 @@ object CastleWarsGameState { "saradomin2" -> saradominBoulder2State = state "zamorak1" -> zamorakBoulder1State = state "zamorak2" -> zamorakBoulder2State = state + else -> throw IllegalArgumentException("Unknown Castle Wars boulder key: $boulderKey") } } @@ -1561,7 +1543,7 @@ object CastleWarsGameState { player, CASTLE_WARS_TIMER_VARP, remainingMinutes ) // Boulder varbits: 0 = collapsed/blocked (green/secure), 1 = cleared (red/open) - val team = getPlayerTeam(player) ?: return + val team = getActiveGameTeam(player) ?: return when (team) { CastleWarsTeam.SARADOMIN -> { setVarbit(player, SARA_DOOR_HEALTH_VARBIT, saradominDoorHealth) @@ -1581,48 +1563,11 @@ object CastleWarsGameState { } } - /** - * Get all players currently in the Castle Wars game area zones. - * Uses RegionManager to find all players in the zone borders, - * ensuring we reach players even if they weren't properly added to the tracked sets. - * @return Set of all players in the game area who have a valid team - */ + /** Get all connected players tracked as active participants. */ private fun getAllGamePlayers(): Set { - val players = mutableSetOf() - players.addAll((CastleWarsGameArea.saradominPlayers + CastleWarsGameArea.zamorakPlayers).filter { + return (CastleWarsGameArea.saradominPlayers + CastleWarsGameArea.zamorakPlayers).filter { it.isActive && it.session != null - }) - for (border in CastleWarsGameArea.areaBorders) { - val centerX = (border.southWestX + border.northEastX) / 2 - val centerY = (border.southWestY + border.northEastY) / 2 - val plane = border.plane - val radius = maxOf( - kotlin.math.abs(border.northEastX - border.southWestX), - kotlin.math.abs(border.northEastY - border.southWestY) - ) / 2 + 10 // Add buffer - val centerLocation = Location.create(centerX, centerY, plane) - for (player in RegionManager.getLocalPlayers( - centerLocation, radius - )) { - if (border.insideBorder(player.location) && getPlayerTeam(player) != null) { - players.add(player) - } - } - } - val saraRespawn = CastleWarsRespawnArea.saradominRespawnRoom - val zamRespawn = CastleWarsRespawnArea.zamorakRespawnRoom - for (respawnBorder in arrayOf(saraRespawn, zamRespawn)) { - val centerX = (respawnBorder.southWestX + respawnBorder.northEastX) / 2 - val centerY = (respawnBorder.southWestY + respawnBorder.northEastY) / 2 - val plane = respawnBorder.plane - val centerLocation = Location.create(centerX, centerY, plane) - for (player in RegionManager.getLocalPlayers(centerLocation, 20)) { - if (respawnBorder.insideBorder(player.location) && getPlayerTeam(player) != null) { - players.add(player) - } - } - } - return players + }.toSet() } /** @@ -1631,7 +1576,7 @@ object CastleWarsGameState { * @return Set of all players on the specified team */ private fun getTeamPlayers(team: CastleWarsTeam): Set { - return getAllGamePlayers().filter { getPlayerTeam(it) == team }.toSet() + return getAllGamePlayers().filter { getActiveGameTeam(it) == team }.toSet() } /** @@ -1743,8 +1688,8 @@ object CastleWarsGameState { sendMessage(player, "You can't steal the flag from yourself.") return false } - val playerTeam = getPlayerTeam(player) - val carrierTeam = getPlayerTeam(carrier) + val playerTeam = getActiveGameTeam(player) + val carrierTeam = getActiveGameTeam(carrier) if (playerTeam == null || carrierTeam == null) { sendMessage(player, "You can only steal flags in Castle Wars.") return false diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsGameZone.kt b/Server/src/main/content/minigame/castlewars/CastleWarsGameZone.kt index d9aa4e91a..42bda2e67 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsGameZone.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsGameZone.kt @@ -8,50 +8,24 @@ import core.game.node.entity.impl.PulseManager import core.game.node.entity.player.Player import core.game.system.timer.impl.Poison import core.game.world.map.Location -import core.game.world.map.RegionManager.getLocalNPCs import core.game.world.map.path.Pathfinder import core.game.world.map.zone.* /** * MapZone for Castle Wars game area that handles death and respawn. - * This is registered in addition to the MapArea to provide death handling, - * since MapArea doesn't support overriding the death method. + * This is registered in addition to the MapArea to provide death handling. */ class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDOM_EVENTS), StartupListener { companion object { - private val saradominStandardFloor = ZoneBorders(Location.create(2426, 3073, 3), Location.create(2430, 3077, 3)) - private val zamorakStandardFloor = ZoneBorders(Location.create(2373, 3134, 3), Location.create(2369, 3130, 3)) - private val saradominUpperFloor = ZoneBorders(Location.create(2431, 3080, 2), Location.create(2423, 3072, 2)) - private val zamorakUpperFloor = ZoneBorders(Location.create(2368, 3127, 2), Location.create(2376, 3135, 2)) - private val saradominFloor = ZoneBorders(Location.create(2420, 3072, 1), Location.create(2431, 3083, 1)) - private val zamorakFloor = ZoneBorders(Location.create(2379, 3135, 1), Location.create(2368, 3124, 1)) - private val battleField = ZoneBorders(Location.create(2368, 3135, 0), Location.create(2431, 3072, 0)) - private val saradominTunnels = ZoneBorders(Location.create(2430, 9481, 0), Location.create(2385, 9504, 0)) - private val zamorakTunnels = ZoneBorders(Location.create(2401, 9503, 0), Location.create(2366, 9529, 0)) - private val centerTunnels = ZoneBorders(Location.create(2396, 9493, 0), Location.create(2405, 9515, 0)) - val allBorders = arrayOf( - saradominStandardFloor, - zamorakStandardFloor, - saradominUpperFloor, - zamorakUpperFloor, - saradominFloor, - zamorakFloor, - battleField, - saradominTunnels, - zamorakTunnels, - centerTunnels - ) - /** * Handle Castle Wars death with poison preservation. - * Shared between CastleWarsGameZone.death() and CastleWarsGameArea.handleDeath(). * In Castle Wars, poison persists through death unlike normal safe deaths. */ fun handleCastleWarsDeath(player: Player): Boolean { if (CastleWarsGameArea.ticksLeftInGame <= 0) return false - val team = CastleWarsGameState.getPlayerTeam(player) ?: return false + val team = CastleWarsGameState.getActiveGameTeam(player) ?: return false if (CastleWarsGameState.isCarryingFlag(player)) { CastleWarsGameState.dropFlag(player) @@ -83,7 +57,7 @@ class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDO override fun startup() { ZoneBuilder.configure(this) - for (border in allBorders) { + for (border in CastleWarsGameArea.areaBorders) { register(border) } } @@ -103,7 +77,7 @@ class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDO val isMovingUp = to.z > from.z val isChangingZLevel = to.z != from.z - for (n in getLocalNPCs(e.location, 5)) { + for (n in findLocalNPCs(e.location, 5)) { if (n.isInvisible || n === e) { continue } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt index ab29eed83..111311044 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt @@ -8,7 +8,6 @@ import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_MAX_TIC import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_PULSE_INTERVAL import content.minigame.castlewars.CastleWarsConstants.EXPLOSIVE_POTION_SELF_DAMAGE import content.minigame.castlewars.CastleWarsConstants.MAX_BARRICADES_PER_TEAM -import content.minigame.castlewars.areas.CastleWarsGameArea import core.api.* import core.game.interaction.Clocks import core.game.interaction.IntType @@ -63,11 +62,10 @@ class CastleWarsItemHandler : InteractionListener { } fun dealExplosiveAoeDamage(attacker: Player, location: Location, radius: Int = 1) { - val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return + val attackerTeam = CastleWarsGameState.getActiveGameTeam(attacker) ?: return RegionManager.getLocalPlayers(location, radius).filter { target -> - target != attacker && target.isActive && CastleWarsGameState.getPlayerTeam(target) != null && CastleWarsGameState.getPlayerTeam( - target - ) != attackerTeam + val targetTeam = CastleWarsGameState.getActiveGameTeam(target) + target != attacker && target.isActive && targetTeam != null && targetTeam != attackerTeam }.forEach { target -> val damage = CastleWarsConstants.EXPLOSIVE_POTION_SELF_DAMAGE.coerceAtMost(target.skills.lifepoints) target.impactHandler.manualHit( @@ -100,7 +98,7 @@ class CastleWarsItemHandler : InteractionListener { return@on false } - val team = CastleWarsGameState.getPlayerTeam(player) + val team = CastleWarsGameState.getActiveGameTeam(player) if (team == null) { sendMessage(player, "You're not in a Castle Wars game.") return@on false @@ -143,7 +141,7 @@ class CastleWarsItemHandler : InteractionListener { ) { player, used, with -> val barricade = with as? NPC ?: return@onUseWith false - if (!CastleWarsGameState.isGameActive) { + if (!isActiveGamePlayer(player)) { return@onUseWith false } @@ -170,7 +168,7 @@ class CastleWarsItemHandler : InteractionListener { } on(CastleWars.cwExplosivePotion, IntType.ITEM, "drop") { player, node -> - if (!CastleWarsGameState.isGameActive) { + if (!isActiveGamePlayer(player)) { return@on false } @@ -205,7 +203,7 @@ class CastleWarsItemHandler : InteractionListener { ) { player, _, with -> val barricade = with as? NPC ?: return@onUseWith false - if (!CastleWarsGameState.isGameActive) { + if (!isActiveGamePlayer(player)) { return@onUseWith false } @@ -225,7 +223,9 @@ class CastleWarsItemHandler : InteractionListener { val firePulse = object : core.game.system.task.Pulse(BURNING_BARRICADE_PULSE_INTERVAL) { var tickCount = 0 override fun pulse(): Boolean { - if (!barricade.isActive || barricade.id != NPCs.BARRICADE_1533 || tickCount >= BURNING_BARRICADE_MAX_TICKS) { + if (!CastleWarsGameState.isGameActive || !barricade.isActive || + barricade.id != NPCs.BARRICADE_1533 || tickCount >= BURNING_BARRICADE_MAX_TICKS + ) { burningBarricadePulses.remove(barricade) if (barricade.isActive && barricade.id == NPCs.BARRICADE_1533) { barricade.skills.lifepoints = 0 @@ -249,7 +249,7 @@ class CastleWarsItemHandler : InteractionListener { onUseWith(IntType.NPC, Items.BUCKET_OF_WATER_1929, NPCs.BARRICADE_1533) { player, used, with -> val barricade = with as? NPC ?: return@onUseWith false - if (!CastleWarsGameState.isGameActive) { + if (!isActiveGamePlayer(player)) { return@onUseWith false } @@ -276,8 +276,8 @@ class CastleWarsItemHandler : InteractionListener { sendMessage(user, "You can only use bandages during a Castle Wars game.") return false } - val userTeam = getActiveGameTeam(user) - val targetTeam = getActiveGameTeam(target) + val userTeam = CastleWarsGameState.getActiveGameTeam(user) + val targetTeam = CastleWarsGameState.getActiveGameTeam(target) if (userTeam == null || targetTeam == null) { sendMessage(user, "You can only use bandages during a Castle Wars game.") return false @@ -328,12 +328,8 @@ class CastleWarsItemHandler : InteractionListener { return true } - private fun getActiveGameTeam(player: Player): CastleWarsTeam? { - return when { - CastleWarsGameArea.saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN - CastleWarsGameArea.zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK - else -> null - } + private fun isActiveGamePlayer(player: Player): Boolean { + return CastleWarsGameState.isGameActive && CastleWarsGameState.getActiveGameTeam(player) != null } private fun getLocalBarricades(location: Location, radius: Int): List { @@ -341,6 +337,6 @@ class CastleWarsItemHandler : InteractionListener { NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535 ) - return RegionManager.getLocalNPCs(location, radius).filter { it.id in barricadeIds && it.isActive } + return findLocalNPCs(location, radius).filter { it.id in barricadeIds && it.isActive } } } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt index bdbd97134..c2d5114af 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt @@ -25,7 +25,6 @@ import core.game.world.GameWorld import core.game.world.map.Direction import core.game.world.map.Location import core.game.world.map.RegionManager -import core.game.world.update.flag.chunk.ObjectUpdateFlag import core.game.world.update.flag.context.Animation import core.tools.Log import core.tools.RandomFunction @@ -246,20 +245,20 @@ class CastleWarsListeners : InteractionListener { onUseWith( SCENERY, CastleWars.cwClimbingRope, *CastleWars.cwCastleBattlementsMap.keys.toIntArray() ) { player, rope, wall -> + if (!requireActiveGamePlayer(player)) return@onUseWith false 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) + SceneryBuilder.replace(wallScenery, ropeWall, false, false) val hangingRope = Scenery(CastleWars.cwCastleClimbingRope, wall.location, 4, wall.direction.toInteger()) - CastleWarsGameState.registerClimbingRope(hangingRope, wallScenery) - RegionManager.getRegionChunk(hangingRope.location).flag(ObjectUpdateFlag(hangingRope, false)) + CastleWarsGameState.registerClimbingRope(hangingRope, wallScenery, ropeWall) + removeItem(player, rope) GameWorld.Pulser.submit(object : Pulse(CastleWars.ropeAliveTicks) { override fun pulse(): Boolean { - CastleWarsGameState.removeClimbingRope(hangingRope.location) + CastleWarsGameState.removeClimbingRope(hangingRope) return true } }) @@ -301,7 +300,7 @@ class CastleWarsListeners : InteractionListener { } on(CastleWars.saradominEnergyBarrier, IntType.SCENERY, "Pass") { player, barrier -> - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam != CastleWarsTeam.SARADOMIN) { sendMessage(player, "Only the Saradomin team may pass through this barrier.") return@on true @@ -330,7 +329,7 @@ class CastleWarsListeners : InteractionListener { return@on true } on(CastleWars.zamorakEnergyBarrier, IntType.SCENERY, "Pass") { player, barrier -> - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam != CastleWarsTeam.ZAMORAK) { sendMessage(player, "Only the Zamorak team may pass through this barrier.") return@on true @@ -370,8 +369,8 @@ class CastleWarsListeners : InteractionListener { on(CastleWars.largeDoorIds, IntType.SCENERY, "Open") { player, node -> val door = node.asScenery() - val doorTeam = getDoorTeam(door.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val doorTeam = getCastleTeam(door.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { // Not in Castle Wars game - shouldn't happen in game area @@ -404,8 +403,8 @@ class CastleWarsListeners : InteractionListener { on(CastleWars.largeDoorOpenIds, IntType.SCENERY, "Close") { player, node -> val door = node.asScenery() - val doorTeam = getDoorTeam(door.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val doorTeam = getCastleTeam(door.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { return@on false @@ -429,8 +428,8 @@ class CastleWarsListeners : InteractionListener { on(CastleWars.largeDoorIds, IntType.SCENERY, "Attack") { player, node -> val door = node.asScenery() - val doorTeam = getDoorTeam(door.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val doorTeam = getCastleTeam(door.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { return@on false @@ -454,8 +453,8 @@ class CastleWarsListeners : InteractionListener { SCENERY, CastleWars.cwExplosivePotion, *(CastleWars.largeDoorIds + CastleWars.largeDoorOpenIds) ) { player, used, with -> val door = with.asScenery() - val doorTeam = getDoorTeam(door.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val doorTeam = getCastleTeam(door.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { sendMessage(player, "You can only use this in Castle Wars.") @@ -488,10 +487,10 @@ class CastleWarsListeners : InteractionListener { onUseWith( SCENERY, CastleWars.cwToolkit, *(CastleWars.largeDoorIds + CastleWars.largeDoorOpenIds) - ) { player, used, with -> + ) { player, _, with -> val door = with.asScenery() - val doorTeam = getDoorTeam(door.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val doorTeam = getCastleTeam(door.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { sendMessage(player, "You can only use this in Castle Wars.") @@ -517,7 +516,7 @@ class CastleWarsListeners : InteractionListener { val door = node.asScenery() val doorTeam = if (door.id == CastleWars.saradominBackdoor) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false if (playerTeam == doorTeam) { openBackdoor(player, door) } else { @@ -535,7 +534,7 @@ class CastleWarsListeners : InteractionListener { val door = node.asScenery() val doorTeam = if (door.id == CastleWars.saradominBackdoor) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false if (playerTeam == doorTeam) { sendMessage(player, "You don't need to pick the lock on your own door.") return@on true @@ -568,7 +567,7 @@ class CastleWarsListeners : InteractionListener { val door = node.asScenery() val doorTeam = if (door.id == CastleWars.saradominBackdoorOpen) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false if (playerTeam != doorTeam) { sendMessage(player, "You can't close the enemy's door!") return@on true @@ -579,6 +578,7 @@ class CastleWarsListeners : InteractionListener { } on(CastleWars.tunnelRocks, IntType.SCENERY, "Mine") { player, node -> + if (!requireActiveGamePlayer(player)) return@on true val rocks = node.asScenery() val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location) if (boulderKey == null) { @@ -590,6 +590,7 @@ class CastleWarsListeners : InteractionListener { } on(CastleWars.tunnelRocksWeakened, IntType.SCENERY, "Mine") { player, node -> + if (!requireActiveGamePlayer(player)) return@on true val rocks = node.asScenery() val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location) if (boulderKey == null) { @@ -601,6 +602,7 @@ class CastleWarsListeners : InteractionListener { } onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.tunnelRocks) { player, used, with -> + if (!requireActiveGamePlayer(player)) return@onUseWith false val rocks = with.asScenery() val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location) if (boulderKey == null) { @@ -616,6 +618,7 @@ class CastleWarsListeners : InteractionListener { } onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.tunnelRocksWeakened) { player, used, with -> + if (!requireActiveGamePlayer(player)) return@onUseWith false val rocks = with.asScenery() val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location) if (boulderKey == null) { @@ -631,6 +634,7 @@ class CastleWarsListeners : InteractionListener { } onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.caveWall) { player, used, with -> + if (!requireActiveGamePlayer(player)) return@onUseWith false val wall = with.asScenery() val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location) if (boulderKey == null) { @@ -649,7 +653,8 @@ class CastleWarsListeners : InteractionListener { return@onUseWith true } - onUseWith(SCENERY, pickaxeIds, CastleWars.caveWall) { player, used, with -> + onUseWith(SCENERY, pickaxeIds, CastleWars.caveWall) { player, _, with -> + if (!requireActiveGamePlayer(player)) return@onUseWith false val wall = with.asScenery() val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location) if (boulderKey == null) { @@ -665,6 +670,7 @@ class CastleWarsListeners : InteractionListener { } on(CastleWars.caveWall, IntType.SCENERY, "Collapse") { player, node -> + if (!requireActiveGamePlayer(player)) return@on true val wall = node.asScenery() val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location) if (boulderKey == null) { @@ -681,8 +687,8 @@ class CastleWarsListeners : InteractionListener { on(CastleWars.allCatapultIds, IntType.SCENERY, "Operate") { player, node -> val catapult = node.asScenery() - val catapultTeam = getCatapultTeam(catapult.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val catapultTeam = getCastleTeam(catapult.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { sendMessage(player, "You need to be in a Castle Wars game to use this!") return@on true @@ -713,15 +719,15 @@ class CastleWarsListeners : InteractionListener { } setVarbit(player, CastleWarsGameState.VARBIT_CATAPULT_X, CatapultConfig.DEFAULT_X) setVarbit(player, CastleWarsGameState.VARBIT_CATAPULT_Y, CatapultConfig.DEFAULT_Y) - player.setAttribute("catapult:location", catapult.location) + player.setAttribute(CatapultConfig.ATTR_LOCATION, catapult.location) player.interfaceManager.open(Component(Components.CASTLEWARS_CATAPULT_54)) return@on true } onUseWith(SCENERY, org.rs09.consts.Items.TINDERBOX_590, *CastleWars.allCatapultIds) { player, _, with -> val catapult = with.asScenery() - val catapultTeam = getCatapultTeam(catapult.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false + val catapultTeam = getCastleTeam(catapult.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false if (playerTeam == catapultTeam) { sendMessage(player, "You can't destroy your own catapult!") return@onUseWith false @@ -753,8 +759,8 @@ class CastleWarsListeners : InteractionListener { onUseWith(SCENERY, CastleWars.cwExplosivePotion, *CastleWars.allCatapultIds) { player, used, with -> val catapult = with.asScenery() - val catapultTeam = getCatapultTeam(catapult.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false + val catapultTeam = getCastleTeam(catapult.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false if (playerTeam == catapultTeam) { sendMessage(player, "You can't destroy your own catapult!") return@onUseWith false @@ -772,8 +778,8 @@ class CastleWarsListeners : InteractionListener { onUseWith(SCENERY, Items.BUCKET_OF_WATER_1929, *CastleWars.allCatapultIds) { player, used, with -> val catapult = with.asScenery() - val catapultTeam = getCatapultTeam(catapult.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false + val catapultTeam = getCastleTeam(catapult.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false if (playerTeam != catapultTeam) { sendMessage(player, "You can't help the enemy's catapult!") return@onUseWith false @@ -789,10 +795,10 @@ class CastleWarsListeners : InteractionListener { return@onUseWith true } - onUseWith(SCENERY, CastleWars.cwToolkit, *CastleWars.allCatapultIds) { player, used, with -> + onUseWith(SCENERY, CastleWars.cwToolkit, *CastleWars.allCatapultIds) { player, _, with -> val catapult = with.asScenery() - val catapultTeam = getCatapultTeam(catapult.location) - val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false + val catapultTeam = getCastleTeam(catapult.location) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false if (playerTeam != catapultTeam) { sendMessage(player, "You can't repair the enemy's catapult!") return@onUseWith false @@ -806,8 +812,8 @@ class CastleWarsListeners : InteractionListener { return@onUseWith true } - on(CastleWars.saradominTrapdoor, IntType.SCENERY, "Go-down") { player, node -> - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + on(CastleWars.saradominTrapdoor, IntType.SCENERY, "Go-down") { player, _ -> + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam != CastleWarsTeam.SARADOMIN) { sendMessage(player, "Only the Saradomin team may use this trapdoor.") return@on true @@ -819,8 +825,8 @@ class CastleWarsListeners : InteractionListener { teleport(player, saradominTrapdoorDestination, TeleportManager.TeleportType.INSTANT) return@on true } - on(CastleWars.zamorakTrapdoor, IntType.SCENERY, "Go-down") { player, node -> - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + on(CastleWars.zamorakTrapdoor, IntType.SCENERY, "Go-down") { player, _ -> + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam != CastleWarsTeam.ZAMORAK) { sendMessage(player, "Only the Zamorak team may use this trapdoor.") return@on true @@ -840,7 +846,7 @@ class CastleWarsListeners : InteractionListener { player.properties.teleportLocation = destination return@on true } - val playerTeam = CastleWarsGameState.getPlayerTeam(player) + val playerTeam = CastleWarsGameState.getActiveGameTeam(player) if (playerTeam == null) { player.properties.teleportLocation = destination return@on true @@ -917,12 +923,16 @@ class CastleWarsListeners : InteractionListener { AgilityHandler.forceWalk(player, -1, startTile, endTile, Animation.create(819), 12, 0.0, null) } - private fun getDoorTeam(location: Location): CastleWarsTeam { + private fun getCastleTeam(location: Location): CastleWarsTeam { return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK } - private fun getCatapultTeam(location: Location): CastleWarsTeam { - return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK + private fun requireActiveGamePlayer(player: Player): Boolean { + if (CastleWarsGameState.isGameActive && CastleWarsGameState.getActiveGameTeam(player) != null) { + return true + } + sendMessage(player, "You can only do that during a Castle Wars game.") + return false } private fun getOpenDoorVariant(closedId: Int): Int? = when (closedId) { @@ -933,14 +943,6 @@ class CastleWarsListeners : InteractionListener { else -> null } - private fun getClosedDoorVariant(openId: Int): Int? = when (openId) { - 4425 -> 4423 - 4426 -> 4424 - 4429 -> 4427 - 4430 -> 4428 - else -> null - } - private fun isPlayerOutsideFrontDoor(player: Player, door: Scenery, doorTeam: CastleWarsTeam): Boolean { val playerY = player.location.y val doorY = door.location.y @@ -994,7 +996,7 @@ class CastleWarsListeners : InteractionListener { } private fun leaveGameThroughPortal(player: Player, portalTeam: CastleWarsTeam) { - if (CastleWarsGameState.getPlayerTeam(player) != portalTeam) { + if (CastleWarsGameState.getActiveGameTeam(player) != portalTeam) { sendMessage(player, "Only the ${portalTeam.teamName} team may use this portal.") return } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt index 86dfcf41b..9870ef83d 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt @@ -4,6 +4,7 @@ import content.minigame.castlewars.areas.CastleWarsGameArea import content.minigame.castlewars.areas.CastleWarsRespawnArea import content.minigame.castlewars.areas.CastleWarsWaitingArea import core.api.ShutdownListener +import core.api.removeAttributes import core.game.node.entity.player.Player import core.game.world.map.Location import core.game.world.map.RegionManager @@ -21,6 +22,9 @@ class CastleWarsShutdownHandler : ShutdownListener { allTrackedPlayers += CastleWarsGameArea.zamorakPlayers allTrackedPlayers += CastleWarsWaitingArea.waitingSaradominPlayers allTrackedPlayers += CastleWarsWaitingArea.waitingZamorakPlayers + allTrackedPlayers += CastleWarsRespawnArea.saradominPlayersInRespawnRoom.keys + allTrackedPlayers += CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.keys + allTrackedPlayers += CastleWarsGameArea.loggedOutPlayers.values.map { it.player } val respawnBorders = arrayOf( CastleWarsRespawnArea.saradominRespawnRoom, CastleWarsRespawnArea.zamorakRespawnRoom @@ -54,24 +58,38 @@ class CastleWarsShutdownHandler : ShutdownListener { for (player in allTrackedPlayers) { if (CastleWarsGameState.isCarryingFlag(player)) { - CastleWarsGameState.dropFlag(player) + CastleWarsGameState.dropFlag(player, broadcast = false) } else if (CastleWarsGameState.isCarryingOwnFlag(player)) { - CastleWarsGameState.dropOwnFlag(player) + CastleWarsGameState.dropOwnFlag(player, broadcast = false) } CastleWarsGameState.restoreAndCleanupPlayer(player) - player.location = CastleWars.lobbyBankArea.randomWalkableLoc + val lobbyLocation = CastleWars.lobbyBankArea.randomWalkableLoc + player.location = lobbyLocation + player.properties.teleportLocation = lobbyLocation + removeAttributes( + player, + CastleWarsConstants.ATTR_LOGOUT_TICK, + CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, + CastleWarsConstants.ATTR_LOGOUT_TEAM, + CastleWarsConstants.ATTR_LOGOUT_LOCATION, + CastleWarsConstants.ATTR_JOIN_TICK + ) } CastleWarsItemHandler.clearAllBarricades() - + CastleWarsGameState.clearCastleWarsGroundItems() CastleWarsGameState.resetGame() + CastleWarsGameArea.clearSubstitutionState() CastleWarsGameArea.saradominPlayers.clear() CastleWarsGameArea.zamorakPlayers.clear() CastleWarsGameArea.loggedOutPlayers.clear() CastleWarsWaitingArea.waitingSaradominPlayers.clear() CastleWarsWaitingArea.waitingZamorakPlayers.clear() + CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear() + CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear() CastleWarsGameArea.ticksLeftInGame = -1 + CastleWarsWaitingArea.gameStartCountdown = -1 } } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsSubstitutionDialogue.kt b/Server/src/main/content/minigame/castlewars/CastleWarsSubstitutionDialogue.kt index cddbf6ea1..00ddb2724 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsSubstitutionDialogue.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsSubstitutionDialogue.kt @@ -10,6 +10,7 @@ import core.tools.START_DIALOGUE class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : DialogueFile() { override fun handle(componentID: Int, buttonID: Int) { + val currentPlayer = player ?: return when (stage) { START_DIALOGUE -> showTopics( Topic("Yes.", ACCEPT, true), @@ -18,14 +19,14 @@ class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : Dialogu ) ACCEPT -> { - if (!CastleWarsGameArea.claimVacancy(player!!, team)) { - sendMessage(player!!, "That Castle Wars vacancy is no longer available.") + if (!CastleWarsGameArea.claimVacancy(currentPlayer, team)) { + sendMessage(currentPlayer, "That Castle Wars vacancy is no longer available.") } stage = END_DIALOGUE } DECLINE -> { - CastleWarsGameArea.declineVacancyOffer(player!!, team) + CastleWarsGameArea.declineVacancyOffer(currentPlayer, team) stage = END_DIALOGUE } } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsTunnelMiningPulse.kt b/Server/src/main/content/minigame/castlewars/CastleWarsTunnelMiningPulse.kt index e42d6838c..4cf352da2 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsTunnelMiningPulse.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsTunnelMiningPulse.kt @@ -29,8 +29,6 @@ class CastleWarsTunnelMiningPulse( private const val REQUIRED_LEVEL = 1 private const val RATE = 0.40 - private const val MINING_ANIMATION = 625 - private const val EXPERIENCE = 0.0 } @@ -65,15 +63,18 @@ class CastleWarsTunnelMiningPulse( } private fun checkRequirements(): Boolean { + if (!canContinueCastleWarsMining(player, rocks)) { + return false + } + val expectedState = if (isWeakened) BoulderState.WEAKENED else BoulderState.COLLAPSED + if (CastleWarsGameState.getBoulderState(boulderKey) != expectedState) { + return false + } val pickaxe = SkillingTool.getPickaxe(player) if (pickaxe == null) { sendMessage(player, "You need a pickaxe to mine these rocks.") return false } - if (!rocks.isActive) { - return false - } - return true } @@ -159,6 +160,12 @@ class CastleWarsCaveWallMiningPulse( } private fun checkRequirements(): Boolean { + if (!canContinueCastleWarsMining(player, caveWall)) { + return false + } + if (!CastleWarsGameState.isBoulderCleared(boulderKey)) { + return false + } val pickaxe = SkillingTool.getPickaxe(player) if (pickaxe == null) { sendMessage(player, "You need a pickaxe to mine the cave wall.") @@ -198,3 +205,9 @@ class CastleWarsCaveWallMiningPulse( sendMessage(player, "You collapse the tunnel!") } } + +private fun canContinueCastleWarsMining(player: Player, scenery: Scenery): Boolean { + return player.isActive && CastleWarsGameState.isGameActive && + CastleWarsGameState.getActiveGameTeam(player) != null && scenery.isActive && + player.location.z == scenery.location.z && player.location.getDistance(scenery.location) <= 2.0 +} diff --git a/Server/src/main/content/minigame/castlewars/CatapultFiringHandler.kt b/Server/src/main/content/minigame/castlewars/CatapultFiringHandler.kt index 9eb8d1da6..9e9dc35fa 100644 --- a/Server/src/main/content/minigame/castlewars/CatapultFiringHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CatapultFiringHandler.kt @@ -69,7 +69,7 @@ class CatapultFiringHandler { val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE) val impactDelayTicks = (speed / 30).coerceAtLeast(1) - sendProjectile(catapultLocation, finalLocation) + sendProjectile(catapultLocation, finalLocation, distance, speed) playFireEffects(player, finalLocation, impactDelayTicks) scheduleImpact(player, finalLocation, impactDelayTicks) @@ -88,10 +88,7 @@ class CatapultFiringHandler { return Location.create(finalX, finalY, 0) } - private fun sendProjectile(from: Location, to: Location) { - val distance = from.getDistance(to).toInt() - val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE) - + private fun sendProjectile(from: Location, to: Location, distance: Int, speed: Int) { Projectile.create( from, to, @@ -139,6 +136,7 @@ class CatapultFiringHandler { } private fun isValidTarget(target: Player): Boolean { - return CastleWarsGameState.getPlayerTeam(target) != null + return CastleWarsGameState.isGameActive && target.isActive && + CastleWarsGameState.getActiveGameTeam(target) != null } } diff --git a/Server/src/main/content/minigame/castlewars/CatapultInterfaceRenderer.kt b/Server/src/main/content/minigame/castlewars/CatapultInterfaceRenderer.kt index f7f80748d..27746e638 100644 --- a/Server/src/main/content/minigame/castlewars/CatapultInterfaceRenderer.kt +++ b/Server/src/main/content/minigame/castlewars/CatapultInterfaceRenderer.kt @@ -1,7 +1,5 @@ package content.minigame.castlewars -import content.minigame.castlewars.CatapultConfig.DEFAULT_X -import content.minigame.castlewars.CatapultConfig.DEFAULT_Y import content.minigame.castlewars.CatapultConfig.DIGIT_CHILD_IDS import content.minigame.castlewars.CatapultConfig.DIGIT_ITEM_IDS import content.minigame.castlewars.CatapultConfig.INTERFACE_ID @@ -17,6 +15,7 @@ import core.game.node.entity.player.Player import core.net.packet.PacketRepository import core.net.packet.context.ChildPositionContext import core.net.packet.out.RepositionChild +import kotlin.math.roundToInt /** * Handles rendering the catapult interface including marker position and digit displays. @@ -61,24 +60,13 @@ class CatapultInterfaceRenderer { sendDigitModel(player, DIGIT_CHILD_IDS[3], yOnes) } - /** - * Resets the interface to default coordinates. - * @param player The player whose interface to reset - */ - fun resetToDefaults(player: Player) { - val defaultCoord = Coordinate(DEFAULT_X, DEFAULT_Y) - renderCoordinate(player, defaultCoord) + internal fun calculateScreenX(coordX: Int): Int { + return MAP_BASE_X + (coordX.toDouble() / MAX_COORDINATE * MAP_WIDTH).roundToInt() } - private fun calculateScreenX(coordX: Int): Int { - val pixelsPerUnit = MAP_WIDTH / MAX_COORDINATE - return MAP_BASE_X + (coordX * pixelsPerUnit) - } - - private fun calculateScreenY(coordY: Int): Int { - val pixelsPerUnit = MAP_HEIGHT / MAX_COORDINATE + internal fun calculateScreenY(coordY: Int): Int { // Y is inverted - 0 is at the bottom, MAX is at the top - return MAP_BASE_Y + ((MAX_COORDINATE - coordY) * pixelsPerUnit) + return MAP_BASE_Y + ((MAX_COORDINATE - coordY).toDouble() / MAX_COORDINATE * MAP_HEIGHT).roundToInt() } private fun sendDigitModel(player: Player, childId: Int, digit: Int) { diff --git a/Server/src/main/content/minigame/castlewars/Lanthus.kt b/Server/src/main/content/minigame/castlewars/Lanthus.kt index 4be45c92a..ccccb0cdb 100644 --- a/Server/src/main/content/minigame/castlewars/Lanthus.kt +++ b/Server/src/main/content/minigame/castlewars/Lanthus.kt @@ -81,10 +81,13 @@ class Lanthus : InteractionListener, TickListener { "your patron god, do you? Other than that, just have fun and enjoy it!" ).also { stage++ } - 50 -> npcl( - FacialExpression.FRIENDLY, "Sure, here you go." - ).also { stage = END_DIALOGUE } - .also { player.inventory.add(Item(Items.CASTLEWARS_MANUAL_4055)) } + 50 -> { + if (addItem(player, Items.CASTLEWARS_MANUAL_4055)) { + npcl(FacialExpression.FRIENDLY, "Sure, here you go.") + } else { + npcl(FacialExpression.FRIENDLY, "You don't have enough inventory space.") + }.also { stage = END_DIALOGUE } + } 83 -> player("Great! Oh, how do I win the game?").also { stage++ } 84 -> npcl( diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt index a23734ff5..037b89739 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt @@ -10,9 +10,14 @@ import core.api.sendMessage import core.game.interaction.InteractionListener import core.game.node.entity.Entity import core.game.node.entity.player.Player +import core.game.node.item.Item abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { + override fun canEquipItem(player: Player, item: Item, equipmentSlot: Int): Boolean? { + return !CastleWarsEquipmentRestrictions.blocksEquip(player, equipmentSlot) + } + override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return CastleWarsEquipmentRestrictions.removeInvalidEquipment(player) @@ -64,18 +69,11 @@ abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { override fun defineListeners() { onUnequip(intArrayOf(CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak)) { player, _ -> - val allCastleWarsBorders = - CastleWarsGameArea.areaBorders.toList() + CastleWarsWaitingArea.areaBorders.toList() + listOf( - CastleWarsRespawnArea.saradominRespawnRoom, - CastleWarsRespawnArea.zamorakRespawnRoom - ) - for (border in allCastleWarsBorders) { - if (border.insideBorder(player)) { - sendMessage(player, "You can't remove your team's colours") - return@onUnequip false - } + if (CastleWarsEquipmentRestrictions.isInCastleWarsArea(player)) { + sendMessage(player, "You can't remove your team's colours") + return@onUnequip false } - return@onUnequip true + true } } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt index d9a1e660f..af7e8cddf 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt @@ -4,6 +4,7 @@ import content.minigame.castlewars.* import content.minigame.castlewars.CastleWarsConstants.PLAYER_CLEANUP_INTERVAL_TICKS import core.api.* import core.game.component.Component +import core.game.container.impl.EquipmentContainer import core.game.event.EventHook import core.game.event.InterfaceCloseEvent import core.game.interaction.Option @@ -18,6 +19,7 @@ import core.game.world.map.zone.ZoneBorders import core.game.world.map.zone.ZoneType import core.tools.Log import org.rs09.consts.Components +import java.util.Locale /** * Handles the Castle Wars game map @@ -111,14 +113,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } } - /** - * Handle player death in Castle Wars - delegates to CastleWarsGameZone. - * This ensures consistent death behavior including poison preservation. - */ - fun handleDeath(player: Player, killer: Entity?): Boolean { - return CastleWarsGameZone.handleCastleWarsDeath(player) - } - fun clearSubstitutionState() { activeVacancyOffers.clear() activeVacancyOfferTicks.clear() @@ -263,9 +257,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { clearLogoutState(player) saradominPlayers.remove(player) zamorakPlayers.remove(player) - CastleWarsGameState.clearHintIconForPlayer(player) - player.interaction.remove(Option._P_ATTACK) - player.interaction.remove(Option._P_STEAL_FROM) if (offerReplacement) { openVacancy(leavingTeam) fillVacancy(leavingTeam) @@ -278,7 +269,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } fun playerKey(player: Player): String { - return player.username.lowercase() + return player.username.lowercase(Locale.ROOT) } private fun setTeam(player: Player, team: CastleWarsTeam) { @@ -334,8 +325,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { CastleWarsGameState.clearHintIconForPlayer(player) CastleWarsGameState.restoreAndCleanupPlayer(player) player.interaction.remove(Option._P_ATTACK) - player.location = waitingRoomFor(team).randomWalkableLoc - player.equipment.replace(Item(team.cloakId), 1) + relocateImmediately(player, waitingRoomFor(team).randomWalkableLoc) + player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE) waitingSetFor(team).add(player) clearLogoutAttributes(player) } @@ -347,11 +338,17 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { player.interaction.remove(Option._P_ATTACK) player.interaction.remove(Option._P_STEAL_FROM) val lobbyLocation = CastleWars.lobbyBankArea.randomWalkableLoc - player.location = lobbyLocation - player.properties.teleportLocation = lobbyLocation + relocateImmediately(player, lobbyLocation) clearLogoutState(player) } + @Suppress("DEPRECATION") + private fun relocateImmediately(player: Player, destination: Location) { + // Logout restoration may run before the player can process a normal teleport. + player.location = destination + player.properties.teleportLocation = destination + } + fun restoreActiveLogoutOnLogin(player: Player) { val savedTeamName = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "") val savedLogoutTime = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, 0L) @@ -372,7 +369,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { return } if (ticksLeftInGame > 0 && elapsedMillis < CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS) { - player.location = Location.create(logoutLocation) + relocateImmediately(player, Location.create(logoutLocation)) setTeam(player, team) clearLogoutAttributes(player) return @@ -384,7 +381,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } } - object SaradominInterfaceCloseHook : EventHook { // TODO: this might be useless. + object SaradominInterfaceCloseHook : EventHook { override fun process(entity: Entity, event: InterfaceCloseEvent) { val player = entity as? Player ?: return // Only restore if player is still in the game @@ -398,7 +395,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } } - object ZamorakInterfaceCloseHook : EventHook { // TODO: this might be useless. + object ZamorakInterfaceCloseHook : EventHook { override fun process(entity: Entity, event: InterfaceCloseEvent) { val player = entity as? Player ?: return if (!zamorakPlayers.contains(player)) return @@ -453,29 +450,36 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return + restoreActiveLogoutOnLogin(player) + if (areaBorders.none { it.insideBorder(player.location) }) { + return + } + val team = CastleWarsGameState.getActiveGameTeam(player) + if (team == null || ticksLeftInGame <= 0) { + log(this::class.java, Log.WARN, "${player.name} entered Castle Wars without an active match membership") + player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc + return + } + super.areaEnter(player) zone.zoneType = ZoneType.CASTLE_WARS.id registerTimer( player, spawnTimer("teleblock", CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE, false) ) - val team = CastleWarsGameState.getPlayerTeam(player) when (team) { CastleWarsTeam.SARADOMIN -> { player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) - player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) + player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), EquipmentContainer.SLOT_CAPE) player.hook(Event.InterfaceClosed, SaradominInterfaceCloseHook) } CastleWarsTeam.ZAMORAK -> { player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) - player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) + player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), EquipmentContainer.SLOT_CAPE) player.hook(Event.InterfaceClosed, ZamorakInterfaceCloseHook) } - null -> { - log(this::class.java, Log.WARN, "${player.name} entered Castle Wars but is NOT on a team?!") - } } CastleWarsGameState.setActiveGamePlayerOptions(player) } @@ -546,22 +550,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } CastleWarsGameState.checkFlagCarriers() CastleWarsGameState.checkDroppedFlagTimeouts() - for (player in saradominPlayers) { - val currentOverlayId = player.interfaceManager.overlay?.id - if (currentOverlayId == null || currentOverlayId != 58) { - player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) - } - // TODO: this... should not be done lmfao CRITICAL TODO: PROPER CW MATCH UI UPDATING THATS NOT SHIT - CastleWarsGameState.updateOverlay(player) - } - for (player in zamorakPlayers) { - val currentOverlayId = player.interfaceManager.overlay?.id - if (currentOverlayId == null || currentOverlayId != 59) { - player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) - } - // TODO: this... should not be done lmfao CRITICAL TODO: PROPER CW MATCH UI UPDATING THATS NOT SHIT - CastleWarsGameState.updateOverlay(player) - } + saradominPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.SARADOMIN) } + zamorakPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.ZAMORAK) } if (ticksLeftInGame % PLAYER_CLEANUP_INTERVAL_TICKS == 0) { cleanupInactivePlayers() } @@ -609,12 +599,15 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { CastleWarsGameState.cleanupInactiveHintIcons() } - /** - * Handle death in Castle Wars - respawn at team base instead of normal death - */ - fun death(entity: Entity, killer: Entity?): Boolean { - if (entity !is Player) return false - return handleDeath(entity, killer) + private fun ensureGameOverlay(player: Player, team: CastleWarsTeam) { + val overlayId = when (team) { + CastleWarsTeam.SARADOMIN -> Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58 + CastleWarsTeam.ZAMORAK -> Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59 + } + if (player.interfaceManager.overlay?.id != overlayId) { + player.interfaceManager.openOverlay(Component(overlayId)) + CastleWarsGameState.updateOverlay(player) + } } override fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { @@ -623,8 +616,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { if (ticksLeftInGame <= 0) { return false } - val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return false - val victimTeam = CastleWarsGameState.getPlayerTeam(victim) ?: return false + val attackerTeam = CastleWarsGameState.getActiveGameTeam(attacker) ?: return false + val victimTeam = CastleWarsGameState.getActiveGameTeam(victim) ?: return false if (attackerTeam == victimTeam) { if (message) { sendMessage(attacker, "You can only attack players on the opposite team!") @@ -634,4 +627,15 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { return true } + override fun modifyOutgoingHitInArea(attacker: Entity, target: Entity, hit: Int): Int? { + val attackingPlayer = attacker as? Player ?: return null + val victim = target as? Player ?: return null + if (!CastleWarsGameState.isGameActive || CastleWarsGameState.getActiveGameTeam(attackingPlayer) == null || + CastleWarsGameState.getActiveGameTeam(victim) == null + ) { + return null + } + return CastleWarsGameState.applyBraceletDamageBoost(attackingPlayer, victim, hit) + } + } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt index 2ef507f55..84001273a 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt @@ -3,11 +3,13 @@ package content.minigame.castlewars.areas import content.minigame.castlewars.CastleWars import content.minigame.castlewars.CastleWarsConstants import content.minigame.castlewars.CastleWarsGameState +import content.minigame.castlewars.CastleWarsTeam import core.api.TickListener import core.api.getWorldTicks import core.api.sendMessage import core.game.node.entity.Entity import core.game.node.entity.player.Player +import core.game.container.impl.EquipmentContainer import core.game.node.item.Item import core.game.world.map.Location import core.game.world.map.zone.ZoneBorders @@ -33,6 +35,11 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return + val team = CastleWarsGameState.getActiveGameTeam(player) + if (team == null || CastleWarsGameArea.ticksLeftInGame <= 0) { + player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc + return + } super.areaEnter(player) if (CastleWarsGameState.isCarryingAnyFlag(player)) { if (CastleWarsGameState.isCarryingFlag(player)) { @@ -42,12 +49,24 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { } sendMessage(player, "You cannot take the flag into your respawn room!") } - if (saradominRespawnRoom.insideBorder(player.location)) { - player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) - saradominPlayersInRespawnRoom[player] = getWorldTicks() - } else if (zamorakRespawnRoom.insideBorder(player.location)) { - player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) - zamorakPlayersInRespawnRoom[player] = getWorldTicks() + when (team) { + CastleWarsTeam.SARADOMIN -> { + if (!saradominRespawnRoom.insideBorder(player.location)) { + player.properties.teleportLocation = saradominRespawnRoom.randomWalkableLoc + return + } + player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE) + saradominPlayersInRespawnRoom[player] = getWorldTicks() + } + + CastleWarsTeam.ZAMORAK -> { + if (!zamorakRespawnRoom.insideBorder(player.location)) { + player.properties.teleportLocation = zamorakRespawnRoom.randomWalkableLoc + return + } + player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE) + zamorakPlayersInRespawnRoom[player] = getWorldTicks() + } } CastleWarsGameState.setActiveGamePlayerOptions(player) } @@ -82,7 +101,7 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { } saradominPlayersInRespawnRoom.remove(player) zamorakPlayersInRespawnRoom.remove(player) - if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getPlayerTeam(player) != null) { + if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getActiveGameTeam(player) != null) { CastleWarsGameArea().logout(player) return } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt index e66a93747..77bf1e878 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt @@ -8,6 +8,7 @@ import core.api.* import core.game.component.Component import core.game.node.entity.Entity import core.game.node.entity.player.Player +import core.game.container.impl.EquipmentContainer import core.game.node.item.Item import core.game.world.map.zone.ZoneBorders import org.rs09.consts.Components @@ -50,11 +51,11 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { // Set team attribute and equip the hooded cloak on the entity based on which waiting room they're in val joinedTeam = if (zamorakWaitingRoom.insideBorder(player.location)) { - player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) + player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), EquipmentContainer.SLOT_CAPE) waitingZamorakPlayers.add(player) CastleWarsTeam.ZAMORAK } else if (saradominWaitingRoom.insideBorder(player.location)) { - player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) + player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), EquipmentContainer.SLOT_CAPE) waitingSaradominPlayers.add(player) CastleWarsTeam.SARADOMIN } else { diff --git a/Server/src/main/core/api/MapArea.kt b/Server/src/main/core/api/MapArea.kt index 6b72737ef..f8b4e29a0 100644 --- a/Server/src/main/core/api/MapArea.kt +++ b/Server/src/main/core/api/MapArea.kt @@ -3,6 +3,8 @@ package core.api import core.game.node.Node import core.game.node.entity.Entity import core.game.node.entity.combat.CombatStyle +import core.game.node.entity.player.Player +import core.game.node.item.Item import core.game.world.map.Location import core.game.world.map.zone.MapZone import core.game.world.map.zone.ZoneBorders @@ -32,12 +34,17 @@ interface MapArea : ContentInterface { * Return null if you want the default check in MapZone. (default) **/ fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { return null } + /** - * Extends MapZone to cater to pvp, team games and all other unique targeting systems. - * Return true/false if you want to allow the deaths, especially false for "safe" areas. - * Return null if you want the default check in MapZone. (default) + * Allows an area to reject equipment changes without coupling core equipment code + * to a particular piece of content. **/ - fun canStartDeath(entity: Entity, killer: Entity?): Boolean? { return null } + fun canEquipItem(player: Player, item: Item, equipmentSlot: Int): Boolean? { return null } + + /** + * Allows an area to modify damage dealt by an entity inside it. + */ + fun modifyOutgoingHitInArea(attacker: Entity, target: Entity, hit: Int): Int? { return null } companion object { val zoneMaps = HashMap() diff --git a/Server/src/main/core/game/container/impl/EquipmentContainer.java b/Server/src/main/core/game/container/impl/EquipmentContainer.java index a524534e8..5964ecd5f 100644 --- a/Server/src/main/core/game/container/impl/EquipmentContainer.java +++ b/Server/src/main/core/game/container/impl/EquipmentContainer.java @@ -2,7 +2,6 @@ package core.game.container.impl; import content.global.skill.skillcapeperks.SkillcapeEquipmentPlugin; import content.global.skill.skillcapeperks.SkillcapePerks; -import content.minigame.castlewars.CastleWars; import core.game.container.Container; import core.game.container.ContainerEvent; import core.game.container.ContainerListener; @@ -151,7 +150,7 @@ public final class EquipmentContainer extends Container { return false; } ArrayList itemsToMoveToInventory = new ArrayList<>(current); - itemsToMoveToInventory.removeIf(item -> isCastleWarsFlag(item) && !containsItem(item)); + itemsToMoveToInventory.removeIf(item -> canUnequipWithoutInventorySpace(item) && !containsItem(item)); if(itemsToMoveToInventory.isEmpty()) { return true; } @@ -191,7 +190,7 @@ public final class EquipmentContainer extends Container { int neededSlots = 0; for(Item item : current) { - if(isCastleWarsFlag(item)) { + if(canUnequipWithoutInventorySpace(item)) { continue; } if(!item.getDefinition().isStackable()) { @@ -205,8 +204,8 @@ public final class EquipmentContainer extends Container { return neededSlots; } - private boolean isCastleWarsFlag(Item item) { - return item.getId() == CastleWars.saradominFlag || item.getId() == CastleWars.zamorakFlag; + private boolean canUnequipWithoutInventorySpace(Item item) { + return item.getDefinition().getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false); } @Nullable @@ -230,7 +229,7 @@ public final class EquipmentContainer extends Container { if (plugin != null) { Object object = plugin.fireEvent("unequip", player, currentItem); if (object != null && !((Boolean) object)) { - if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) { + if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) { continue; } canContinue = false; @@ -241,7 +240,7 @@ public final class EquipmentContainer extends Container { canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false); if(!canContinue) { - if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) { + if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) { canContinue = true; continue; } diff --git a/Server/src/main/core/game/global/action/EquipHandler.kt b/Server/src/main/core/game/global/action/EquipHandler.kt index 4b44e8e74..b2dc7586d 100644 --- a/Server/src/main/core/game/global/action/EquipHandler.kt +++ b/Server/src/main/core/game/global/action/EquipHandler.kt @@ -1,7 +1,5 @@ package core.game.global.action -import content.minigame.castlewars.CastleWars -import content.minigame.castlewars.CastleWarsEquipmentRestrictions import core.game.event.ItemEquipEvent import core.game.event.ItemUnequipEvent import core.game.container.impl.EquipmentContainer @@ -12,12 +10,14 @@ import core.game.node.Node import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.player.Player import core.game.node.entity.player.link.audio.Audio +import core.game.node.item.Item import core.plugin.Plugin import core.game.interaction.InteractionListener import core.game.interaction.IntType import core.game.interaction.InteractionListeners import core.game.system.config.ItemConfigParser import org.rs09.consts.Sounds +import java.util.Locale /** * Represents the equipment equipping handler plugin. @@ -37,20 +37,20 @@ class EquipHandler : InteractionListener { val item = node.asItem() val itemEquipmentSlot = item.definition.getConfiguration(ItemConfigParser.EQUIP_SLOT, -1) - if (CastleWarsEquipmentRestrictions.blocksEquip(player, itemEquipmentSlot)) { + if (!player.zoneMonitor.canEquip(player, item, itemEquipmentSlot)) { return } val currentEquippedItem = player.equipment[itemEquipmentSlot] - if (item == null || currentEquippedItem == item || item.name.toLowerCase().contains("goblin mail")) { + if (currentEquippedItem == item || item.name.lowercase(Locale.ROOT).contains("goblin mail")) { return } if(currentEquippedItem != null){ if(!InteractionListeners.run(currentEquippedItem.id, player, currentEquippedItem, false)){ - val droppedCastleWarsFlag = - isCastleWarsFlag(currentEquippedItem.id) && player.equipment[itemEquipmentSlot] == null - if (!droppedCastleWarsFlag) { + val listenerRemovedItem = + canUnequipWithoutInventorySpace(currentEquippedItem) && player.equipment[itemEquipmentSlot] == null + if (!listenerRemovedItem) { return } } @@ -128,7 +128,7 @@ class EquipHandler : InteractionListener { if (slot == EquipmentContainer.SLOT_WEAPON) { player.packetDispatch.sendString("", 92, 0) } - if (!isCastleWarsFlag(item.id)) { + if (!canUnequipWithoutInventorySpace(item)) { val maximumAdd = player.inventory.getMaximumAdd(item) if (maximumAdd < item.amount) { player.packetDispatch.sendMessage("Not enough free space in your inventory.") @@ -138,10 +138,16 @@ class EquipHandler : InteractionListener { val plugin = item.definition.getConfiguration>("equipment", null) if (plugin != null) { if (!(plugin.fireEvent("unequip", player, item) as Boolean)) { + if (canUnequipWithoutInventorySpace(item) && player.equipment[slot] == null) { + return + } return } } if (!InteractionListeners.run(itemId, player, item, false)) { + if (canUnequipWithoutInventorySpace(item) && player.equipment[slot] == null) { + return + } return } val maximumAdd = player.inventory.getMaximumAdd(item) @@ -163,8 +169,8 @@ class EquipHandler : InteractionListener { } } - private fun isCastleWarsFlag(itemId: Int): Boolean { - return itemId == CastleWars.saradominFlag || itemId == CastleWars.zamorakFlag + private fun canUnequipWithoutInventorySpace(item: Item): Boolean { + return item.definition.getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false) } } } diff --git a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt index 908a07394..2aee6a2b8 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt @@ -23,7 +23,6 @@ import core.game.world.update.flag.context.Animation import core.tools.RandomFunction import core.game.system.config.ItemConfigParser import core.tools.Log -import content.minigame.castlewars.CastleWarsGameState import org.rs09.consts.Sounds import java.util.* import kotlin.math.floor @@ -587,7 +586,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { hit += (hit.toDouble() * 0.15).toInt() } if (victim is Player) { - hit = CastleWarsGameState.applyBraceletDamageBoost(player, victim.asPlayer(), hit) + hit = player.zoneMonitor.modifyOutgoingHit(player, victim, hit) } } if (attacker is Familiar && victim is Player) { diff --git a/Server/src/main/core/game/node/entity/combat/DeathContext.kt b/Server/src/main/core/game/node/entity/combat/DeathContext.kt index f81f6887b..26192117d 100644 --- a/Server/src/main/core/game/node/entity/combat/DeathContext.kt +++ b/Server/src/main/core/game/node/entity/combat/DeathContext.kt @@ -1,7 +1,7 @@ package core.game.node.entity.combat -import core.game.node.entity.Entity import core.game.world.map.Location +import core.game.world.map.zone.MapZone /** * Stores the state at the moment death commenced. @@ -16,18 +16,11 @@ import core.game.world.map.Location * @param deathLocation The location where death commenced * @param zoneType The zone type ID at death time (from ZoneMonitor.getType()) * @param isSafeZone Whether the player was in a safe zone at death time - * @param wasInWilderness Whether the player was in wilderness at death time - * @param wasSkulled Whether the player was skulled at death time - * @param killer The entity that killed the player (may be null for environmental deaths) - * @param deathTick The game tick when death commenced + * @param deathZones Snapshot of the zone handlers active when death commenced */ data class DeathContext( val deathLocation: Location, val zoneType: Int, val isSafeZone: Boolean, - val wasInWilderness: Boolean, - val wasSkulled: Boolean, - val killer: Entity?, - val deathTick: Int + val deathZones: List ) - diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 23abb9281..9ffb8d4ca 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -49,6 +49,8 @@ import core.game.world.map.build.DynamicRegion; import core.game.world.map.path.Pathfinder; import core.game.world.map.zone.ZoneRestriction; import core.game.world.map.zone.ZoneType; +import core.game.world.map.zone.MapZone; +import core.game.world.map.zone.RegionZone; import core.game.node.entity.combat.DeathContext; import core.game.world.update.flag.PlayerFlags; import core.game.world.update.flag.*; @@ -87,6 +89,7 @@ import core.worker.ManagementEvents; import core.game.world.update.flag.context.*; import java.util.*; +import java.util.stream.Collectors; import java.util.concurrent.TimeUnit; import static core.api.ContentAPIKt.*; @@ -612,13 +615,13 @@ public class Player extends Entity { public void commenceDeath(Entity killer) { if (!isPlaying()) return; DeathContext context = new DeathContext( - getLocation(), + Location.create(getLocation()), getZoneMonitor().getType(), getProperties().isSafeZone(), - getSkullManager().isWilderness(), - getSkullManager().isSkulled(), - killer, - GameWorld.getTicks() + getZoneMonitor().getZones().stream() + .map(RegionZone::getZone) + .distinct() + .collect(Collectors.toList()) ); setAttribute("death-context", context); super.commenceDeath(killer); @@ -655,7 +658,9 @@ public class Player extends Entity { incrementAttribute("/save:"+STATS_BASE+":"+STATS_DEATHS); packetDispatch.sendTempMusic(90); - boolean zoneHandledDeath = getZoneMonitor().handleDeath(killer); + boolean zoneHandledDeath = deathContext != null + ? deathContext.getDeathZones().stream().anyMatch(zone -> zone.death(this, killer)) + : getZoneMonitor().handleDeath(killer); if (!zoneHandledDeath && (!wasSafeZone && deathZoneType != ZoneType.SAFE.getId()) && getDetails().getRights() != Rights.ADMINISTRATOR) { //If player was a Hardcore Ironman, announce that they died if (this.getIronmanManager().getMode().equals(IronmanMode.HARDCORE)) { //if this was checkRestriction, ultimate irons would be moved to HARDCORE_DEAD as well diff --git a/Server/src/main/core/game/system/config/ItemConfigParser.kt b/Server/src/main/core/game/system/config/ItemConfigParser.kt index 39645961d..962f2c83c 100644 --- a/Server/src/main/core/game/system/config/ItemConfigParser.kt +++ b/Server/src/main/core/game/system/config/ItemConfigParser.kt @@ -227,6 +227,12 @@ class ItemConfigParser { */ const val CASTLE_WARS_TICKET_PRICE = "castle_wars_ticket_price" + /** + * Whether an unequip listener may remove the item without first requiring + * enough inventory space to hold it. + */ + const val UNEQUIP_WITHOUT_INVENTORY_SPACE = "unequip_without_inventory_space" + } val parser = JSONParser() @@ -264,6 +270,7 @@ class ItemConfigParser { "fun_weapon", "rare_item", "bankable", + UNEQUIP_WITHOUT_INVENTORY_SPACE, "two_handed", "has_special", "remove_sleeves", @@ -311,4 +318,4 @@ class ItemConfigParser { } log(this::class.java, Log.FINE, "Parsed $count item configs.") } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/zone/MapZone.java b/Server/src/main/core/game/world/map/zone/MapZone.java index 752598f6f..38290e828 100644 --- a/Server/src/main/core/game/world/map/zone/MapZone.java +++ b/Server/src/main/core/game/world/map/zone/MapZone.java @@ -168,6 +168,22 @@ public abstract class MapZone implements Zone { return true; } + /** + * Checks whether an item may be equipped in this zone. + */ + public boolean canEquip(Player player, Item item, int equipmentSlot) { + return true; + } + + /** + * Applies zone-specific outgoing damage modifiers. + * + * @return the modified hit, or {@code null} when this zone has no modifier. + */ + public Integer modifyOutgoingHit(Entity attacker, Entity target, int hit) { + return null; + } + /** * If multi-zone boundaries should be ignored. * @param attacker The attacker. @@ -514,4 +530,4 @@ public abstract class MapZone implements Zone { public int hashCode() { return Objects.hash(uid, name, overlappable, fireRandomEvents, restriction, zoneType); } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/zone/ZoneMonitor.java b/Server/src/main/core/game/world/map/zone/ZoneMonitor.java index 4fabe5995..3e2af41c3 100644 --- a/Server/src/main/core/game/world/map/zone/ZoneMonitor.java +++ b/Server/src/main/core/game/world/map/zone/ZoneMonitor.java @@ -53,7 +53,7 @@ public final class ZoneMonitor { Items.RING_OF_WEALTH2_14642, Items.RING_OF_WEALTH3_14644, Items.RING_OF_LIFE_2570 - ); + ); /** * The entity. @@ -187,6 +187,32 @@ public final class ZoneMonitor { } return true; } + + /** + * Checks whether all current zones permit an equipment change. + */ + public boolean canEquip(Player player, Item item, int equipmentSlot) { + for (RegionZone z : zones) { + if (!z.getZone().canEquip(player, item, equipmentSlot)) { + return false; + } + } + return true; + } + + /** + * Applies outgoing damage modifiers from the entity's current zones. + */ + public int modifyOutgoingHit(Entity attacker, Entity target, int hit) { + int modifiedHit = hit; + for (RegionZone z : zones) { + Integer zoneHit = z.getZone().modifyOutgoingHit(attacker, target, modifiedHit); + if (zoneHit != null) { + modifiedHit = zoneHit; + } + } + return modifiedHit; + } /** * Checks if the entity can interact with the target. diff --git a/Server/src/main/core/plugin/ClassScanner.kt b/Server/src/main/core/plugin/ClassScanner.kt index 725a5427c..9620f2929 100644 --- a/Server/src/main/core/plugin/ClassScanner.kt +++ b/Server/src/main/core/plugin/ClassScanner.kt @@ -11,11 +11,13 @@ import core.game.node.Node import core.game.node.entity.Entity import core.game.node.entity.combat.CombatStyle import core.game.node.entity.npc.NPCBehavior +import core.game.node.entity.player.Player import core.game.node.entity.player.info.login.LoginConfiguration import core.game.node.entity.player.info.login.PlayerSaveParser import core.game.node.entity.player.info.login.PlayerSaver import core.game.node.entity.player.link.quest.Quest import core.game.node.entity.player.link.quest.QuestRepository +import core.game.node.item.Item import core.game.system.timer.RSTimer import core.game.system.timer.TimerRegistry import core.game.world.GameWorld @@ -141,17 +143,13 @@ object ClassScanner { } } - // Additional hook for startDeath for PVP area controls - override fun startDeath(e: Entity?, killer: Entity?): Boolean { - if (e == null) { - return super.startDeath(null, killer) - } - val clazzAttack = clazz.canStartDeath(e, killer) - if (clazzAttack == null) { // If clazz returns null, continue the usual check. - return super.startDeath(e, killer) - } else { - return clazzAttack // Otherwise return whatever function. - } + override fun canEquip(player: Player, item: Item, equipmentSlot: Int): Boolean { + return clazz.canEquipItem(player, item, equipmentSlot) + ?: super.canEquip(player, item, equipmentSlot) + } + + override fun modifyOutgoingHit(attacker: Entity, target: Entity, hit: Int): Int? { + return clazz.modifyOutgoingHitInArea(attacker, target, hit) } } for(border in clazz.defineAreaBorders()) zone.register(border) diff --git a/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt index a7fac1c40..a162bf685 100644 --- a/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt +++ b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt @@ -21,6 +21,9 @@ import core.game.system.timer.impl.Teleblock import core.game.world.GameWorld import core.game.world.map.Location import core.game.world.map.RegionManager +import core.game.world.map.zone.MapZone +import core.game.world.map.zone.RegionZone +import core.game.world.map.zone.ZoneBorders import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -28,6 +31,16 @@ import org.rs09.consts.Items class CastleWarsTests { + private fun addCastleWarsEquipmentZone(player: Player) { + val area = CastleWarsGameArea() + val zone = object : MapZone("Castle Wars equipment test", true) { + override fun canEquip(player: Player, item: Item, equipmentSlot: Int): Boolean { + return area.canEquipItem(player, item, equipmentSlot) ?: true + } + } + player.zoneMonitor.zones.add(RegionZone(zone, ZoneBorders(player.location, player.location))) + } + companion object { init { TestUtils.preTestSetup() @@ -82,6 +95,26 @@ class CastleWarsTests { } } + @Test + fun waitingRoomMembershipDoesNotGrantActiveGamePrivileges() { + TestUtils.getMockPlayer("cwwaitingonly").use { player -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsWaitingArea.waitingSaradominPlayers.add(player) + + assertTrue(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player)) + assertNull(CastleWarsGameState.getActiveGameTeam(player)) + + player.inventory.add(Item(CastleWars.cwBarricade)) + val setUp = requireNotNull( + InteractionListeners.get(CastleWars.cwBarricade, IntType.ITEM.ordinal, "set-up") + ) + setUp.invoke(player, player.inventory[0]) + + assertEquals(0, CastleWarsItemHandler.getBarricadeCount(CastleWarsTeam.SARADOMIN)) + assertTrue(player.inventory.contains(CastleWars.cwBarricade, 1)) + } + } + @Test fun spawnRoomPortalOffersVacancyAndFirstAcceptanceWins() { TestUtils.getMockPlayer("cwleaver").use { leaver -> @@ -802,6 +835,7 @@ class CastleWarsTests { fun castleWarsPlayersCannotEquipHelmsAfterEntry() { TestUtils.getMockPlayer("cwhelm").use { player -> player.location = Location.create(2380, 9490, 0) + addCastleWarsEquipmentZone(player) player.inventory.add(Item(Items.BRONZE_FULL_HELM_1155)) InteractionListeners.run( @@ -817,6 +851,7 @@ class CastleWarsTests { fun castleWarsPlayersCannotManuallyEquipTeamCloaksAfterEntry() { TestUtils.getMockPlayer("cwcloak").use { player -> player.location = Location.create(2380, 9490, 0) + addCastleWarsEquipmentZone(player) player.inventory.add(Item(Items.HOODED_CLOAK_4041)) InteractionListeners.run( @@ -980,6 +1015,16 @@ class CastleWarsTests { assertEquals(-2, zamorakNorth.y - zamorakBase.y) } + @Test + fun catapultMarkerUsesEntireInterfaceMap() { + val renderer = CatapultInterfaceRenderer() + + assertEquals(CatapultConfig.MAP_BASE_X, renderer.calculateScreenX(0)) + assertEquals(CatapultConfig.MAP_BASE_X + CatapultConfig.MAP_WIDTH, renderer.calculateScreenX(30)) + assertEquals(CatapultConfig.MAP_BASE_Y + CatapultConfig.MAP_HEIGHT, renderer.calculateScreenY(0)) + assertEquals(CatapultConfig.MAP_BASE_Y, renderer.calculateScreenY(30)) + } + @Test fun catapultShotInFlightBlocksOnlyShooterUntilLanding() { TestUtils.getMockPlayer("cwcatlock1").use { shooter -> @@ -996,6 +1041,9 @@ class CastleWarsTests { )!! CastleWarsGameArea.saradominPlayers.addAll(listOf(shooter, other)) + CastleWarsGameArea.ticksLeftInGame = 100 + shooter.location = catapult.location + other.location = catapult.location shooter.inventory.add(Item(CastleWars.cwRock, 2)) other.inventory.add(Item(CastleWars.cwRock)) @@ -1048,6 +1096,8 @@ class CastleWarsTests { catapult.id, IntType.SCENERY.ordinal, "operate" )!! CastleWarsGameArea.saradominPlayers.add(shooter) + CastleWarsGameArea.ticksLeftInGame = 100 + shooter.location = catapult.location shooter.inventory.add(Item(CastleWars.cwRock)) operateCatapult(shooter, catapult) @@ -1064,10 +1114,39 @@ class CastleWarsTests { } } + @Test + fun staleCatapultInterfaceCannotFireAfterPlayerMovesAway() { + TestUtils.getMockPlayer("cwcatmoved").use { shooter -> + val catapult = Scenery( + CastleWars.getOperationalCatapultId(CastleWarsTeam.SARADOMIN), + CastleWarsGameState.saradominCatapultLocation, + 10, + 0 + ) + ensureCatapultObject(catapult) + CastleWarsGameArea.saradominPlayers.add(shooter) + CastleWarsGameArea.ticksLeftInGame = 100 + shooter.location = catapult.location + shooter.inventory.add(Item(CastleWars.cwRock)) + shooter.setAttribute(CatapultConfig.ATTR_LOCATION, catapult.location) + shooter.interfaceManager.open(Component(CatapultConfig.INTERFACE_ID)) + shooter.location = Location.create(catapult.location.x + 10, catapult.location.y, catapult.location.z) + + InterfaceListeners.run( + shooter, Component(CatapultConfig.INTERFACE_ID), 0, CatapultConfig.BUTTON_FIRE, 0, 0 + ) + + assertEquals(1, shooter.inventory.getAmount(CastleWars.cwRock)) + assertFalse(CatapultFiringHandler.hasShotInFlight(shooter)) + assertFalse(shooter.interfaceManager.hasMainComponent(CatapultConfig.INTERFACE_ID)) + } + } + @Test fun staleCatapultInterfaceCannotFireWhenCatapultObjectIsMissing() { TestUtils.getMockPlayer("cwcatmissing").use { shooter -> CastleWarsGameArea.saradominPlayers.add(shooter) + CastleWarsGameArea.ticksLeftInGame = 100 shooter.inventory.add(Item(CastleWars.cwRock)) shooter.setAttribute(CatapultConfig.ATTR_LOCATION, Location.create(2400, 3090, 0)) shooter.interfaceManager.open(Component(CatapultConfig.INTERFACE_ID)) @@ -1120,6 +1199,13 @@ class CastleWarsTests { assertTrue(canClimbUpClimbingRope(rope.location, rope)) } + @Test + fun invalidBoulderKeysFailFast() { + assertThrows(IllegalArgumentException::class.java) { + CastleWarsGameState.getBoulderState("not-a-boulder") + } + } + @Test fun respawnEnergyBarrierPassesAcrossOneWallTile() { val barrierTile = Location.create(2426, 3080, 1) @@ -1171,6 +1257,7 @@ class CastleWarsTests { TestUtils.getMockPlayer("cwbarrfire").use { player -> val barricade = CastleWarsBarricadeNPC.spawn(Location.create(2400, 3100, 0), CastleWarsTeam.SARADOMIN) CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(player) CastleWarsItemHandler.saradominBarricades.add(barricade) player.inventory.add(Item(Items.BUCKET_OF_WATER_1929))