From 9716d6caf53df8cee5d0de272adb099cfe0bc7df Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 27 May 2026 13:31:07 +0300 Subject: [PATCH] Unforeseen consequences Messageless teleblock, hard ass helmet/cape slot blocking in Castle Wars, many CW fixes --- .../castlewars/CastleWarsCatapultInterface.kt | 45 +- .../minigame/castlewars/CastleWarsCommands.kt | 21 +- .../castlewars/CastleWarsConstants.kt | 3 + .../castlewars/CastleWarsDoorAttackPulse.kt | 31 +- .../castlewars/CastleWarsFlagHandler.kt | 5 +- .../castlewars/CastleWarsGameState.kt | 161 +++-- .../castlewars/CastleWarsItemHandler.kt | 40 +- .../castlewars/CastleWarsListeners.kt | 9 +- .../castlewars/areas/CastleWarsGameArea.kt | 151 ++++- .../castlewars/areas/CastleWarsRespawnArea.kt | 3 + .../castlewars/areas/CastleWarsWaitingArea.kt | 18 +- .../container/impl/EquipmentContainer.java | 49 +- .../core/game/global/action/EquipHandler.kt | 26 +- .../core/game/system/timer/impl/Teleblock.kt | 6 +- Server/src/test/kotlin/content/EquipTests.kt | 27 +- .../minigame/castlewars/CastleWarsTests.kt | 590 +++++++++++++++++- 16 files changed, 1043 insertions(+), 142 deletions(-) diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt b/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt index c4db59533..830d38650 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsCatapultInterface.kt @@ -9,6 +9,7 @@ import core.game.interaction.InterfaceListener import core.game.node.entity.player.Player import core.game.system.task.Pulse import core.game.world.map.Location +import core.game.world.map.RegionManager class CastleWarsCatapultInterface : InterfaceListener { @@ -67,18 +68,58 @@ class CastleWarsCatapultInterface : InterfaceListener { sendMessage(player, "Error: Catapult location not found.") return true } + val catapultTeam = getCatapultTeam(catapultLocation) + if (!canFireCatapult(player, team, catapultTeam, catapultLocation)) { + return closeAndReturn(player, true) + } if (!removeItem(player, CastleWars.cwRock)) { sendMessage(player, "You don't have any rocks to fire!") return true } - val catapultTeam = CatapultTeam.fromCastleWarsTeam(team) - firingHandler.fire(player, coord, catapultTeam, catapultLocation) + firingHandler.fire(player, coord, CatapultTeam.fromCastleWarsTeam(catapultTeam), catapultLocation) closeInterface(player) return true } + private fun canFireCatapult( + player: Player, + playerTeam: CastleWarsTeam, + catapultTeam: CastleWarsTeam, + catapultLocation: Location + ): Boolean { + if (playerTeam != catapultTeam) { + sendMessage(player, "You can't use the enemy's catapult!") + return false + } + + val currentCatapult = RegionManager.getObject(catapultLocation) + if (currentCatapult == null || !currentCatapult.isActive || currentCatapult.id !in CastleWars.allCatapultIds) { + sendMessage(player, "The catapult is no longer there.") + return false + } + if (currentCatapult.id in CastleWars.damagedCatapultIds || !CastleWarsGameState.isCatapultOperational( + catapultTeam + ) + ) { + sendMessage(player, "The catapult is destroyed! Use a toolkit to repair it.") + return false + } + if (currentCatapult.id in CastleWars.burningCatapultIds || CastleWarsGameState.isCatapultOnFire( + catapultTeam + ) + ) { + sendMessage(player, "The catapult is on fire! Use a bucket of water to extinguish it.") + return false + } + return true + } + + private fun getCatapultTeam(location: Location): CastleWarsTeam { + return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK + } + private fun handleCoordinateChange( player: Player, currentCoord: Coordinate, button: CatapultButton ): Boolean { diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt b/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt index aae404781..f7ab01484 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt @@ -1,12 +1,9 @@ package content.minigame.castlewars import content.minigame.castlewars.areas.CastleWarsGameArea -import content.minigame.castlewars.areas.CastleWarsGameArea.Companion.saradominPlayers -import content.minigame.castlewars.areas.CastleWarsGameArea.Companion.zamorakPlayers import content.minigame.castlewars.areas.CastleWarsWaitingArea import core.api.Commands import core.api.sendMessage -import core.game.interaction.Option import core.game.system.command.Privilege /** @@ -48,23 +45,7 @@ class CastleWarsCommands : Commands { sendMessage(player, "No Castle Wars game is currently running.") return@define } - // Snapshot the sets before endGame() modifies them via teleport callbacks - val saraSnapshot = saradominPlayers.toSet() - val zamSnapshot = zamorakPlayers.toSet() - CastleWarsGameState.endGame() - CastleWarsItemHandler.clearAllBarricades() - CastleWarsGameArea.ticksLeftInGame = -1 - CastleWarsWaitingArea.gameStartCountdown = -1 - for (p in saraSnapshot) { - p.interaction.remove(Option._P_ATTACK) - p.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc - } - for (p in zamSnapshot) { - p.interaction.remove(Option._P_ATTACK) - p.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc - } - saradominPlayers.clear() - zamorakPlayers.clear() + CastleWarsGameArea.endCurrentGame() sendMessage(player, "Castle Wars game ended by admin command.") } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsConstants.kt b/Server/src/main/content/minigame/castlewars/CastleWarsConstants.kt index 689828bf9..856706b4c 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsConstants.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsConstants.kt @@ -120,6 +120,9 @@ object CastleWarsConstants { /** Waiting-room cycle while no game is active: 2 minutes = 200 ticks */ const val WAITING_ROOM_COUNTDOWN_TICKS = 2 * TICKS_PER_MINUTE + /** How long a substitution offer can block later vacancy offers */ + const val VACANCY_OFFER_TIMEOUT_TICKS = TICKS_PER_MINUTE + // ==================== Player Attribute Keys ==================== /** Attribute key: world tick when player joined the current game */ const val ATTR_JOIN_TICK = "cw:join_tick" diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt b/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt index 5d8a0269d..3883e6d6d 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsDoorAttackPulse.kt @@ -3,11 +3,14 @@ package content.minigame.castlewars import core.api.animate import core.api.playAudio import core.game.container.impl.EquipmentContainer +import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.player.Player import core.game.node.entity.skill.Skills import core.game.node.scenery.Scenery import core.game.system.task.Pulse +import core.tools.RandomFunction import org.rs09.consts.Sounds +import kotlin.math.floor /** * Handles attacking the Castle Wars front doors. @@ -54,18 +57,24 @@ class CastleWarsDoorAttackPulse( } private fun calculateDamage(): Int { - // Doors are weak to crush > slash > stab - val strengthLevel = player.skills.getLevel(Skills.STRENGTH) + var effectiveStrength = player.skills.getLevel(Skills.STRENGTH).toDouble() + when (player.properties.attackStyle.style) { + WeaponInterface.STYLE_AGGRESSIVE -> effectiveStrength += 3.0 + WeaponInterface.STYLE_CONTROLLED -> effectiveStrength += 1.0 + } + effectiveStrength += 8.0 + + val strengthBonus = player.properties.bonuses[11] + 64 + val maxHit = floor(0.5 + (effectiveStrength * strengthBonus / 640.0)).toInt().coerceAtLeast(1) val bonusType = player.properties.attackStyle.bonusType - val styleBonus = when (bonusType) { - 2 -> 10 // Crush - 1 -> 4 // Slash - 0 -> 1 // Stab - else -> 3 // Default - } // Calculate damage: base from strength + style bonus - val damage = - (strengthLevel / 12) + styleBonus // Clamp to 1-25 range (max 25% of door health per hit) TODO: 100% inauthentic - return damage.coerceIn(1, 25) + val styleMultiplier = when (bonusType) { + WeaponInterface.BONUS_CRUSH -> 1.25 + WeaponInterface.BONUS_SLASH -> 1.0 + WeaponInterface.BONUS_STAB -> 0.85 + else -> 0.75 + } + val styleAdjustedMax = floor(maxHit * styleMultiplier).toInt().coerceAtLeast(1) + return RandomFunction.random(styleAdjustedMax + 1).coerceIn(0, 25) } private fun getAttackAnimation(): Int { diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt index 285296ec2..e820a863e 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsFlagHandler.kt @@ -117,10 +117,12 @@ class CastleWarsFlagHandler : InteractionListener { onUnequip(intArrayOf(CastleWars.saradominFlag, CastleWars.zamorakFlag)) { player, _ -> if (CastleWarsGameState.isCarryingFlag(player)) { CastleWarsGameState.dropFlag(player) + return@onUnequip false } else if (CastleWarsGameState.isCarryingOwnFlag(player)) { CastleWarsGameState.dropOwnFlag(player) + return@onUnequip false } - return@onUnequip false + return@onUnequip true } on(CastleWars.droppedFlagSceneryIds, IntType.SCENERY, "take") { player, node -> if (!CastleWarsGameState.isGameActive) { @@ -158,4 +160,3 @@ class CastleWarsFlagHandler : InteractionListener { } } } - diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt index 97a9ae181..46b52e72c 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt @@ -13,6 +13,7 @@ import core.game.node.entity.player.Player import core.game.node.entity.player.link.HintIconManager import core.game.node.item.GroundItemManager import core.game.node.item.Item +import core.game.node.scenery.Constructed import core.game.node.scenery.Scenery import core.game.node.scenery.SceneryBuilder import core.game.world.map.Location @@ -165,6 +166,8 @@ object CastleWarsGameState { private set var zamorakCatapultOnFire: Boolean = false private set + private var saradominCatapultFireGeneration: Int = 0 + private var zamorakCatapultFireGeneration: Int = 0 // Boulder locations for lookup val boulderLocations = mapOf( @@ -262,6 +265,8 @@ object CastleWarsGameState { zamorakCatapultOperational = true saradominCatapultOnFire = false zamorakCatapultOnFire = false + invalidateCatapultFire(CastleWarsTeam.SARADOMIN) + invalidateCatapultFire(CastleWarsTeam.ZAMORAK) for (key in boulderLocations.keys) { updateBoulderScenery(key, BoulderState.COLLAPSED) } @@ -331,28 +336,62 @@ object CastleWarsGameState { 2418..2432 to 3070..3085, // Saradomin castle area 2368..2382 to 3125..3140 // Zamorak castle area ) + val resetDoorOpenIds = CastleWars.largeDoorOpenIds + CastleWars.backdoorOpenIds for ((xRange, yRange) in doorResetAreas) { for (x in xRange) { for (y in yRange) { val loc = Location.create(x, y, 0) val obj = RegionManager.getObject(loc) - if (obj != null && obj.id in CastleWars.largeDoorOpenIds) { - val closedId = when (obj.id) { - 4425 -> 4423 - 4426 -> 4424 - 4429 -> 4427 - 4430 -> 4428 - else -> null - } - if (closedId != null) { - SceneryBuilder.replace(obj, Scenery(closedId, loc, obj.type, obj.rotation)) - } + if (obj != null && obj.id in resetDoorOpenIds) { + closeResetDoor(obj) } } } } } + private fun closeResetDoor(obj: Scenery) { + if (obj is Constructed) { + val replaced = obj.replaced + if (replaced != null) { + SceneryBuilder.replace(obj, replaced) + return + } + } + val closedId = getClosedDoorId(obj.id) ?: getClosedBackdoorId(obj.id) ?: return + SceneryBuilder.replace(obj, Scenery(closedId, obj.location, obj.type, obj.rotation)) + } + + private fun getClosedDoorId(openDoorId: Int): Int? = when (openDoorId) { + 4425 -> 4423 + 4426 -> 4424 + 4429 -> 4427 + 4430 -> 4428 + else -> null + } + + private fun getClosedBackdoorId(openDoorId: Int): Int? = when (openDoorId) { + CastleWars.saradominBackdoorOpen -> CastleWars.saradominBackdoor + CastleWars.zamorakBackdoorOpen -> CastleWars.zamorakBackdoor + else -> null + } + + fun clearCastleWarsGroundItems() { + GroundItemManager.getItems().toList() + .filter { item -> + val location = item.location ?: return@filter false + item.id in CastleWars.CW_ITEMS_TO_REMOVE && isCastleWarsGroundCleanupLocation(location) + } + .forEach { GroundItemManager.destroy(it) } + } + + private fun isCastleWarsGroundCleanupLocation(location: Location): Boolean { + return CastleWarsGameArea.areaBorders.any { it.insideBorder(location) } || + CastleWarsWaitingArea.areaBorders.any { it.insideBorder(location) } || + CastleWarsRespawnArea.saradominRespawnRoom.insideBorder(location) || + 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. @@ -566,7 +605,7 @@ object CastleWarsGameState { * Drop a flag (when a player dies or leaves) - creates scenery object * @param player The player dropping the flag */ - fun dropFlag(player: Player) { + fun dropFlag(player: Player, broadcast: Boolean = true) { val carriedFlag = getCarriedFlag(player) ?: return val dropLocation = Location.create(player.location) val shouldReturnToStand = CastleWars.isSteppingStoneTile(dropLocation) @@ -593,8 +632,8 @@ object CastleWarsGameState { } removeAttribute(player, CastleWarsConstants.ATTR_FLAG_PICKUP_TICK) if (shouldReturnToStand) { - returnFlagToStand(carriedFlag) - } else { + returnFlagToStand(carriedFlag, broadcast) + } else if (broadcast) { announceToAllPlayers("${carriedFlag.teamName} flag has been dropped!") updateAllOverlays() updateAllHintIcons() @@ -605,7 +644,7 @@ object CastleWarsGameState { * Drop own flag (when a player carrying their own team's flag dies or leaves) * @param player The player dropping the flag */ - fun dropOwnFlag(player: Player) { + fun dropOwnFlag(player: Player, broadcast: Boolean = true) { val carriedFlag = getOwnCarriedFlag(player) ?: return val dropLocation = Location.create(player.location) val shouldReturnToStand = CastleWars.isSteppingStoneTile(dropLocation) @@ -631,8 +670,8 @@ object CastleWarsGameState { player.inventory.remove(Item(carriedFlag.flagItemId)) } if (shouldReturnToStand) { - returnFlagToStand(carriedFlag) - } else { + returnFlagToStand(carriedFlag, broadcast) + } else if (broadcast) { announceToAllPlayers("${carriedFlag.teamName} flag has been dropped!") updateAllOverlays() updateAllHintIcons() @@ -859,7 +898,7 @@ object CastleWarsGameState { * Return a dropped flag directly to its stand (instant, no player involved). * Used for auto-return (timeout, stepping stone) scenarios. */ - fun returnFlagToStand(flagTeam: CastleWarsTeam) { + fun returnFlagToStand(flagTeam: CastleWarsTeam, broadcast: Boolean = true) { when (flagTeam) { CastleWarsTeam.SARADOMIN -> { saradominDroppedFlagScenery?.let { SceneryBuilder.remove(it) } @@ -879,9 +918,11 @@ object CastleWarsGameState { updateFlagStandObject(CastleWarsTeam.ZAMORAK, FlagState.SAFE) } } - announceToAllPlayers("The ${flagTeam.teamName} flag has been returned to its stand!") - updateAllOverlays() - updateAllHintIcons() + if (broadcast) { + announceToAllPlayers("The ${flagTeam.teamName} flag has been returned to its stand!") + updateAllOverlays() + updateAllHintIcons() + } } /** @@ -1077,6 +1118,7 @@ object CastleWarsGameState { player.inventory.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) player.equipment.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) (player.familiarManager.familiar as? BurdenBeast)?.container?.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) + removeTimer(player, "teleblock") clearActiveCastleWarsBracelet(player) player.interaction.remove(Option._P_ATTACK) player.interaction.remove(Option._P_STEAL_FROM) @@ -1085,27 +1127,34 @@ object CastleWarsGameState { /** * End the game and distribute rewards */ - fun endGame() { - val allPlayers = getAllGamePlayers() - for (player in allPlayers) { + fun endGame(extraRewardRecipients: Map = emptyMap()) { + val rewardRecipients = linkedMapOf() + for (player in getAllGamePlayers()) { val team = getPlayerTeam(player) ?: continue + rewardRecipients[player] = team + } + for ((player, team) in extraRewardRecipients) { + if (!rewardRecipients.containsKey(player)) { + rewardRecipients[player] = team + } + } + for ((player, team) in rewardRecipients) { val tickets = when { saradominScore > zamorakScore && team == CastleWarsTeam.SARADOMIN -> 2 zamorakScore > saradominScore && team == CastleWarsTeam.ZAMORAK -> 2 saradominScore == zamorakScore -> 1 else -> 0 } - if (tickets > 0) { - addItem(player, Items.CASTLE_WARS_TICKET_4067, tickets) - sendMessage(player, "You have received $tickets Castle Wars ticket${if (tickets > 1) "s" else ""}!") - } if (isCarryingFlag(player)) { - dropFlag(player) + dropFlag(player, broadcast = false) } else if (isCarryingOwnFlag(player)) { - dropOwnFlag(player) + dropOwnFlag(player, broadcast = false) } player.depleteCastleWarsBracelet() restoreAndCleanupPlayer(player) + if (tickets > 0) { + awardCastleWarsTickets(player, tickets) + } } val resultMessage = when { saradominScore > zamorakScore -> "Saradomin wins the game!" @@ -1116,6 +1165,17 @@ object CastleWarsGameState { resetGame() } + private fun awardCastleWarsTickets(player: Player, tickets: Int) { + val reward = Item(Items.CASTLE_WARS_TICKET_4067, tickets) + val ticketText = "Castle Wars ticket${if (tickets > 1) "s" else ""}" + if (player.inventory.add(reward)) { + sendMessage(player, "You have received $tickets $ticketText!") + return + } + GroundItemManager.create(reward, CastleWars.lobbyBankArea.randomWalkableLoc, player) + sendMessage(player, "Your $ticketText dropped to the ground because your inventory is full.") + } + /** * Handle a player leaving the game */ @@ -1389,11 +1449,13 @@ object CastleWarsGameState { CastleWarsTeam.SARADOMIN -> { saradominCatapultOperational = false saradominCatapultOnFire = false + invalidateCatapultFire(team) } CastleWarsTeam.ZAMORAK -> { zamorakCatapultOperational = false zamorakCatapultOnFire = false + invalidateCatapultFire(team) } } updateCatapultScenery(team) @@ -1404,13 +1466,21 @@ object CastleWarsGameState { * Set a team's catapult on fire (degrades over time, not instantly destroyed). * Tinderbox sets fire, catapult degrades until extinguished or destroyed. */ - fun setCatapultOnFire(team: CastleWarsTeam) { + fun setCatapultOnFire(team: CastleWarsTeam): Int { when (team) { - CastleWarsTeam.SARADOMIN -> saradominCatapultOnFire = true - CastleWarsTeam.ZAMORAK -> zamorakCatapultOnFire = true + CastleWarsTeam.SARADOMIN -> { + saradominCatapultOnFire = true + invalidateCatapultFire(team) + } + + CastleWarsTeam.ZAMORAK -> { + zamorakCatapultOnFire = true + invalidateCatapultFire(team) + } } updateCatapultScenery(team) updateAllOverlays() + return getCatapultFireGeneration(team) } /** @@ -1418,13 +1488,34 @@ object CastleWarsGameState { */ fun extinguishCatapult(team: CastleWarsTeam) { when (team) { - CastleWarsTeam.SARADOMIN -> saradominCatapultOnFire = false - CastleWarsTeam.ZAMORAK -> zamorakCatapultOnFire = false + CastleWarsTeam.SARADOMIN -> { + saradominCatapultOnFire = false + invalidateCatapultFire(team) + } + + CastleWarsTeam.ZAMORAK -> { + zamorakCatapultOnFire = false + invalidateCatapultFire(team) + } } updateCatapultScenery(team) updateAllOverlays() } + fun getCatapultFireGeneration(team: CastleWarsTeam): Int { + return when (team) { + CastleWarsTeam.SARADOMIN -> saradominCatapultFireGeneration + CastleWarsTeam.ZAMORAK -> zamorakCatapultFireGeneration + } + } + + private fun invalidateCatapultFire(team: CastleWarsTeam) { + when (team) { + CastleWarsTeam.SARADOMIN -> saradominCatapultFireGeneration++ + CastleWarsTeam.ZAMORAK -> zamorakCatapultFireGeneration++ + } + } + /** * Check if a team's catapult is on fire */ diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt index 1f80e65e2..497a12bfd 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsItemHandler.kt @@ -8,6 +8,7 @@ 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 @@ -87,20 +88,6 @@ class CastleWarsItemHandler : InteractionListener { onUseWithPlayer(CastleWars.cwBandages) { player, used, with -> val target = with as? Player ?: return@onUseWithPlayer false - - val playerTeam = CastleWarsGameState.getPlayerTeam(player) - val targetTeam = CastleWarsGameState.getPlayerTeam(target) - - if (playerTeam == null || targetTeam == null) { - sendMessage(player, "You can only use bandages in Castle Wars.") - return@onUseWithPlayer false - } - - if (playerTeam != targetTeam) { - sendMessage(player, "You can only heal your teammates!") - return@onUseWithPlayer false - } - if (useBandage(player, target)) { removeItem(player, used.asItem()) } @@ -231,6 +218,7 @@ class CastleWarsItemHandler : InteractionListener { playAudio(player, 2599) + burningBarricadePulses.remove(barricade)?.stop() barricade.transform(burningId) sendMessage(player, "You set the barricade on fire!") @@ -272,6 +260,7 @@ class CastleWarsItemHandler : InteractionListener { } barricade.transform(originalId) + burningBarricadePulses.remove(barricade)?.stop() replaceSlot(player, used.asItem().slot, Item(Items.BUCKET_1925)) @@ -283,6 +272,21 @@ class CastleWarsItemHandler : InteractionListener { } private fun useBandage(user: Player, target: Player): Boolean { + if (!CastleWarsGameState.isGameActive) { + sendMessage(user, "You can only use bandages during a Castle Wars game.") + return false + } + val userTeam = getActiveGameTeam(user) + val targetTeam = getActiveGameTeam(target) + if (userTeam == null || targetTeam == null) { + sendMessage(user, "You can only use bandages during a Castle Wars game.") + return false + } + if (userTeam != targetTeam) { + sendMessage(user, "You can only heal your teammates!") + return false + } + if (CastleWarsGameState.isCarryingOwnFlag(target)) { if (user == target) { sendMessage(user, "You can't heal while carrying your team's flag!") @@ -324,6 +328,14 @@ 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 getLocalBarricades(location: Location, radius: Int): List { val barricadeIds = intArrayOf( NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535 diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt index c3c157c89..bdbd97134 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsListeners.kt @@ -473,7 +473,7 @@ class CastleWarsListeners : InteractionListener { } val previousHealth = CastleWarsGameState.getDoorHealth(doorTeam) - CastleWarsGameState.damageDoor(doorTeam, 25) + CastleWarsGameState.damageDoor(doorTeam, CastleWarsConstants.EXPLOSIVE_POTION_DOOR_DAMAGE) removeItem(player, used.asItem()) playAudio(player, 2599) animate(player, 827) @@ -508,7 +508,7 @@ class CastleWarsListeners : InteractionListener { sendMessage(player, "The door is already fully repaired.") return@onUseWith false } - CastleWarsGameState.repairDoor(doorTeam, 10) + CastleWarsGameState.repairDoor(doorTeam, CastleWarsConstants.TOOLKIT_DOOR_REPAIR) animate(player, 898) sendMessage(player, "You repair some of the door's damage.") return@onUseWith true @@ -734,16 +734,15 @@ class CastleWarsListeners : InteractionListener { sendMessage(player, "The catapult is already on fire!") return@onUseWith false } - CastleWarsGameState.setCatapultOnFire(catapultTeam) + val fireGeneration = CastleWarsGameState.setCatapultOnFire(catapultTeam) playAudio(player, 2599) sendMessage(player, "You set the catapult on fire!") GameWorld.Pulser.submit(object : Pulse(50) { override fun pulse(): Boolean { - if (CastleWarsGameState.isCatapultOnFire(catapultTeam) && CastleWarsGameState.isCatapultOperational( + if (CastleWarsGameState.getCatapultFireGeneration(catapultTeam) == fireGeneration && CastleWarsGameState.isCatapultOnFire(catapultTeam) && CastleWarsGameState.isCatapultOperational( catapultTeam ) ) { - CastleWarsGameState.extinguishCatapult(catapultTeam) CastleWarsGameState.destroyCatapult(catapultTeam) } return true diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt index d5d1d86d8..d9a1e660f 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt @@ -11,6 +11,7 @@ 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.entity.player.info.login.PlayerParser import core.game.node.item.Item import core.game.world.map.Location import core.game.world.map.zone.ZoneBorders @@ -73,7 +74,9 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { var ticksLeftInGame = -1 private val activeVacancyOffers = mutableMapOf>() + private val activeVacancyOfferTicks = mutableMapOf() private val declinedSubstitutionOffers = mutableMapOf>() + private val openVacancies = mutableMapOf() fun startGame() { if (!CastleWarsWaitingArea.hasMinimumPlayers()) { @@ -118,7 +121,9 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { fun clearSubstitutionState() { activeVacancyOffers.clear() + activeVacancyOfferTicks.clear() declinedSubstitutionOffers.clear() + openVacancies.clear() } fun selectGuthixTeam(): CastleWarsTeam { @@ -139,15 +144,18 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { */ fun fillVacancy(team: CastleWarsTeam) { if (ticksLeftInGame <= 0) return - if (activeVacancyOffers[team]?.isNotEmpty() == true) return + if ((openVacancies[team] ?: 0) <= 0) return val waitingSet = when (team) { CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers } + pruneVacancyOffer(team, waitingSet) + if (activeVacancyOffers[team]?.isNotEmpty() == true) return val declined = declinedSubstitutionOffers[team].orEmpty() val candidates = waitingSet.filter { it !in declined } if (candidates.isEmpty()) return activeVacancyOffers[team] = candidates.toMutableSet() + activeVacancyOfferTicks[team] = getWorldTicks() for (candidate in candidates) { openDialogue(candidate, CastleWarsSubstitutionDialogue(team)) } @@ -160,14 +168,30 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers } - if (!waitingSet.remove(player)) return false + if (!waitingSet.remove(player)) { + offer.remove(player) + if (offer.isEmpty()) { + activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) + } + return false + } activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) declinedSubstitutionOffers[team]?.remove(player) + val remainingVacancies = ((openVacancies[team] ?: 0) - 1).coerceAtLeast(0) + if (remainingVacancies > 0) { + openVacancies[team] = remainingVacancies + } else { + openVacancies.remove(team) + } val gameSet = when (team) { CastleWarsTeam.SARADOMIN -> saradominPlayers CastleWarsTeam.ZAMORAK -> zamorakPlayers } gameSet.add(player) + CastleWarsGameState.activateBraceletIfEquipped(player, team) + CastleWarsGameState.setActiveGamePlayerOptions(player) setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks()) val respawnLoc = when (team) { CastleWarsTeam.SARADOMIN -> CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc @@ -175,6 +199,9 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } player.properties.teleportLocation = respawnLoc sendMessage(player, "A spot has opened up! You have been moved into the Castle Wars game.") + if ((openVacancies[team] ?: 0) > 0) { + fillVacancy(team) + } return true } @@ -183,6 +210,42 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { declinedSubstitutionOffers.getOrPut(team) { mutableSetOf() }.add(player) if (activeVacancyOffers[team]?.isEmpty() == true) { activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) + fillVacancy(team) + } + } + + fun removeVacancyOffer(player: Player) { + val teamsToRefill = mutableSetOf() + for (team in CastleWarsTeam.values()) { + activeVacancyOffers[team]?.remove(player) + declinedSubstitutionOffers[team]?.remove(player) + if (activeVacancyOffers[team]?.isEmpty() == true) { + activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) + teamsToRefill.add(team) + } + if (declinedSubstitutionOffers[team]?.isEmpty() == true) { + declinedSubstitutionOffers.remove(team) + } + } + for (team in teamsToRefill) { + fillVacancy(team) + } + } + + private fun pruneVacancyOffer(team: CastleWarsTeam, waitingSet: Set) { + val offer = activeVacancyOffers[team] ?: return + val createdTick = activeVacancyOfferTicks[team] ?: getWorldTicks() + if (getWorldTicks() - createdTick >= CastleWarsConstants.VACANCY_OFFER_TIMEOUT_TICKS) { + activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) + return + } + offer.removeAll { it !in waitingSet || !it.isActive } + if (offer.isEmpty()) { + activeVacancyOffers.remove(team) + activeVacancyOfferTicks.remove(team) } } @@ -204,11 +267,16 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { player.interaction.remove(Option._P_ATTACK) player.interaction.remove(Option._P_STEAL_FROM) if (offerReplacement) { + openVacancy(leavingTeam) fillVacancy(leavingTeam) } return leavingTeam } + private fun openVacancy(team: CastleWarsTeam) { + openVacancies[team] = (openVacancies[team] ?: 0) + 1 + } + fun playerKey(player: Player): String { return player.username.lowercase() } @@ -272,6 +340,18 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { clearLogoutAttributes(player) } + private fun moveLoggedOutPlayerToLobby(player: Player) { + removeFromTeam(player) + CastleWarsGameState.clearHintIconForPlayer(player) + CastleWarsGameState.restoreAndCleanupPlayer(player) + 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 + clearLogoutState(player) + } + fun restoreActiveLogoutOnLogin(player: Player) { val savedTeamName = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "") val savedLogoutTime = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, 0L) @@ -287,6 +367,10 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { val logoutLocation = getAttribute( player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, record?.logoutLocation ?: player.location ) + if (ticksLeftInGame <= 0) { + moveLoggedOutPlayerToLobby(player) + return + } if (ticksLeftInGame > 0 && elapsedMillis < CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS) { player.location = Location.create(logoutLocation) setTeam(player, team) @@ -295,6 +379,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } moveLoggedOutPlayerToWaitingRoom(player, team) if (ticksLeftInGame > 0) { + openVacancy(team) fillVacancy(team) } } @@ -324,24 +409,41 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { } } } - } - private fun endGame() { // TODO: ropes, rocks in tunnels, ground items, castle doors, catapults - CastleWarsGameState.endGame() - CastleWarsItemHandler.clearAllBarricades() - clearSubstitutionState() - ticksLeftInGame = -1 - saradominPlayers.forEach { player -> - player.unhook(SaradominInterfaceCloseHook) - player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc + fun endCurrentGame(startCooldown: Boolean = true) { + val saraSnapshot = saradominPlayers.toSet() + val zamSnapshot = zamorakPlayers.toSet() + val loggedOutSnapshot = loggedOutPlayers.values.toList() + val loggedOutRewardRecipients = loggedOutSnapshot.associate { it.player to it.team } + CastleWarsGameState.endGame(loggedOutRewardRecipients) + for (record in loggedOutSnapshot) { + moveLoggedOutPlayerToLobby(record.player) + if (!record.player.isActive || record.player.session == null) { + PlayerParser.saveImmediately(record.player) + } + } + CastleWarsItemHandler.clearAllBarricades() + CastleWarsGameState.clearCastleWarsGroundItems() + clearSubstitutionState() + ticksLeftInGame = -1 + CastleWarsWaitingArea.gameStartCountdown = if (startCooldown) { + CastleWars.gameCooldownMinutes * CastleWarsConstants.TICKS_PER_MINUTE + } else { + -1 + } + saraSnapshot.forEach { player -> + player.unhook(SaradominInterfaceCloseHook) + player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc + } + zamSnapshot.forEach { player -> + player.unhook(ZamorakInterfaceCloseHook) + player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc + } + saradominPlayers.clear() + zamorakPlayers.clear() + CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear() + CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear() } - zamorakPlayers.forEach { player -> - player.unhook(ZamorakInterfaceCloseHook) - player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc - } - saradominPlayers.clear() - zamorakPlayers.clear() - loggedOutPlayers.clear() } override fun defineAreaBorders(): Array { @@ -353,7 +455,10 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { super.areaEnter(player) zone.zoneType = ZoneType.CASTLE_WARS.id - registerTimer(player, spawnTimer("teleblock", (CastleWars.gameTimeMinutes) * 60 * 2)) + registerTimer( + player, + spawnTimer("teleblock", CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE, false) + ) val team = CastleWarsGameState.getPlayerTeam(player) when (team) { CastleWarsTeam.SARADOMIN -> { @@ -425,7 +530,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { return } if (ticksLeftInGame == 0) { - endGame() + endCurrentGame() return } val previousMinutes = @@ -434,7 +539,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { val currentMinutes = (ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE if (ticksLeftInGame == 0) { - endGame() + endCurrentGame() return } else if (currentMinutes != previousMinutes) { CastleWarsGameState.updateAllOverlays() @@ -474,9 +579,11 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { for ((key, record) in expiredLogouts) { val player = record.player loggedOutPlayers.remove(key) + clearLogoutAttributes(player) removeFromTeam(player) CastleWarsGameState.clearHintIconForPlayer(player) CastleWarsGameState.restoreAndCleanupPlayer(player) + openVacancy(record.team) fillVacancy(record.team) } // Remove truly inactive players (not in grace period) @@ -489,11 +596,13 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener { for (player in inactiveSara) { saradominPlayers.remove(player) CastleWarsGameState.clearHintIconForPlayer(player) + openVacancy(CastleWarsTeam.SARADOMIN) fillVacancy(CastleWarsTeam.SARADOMIN) } for (player in inactiveZam) { zamorakPlayers.remove(player) CastleWarsGameState.clearHintIconForPlayer(player) + openVacancy(CastleWarsTeam.ZAMORAK) fillVacancy(CastleWarsTeam.ZAMORAK) } // Also cleanup hint icons for any other inactive players diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt index 2fd8dac7d..2ef507f55 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt @@ -63,6 +63,7 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { } private fun removePlayersOverSpawnLimit(players: MutableMap) { + players.entries.removeIf { (player, _) -> !player.isActive || player.session == null } val currentTick = getWorldTicks() val expiredPlayers = players.filter { (player, entryTick) -> player.isActive && player.session != null && currentTick - entryTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS @@ -79,6 +80,8 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { if (!defineAreaBorders().any { it.insideBorder(player.location) }) { return } + saradominPlayersInRespawnRoom.remove(player) + zamorakPlayersInRespawnRoom.remove(player) if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getPlayerTeam(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 bacd07a61..e66a93747 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt @@ -3,6 +3,7 @@ package content.minigame.castlewars.areas import content.minigame.castlewars.CastleWars import content.minigame.castlewars.CastleWarsConstants import content.minigame.castlewars.CastleWarsOverlay +import content.minigame.castlewars.CastleWarsTeam import core.api.* import core.game.component.Component import core.game.node.entity.Entity @@ -39,16 +40,28 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { val player = entity as? Player ?: return super.areaEnter(player) registerTimer( - player, spawnTimer("teleblock", (CastleWars.gameCooldownMinutes + CastleWars.gameTimeMinutes) * 60 * 2) + player, + spawnTimer( + "teleblock", + (CastleWars.gameCooldownMinutes + CastleWars.gameTimeMinutes) * CastleWarsConstants.TICKS_PER_MINUTE, + false + ) ) // Set team attribute and equip the hooded cloak on the entity based on which waiting room they're in - if (zamorakWaitingRoom.insideBorder(player.location)) { + val joinedTeam = if (zamorakWaitingRoom.insideBorder(player.location)) { player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) waitingZamorakPlayers.add(player) + CastleWarsTeam.ZAMORAK } else if (saradominWaitingRoom.insideBorder(player.location)) { player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) waitingSaradominPlayers.add(player) + CastleWarsTeam.SARADOMIN + } else { + null + } + if (joinedTeam != null && CastleWarsGameArea.ticksLeftInGame > 0) { + CastleWarsGameArea.fillVacancy(joinedTeam) } var transformed = false @@ -118,6 +131,7 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { // Remove player from waiting room set (whichever one that is) waitingSaradominPlayers.remove(player) waitingZamorakPlayers.remove(player) + CastleWarsGameArea.removeVacancyOffer(player) } override fun tick() { diff --git a/Server/src/main/core/game/container/impl/EquipmentContainer.java b/Server/src/main/core/game/container/impl/EquipmentContainer.java index d398d9f3b..a524534e8 100644 --- a/Server/src/main/core/game/container/impl/EquipmentContainer.java +++ b/Server/src/main/core/game/container/impl/EquipmentContainer.java @@ -2,6 +2,7 @@ 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; @@ -146,9 +147,17 @@ public final class EquipmentContainer extends Container { } boolean listenersSayWeCanUnequip = runUnequipHooks(current, newItem); + if(!listenersSayWeCanUnequip) { + return false; + } + ArrayList itemsToMoveToInventory = new ArrayList<>(current); + itemsToMoveToInventory.removeIf(item -> isCastleWarsFlag(item) && !containsItem(item)); + if(itemsToMoveToInventory.isEmpty()) { + return true; + } boolean allRemoved = true; - for(Item item : current) { + for(Item item : itemsToMoveToInventory) { if(!remove(item)) { allRemoved = false; break; @@ -157,7 +166,7 @@ public final class EquipmentContainer extends Container { boolean allAdded = allRemoved; if(allRemoved) { - for (Item item : current) { + for (Item item : itemsToMoveToInventory) { if (!player.getInventory().add(item, true, preferredSlot)) { allAdded = false; break; @@ -168,7 +177,7 @@ public final class EquipmentContainer extends Container { if (listenersSayWeCanUnequip && allRemoved && allAdded) return true; else { //put things back if we couldn't remove everything - for(Item item : current) { + for(Item item : itemsToMoveToInventory) { if(!containsItem(item)) { add(item); } @@ -182,6 +191,9 @@ public final class EquipmentContainer extends Container { int neededSlots = 0; for(Item item : current) { + if(isCastleWarsFlag(item)) { + continue; + } if(!item.getDefinition().isStackable()) { neededSlots++; } else { @@ -193,6 +205,10 @@ public final class EquipmentContainer extends Container { return neededSlots; } + private boolean isCastleWarsFlag(Item item) { + return item.getId() == CastleWars.saradominFlag || item.getId() == CastleWars.zamorakFlag; + } + @Nullable private Item getSecondaryEquipIfApplicable(Item newItem, int equipmentSlot) { Item secondaryEquipItem = null; @@ -211,19 +227,28 @@ public final class EquipmentContainer extends Container { for(Item currentItem : currentItems) { Plugin plugin = currentItem.getDefinition().getConfiguration("equipment", null); - if (plugin != null) { - Object object = plugin.fireEvent("unequip", player, currentItem); - if (object != null && !((Boolean) object)) { - canContinue = false; + if (plugin != null) { + Object object = plugin.fireEvent("unequip", player, currentItem); + if (object != null && !((Boolean) object)) { + if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) { + continue; + } + canContinue = false; + break; + } + } + + canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false); + + if(!canContinue) { + if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) { + canContinue = true; + continue; + } break; } } - canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false); - - if(!canContinue) break; - } - return canContinue; } diff --git a/Server/src/main/core/game/global/action/EquipHandler.kt b/Server/src/main/core/game/global/action/EquipHandler.kt index 3f9f681a5..4b44e8e74 100644 --- a/Server/src/main/core/game/global/action/EquipHandler.kt +++ b/Server/src/main/core/game/global/action/EquipHandler.kt @@ -1,5 +1,6 @@ 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 @@ -47,7 +48,11 @@ class EquipHandler : InteractionListener { if(currentEquippedItem != null){ if(!InteractionListeners.run(currentEquippedItem.id, player, currentEquippedItem, false)){ - return + val droppedCastleWarsFlag = + isCastleWarsFlag(currentEquippedItem.id) && player.equipment[itemEquipmentSlot] == null + if (!droppedCastleWarsFlag) { + return + } } } @@ -123,10 +128,12 @@ class EquipHandler : InteractionListener { if (slot == EquipmentContainer.SLOT_WEAPON) { player.packetDispatch.sendString("", 92, 0) } - val maximumAdd = player.inventory.getMaximumAdd(item) - if (maximumAdd < item.amount) { - player.packetDispatch.sendMessage("Not enough free space in your inventory.") - return + if (!isCastleWarsFlag(item.id)) { + val maximumAdd = player.inventory.getMaximumAdd(item) + if (maximumAdd < item.amount) { + player.packetDispatch.sendMessage("Not enough free space in your inventory.") + return + } } val plugin = item.definition.getConfiguration>("equipment", null) if (plugin != null) { @@ -137,6 +144,11 @@ class EquipHandler : InteractionListener { if (!InteractionListeners.run(itemId, player, item, false)) { return } + val maximumAdd = player.inventory.getMaximumAdd(item) + if (maximumAdd < item.amount) { + player.packetDispatch.sendMessage("Not enough free space in your inventory.") + return + } if (player.equipment.remove(item)) { /* TODO: Send different unequip sound based on what is being unequipped.*/ playAudio(player, Sounds.EQUIP_FUN_2238) @@ -150,5 +162,9 @@ class EquipHandler : InteractionListener { SlayerEquipmentFlags.updateFlags(player) } } + + private fun isCastleWarsFlag(itemId: Int): Boolean { + return itemId == CastleWars.saradominFlag || itemId == CastleWars.zamorakFlag + } } } diff --git a/Server/src/main/core/game/system/timer/impl/Teleblock.kt b/Server/src/main/core/game/system/timer/impl/Teleblock.kt index 47f33a8d4..69d96944a 100644 --- a/Server/src/main/core/game/system/timer/impl/Teleblock.kt +++ b/Server/src/main/core/game/system/timer/impl/Teleblock.kt @@ -7,18 +7,20 @@ import core.game.system.timer.PersistTimer import core.game.system.timer.RSTimer import core.game.system.timer.TimerFlag -class Teleblock : PersistTimer (1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) { +class Teleblock(private val sendChatboxMessage: Boolean = true) : + PersistTimer(1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) { override fun run (entity: Entity) : Boolean { return false } override fun onRegister (entity: Entity) { if (entity !is Player) return + if (!sendChatboxMessage) return sendMessage (entity, "You have been teleblocked.") } override fun getTimer (vararg args: Any) : RSTimer { - val t = Teleblock() + val t = Teleblock(args.getOrNull(1) as? Boolean ?: true) t.runInterval = args.getOrNull(0) as? Int ?: 100 return t } diff --git a/Server/src/test/kotlin/content/EquipTests.kt b/Server/src/test/kotlin/content/EquipTests.kt index 96a903a8a..a078296db 100644 --- a/Server/src/test/kotlin/content/EquipTests.kt +++ b/Server/src/test/kotlin/content/EquipTests.kt @@ -17,7 +17,12 @@ import core.game.node.entity.player.info.Rights class EquipTests { companion object { - init {TestUtils.preTestSetup(); EquipHandler().defineListeners()} + init { + TestUtils.preTestSetup() + if (InteractionListeners.get("equip", IntType.ITEM.ordinal) == null) { + EquipHandler().defineListeners() + } + } } @Test fun equipShouldFireEquipListeners() { @@ -52,6 +57,26 @@ class EquipTests { Assertions.assertEquals(true, didRun) } + @Test fun fullInventoryUnequipShouldNotFireUnequipListeners() { + var didRun = false + val listener = object : InteractionListener { + override fun defineListeners() { + onUnequip(Items.ABYSSAL_WHIP_4151) { _, _ -> didRun = true; return@onUnequip true } + } + } + listener.defineListeners() + + TestUtils.getMockPlayer("fullunequip").use { player -> + player.equipment.replace(Item(Items.ABYSSAL_WHIP_4151), EquipmentSlot.WEAPON.ordinal) + player.inventory.add(Item(Items.CUP_OF_TEA_1978, player.inventory.capacity())) + + EquipHandler.unequip(player, EquipmentSlot.WEAPON.ordinal, Items.ABYSSAL_WHIP_4151) + + Assertions.assertEquals(false, didRun) + Assertions.assertEquals(Items.ABYSSAL_WHIP_4151, player.equipment[EquipmentSlot.WEAPON.ordinal].id) + } + } + @Test fun equippingItemThatReplacesAnotherItemShouldCallUnequipListenersForTheReplacedItem() { var didRun = false val listener = object : InteractionListener { diff --git a/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt index 7e1530181..a7fac1c40 100644 --- a/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt +++ b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt @@ -1,6 +1,7 @@ package content.minigame.castlewars import TestUtils +import MockSession import content.minigame.castlewars.areas.CastleWarsGameArea import content.minigame.castlewars.areas.CastleWarsRespawnArea import content.minigame.castlewars.areas.CastleWarsWaitingArea @@ -8,12 +9,18 @@ import core.api.* import core.game.component.Component import core.game.global.action.EquipHandler import core.game.interaction.* +import core.game.node.Node import core.game.node.entity.player.Player import core.game.node.entity.skill.Skills +import core.game.node.item.GroundItemManager import core.game.node.item.Item import core.game.node.scenery.Scenery +import core.game.node.scenery.SceneryBuilder +import core.game.system.timer.TimerRegistry +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 org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test @@ -30,6 +37,7 @@ class CastleWarsTests { if (InteractionListeners.get(CastleWars.saradominBackdoorOpen, IntType.SCENERY.ordinal, "lock") == null) { CastleWarsListeners().defineDestinationOverrides() CastleWarsListeners().defineListeners() + CastleWarsFlagHandler().defineListeners() } if (InteractionListeners.get(CastleWars.cwExplosivePotion, IntType.ITEM.ordinal, "drop") == null) { CastleWarsItemHandler().defineListeners() @@ -37,6 +45,9 @@ class CastleWarsTests { if (InterfaceListeners.get(CatapultConfig.INTERFACE_ID) == null) { CastleWarsCatapultInterface().defineInterfaceListeners() } + if (!TimerRegistry.timerMap.containsKey("teleblock")) { + TimerRegistry.registerTimer(Teleblock()) + } } } @@ -52,6 +63,7 @@ class CastleWarsTests { CastleWarsGameArea.clearSubstitutionState() CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear() CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear() + CastleWarsItemHandler.clearAllBarricades() CastleWarsGameState.resetGame() } @@ -97,18 +109,125 @@ class CastleWarsTests { } } + @Test + fun enteringWaitingRoomAfterVacancyReceivesSubstitutionOffer() { + TestUtils.getMockPlayer("cwlateleaver").use { leaver -> + TestUtils.getMockPlayer("cwlatesub").use { waiting -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(leaver) + + CastleWarsGameArea.removePlayerFromGame(leaver) + waiting.location = CastleWarsWaitingArea.saradominWaitingRoom.randomWalkableLoc + CastleWarsWaitingArea().areaEnter(waiting) + + assertSubstitutionDialogue(waiting) + } + } + } + @Test fun decliningSubstitutionKeepsPlayerInWaitingRoom() { - TestUtils.getMockPlayer("cwdecline").use { waiting -> - CastleWarsGameArea.ticksLeftInGame = 100 - CastleWarsWaitingArea.waitingZamorakPlayers.add(waiting) + TestUtils.getMockPlayer("cwdeclineleave").use { leaver -> + TestUtils.getMockPlayer("cwdecline").use { waiting -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.zamorakPlayers.add(leaver) + CastleWarsWaitingArea.waitingZamorakPlayers.add(waiting) - CastleWarsGameArea.fillVacancy(CastleWarsTeam.ZAMORAK) - CastleWarsGameArea.declineVacancyOffer(waiting, CastleWarsTeam.ZAMORAK) + CastleWarsGameArea.removePlayerFromGame(leaver) + CastleWarsGameArea.declineVacancyOffer(waiting, CastleWarsTeam.ZAMORAK) - assertTrue(CastleWarsWaitingArea.waitingZamorakPlayers.contains(waiting)) - assertFalse(CastleWarsGameArea.zamorakPlayers.contains(waiting)) - assertFalse(CastleWarsGameArea.claimVacancy(waiting, CastleWarsTeam.ZAMORAK)) + assertTrue(CastleWarsWaitingArea.waitingZamorakPlayers.contains(waiting)) + assertFalse(CastleWarsGameArea.zamorakPlayers.contains(waiting)) + assertFalse(CastleWarsGameArea.claimVacancy(waiting, CastleWarsTeam.ZAMORAK)) + } + } + } + + @Test + fun decliningLastSubstitutionOfferReissuesToAvailableWaitingPlayer() { + TestUtils.getMockPlayer("cwdeclinereissueleave").use { leaver -> + TestUtils.getMockPlayer("cwdeclinereissueold").use { declined -> + TestUtils.getMockPlayer("cwdeclinereissuenew").use { replacement -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.zamorakPlayers.add(leaver) + CastleWarsWaitingArea.waitingZamorakPlayers.add(declined) + + CastleWarsGameArea.removePlayerFromGame(leaver) + assertSubstitutionDialogue(declined) + + CastleWarsWaitingArea.waitingZamorakPlayers.add(replacement) + CastleWarsGameArea.declineVacancyOffer(declined, CastleWarsTeam.ZAMORAK) + + assertSubstitutionDialogue(replacement) + assertFalse(CastleWarsGameArea.claimVacancy(declined, CastleWarsTeam.ZAMORAK)) + assertTrue(CastleWarsGameArea.claimVacancy(replacement, CastleWarsTeam.ZAMORAK)) + } + } + } + } + + @Test + fun leavingWaitingRoomClearsStaleSubstitutionOffer() { + TestUtils.getMockPlayer("cwstaleleaver").use { leaver -> + TestUtils.getMockPlayer("cwstalesub").use { stale -> + TestUtils.getMockPlayer("cwfreshsub").use { fresh -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(leaver) + stale.location = CastleWarsWaitingArea.saradominWaitingRoom.randomWalkableLoc + CastleWarsWaitingArea.waitingSaradominPlayers.add(stale) + + CastleWarsGameArea.removePlayerFromGame(leaver) + CastleWarsWaitingArea().exitArea(stale) + CastleWarsWaitingArea.waitingSaradominPlayers.add(fresh) + CastleWarsGameArea.fillVacancy(CastleWarsTeam.SARADOMIN) + + assertSubstitutionDialogue(fresh) + assertTrue(CastleWarsGameArea.claimVacancy(fresh, CastleWarsTeam.SARADOMIN)) + } + } + } + } + + @Test + fun leavingLastSubstitutionOfferReissuesToAvailableWaitingPlayer() { + TestUtils.getMockPlayer("cwleavereissueleaver").use { leaver -> + TestUtils.getMockPlayer("cwleavereissueold").use { stale -> + TestUtils.getMockPlayer("cwleavereissuenew").use { replacement -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(leaver) + stale.location = CastleWarsWaitingArea.saradominWaitingRoom.randomWalkableLoc + CastleWarsWaitingArea.waitingSaradominPlayers.add(stale) + + CastleWarsGameArea.removePlayerFromGame(leaver) + assertSubstitutionDialogue(stale) + + CastleWarsWaitingArea.waitingSaradominPlayers.add(replacement) + CastleWarsWaitingArea().exitArea(stale) + + assertSubstitutionDialogue(replacement) + assertTrue(CastleWarsGameArea.claimVacancy(replacement, CastleWarsTeam.SARADOMIN)) + } + } + } + } + + @Test + fun expiredSubstitutionOfferDoesNotBlockLaterVacancy() { + TestUtils.getMockPlayer("cwexpireleaver").use { leaver -> + TestUtils.getMockPlayer("cwignoredsub").use { ignored -> + TestUtils.getMockPlayer("cwlatersub").use { later -> + CastleWarsGameArea.ticksLeftInGame = 200 + CastleWarsGameArea.zamorakPlayers.add(leaver) + CastleWarsWaitingArea.waitingZamorakPlayers.add(ignored) + + CastleWarsGameArea.removePlayerFromGame(leaver) + TestUtils.advanceTicks(CastleWarsConstants.VACANCY_OFFER_TIMEOUT_TICKS, false) + CastleWarsWaitingArea.waitingZamorakPlayers.add(later) + CastleWarsGameArea.fillVacancy(CastleWarsTeam.ZAMORAK) + + assertSubstitutionDialogue(later) + } + } } } @@ -201,13 +320,16 @@ class CastleWarsTests { } @Test - fun matchEndReturnsLifecycleToIdle() { + fun matchEndStartsPostGameCooldown() { CastleWarsGameArea.ticksLeftInGame = 1 CastleWarsGameArea().tick() assertEquals(-1, CastleWarsGameArea.ticksLeftInGame) - assertEquals(-1, CastleWarsWaitingArea.gameStartCountdown) + assertEquals( + CastleWars.gameCooldownMinutes * CastleWarsConstants.TICKS_PER_MINUTE, + CastleWarsWaitingArea.gameStartCountdown + ) } @Test @@ -256,6 +378,91 @@ class CastleWarsTests { } } + @Test + fun endedGameClearsLoggedOutPlayerState() { + TestUtils.getMockPlayer("cwendedlogout").use { player -> + player.location = Location.create(2401, 3101, 0) + CastleWarsGameArea.ticksLeftInGame = 500 + CastleWarsGameArea.saradominPlayers.add(player) + player.inventory.add(Item(CastleWars.cwBandages)) + registerTimer(player, spawnTimer("teleblock", 100, false)) + + CastleWarsGameArea().logout(player) + assertEquals(CastleWarsTeam.SARADOMIN.name, getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")) + + CastleWarsGameArea.endCurrentGame(startCooldown = false) + + assertFalse(CastleWarsGameArea.loggedOutPlayers.containsKey(CastleWarsGameArea.playerKey(player))) + assertEquals("", getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")) + assertFalse(player.inventory.contains(CastleWars.cwBandages, 1)) + assertFalse(hasTimerActive(player, "teleblock")) + assertTrue(CastleWars.lobbyBankArea.insideBorder(player.location)) + } + } + + @Test + fun expiredLogoutCleanupClearsSavedStateBeforePlayerRelogs() { + TestUtils.getMockPlayer("cwexpiredlogout").use { player -> + TestUtils.getMockPlayer("cwexpiredfirstsub").use { firstReplacement -> + TestUtils.getMockPlayer("cwexpiredsecondsub").use { secondReplacement -> + val logoutLocation = Location.create(2400, 3100, 0) + player.location = logoutLocation + CastleWarsGameArea.ticksLeftInGame = CastleWarsConstants.PLAYER_CLEANUP_INTERVAL_TICKS + 1 + CastleWarsGameArea.saradominPlayers.add(player) + CastleWarsGameArea.loggedOutPlayers[CastleWarsGameArea.playerKey(player)] = + CastleWarsGameArea.Companion.LogoutRecord( + player, + CastleWarsTeam.SARADOMIN, + getWorldTicks() - CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS, + logoutLocation + ) + setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, CastleWarsTeam.SARADOMIN.name) + setAttribute( + player, + CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, + System.currentTimeMillis() - CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS - 1 + ) + setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, Location.create(logoutLocation)) + CastleWarsWaitingArea.waitingSaradominPlayers.add(firstReplacement) + + CastleWarsGameArea().tick() + + assertEquals("", getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")) + assertFalse(CastleWarsGameArea.loggedOutPlayers.containsKey(CastleWarsGameArea.playerKey(player))) + assertSubstitutionDialogue(firstReplacement) + assertTrue(CastleWarsGameArea.claimVacancy(firstReplacement, CastleWarsTeam.SARADOMIN)) + + CastleWarsWaitingArea.waitingSaradominPlayers.add(secondReplacement) + CastleWarsGameArea().login(player) + + assertFalse(secondReplacement.dialogueInterpreter.dialogue?.file is CastleWarsSubstitutionDialogue) + assertFalse(CastleWarsGameArea.claimVacancy(secondReplacement, CastleWarsTeam.SARADOMIN)) + } + } + } + } + + @Test + fun staleLogoutAttributesAfterEndedGameRestoreToLobbyOnLogin() { + TestUtils.getMockPlayer("cwstalelogin").use { player -> + player.location = Location.create(2401, 3101, 0) + CastleWarsGameArea.ticksLeftInGame = -1 + setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, CastleWarsTeam.SARADOMIN.name) + setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, System.currentTimeMillis()) + setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, Location.create(player.location)) + player.inventory.add(Item(CastleWars.cwBandages)) + registerTimer(player, spawnTimer("teleblock", 100, false)) + + CastleWarsGameArea().login(player) + + assertFalse(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player)) + assertEquals("", getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")) + assertFalse(player.inventory.contains(CastleWars.cwBandages, 1)) + assertFalse(hasTimerActive(player, "teleblock")) + assertTrue(CastleWars.lobbyBankArea.insideBorder(player.location)) + } + } + @Test fun activeGameLogoutAfterTwoMinutesReturnsPlayerToTeamWaitingRoom() { TestUtils.getMockPlayer("cwslowrelog").use { player -> @@ -295,6 +502,21 @@ class CastleWarsTests { } } + @Test + fun spawnRoomLogoutClearsRespawnRoomTracking() { + TestUtils.getMockPlayer("cwspawntrack").use { player -> + player.location = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc + CastleWarsGameArea.ticksLeftInGame = 500 + CastleWarsGameArea.saradominPlayers.add(player) + CastleWarsRespawnArea().areaEnter(player) + + CastleWarsRespawnArea().logout(player) + + assertFalse(CastleWarsRespawnArea.saradominPlayersInRespawnRoom.containsKey(player)) + assertFalse(CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.containsKey(player)) + } + } + @Test fun waitingRoomLogoutReturnsPlayerToLobbyAndLosesQueuePosition() { TestUtils.getMockPlayer("cwwaitlogout").use { player -> @@ -410,6 +632,122 @@ class CastleWarsTests { } } + @Test + fun loggedOutGracePlayerReceivesEndGameTicket() { + TestUtils.getMockPlayer("cwlogoutticket").use { player -> + TestUtils.getMockPlayer("cwlogoutticketz").use { opponent -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(player) + CastleWarsGameArea.zamorakPlayers.add(opponent) + CastleWarsGameArea.loggedOutPlayers[CastleWarsGameArea.playerKey(player)] = + CastleWarsGameArea.Companion.LogoutRecord( + player, + CastleWarsTeam.SARADOMIN, + getWorldTicks(), + Location.create(2400, 3100, 0) + ) + player.isActive = false + try { + CastleWarsGameArea.endCurrentGame(startCooldown = false) + } finally { + player.isActive = true + } + + assertEquals(1, player.inventory.getAmount(CastleWars.cwTicketRewardCurrency)) + assertTrue(CastleWars.lobbyBankArea.insideBorder(player.location)) + } + } + } + + @Test + fun gameEndCleansCastleWarsSuppliesBeforeAwardingTickets() { + TestUtils.getMockPlayer("cwfullticket").use { player -> + TestUtils.getMockPlayer("cwfullticketz").use { opponent -> + player.inventory.add(Item(CastleWars.cwBandages, player.inventory.capacity())) + CastleWarsGameArea.saradominPlayers.add(player) + CastleWarsGameArea.zamorakPlayers.add(opponent) + + CastleWarsGameState.endGame() + + assertEquals(1, player.inventory.getAmount(CastleWars.cwTicketRewardCurrency)) + assertFalse(player.inventory.contains(CastleWars.cwBandages, 1)) + } + } + } + + @Test + fun matchEndClearsCastleWarsGroundItemsInArena() { + TestUtils.getMockPlayer("cwgroundcleanup").use { player -> + val arenaItem = GroundItemManager.create( + Item(CastleWars.cwBandages), + Location.create(2400, 3100, 0), + player + ) + + assertTrue(GroundItemManager.getItems().contains(arenaItem)) + + CastleWarsGameArea.endCurrentGame(startCooldown = false) + + assertFalse(GroundItemManager.getItems().contains(arenaItem)) + } + } + + @Test + fun fullInventoryEndGameTicketsDropAtLobbyBank() { + TestUtils.getMockPlayer("cwfullnormal").use { player -> + TestUtils.getMockPlayer("cwfullnormalz").use { opponent -> + player.location = Location.create(2400, 3100, 0) + player.inventory.add(Item(Items.CUP_OF_TEA_1978, player.inventory.capacity())) + CastleWarsGameArea.saradominPlayers.add(player) + CastleWarsGameArea.zamorakPlayers.add(opponent) + + CastleWarsGameState.endGame() + + val droppedTickets = GroundItemManager.getItems().filter { + it.id == CastleWars.cwTicketRewardCurrency && it.droppedBy(player) + } + assertTrue(droppedTickets.isNotEmpty()) + assertTrue(droppedTickets.all { CastleWars.lobbyBankArea.insideBorder(it.location) }) + droppedTickets.forEach { GroundItemManager.destroy(it) } + } + } + } + + @Test + fun gameEndCleansCarriedFlagsWithoutDropOrReturnMessages() { + TestUtils.getMockPlayer("cwquietflag").use { carrier -> + TestUtils.getMockPlayer("cwquietflagz").use { opponent -> + val carrierSession = carrier.session as MockSession + val opponentSession = opponent.session as MockSession + + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(carrier) + CastleWarsGameArea.zamorakPlayers.add(opponent) + assertTrue(CastleWarsGameState.pickUpFlag(carrier, CastleWarsTeam.ZAMORAK)) + carrierSession.clear() + opponentSession.clear() + + CastleWarsGameState.endGame() + + val messages = gameMessagePayloads(carrierSession) + gameMessagePayloads(opponentSession) + assertFalse(messages.any { it.contains("flag has been dropped") }) + assertFalse(messages.any { it.contains("flag has been returned") }) + } + } + } + + @Test + fun cleanupRemovesCastleWarsTeleblock() { + TestUtils.getMockPlayer("cwcleanblock").use { player -> + registerTimer(player, spawnTimer("teleblock", 100, false)) + assertTrue(hasTimerActive(player, "teleblock")) + + CastleWarsGameState.restoreAndCleanupPlayer(player) + + assertFalse(hasTimerActive(player, "teleblock")) + } + } + @Test fun joinPortalRejectsFoodAndInventoryHelmets() { withCastleWarsEnabled { @@ -548,6 +886,42 @@ class CastleWarsTests { } } + @Test + fun fullInventoryFlagCarrierCanUnequipToDropFlag() { + TestUtils.getMockPlayer("cwfulldrop").use { carrier -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(carrier) + + assertTrue(CastleWarsGameState.pickUpFlag(carrier, CastleWarsTeam.ZAMORAK)) + carrier.inventory.add(Item(Items.CUP_OF_TEA_1978, carrier.inventory.capacity())) + + EquipHandler.unequip(carrier, EquipmentSlot.WEAPON.ordinal, CastleWars.zamorakFlag) + + assertFalse(CastleWarsGameState.isCarryingFlag(carrier)) + assertNull(carrier.equipment[EquipmentSlot.WEAPON.ordinal]) + } + } + + @Test + fun equippingWeaponWhileCarryingFlagDropsFlagAndEquipsWeapon() { + TestUtils.getMockPlayer("cwflagswap").use { carrier -> + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(carrier) + carrier.inventory.add(Item(Items.BRONZE_SWORD_1277)) + + assertTrue(CastleWarsGameState.pickUpFlag(carrier, CastleWarsTeam.ZAMORAK)) + + InteractionListeners.run( + Items.BRONZE_SWORD_1277, IntType.ITEM, "wield", carrier, carrier.inventory[0] + ) + + assertFalse(CastleWarsGameState.isCarryingFlag(carrier)) + assertEquals(Items.BRONZE_SWORD_1277, carrier.equipment[EquipmentSlot.WEAPON.ordinal]?.id) + assertFalse(carrier.inventory.contains(CastleWars.zamorakFlag, 1)) + assertEquals(FlagState.DROPPED, CastleWarsGameState.zamorakFlagState) + } + } + @Test fun ownTeamBackdoorLockOptionIsHandled() { assertNotNull( @@ -558,6 +932,27 @@ class CastleWarsTests { ) } + @Test + fun resetGameRestoresOpenedBackdoorsToClosedScenery() { + val loc = (2418..2432).asSequence() + .flatMap { x -> (3070..3085).asSequence().map { y -> Location.create(x, y, 0) } } + .first { RegionManager.getObject(it) == null } + val closed = Scenery(CastleWars.saradominBackdoor, loc, 10, 0) + val open = Scenery(CastleWars.saradominBackdoorOpen, loc, 10, 0) + try { + SceneryBuilder.add(closed) + SceneryBuilder.replace(requireNotNull(RegionManager.getObject(loc)), open, -1, true) + + assertEquals(CastleWars.saradominBackdoorOpen, RegionManager.getObject(loc)?.id) + + CastleWarsGameState.resetGame() + + assertEquals(CastleWars.saradominBackdoor, RegionManager.getObject(loc)?.id) + } finally { + RegionManager.getObject(loc)?.let { SceneryBuilder.remove(it) } + } + } + @Test fun backdoorPicklockUsesLinearThievingCurveFromLevelOneToNinetyNine() { assertEquals(6.3, getBackdoorPicklockSuccessChance(1), 0.0001) @@ -595,6 +990,7 @@ class CastleWarsTests { 10, 0 ) + ensureCatapultObject(catapult) val operateCatapult = InteractionListeners.get( catapult.id, IntType.SCENERY.ordinal, "operate" )!! @@ -638,6 +1034,84 @@ class CastleWarsTests { } } + @Test + fun staleCatapultInterfaceCannotFireAfterCatapultIsDestroyed() { + TestUtils.getMockPlayer("cwcatstale").use { shooter -> + val catapult = Scenery( + CastleWars.getOperationalCatapultId(CastleWarsTeam.SARADOMIN), + CastleWarsGameState.saradominCatapultLocation, + 10, + 0 + ) + ensureCatapultObject(catapult) + val operateCatapult = InteractionListeners.get( + catapult.id, IntType.SCENERY.ordinal, "operate" + )!! + CastleWarsGameArea.saradominPlayers.add(shooter) + shooter.inventory.add(Item(CastleWars.cwRock)) + + operateCatapult(shooter, catapult) + assertTrue(shooter.interfaceManager.hasMainComponent(CatapultConfig.INTERFACE_ID)) + CastleWarsGameState.destroyCatapult(CastleWarsTeam.SARADOMIN) + + 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) + shooter.inventory.add(Item(CastleWars.cwRock)) + shooter.setAttribute(CatapultConfig.ATTR_LOCATION, Location.create(2400, 3090, 0)) + shooter.interfaceManager.open(Component(CatapultConfig.INTERFACE_ID)) + + 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 extinguishedCatapultFirePulseDoesNotDestroyRelitCatapult() { + TestUtils.getMockPlayer("cwcatfirez").use { attacker -> + TestUtils.getMockPlayer("cwcatfires").use { defender -> + val catapult = Scenery( + CastleWars.getOperationalCatapultId(CastleWarsTeam.SARADOMIN), + CastleWarsGameState.saradominCatapultLocation, + 10, + 0 + ) + ensureCatapultObject(catapult) + CastleWarsGameArea.zamorakPlayers.add(attacker) + CastleWarsGameArea.saradominPlayers.add(defender) + CastleWarsGameArea.ticksLeftInGame = 100 + defender.inventory.add(Item(Items.BUCKET_OF_WATER_1929)) + + assertTrue(runUseWith(Item(Items.TINDERBOX_590), catapult, IntType.SCENERY, attacker)) + TestUtils.advanceTicks(25, false) + assertTrue(runUseWith(defender.inventory[0], catapult, IntType.SCENERY, defender)) + assertFalse(CastleWarsGameState.isCatapultOnFire(CastleWarsTeam.SARADOMIN)) + + assertTrue(runUseWith(Item(Items.TINDERBOX_590), catapult, IntType.SCENERY, attacker)) + TestUtils.advanceTicks(25, false) + + assertTrue(CastleWarsGameState.isCatapultOperational(CastleWarsTeam.SARADOMIN)) + assertTrue(CastleWarsGameState.isCatapultOnFire(CastleWarsTeam.SARADOMIN)) + } + } + } + @Test fun climbingRopeOnPlayerTileCanBeClimbed() { val rope = Scenery(CastleWars.cwCastleClimbingRope, Location.create(2420, 3078, 1), 4, 0) @@ -692,6 +1166,33 @@ class CastleWarsTests { } } + @Test + fun extinguishingBarricadeStopsPreviousBurnPulseBeforeRelight() { + TestUtils.getMockPlayer("cwbarrfire").use { player -> + val barricade = CastleWarsBarricadeNPC.spawn(Location.create(2400, 3100, 0), CastleWarsTeam.SARADOMIN) + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsItemHandler.saradominBarricades.add(barricade) + player.inventory.add(Item(Items.BUCKET_OF_WATER_1929)) + + assertTrue(runUseWith(Item(Items.TINDERBOX_590), barricade, IntType.NPC, player)) + val firstPulse = CastleWarsItemHandler.burningBarricadePulses[barricade] + assertNotNull(firstPulse) + assertTrue(firstPulse!!.isRunning) + + assertTrue(runUseWith(player.inventory[0], barricade, IntType.NPC, player)) + + assertFalse(firstPulse.isRunning) + assertFalse(CastleWarsItemHandler.burningBarricadePulses.containsKey(barricade)) + + assertTrue(runUseWith(Item(Items.TINDERBOX_590), barricade, IntType.NPC, player)) + val secondPulse = CastleWarsItemHandler.burningBarricadePulses[barricade] + assertNotNull(secondPulse) + assertNotSame(firstPulse, secondPulse) + assertFalse(firstPulse.isRunning) + barricade.clear() + } + } + @Test fun matchStartActivatesBraceletEffectsAndStealFromOption() { TestUtils.getMockPlayer("cwbrace1").use { braceletPlayer -> @@ -716,6 +1217,24 @@ class CastleWarsTests { } } + @Test + fun substitutionActivatesBraceletEffects() { + TestUtils.getMockPlayer("cwsubbraceleave").use { leaver -> + TestUtils.getMockPlayer("cwsubbrace").use { substitute -> + substitute.equipment.replace(Item(Items.CASTLEWAR_BRACE3_11079), EquipmentSlot.HANDS.ordinal) + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(leaver) + CastleWarsWaitingArea.waitingSaradominPlayers.add(substitute) + + CastleWarsGameArea.removePlayerFromGame(leaver) + + assertTrue(CastleWarsGameArea.claimVacancy(substitute, CastleWarsTeam.SARADOMIN)) + assertEquals(CastleWarsTeam.SARADOMIN, substitute.activeCastleWarsBraceletTeam()) + assertNotNull(substitute.interaction[Option._P_STEAL_FROM.index]) + } + } + } + @Test fun bandagesHealTenPercentRestoreRunEnergyAndCurePoison() { TestUtils.getMockPlayer("cwbandage").use { player -> @@ -738,6 +1257,36 @@ class CastleWarsTests { } } + @Test + fun bandagesCannotBeUsedOutsideActiveCastleWarsGame() { + TestUtils.getMockPlayer("cwbandageoutside").use { player -> + player.skills.setStaticLevel(Skills.HITPOINTS, 99) + player.skills.lifepoints = 50 + player.inventory.add(Item(CastleWars.cwBandages)) + + InteractionListeners.run( + CastleWars.cwBandages, IntType.ITEM, "Heal", player, player.inventory[0] + ) + + assertEquals(50, player.skills.lifepoints) + assertTrue(player.inventory.contains(CastleWars.cwBandages, 1)) + } + } + + @Test + fun castleWarsTeleblockDoesNotSendChatboxMessage() { + TestUtils.getMockPlayer("cwsilentteleblock").use { player -> + val session = player.session as MockSession + player.location = CastleWarsWaitingArea.saradominWaitingRoom.randomWalkableLoc + session.clear() + + CastleWarsWaitingArea().areaEnter(player) + + assertTrue(hasTimerActive(player, "teleblock")) + assertFalse(session.hasPacketReady(70)) + } + } + @Test fun activeBraceletBandagesHealTeammatesForFifteenPercent() { TestUtils.getMockPlayer("cwhealer").use { healer -> @@ -815,6 +1364,27 @@ class CastleWarsTests { assertTrue(player.dialogueInterpreter.dialogue.file is CastleWarsSubstitutionDialogue) } + private fun ensureCatapultObject(catapult: Scenery) { + val current = RegionManager.getObject(catapult.location) + if (current != null && current.id in CastleWars.allCatapultIds) { + if (current.id != catapult.id) { + SceneryBuilder.replace(current, catapult) + } + return + } + SceneryBuilder.add(catapult) + } + + private fun runUseWith(used: Item, with: Node, type: IntType, player: Player): Boolean { + val handler = InteractionListeners.get(used.id, with.id, type.ordinal) + assertNotNull(handler) + return handler!!.invoke(player, used, with) + } + + private fun gameMessagePayloads(session: MockSession): List { + return session.getPacketsWithOpcode(70).map { String(it, Charsets.ISO_8859_1) } + } + private fun withCastleWarsEnabled(block: () -> Unit) { val settings = GameWorld.settings!! val original = settings.enable_castle_wars