diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt b/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt index af2efca53..9f6dff6aa 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsCommands.kt @@ -53,6 +53,8 @@ class CastleWarsCommands : Commands { 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 diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt b/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt new file mode 100644 index 000000000..015edc79b --- /dev/null +++ b/Server/src/main/content/minigame/castlewars/CastleWarsEquipmentRestrictions.kt @@ -0,0 +1,53 @@ +package content.minigame.castlewars + +import content.minigame.castlewars.areas.CastleWarsGameArea +import content.minigame.castlewars.areas.CastleWarsRespawnArea +import content.minigame.castlewars.areas.CastleWarsWaitingArea +import core.api.EquipmentSlot +import core.api.sendMessage +import core.game.node.entity.player.Player +import core.game.node.item.GroundItemManager + +object CastleWarsEquipmentRestrictions { + private val restrictedSlots = intArrayOf( + EquipmentSlot.HEAD.ordinal, + EquipmentSlot.CAPE.ordinal + ) + private val castleWarsTeamCapeIds = intArrayOf( + CastleWars.saradominTeamHoodedCloak, + CastleWars.zamorakTeamHoodedCloak + ) + + fun blocksEquip(player: Player, equipmentSlot: Int): Boolean { + if (equipmentSlot !in restrictedSlots || !isInCastleWarsArea(player)) { + return false + } + + sendMessage(player, "You can't wear hats, capes, or helms in the arena.") + return true + } + + fun removeInvalidEquipment(player: Player) { + removeEquipment(player, EquipmentSlot.HEAD.ordinal) + + val cape = player.equipment[EquipmentSlot.CAPE.ordinal] ?: return + if (cape.id !in castleWarsTeamCapeIds) { + removeEquipment(player, EquipmentSlot.CAPE.ordinal) + } + } + + private fun removeEquipment(player: Player, slot: Int) { + val item = player.equipment[slot] ?: return + player.equipment.remove(item) + if (!player.inventory.add(item)) { + 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/CastleWarsGameState.kt b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt index ab8abb64b..aceccf790 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt @@ -513,7 +513,7 @@ object CastleWarsGameState { sendMessage(player, "You can't pick up your own team's flag!") return false } - if (isCarryingFlag(player)) { + if (isCarryingAnyFlag(player)) { sendMessage(player, "You're already carrying a flag!") return false } @@ -1643,6 +1643,10 @@ object CastleWarsGameState { */ fun transferFlag(from: Player, to: Player) { val carriedFlag = getCarriedFlag(from) ?: return + if (isCarryingAnyFlag(to)) { + sendMessage(to, "You're already carrying a flag!") + return + } if (from.equipment.get(EquipmentContainer.SLOT_WEAPON)?.id == carriedFlag.flagItemId) { from.equipment.remove(Item(carriedFlag.flagItemId), EquipmentContainer.SLOT_WEAPON, true) } diff --git a/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt b/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt index 50093ed48..48584b005 100644 --- a/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt +++ b/Server/src/main/content/minigame/castlewars/CastleWarsShutdownHandler.kt @@ -73,6 +73,6 @@ class CastleWarsShutdownHandler : ShutdownListener { CastleWarsWaitingArea.waitingSaradominPlayers.clear() CastleWarsWaitingArea.waitingZamorakPlayers.clear() - CastleWarsGameArea.ticksLeftInGame = 0 + CastleWarsGameArea.ticksLeftInGame = -1 } } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt index 5743682d1..5ac678922 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt @@ -2,6 +2,7 @@ package content.minigame.castlewars.areas import content.global.skill.summoning.familiar.BurdenBeast import content.minigame.castlewars.CastleWars +import content.minigame.castlewars.CastleWarsEquipmentRestrictions import core.api.LogoutListener import core.api.MapArea import core.api.removeTimer @@ -12,6 +13,11 @@ import core.game.node.entity.player.Player abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { + override fun areaEnter(entity: Entity) { + val player = entity as? Player ?: return + CastleWarsEquipmentRestrictions.removeInvalidEquipment(player) + } + override fun areaLeave(entity: Entity, logout: Boolean) { super.areaLeave(entity, logout) exitArea(entity as? Player ?: return) diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt index 3ff5a0850..78f98f1cb 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt @@ -67,11 +67,12 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener { /** Players who have logged out but are within the grace period. Maps Player -> logout tick. */ val loggedOutPlayers = mutableMapOf() - var ticksLeftInGame = 0 + var ticksLeftInGame = -1 fun startGame() { CastleWarsGameState.resetGame() CastleWarsItemHandler.clearAllBarricades() + CastleWarsWaitingArea.gameStartCountdown = -1 saradominPlayers.addAll(CastleWarsWaitingArea.waitingSaradominPlayers) zamorakPlayers.addAll(CastleWarsWaitingArea.waitingZamorakPlayers) CastleWarsWaitingArea.waitingSaradominPlayers.clear() @@ -159,6 +160,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener { CastleWarsWaitingArea.postGameCooldown = true CastleWarsGameState.endGame() CastleWarsItemHandler.clearAllBarricades() + ticksLeftInGame = -1 saradominPlayers.forEach { player -> player.unhook(SaradominInterfaceCloseHook) player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc @@ -242,6 +244,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener { } CastleWarsGameState.clearHintIconForPlayer(player) // Do NOT remove from team sets -- grace period keeps them on the team + return } else { // No active game or not on a team -- full cleanup CastleWarsGameState.handlePlayerLeave(player) @@ -253,6 +256,13 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener { } override fun tick() { + if (ticksLeftInGame < 0) { + return + } + if (ticksLeftInGame == 0) { + endGame() + return + } val previousMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute ticksLeftInGame-- val currentMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute @@ -266,6 +276,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener { } if (ticksLeftInGame == 0) { endGame() + return } else if (currentMinutes != previousMinutes) { CastleWarsGameState.updateAllOverlays() } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt index f0a306e50..117e96cfb 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt @@ -31,6 +31,7 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return + super.areaEnter(player) if (CastleWarsGameState.isCarryingAnyFlag(player)) { if (CastleWarsGameState.isCarryingFlag(player)) { CastleWarsGameState.dropFlag(player) diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt index eb8f2a93e..6564cc55d 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt @@ -116,7 +116,7 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { override fun tick() { var nextStart = Int.MAX_VALUE // The game will start when there's enough players and CastleWarsGameArea.ticksLeftInGame = -5 minutes - if (CastleWarsGameArea.ticksLeftInGame >= 0) { + if (CastleWarsGameArea.ticksLeftInGame > 0) { // A game is going on, display 5 minutes + remaining game time gameStartCountdown = -1 // Reset countdown while game is active nextStart = CastleWarsGameArea.ticksLeftInGame + CastleWars.gameCooldownMinutes * ticksPerMinute @@ -144,7 +144,7 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { for (player in waitingSaradominPlayers + waitingZamorakPlayers) { CastleWarsOverlay.sendLobbyUpdate( player, - (waitingSaradominPlayers.size >= minPlayers && waitingZamorakPlayers.size >= minPlayers) || CastleWarsGameArea.ticksLeftInGame >= 0, + (waitingSaradominPlayers.size >= minPlayers && waitingZamorakPlayers.size >= minPlayers) || CastleWarsGameArea.ticksLeftInGame > 0, (nextStart - 1) / ticksPerMinute + 1 // Displays max 5 minutes, min 1 minute, seems authentic ) } diff --git a/Server/src/main/core/game/global/action/EquipHandler.kt b/Server/src/main/core/game/global/action/EquipHandler.kt index 29299e238..3f9f681a5 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.CastleWarsEquipmentRestrictions import core.game.event.ItemEquipEvent import core.game.event.ItemUnequipEvent import core.game.container.impl.EquipmentContainer @@ -35,6 +36,10 @@ class EquipHandler : InteractionListener { val item = node.asItem() val itemEquipmentSlot = item.definition.getConfiguration(ItemConfigParser.EQUIP_SLOT, -1) + if (CastleWarsEquipmentRestrictions.blocksEquip(player, itemEquipmentSlot)) { + return + } + val currentEquippedItem = player.equipment[itemEquipmentSlot] if (item == null || currentEquippedItem == item || item.name.toLowerCase().contains("goblin mail")) { return diff --git a/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt new file mode 100644 index 000000000..55394bbb5 --- /dev/null +++ b/Server/src/test/kotlin/content/minigame/castlewars/CastleWarsTests.kt @@ -0,0 +1,179 @@ +package content.minigame.castlewars + +import TestUtils +import content.minigame.castlewars.areas.CastleWarsGameArea +import content.minigame.castlewars.areas.CastleWarsWaitingArea +import core.api.EquipmentSlot +import core.game.global.action.EquipHandler +import core.game.interaction.IntType +import core.game.interaction.InteractionListener +import core.game.interaction.InteractionListeners +import core.game.node.item.Item +import core.game.world.map.Location +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.rs09.consts.Items + +class CastleWarsTests { + + companion object { + init { + TestUtils.preTestSetup() + if (InteractionListeners.get("equip", IntType.ITEM.ordinal) == null) { + EquipHandler().defineListeners() + } + } + } + + @AfterEach + fun cleanup() { + CastleWarsGameArea.saradominPlayers.clear() + CastleWarsGameArea.zamorakPlayers.clear() + CastleWarsGameArea.loggedOutPlayers.clear() + CastleWarsWaitingArea.waitingSaradominPlayers.clear() + CastleWarsWaitingArea.waitingZamorakPlayers.clear() + CastleWarsWaitingArea.gameStartCountdown = -1 + CastleWarsWaitingArea.postGameCooldown = false + CastleWarsGameArea.ticksLeftInGame = -1 + CastleWarsGameState.resetGame() + } + + @Test + fun idleGameAreaTickDoesNotCountDownForever() { + CastleWarsGameArea.ticksLeftInGame = -1 + + CastleWarsGameArea().tick() + + assertEquals(-1, CastleWarsGameArea.ticksLeftInGame) + } + + @Test + fun matchEndReturnsLifecycleToIdle() { + CastleWarsGameArea.ticksLeftInGame = 1 + + CastleWarsGameArea().tick() + + assertEquals(-1, CastleWarsGameArea.ticksLeftInGame) + assertTrue(CastleWarsWaitingArea.postGameCooldown) + } + + @Test + fun activeGameLogoutKeepsPlayerInGraceSet() { + TestUtils.getMockPlayer("cwlogout").use { player -> + val startLocation = Location.create(2400, 3100, 0) + player.location = startLocation + CastleWarsGameArea.ticksLeftInGame = 100 + CastleWarsGameArea.saradominPlayers.add(player) + + CastleWarsGameArea().logout(player) + + assertTrue(CastleWarsGameArea.saradominPlayers.contains(player)) + assertTrue(CastleWarsGameArea.loggedOutPlayers.containsKey(player)) + assertEquals(startLocation, player.location) + } + } + + @Test + fun castleWarsPlayersCannotEquipHelmsAfterEntry() { + TestUtils.getMockPlayer("cwhelm").use { player -> + player.location = Location.create(2380, 9490, 0) + player.inventory.add(Item(Items.BRONZE_FULL_HELM_1155)) + + InteractionListeners.run( + Items.BRONZE_FULL_HELM_1155, + IntType.ITEM, + "wear", + player, + player.inventory[0] + ) + + assertNull(player.equipment[EquipmentSlot.HEAD.ordinal]) + assertTrue(player.inventory.contains(Items.BRONZE_FULL_HELM_1155, 1)) + } + } + + @Test + fun castleWarsPlayersCannotManuallyEquipTeamCloaksAfterEntry() { + TestUtils.getMockPlayer("cwcloak").use { player -> + player.location = Location.create(2380, 9490, 0) + player.inventory.add(Item(Items.HOODED_CLOAK_4041)) + + InteractionListeners.run( + Items.HOODED_CLOAK_4041, + IntType.ITEM, + "wear", + player, + player.inventory[0] + ) + + assertNull(player.equipment[EquipmentSlot.CAPE.ordinal]) + assertTrue(player.inventory.contains(Items.HOODED_CLOAK_4041, 1)) + } + } + + @Test + fun castleWarsRestrictionDoesNotReplaceNormalEquipListenersOutsideArena() { + var didRun = false + object : InteractionListener { + override fun defineListeners() { + onEquip(Items.BRONZE_MED_HELM_1139) { _, _ -> + didRun = true + return@onEquip true + } + } + }.defineListeners() + + TestUtils.getMockPlayer("cwoutside").use { player -> + player.location = Location.create(3222, 3222, 0) + player.inventory.add(Item(Items.BRONZE_MED_HELM_1139)) + + InteractionListeners.run( + Items.BRONZE_MED_HELM_1139, + IntType.ITEM, + "wear", + player, + player.inventory[0] + ) + + assertTrue(didRun) + assertEquals(Items.BRONZE_MED_HELM_1139, player.equipment[EquipmentSlot.HEAD.ordinal]?.id) + } + } + + @Test + fun castleWarsAreaEntryRemovesBypassedHelms() { + TestUtils.getMockPlayer("cwentryhelm").use { player -> + player.location = CastleWarsWaitingArea.saradominWaitingRoom.randomWalkableLoc + player.equipment.replace(Item(Items.BRONZE_FULL_HELM_1155), EquipmentSlot.HEAD.ordinal) + + CastleWarsWaitingArea().areaEnter(player) + + assertNull(player.equipment[EquipmentSlot.HEAD.ordinal]) + assertTrue(player.inventory.contains(Items.BRONZE_FULL_HELM_1155, 1)) + } + } + + @Test + fun ownFlagCarrierCannotPickUpEnemyFlag() { + TestUtils.getMockPlayer("cwsara").use { saradomin -> + TestUtils.getMockPlayer("cwzam").use { zamorak -> + saradomin.location = Location.create(2400, 3100, 0) + zamorak.location = Location.create(2401, 3100, 0) + CastleWarsGameArea.saradominPlayers.add(saradomin) + CastleWarsGameArea.zamorakPlayers.add(zamorak) + + assertTrue(CastleWarsGameState.pickUpFlag(zamorak, CastleWarsTeam.SARADOMIN)) + CastleWarsGameState.dropFlag(zamorak) + assertTrue(CastleWarsGameState.pickUpOwnFlag(saradomin, CastleWarsTeam.SARADOMIN)) + + assertFalse(CastleWarsGameState.pickUpFlag(saradomin, CastleWarsTeam.ZAMORAK)) + assertTrue(CastleWarsGameState.isCarryingOwnFlag(saradomin)) + assertFalse(CastleWarsGameState.isCarryingFlag(saradomin)) + } + } + } +}