Match idle dur to -1, logging out grace fix, head/cape fixes, flag carrying fixes, some tests

This commit is contained in:
dam 2026-05-09 21:19:05 +03:00
parent 6b76a3f8d0
commit 8fb52b6eda
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
10 changed files with 266 additions and 5 deletions

View file

@ -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

View file

@ -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) }
}
}

View file

@ -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)
}

View file

@ -73,6 +73,6 @@ class CastleWarsShutdownHandler : ShutdownListener {
CastleWarsWaitingArea.waitingSaradominPlayers.clear()
CastleWarsWaitingArea.waitingZamorakPlayers.clear()
CastleWarsGameArea.ticksLeftInGame = 0
CastleWarsGameArea.ticksLeftInGame = -1
}
}

View file

@ -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)

View file

@ -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<Player, Int>()
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()
}

View file

@ -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)

View file

@ -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
)
}

View file

@ -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<Int>(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

View file

@ -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))
}
}
}
}