Quite a lot

- Waiting-room timer now stays visible and uses 2-minute cycles when idle, and active-game wait displays remaining game time +
5 minutes.
- Game start now requires 2 Saradomin + 2 Zamorak players, with no dev-mode shortcut or team cap logic.
- Removed combat-level “vacancy” substitution; substitutions now trigger only from real same-team departures.
- Added spawn-room two-minute removal with vacancy offers.
- Exit portals now only work for the matching team.
- Active-game logout/relogin now preserves location within two minutes and sends expired logouts to the team waiting room;
waiting-room logout returns to lobby and loses queue.
- End-game tickets now follow 2 win / 1 draw / 0 loss, without the old 5-per-team or 10-minute gate.
- Active games no longer end just because one team is temporarily empty.
- Join validation now rejects food, capes/helmets, coins, noted/non-combat items while allowing combat gear, potions, runes,
and Hunter butterflies.
This commit is contained in:
dam 2026-05-11 19:44:18 +03:00
parent 76abee373c
commit b417156cba
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
11 changed files with 609 additions and 199 deletions

View file

@ -32,10 +32,12 @@ class CastleWarsCommands : Commands {
}
val saraCount = CastleWarsWaitingArea.waitingSaradominPlayers.size
val zamCount = CastleWarsWaitingArea.waitingZamorakPlayers.size
if (saraCount == 0 || zamCount == 0) {
if (saraCount < CastleWarsConstants.MIN_PLAYERS_PER_TEAM ||
zamCount < CastleWarsConstants.MIN_PLAYERS_PER_TEAM
) {
sendMessage(
player,
"Both teams need at least 1 player. Saradomin: $saraCount, Zamorak: $zamCount"
"Both teams need at least ${CastleWarsConstants.MIN_PLAYERS_PER_TEAM} players. Saradomin: $saraCount, Zamorak: $zamCount"
)
return@define
}

View file

@ -42,8 +42,8 @@ object CastleWarsConstants {
/** Minimum players per team required to start a game */
const val MIN_PLAYERS_PER_TEAM = 2
/** Combat-level gap that opens a Castle Wars substitution vacancy */
const val COMBAT_LEVEL_VACANCY_THRESHOLD = 60
/** Castle Wars timing uses the server's 600ms game tick: 100 ticks = 1 minute */
const val TICKS_PER_MINUTE = 100
/** Catapult targeting inaccuracy radius */
const val CATAPULT_INACCURACY_RADIUS = 2
@ -120,21 +120,8 @@ object CastleWarsConstants {
const val GAME_COOLDOWN_MINUTES = 5
// ==================== Waiting Room ====================
/** Authentic waiting room countdown: GAME_COOLDOWN_MINUTES in ticks (5 min = 500 ticks) */
const val WAITING_ROOM_COUNTDOWN_TICKS = GAME_COOLDOWN_MINUTES * 100 // ticksPerMinute = 100
/** Dev-mode fast countdown for testing (~10 seconds) */
const val DEV_COUNTDOWN_TICKS = 17
/** Post-game shortened countdown in ticks (~2 minutes) for quick re-start */
const val POST_GAME_COUNTDOWN_TICKS = 200 // 2 minutes = 200 ticks
// ==================== Ticket Reward Requirements ====================
/** Minimum play time in minutes to be eligible for tickets */
const val MIN_PLAY_TIME_MINUTES = 10
/** Minimum team size for tickets to be awarded */
const val MIN_TEAM_SIZE_FOR_TICKETS = 5
/** Waiting-room cycle while no game is active: 2 minutes = 200 ticks */
const val WAITING_ROOM_COUNTDOWN_TICKS = 2 * TICKS_PER_MINUTE
// ==================== Player Attribute Keys ====================
/** Attribute key: world tick when player joined the current game */
@ -145,11 +132,23 @@ object CastleWarsConstants {
// ==================== Logout Grace Period ====================
/** Grace period after logout before removing player from team (2 minutes = 200 ticks) */
const val LOGOUT_GRACE_PERIOD_TICKS = 200
const val LOGOUT_GRACE_PERIOD_TICKS = 2 * TICKS_PER_MINUTE
/** Wall-clock logout grace for saved login state (2 minutes) */
const val LOGOUT_GRACE_PERIOD_MILLIS = 120_000L
/** Saved attribute key: wall-clock millis when player logged out of an active match */
const val ATTR_LOGOUT_TIME_MILLIS = "/save:cw:logout_time_millis"
/** Saved attribute key: team name for an active-match logout */
const val ATTR_LOGOUT_TEAM = "/save:cw:logout_team"
/** Saved attribute key: exact active-match logout location */
const val ATTR_LOGOUT_LOCATION = "/save:cw:logout_location"
// ==================== Flag Steal Timer ====================
/** Time in ticks before teammates can steal the flag from a carrier (3 minutes = 300 ticks) */
const val FLAG_STEAL_TIMER_TICKS = 300
const val FLAG_STEAL_TIMER_TICKS = 3 * TICKS_PER_MINUTE
/** Player attribute key for tracking when enemy flag was picked up */
const val ATTR_FLAG_PICKUP_TICK = "cw:flag_pickup_tick"

View file

@ -18,7 +18,6 @@ import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.zone.ZoneBorders
import core.game.world.update.flag.chunk.ObjectUpdateFlag
import core.tools.ticksPerMinute
import org.rs09.consts.Items
/**
@ -226,7 +225,8 @@ object CastleWarsGameState {
* Get the remaining game time in minutes
*/
val remainingMinutes: Int
get() = (CastleWarsGameArea.ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute
get() = (CastleWarsGameArea.ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) /
CastleWarsConstants.TICKS_PER_MINUTE
/**
* Reset the game state for a new game
@ -1083,24 +1083,14 @@ object CastleWarsGameState {
*/
fun endGame() {
val allPlayers = getAllGamePlayers()
val saraCount = allPlayers.count { getPlayerTeam(it) == CastleWarsTeam.SARADOMIN }
val zamCount = allPlayers.count { getPlayerTeam(it) == CastleWarsTeam.ZAMORAK }
val teamsLargeEnough =
saraCount >= CastleWarsConstants.MIN_TEAM_SIZE_FOR_TICKETS && zamCount >= CastleWarsConstants.MIN_TEAM_SIZE_FOR_TICKETS
for (player in allPlayers) {
val team = getPlayerTeam(player) ?: continue
val baseTickets = when {
val tickets = when {
saradominScore > zamorakScore && team == CastleWarsTeam.SARADOMIN -> 2
zamorakScore > saradominScore && team == CastleWarsTeam.ZAMORAK -> 2
saradominScore == zamorakScore -> 1
else -> 0
}
val tickets =
if (baseTickets > 0 && teamsLargeEnough && player.hasPlayedMinimumTime(CastleWarsConstants.MIN_PLAY_TIME_MINUTES)) {
baseTickets
} 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 ""}!")
@ -1511,6 +1501,9 @@ object CastleWarsGameState {
*/
private fun getAllGamePlayers(): Set<Player> {
val players = mutableSetOf<Player>()
players.addAll((CastleWarsGameArea.saradominPlayers + CastleWarsGameArea.zamorakPlayers).filter {
it.isActive && it.session != null
})
for (border in CastleWarsGameArea.areaBorders) {
val centerX = (border.southWestX + border.northEastX) / 2
val centerY = (border.southWestY + border.northEastY) / 2
@ -1676,18 +1669,6 @@ object CastleWarsGameState {
}
}
/**
* Check if this player has played for the minimum required time in the current game.
* @param minMinutes Minimum minutes required
* @return true if the player has been in the game for at least minMinutes
*/
fun Player.hasPlayedMinimumTime(minMinutes: Int): Boolean {
val joinTick = getAttribute(this, CastleWarsConstants.ATTR_JOIN_TICK, 0)
if (joinTick == 0) return false
val elapsed = getWorldTicks() - joinTick
return elapsed >= minMinutes * 100 // ticksPerMinute = 100
}
/**
* Check if the player has a Castle Wars bracelet equipped.
* The bracelet gives bonus damage against flag carriers and improved bandage healing.

View file

@ -373,15 +373,11 @@ class CastleWarsListeners : InteractionListener {
}
on(CastleWars.saradominExitPortal, IntType.SCENERY, "Leave") { player, _ ->
CastleWarsGameArea.removePlayerFromGame(player)
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
sendMessage(player, "You have left the Castle Wars game.")
leaveGameThroughPortal(player, CastleWarsTeam.SARADOMIN)
return@on true
}
on(CastleWars.zamorakExitPortal, IntType.SCENERY, "Leave") { player, _ ->
CastleWarsGameArea.removePlayerFromGame(player)
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
sendMessage(player, "You have left the Castle Wars game.")
leaveGameThroughPortal(player, CastleWarsTeam.ZAMORAK)
return@on true
}
@ -1018,14 +1014,52 @@ class CastleWarsListeners : InteractionListener {
CastleWarsGameState.dropFlagAtLocation(player, barrierData.outsideTile)
}
private fun hasNonCombatItems(container: Container): Boolean {
private fun leaveGameThroughPortal(player: Player, portalTeam: CastleWarsTeam) {
if (CastleWarsGameState.getPlayerTeam(player) != portalTeam) {
sendMessage(player, "Only the ${portalTeam.teamName} team may use this portal.")
return
}
CastleWarsGameArea.removePlayerFromGame(player)
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
sendMessage(player, "You have left the Castle Wars game.")
}
private fun hasDisallowedCastleWarsItems(container: Container): Boolean {
for (item in container.toArray()) {
if (item?.id != null && (item.id == Items.COINS_995 || item.definition?.noteId == item.id))
if (item != null && !isAllowedCastleWarsInventoryItem(item)) {
return true
}
}
return false
}
private fun isAllowedCastleWarsInventoryItem(item: Item): Boolean {
if (item.id == Items.COINS_995) return false
val definition = item.definition ?: return false
if (!definition.isUnnoted) return false
val equipmentSlot = equipSlot(item.id)
if (equipmentSlot != null) {
return equipmentSlot != EquipmentSlot.HEAD && equipmentSlot != EquipmentSlot.CAPE
}
val itemName = definition.name.lowercase()
return isRune(itemName) ||
definition.hasAction("drink") ||
itemName.contains("potion") ||
isHunterButterfly(itemName)
}
private fun isRune(itemName: String): Boolean {
return itemName.endsWith(" rune") || itemName.endsWith(" runes")
}
private fun isHunterButterfly(itemName: String): Boolean {
return itemName.contains("butterfly") ||
itemName.contains("ruby harvest") ||
itemName.contains("sapphire glacialis") ||
itemName.contains("snowy knight") ||
itemName.contains("black warlock")
}
private fun capeOrHelmetError(player: Player): String? {
val wornCape = getItemFromEquipment(player, EquipmentSlot.CAPE)?.id ?: -1
val wornHelmet = getItemFromEquipment(player, EquipmentSlot.HEAD)?.id ?: -1
@ -1035,7 +1069,7 @@ class CastleWarsListeners : InteractionListener {
}
private fun nonCombatItemsCheck(player: Player): String? {
if (hasNonCombatItems(player.inventory)) return "You can't take non-combat items into the arena."
if (hasDisallowedCastleWarsItems(player.inventory)) return "You can't take non-combat items into the arena."
return null
}
@ -1043,7 +1077,7 @@ class CastleWarsListeners : InteractionListener {
private fun familiarCheck(player: Player): String? {
val familiar: BurdenBeast = player.familiarManager.familiar as? BurdenBeast ?: return null
if (hasNonCombatItems(familiar.container)) return "Your familiar can't take non-combat items into the arena."
if (hasDisallowedCastleWarsItems(familiar.container)) return "Your familiar can't take non-combat items into the arena."
return null
}

View file

@ -9,8 +9,7 @@ import core.game.node.entity.player.Player
*/
object CastleWarsOverlay {
@JvmStatic
fun sendLobbyUpdate(player: Player, enoughPlayers: Boolean, gameStartMinutes: Int) {
// When not enough players, send -1 to hide the timer display
setVarp(player, 380, if (enoughPlayers) gameStartMinutes else -1)
fun sendLobbyUpdate(player: Player, gameStartMinutes: Int) {
setVarp(player, 380, gameStartMinutes)
}
}

View file

@ -0,0 +1,38 @@
package content.minigame.castlewars
import content.minigame.castlewars.areas.CastleWarsGameArea
import core.api.sendMessage
import core.game.dialogue.DialogueFile
import core.game.dialogue.Topic
import core.tools.END_DIALOGUE
import core.tools.START_DIALOGUE
class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : DialogueFile() {
override fun handle(componentID: Int, buttonID: Int) {
when (stage) {
START_DIALOGUE -> showTopics(
Topic("Yes.", ACCEPT, true),
Topic("No.", DECLINE, true),
title = "Do you wish to join the current game?"
)
ACCEPT -> {
if (!CastleWarsGameArea.claimVacancy(player!!, team)) {
sendMessage(player!!, "That Castle Wars vacancy is no longer available.")
}
stage = END_DIALOGUE
}
DECLINE -> {
CastleWarsGameArea.declineVacancyOffer(player!!, team)
stage = END_DIALOGUE
}
}
}
companion object {
private const val ACCEPT = 10
private const val DECLINE = 20
}
}

View file

@ -13,7 +13,6 @@ import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.item.Item
import core.tools.END_DIALOGUE
import core.tools.ticksPerMinute
import org.rs09.consts.Items
import org.rs09.consts.NPCs
@ -142,7 +141,8 @@ class Lanthus: InteractionListener, TickListener {
return
}
val minutesLeft = (countdown + ticksPerMinute - 1) / ticksPerMinute
val minutesLeft =
(countdown + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
// Announce at minute boundaries, only once per distinct minute value
if (minutesLeft != lastAnnouncedMinute && minutesLeft in 1..5) {
@ -152,4 +152,4 @@ class Lanthus: InteractionListener, TickListener {
sendChat(lanthus, "The next game will start in $minutesLeft $minuteWord!")
}
}
}
}

View file

@ -16,13 +16,12 @@ import core.game.world.map.Location
import core.game.world.map.zone.ZoneBorders
import core.game.world.map.zone.ZoneType
import core.tools.Log
import core.tools.ticksPerMinute
import org.rs09.consts.Components
/**
* Handles the Castle Wars game map
*/
class CastleWarsGameArea : CastleWarsArea(), TickListener {
class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
companion object {
private val saradominStandardFloor: ZoneBorders =
@ -64,8 +63,15 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
val saradominPlayers = mutableSetOf<Player>()
val zamorakPlayers = mutableSetOf<Player>()
/** Players who have logged out but are within the grace period. Maps Player -> logout tick. */
val loggedOutPlayers = mutableMapOf<Player, Int>()
data class LogoutRecord(
val player: Player,
val team: CastleWarsTeam,
val logoutTick: Int,
val logoutLocation: Location
)
/** Players who have logged out but are within the grace period. Maps normalized username -> logout record. */
val loggedOutPlayers = mutableMapOf<String, LogoutRecord>()
var ticksLeftInGame = -1
@ -73,6 +79,10 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
private val declinedSubstitutionOffers = mutableMapOf<CastleWarsTeam, MutableSet<Player>>()
fun startGame() {
if (!CastleWarsWaitingArea.hasMinimumPlayers()) {
CastleWarsWaitingArea.gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
return
}
clearSubstitutionState()
CastleWarsGameState.resetGame()
CastleWarsItemHandler.clearAllBarricades()
@ -81,7 +91,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
zamorakPlayers.addAll(CastleWarsWaitingArea.waitingZamorakPlayers)
CastleWarsWaitingArea.waitingSaradominPlayers.clear()
CastleWarsWaitingArea.waitingZamorakPlayers.clear()
ticksLeftInGame = CastleWars.gameTimeMinutes * ticksPerMinute
ticksLeftInGame = CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE
saradominPlayers.forEach { player ->
player.properties.teleportLocation = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks())
@ -111,8 +121,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
fun selectGuthixTeam(): CastleWarsTeam {
combatLevelVacancyTeam()?.let { return it }
val saraTotal = CastleWarsWaitingArea.waitingSaradominPlayers.size + saradominPlayers.size
val zamTotal = CastleWarsWaitingArea.waitingZamorakPlayers.size + zamorakPlayers.size
return when {
@ -123,21 +131,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
}
fun combatLevelVacancyTeam(): CastleWarsTeam? {
if (ticksLeftInGame <= 0) return null
val saraTotal = teamCombatLevelTotal(saradominPlayers)
val zamTotal = teamCombatLevelTotal(zamorakPlayers)
return when {
zamTotal - saraTotal > CastleWarsConstants.COMBAT_LEVEL_VACANCY_THRESHOLD -> CastleWarsTeam.SARADOMIN
saraTotal - zamTotal > CastleWarsConstants.COMBAT_LEVEL_VACANCY_THRESHOLD -> CastleWarsTeam.ZAMORAK
else -> null
}
}
private fun teamCombatLevelTotal(players: Set<Player>): Int {
return players.sumOf { it.properties.currentCombatLevel }
}
/**
* Offer a vacancy on the given team to every eligible player in that team's waiting room.
* Called when a player leaves an active game.
@ -159,11 +152,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
}
fun offerCombatLevelVacancyIfNeeded() {
val team = combatLevelVacancyTeam() ?: return
fillVacancy(team)
}
fun claimVacancy(player: Player, team: CastleWarsTeam): Boolean {
val offer = activeVacancyOffers[team] ?: return false
if (!offer.contains(player)) return false
@ -186,7 +174,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
player.properties.teleportLocation = respawnLoc
sendMessage(player, "A spot has opened up! You have been moved into the Castle Wars game.")
offerCombatLevelVacancyIfNeeded()
return true
}
@ -209,18 +196,109 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
CastleWarsGameState.handlePlayerLeave(player)
player.unhook(SaradominInterfaceCloseHook)
player.unhook(ZamorakInterfaceCloseHook)
loggedOutPlayers.remove(player)
clearLogoutState(player)
saradominPlayers.remove(player)
zamorakPlayers.remove(player)
CastleWarsGameState.clearHintIconForPlayer(player)
player.interaction.remove(Option._P_ATTACK)
if (offerReplacement) {
fillVacancy(leavingTeam)
offerCombatLevelVacancyIfNeeded()
}
return leavingTeam
}
fun playerKey(player: Player): String {
return player.username.lowercase()
}
private fun setTeam(player: Player, team: CastleWarsTeam) {
when (team) {
CastleWarsTeam.SARADOMIN -> {
zamorakPlayers.remove(player)
saradominPlayers.add(player)
}
CastleWarsTeam.ZAMORAK -> {
saradominPlayers.remove(player)
zamorakPlayers.add(player)
}
}
}
private fun removeFromTeam(player: Player) {
saradominPlayers.remove(player)
zamorakPlayers.remove(player)
}
private fun clearLogoutAttributes(player: Player) {
removeAttributes(
player,
CastleWarsConstants.ATTR_LOGOUT_TICK,
CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS,
CastleWarsConstants.ATTR_LOGOUT_TEAM,
CastleWarsConstants.ATTR_LOGOUT_LOCATION
)
}
private fun clearLogoutState(player: Player) {
loggedOutPlayers.remove(playerKey(player))
clearLogoutAttributes(player)
}
private fun waitingRoomFor(team: CastleWarsTeam): ZoneBorders {
return when (team) {
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.saradominWaitingRoom
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.zamorakWaitingRoom
}
}
private fun waitingSetFor(team: CastleWarsTeam): MutableSet<Player> {
return when (team) {
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers
}
}
private fun moveLoggedOutPlayerToWaitingRoom(player: Player, team: CastleWarsTeam) {
removeFromTeam(player)
CastleWarsGameState.clearHintIconForPlayer(player)
CastleWarsGameState.restoreAndCleanupPlayer(player)
player.interaction.remove(Option._P_ATTACK)
player.location = waitingRoomFor(team).randomWalkableLoc
player.equipment.replace(Item(team.cloakId), 1)
waitingSetFor(team).add(player)
clearLogoutAttributes(player)
}
fun restoreActiveLogoutOnLogin(player: Player) {
val savedTeamName = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")
val savedLogoutTime = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, 0L)
if (savedTeamName.isEmpty() || savedLogoutTime <= 0L) return
val team = CastleWarsTeam.values().firstOrNull { it.name == savedTeamName } ?: return
val key = playerKey(player)
val record = loggedOutPlayers.remove(key)
val previousPlayer = record?.player
if (previousPlayer != null && previousPlayer != player) {
removeFromTeam(previousPlayer)
}
val elapsedMillis = System.currentTimeMillis() - savedLogoutTime
val logoutLocation = getAttribute(
player,
CastleWarsConstants.ATTR_LOGOUT_LOCATION,
record?.logoutLocation ?: player.location
)
if (ticksLeftInGame > 0 && elapsedMillis < CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS) {
player.location = Location.create(logoutLocation)
setTeam(player, team)
clearLogoutAttributes(player)
return
}
moveLoggedOutPlayerToWaitingRoom(player, team)
if (ticksLeftInGame > 0) {
fillVacancy(team)
}
}
object SaradominInterfaceCloseHook : EventHook<InterfaceCloseEvent> { // TODO: this might be useless.
override fun process(entity: Entity, event: InterfaceCloseEvent) {
val player = entity as? Player ?: return
@ -249,7 +327,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
private fun endGame() { // TODO: ropes, rocks in tunnels, ground items, castle doors, catapults
CastleWarsWaitingArea.postGameCooldown = true
CastleWarsGameState.endGame()
CastleWarsItemHandler.clearAllBarricades()
clearSubstitutionState()
@ -274,13 +351,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
override fun areaEnter(entity: Entity) {
val player = entity as? Player ?: return
// Handle rejoin from logout grace period
if (loggedOutPlayers.containsKey(player)) {
loggedOutPlayers.remove(player)
player.removeAttribute(CastleWarsConstants.ATTR_LOGOUT_TICK)
// join tick is preserved from the original game start -- no changes needed
}
super.areaEnter(player)
zone.zoneType = ZoneType.CASTLE_WARS.id
registerTimer(player, spawnTimer("teleblock", (CastleWars.gameTimeMinutes) * 60 * 2))
@ -316,20 +386,27 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
override fun logout(player: Player) {
// Store logout tick for grace period instead of immediate removal
if (ticksLeftInGame > 0 && (saradominPlayers.contains(player) || zamorakPlayers.contains(player))) {
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TICK, getWorldTicks())
loggedOutPlayers[player] = getWorldTicks()
val team = when {
saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN
zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK
else -> null
}
if (ticksLeftInGame > 0 && team != null) {
val logoutTick = getWorldTicks()
val logoutLocation = Location.create(player.location)
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TICK, logoutTick)
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, System.currentTimeMillis())
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, team.name)
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, logoutLocation)
loggedOutPlayers[playerKey(player)] = LogoutRecord(player, team, logoutTick, logoutLocation)
if (CastleWarsGameState.isCarryingFlag(player)) {
CastleWarsGameState.dropFlag(player)
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
CastleWarsGameState.dropOwnFlag(player)
}
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)
CastleWarsGameState.clearHintIconForPlayer(player)
saradominPlayers.remove(player)
@ -338,6 +415,10 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
super.logout(player)
}
override fun login(player: Player) {
restoreActiveLogoutOnLogin(player)
}
override fun tick() {
if (ticksLeftInGame < 0) {
return
@ -346,17 +427,11 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
endGame()
return
}
val previousMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute
val previousMinutes =
(ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
ticksLeftInGame--
val currentMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute
if (ticksLeftInGame > 0) {
val saraEmpty = saradominPlayers.isEmpty()
val zamEmpty = zamorakPlayers.isEmpty()
if (saraEmpty || zamEmpty) {
endGame()
return
}
}
val currentMinutes =
(ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
if (ticksLeftInGame == 0) {
endGame()
return
@ -365,7 +440,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
}
CastleWarsGameState.checkFlagCarriers()
CastleWarsGameState.checkDroppedFlagTimeouts()
offerCombatLevelVacancyIfNeeded()
for (player in saradominPlayers) {
val currentOverlayId = player.interfaceManager.overlay?.id
if (currentOverlayId == null || currentOverlayId != 58) {
@ -393,28 +467,23 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
private fun cleanupInactivePlayers() {
val currentTick = getWorldTicks()
// Check logged-out players whose grace period has expired
val expiredLogouts = loggedOutPlayers.filter { (_, logoutTick) ->
currentTick - logoutTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
val expiredLogouts = loggedOutPlayers.filter { (_, record) ->
currentTick - record.logoutTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
}
for ((player, _) in expiredLogouts) {
val team = when {
saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN
zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK
else -> null
}
loggedOutPlayers.remove(player)
saradominPlayers.remove(player)
zamorakPlayers.remove(player)
for ((key, record) in expiredLogouts) {
val player = record.player
loggedOutPlayers.remove(key)
removeFromTeam(player)
CastleWarsGameState.clearHintIconForPlayer(player)
CastleWarsGameState.restoreAndCleanupPlayer(player)
if (team != null) fillVacancy(team)
fillVacancy(record.team)
}
// Remove truly inactive players (not in grace period)
val inactiveSara = saradominPlayers.filter { player ->
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(player)
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(playerKey(player))
}
val inactiveZam = zamorakPlayers.filter { player ->
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(player)
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(playerKey(player))
}
for (player in inactiveSara) {
saradominPlayers.remove(player)

View file

@ -1,9 +1,9 @@
package content.minigame.castlewars.areas
import content.minigame.castlewars.CastleWars
import content.minigame.castlewars.CastleWarsConstants
import content.minigame.castlewars.CastleWarsGameState
import core.api.TickListener
import core.api.sendMessage
import core.api.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.node.item.Item
@ -21,8 +21,8 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
val saradominRespawnRoom: ZoneBorders =
ZoneBorders(Location.create(2423, 3080, 1), Location.create(2431, 3072, 1))
val zamorakPlayersInRespawnRoom = mutableSetOf<Player>()
val saradominPlayersInRespawnRoom = mutableSetOf<Player>()
val zamorakPlayersInRespawnRoom = mutableMapOf<Player, Int>()
val saradominPlayersInRespawnRoom = mutableMapOf<Player, Int>()
}
override fun defineAreaBorders(): Array<ZoneBorders> {
@ -42,12 +42,47 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
}
if (saradominRespawnRoom.insideBorder(player.location)) {
player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1)
saradominPlayersInRespawnRoom[player] = getWorldTicks()
} else if (zamorakRespawnRoom.insideBorder(player.location)) {
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1)
zamorakPlayersInRespawnRoom[player] = getWorldTicks()
}
}
override fun tick() {
if (CastleWarsGameArea.ticksLeftInGame <= 0) {
saradominPlayersInRespawnRoom.clear()
zamorakPlayersInRespawnRoom.clear()
return
}
removePlayersOverSpawnLimit(saradominPlayersInRespawnRoom)
removePlayersOverSpawnLimit(zamorakPlayersInRespawnRoom)
}
private fun removePlayersOverSpawnLimit(players: MutableMap<Player, Int>) {
val currentTick = getWorldTicks()
val expiredPlayers = players.filter { (player, entryTick) ->
player.isActive &&
player.session != null &&
currentTick - entryTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
}.keys
for (player in expiredPlayers) {
players.remove(player)
CastleWarsGameArea.removePlayerFromGame(player)
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
sendMessage(player, "You have been removed from Castle Wars for staying in the spawn room too long.")
}
}
override fun logout(player: Player) {
if (!defineAreaBorders().any { it.insideBorder(player.location) }) {
return
}
if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getPlayerTeam(player) != null) {
CastleWarsGameArea().logout(player)
return
}
super.logout(player)
}
override fun exitArea(player: Player) {

View file

@ -8,9 +8,7 @@ import core.game.component.Component
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.game.world.GameWorld
import core.game.world.map.zone.ZoneBorders
import core.tools.ticksPerMinute
import org.rs09.consts.Components
/**
@ -27,7 +25,11 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
val waitingSaradominPlayers = mutableSetOf<Player>()
val waitingZamorakPlayers = mutableSetOf<Player>()
internal var gameStartCountdown = -1
var postGameCooldown = false
fun hasMinimumPlayers(): Boolean {
return waitingSaradominPlayers.size >= CastleWarsConstants.MIN_PLAYERS_PER_TEAM &&
waitingZamorakPlayers.size >= CastleWarsConstants.MIN_PLAYERS_PER_TEAM
}
}
override fun defineAreaBorders(): Array<ZoneBorders> {
@ -105,7 +107,6 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
// Open the white text overlay
player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_57))
CastleWarsGameArea.offerCombatLevelVacancyIfNeeded()
}
override fun exitArea(player: Player) {
@ -116,44 +117,37 @@ 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
val nextStart: Int
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
} else if (waitingSaradominPlayers.size < (if (GameWorld.settings?.isDevMode == true) 1 else CastleWarsConstants.MIN_PLAYERS_PER_TEAM)
|| waitingZamorakPlayers.size < (if (GameWorld.settings?.isDevMode == true) 1 else CastleWarsConstants.MIN_PLAYERS_PER_TEAM)
) {
// A game is not going and there aren't enough players - Reset countdown
gameStartCountdown = -1
CastleWarsGameArea.ticksLeftInGame = -1
nextStart = CastleWarsGameArea.ticksLeftInGame +
CastleWars.gameCooldownMinutes * CastleWarsConstants.TICKS_PER_MINUTE
} else {
// There are enough players - start or continue countdown
if (gameStartCountdown < 0) {
// Start new countdown: dev mode uses short timer, post-game uses 2-min, otherwise standard 5-min
gameStartCountdown = when {
GameWorld.settings?.isDevMode == true -> CastleWarsConstants.DEV_COUNTDOWN_TICKS
postGameCooldown -> CastleWarsConstants.POST_GAME_COUNTDOWN_TICKS
else -> CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
}
postGameCooldown = false // Reset after consuming
gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
}
nextStart = gameStartCountdown
gameStartCountdown--
}
val minPlayers = if (GameWorld.settings?.isDevMode == true) 1 else CastleWarsConstants.MIN_PLAYERS_PER_TEAM
for (player in waitingSaradominPlayers + waitingZamorakPlayers) {
CastleWarsOverlay.sendLobbyUpdate(
player,
(waitingSaradominPlayers.size >= minPlayers && waitingZamorakPlayers.size >= minPlayers) || CastleWarsGameArea.ticksLeftInGame > 0,
(nextStart - 1) / ticksPerMinute + 1 // Displays max 5 minutes, min 1 minute, seems authentic
ticksToDisplayMinutes(nextStart)
)
}
if (nextStart <= 0) {
gameStartCountdown = -1 // Reset countdown
if (CastleWarsGameArea.ticksLeftInGame <= 0 && nextStart <= 0) {
gameStartCountdown = -1
if (!hasMinimumPlayers()) {
gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
return
}
CastleWarsGameArea.startGame()
}
}
private fun ticksToDisplayMinutes(ticks: Int): Int {
if (ticks <= 0) return 0
return (ticks - 1) / CastleWarsConstants.TICKS_PER_MINUTE + 1
}
}

View file

@ -5,7 +5,7 @@ import content.global.skill.thieving.Pickpockets
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.*
import core.game.node.entity.skill.Skills
import core.game.global.action.EquipHandler
import core.game.interaction.IntType
@ -14,6 +14,7 @@ import core.game.interaction.InteractionListeners
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.game.node.scenery.Scenery
import core.game.world.GameWorld
import core.game.world.map.Location
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
@ -50,9 +51,10 @@ class CastleWarsTests {
CastleWarsWaitingArea.waitingSaradominPlayers.clear()
CastleWarsWaitingArea.waitingZamorakPlayers.clear()
CastleWarsWaitingArea.gameStartCountdown = -1
CastleWarsWaitingArea.postGameCooldown = false
CastleWarsGameArea.ticksLeftInGame = -1
CastleWarsGameArea.clearSubstitutionState()
CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear()
CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear()
CastleWarsGameState.resetGame()
}
@ -71,21 +73,6 @@ class CastleWarsTests {
}
}
@Test
fun guthixPortalRoutesToCombatDisadvantagedTeamDuringActiveMatch() {
TestUtils.getMockPlayer("cwweak").use { saradomin ->
TestUtils.getMockPlayer("cwstrong").use { zamorak ->
CastleWarsGameArea.ticksLeftInGame = 100
setCombatLevel(saradomin, 50)
setCombatLevel(zamorak, 120)
CastleWarsGameArea.saradominPlayers.add(saradomin)
CastleWarsGameArea.zamorakPlayers.add(zamorak)
assertEquals(CastleWarsTeam.SARADOMIN, CastleWarsGameArea.selectGuthixTeam())
}
}
}
@Test
fun spawnRoomPortalOffersVacancyAndFirstAcceptanceWins() {
TestUtils.getMockPlayer("cwleaver").use { leaver ->
@ -129,28 +116,84 @@ class CastleWarsTests {
}
@Test
fun combatLevelImbalanceAutomaticallyOffersVacancyToDisadvantagedWaitingRoom() {
TestUtils.getMockPlayer("cwweak2").use { saradomin ->
TestUtils.getMockPlayer("cwstrong2").use { zamorak ->
TestUtils.getMockPlayer("cwsub3").use { waiting ->
CastleWarsGameArea.ticksLeftInGame = 100
setCombatLevel(saradomin, 50)
setCombatLevel(zamorak, 120)
CastleWarsGameArea.saradominPlayers.add(saradomin)
CastleWarsGameArea.zamorakPlayers.add(zamorak)
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
fun activeMatchDoesNotOfferSubstitutionWithoutVacancy() {
TestUtils.getMockPlayer("cwsubnovacancy").use { waiting ->
CastleWarsGameArea.ticksLeftInGame = 100
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
CastleWarsGameArea.offerCombatLevelVacancyIfNeeded()
CastleWarsWaitingArea().areaEnter(waiting)
assertSubstitutionDialogue(waiting)
assertTrue(CastleWarsGameArea.claimVacancy(waiting, CastleWarsTeam.SARADOMIN))
assertTrue(CastleWarsGameArea.saradominPlayers.contains(waiting))
assertFalse(CastleWarsWaitingArea.waitingSaradominPlayers.contains(waiting))
assertFalse(waiting.dialogueInterpreter.dialogue?.file is CastleWarsSubstitutionDialogue)
assertFalse(CastleWarsGameArea.claimVacancy(waiting, CastleWarsTeam.SARADOMIN))
}
}
@Test
fun waitingRoomTimerShowsTwoMinuteMinimumWithoutEnoughPlayers() {
TestUtils.getMockPlayer("cwminwait").use { waiting ->
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
CastleWarsWaitingArea().tick()
assertEquals(2, getVarp(waiting, 380))
assertEquals(CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS - 1, CastleWarsWaitingArea.gameStartCountdown)
}
}
@Test
fun waitingRoomTimerCyclesWhenMinimumPlayersAreMissingAtExpiry() {
TestUtils.getMockPlayer("cwcycle").use { waiting ->
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
CastleWarsWaitingArea.gameStartCountdown = 0
CastleWarsWaitingArea().tick()
assertEquals(-1, CastleWarsGameArea.ticksLeftInGame)
assertTrue(CastleWarsWaitingArea.waitingSaradominPlayers.contains(waiting))
assertEquals(CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS, CastleWarsWaitingArea.gameStartCountdown)
}
}
@Test
fun waitingRoomTimerStartsGameWhenBothTeamsHaveTwoPlayersAtExpiry() {
TestUtils.getMockPlayer("cwstarts1").use { sara1 ->
TestUtils.getMockPlayer("cwstarts2").use { sara2 ->
TestUtils.getMockPlayer("cwstartz1").use { zam1 ->
TestUtils.getMockPlayer("cwstartz2").use { zam2 ->
CastleWarsWaitingArea.waitingSaradominPlayers.addAll(listOf(sara1, sara2))
CastleWarsWaitingArea.waitingZamorakPlayers.addAll(listOf(zam1, zam2))
CastleWarsWaitingArea.gameStartCountdown = 0
CastleWarsWaitingArea().tick()
assertEquals(
CastleWarsConstants.GAME_TIME_MINUTES * CastleWarsConstants.TICKS_PER_MINUTE,
CastleWarsGameArea.ticksLeftInGame
)
assertTrue(CastleWarsGameArea.saradominPlayers.containsAll(listOf(sara1, sara2)))
assertTrue(CastleWarsGameArea.zamorakPlayers.containsAll(listOf(zam1, zam2)))
assertTrue(CastleWarsWaitingArea.waitingSaradominPlayers.isEmpty())
assertTrue(CastleWarsWaitingArea.waitingZamorakPlayers.isEmpty())
assertTrue(CastleWarsRespawnArea.saradominRespawnRoom.insideBorder(sara1.properties.teleportLocation))
assertTrue(CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder(zam1.properties.teleportLocation))
}
}
}
}
}
@Test
fun activeGameWaitingTimerShowsRemainingGamePlusFiveMinutes() {
TestUtils.getMockPlayer("cwactivewait").use { waiting ->
CastleWarsGameArea.ticksLeftInGame = 350
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
CastleWarsWaitingArea().tick()
assertEquals(9, getVarp(waiting, 380))
}
}
@Test
fun idleGameAreaTickDoesNotCountDownForever() {
CastleWarsGameArea.ticksLeftInGame = -1
@ -167,7 +210,19 @@ class CastleWarsTests {
CastleWarsGameArea().tick()
assertEquals(-1, CastleWarsGameArea.ticksLeftInGame)
assertTrue(CastleWarsWaitingArea.postGameCooldown)
assertEquals(-1, CastleWarsWaitingArea.gameStartCountdown)
}
@Test
fun activeGameDoesNotEndEarlyWhenOneTeamIsTemporarilyEmpty() {
TestUtils.getMockPlayer("cwstillrunning").use { player ->
CastleWarsGameArea.ticksLeftInGame = 100
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsGameArea().tick()
assertEquals(99, CastleWarsGameArea.ticksLeftInGame)
}
}
@Test
@ -181,11 +236,203 @@ class CastleWarsTests {
CastleWarsGameArea().logout(player)
assertTrue(CastleWarsGameArea.saradominPlayers.contains(player))
assertTrue(CastleWarsGameArea.loggedOutPlayers.containsKey(player))
assertTrue(CastleWarsGameArea.loggedOutPlayers.containsKey(CastleWarsGameArea.playerKey(player)))
assertEquals(startLocation, player.location)
}
}
@Test
fun activeGameLogoutWithinTwoMinutesRejoinsAtExactLocation() {
TestUtils.getMockPlayer("cwquickrelog").use { player ->
val logoutLocation = Location.create(2401, 3101, 0)
player.location = logoutLocation
CastleWarsGameArea.ticksLeftInGame = 500
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsGameArea().logout(player)
CastleWarsGameArea().login(player)
assertTrue(CastleWarsGameArea.saradominPlayers.contains(player))
assertFalse(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player))
assertFalse(CastleWarsGameArea.loggedOutPlayers.containsKey(CastleWarsGameArea.playerKey(player)))
assertEquals(logoutLocation, player.location)
}
}
@Test
fun activeGameLogoutAfterTwoMinutesReturnsPlayerToTeamWaitingRoom() {
TestUtils.getMockPlayer("cwslowrelog").use { player ->
player.location = Location.create(2402, 3102, 0)
CastleWarsGameArea.ticksLeftInGame = 500
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsGameArea().logout(player)
setAttribute(
player,
CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS,
System.currentTimeMillis() - CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS - 1
)
CastleWarsGameArea().login(player)
assertFalse(CastleWarsGameArea.saradominPlayers.contains(player))
assertTrue(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player))
assertTrue(CastleWarsWaitingArea.saradominWaitingRoom.insideBorder(player.location))
}
}
@Test
fun spawnRoomLogoutUsesActiveGameGraceInsteadOfLobbyRemoval() {
TestUtils.getMockPlayer("cwspawnlogout").use { player ->
val logoutLocation = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
player.location = logoutLocation
CastleWarsGameArea.ticksLeftInGame = 500
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsRespawnArea().areaEnter(player)
CastleWarsRespawnArea().logout(player)
CastleWarsGameArea().login(player)
assertTrue(CastleWarsGameArea.saradominPlayers.contains(player))
assertFalse(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player))
assertEquals(logoutLocation, player.location)
}
}
@Test
fun waitingRoomLogoutReturnsPlayerToLobbyAndLosesQueuePosition() {
TestUtils.getMockPlayer("cwwaitlogout").use { player ->
player.location = CastleWarsWaitingArea.zamorakWaitingRoom.randomWalkableLoc
CastleWarsWaitingArea().areaEnter(player)
CastleWarsWaitingArea().logout(player)
assertFalse(CastleWarsWaitingArea.waitingZamorakPlayers.contains(player))
assertTrue(CastleWars.lobbyBankArea.insideBorder(player.location))
}
}
@Test
fun spawnRoomTwoMinuteLimitRemovesPlayerAndOffersVacancy() {
TestUtils.getMockPlayer("cwspawnidle").use { player ->
TestUtils.getMockPlayer("cwspawnsub").use { waiting ->
player.location = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
CastleWarsGameArea.ticksLeftInGame = 500
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
CastleWarsRespawnArea().areaEnter(player)
CastleWarsRespawnArea.saradominPlayersInRespawnRoom[player] =
getWorldTicks() - CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
CastleWarsRespawnArea().tick()
assertFalse(CastleWarsGameArea.saradominPlayers.contains(player))
assertTrue(CastleWars.lobbyBankArea.insideBorder(player.properties.teleportLocation))
assertSubstitutionDialogue(waiting)
}
}
}
@Test
fun exitPortalCanOnlyBeUsedByMatchingTeamAndOffersVacancy() {
TestUtils.getMockPlayer("cwportalteam").use { player ->
TestUtils.getMockPlayer("cwportalwait").use { waiting ->
CastleWarsGameArea.ticksLeftInGame = 500
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsWaitingArea.waitingSaradominPlayers.add(waiting)
runSceneryInteraction(
CastleWars.zamorakExitPortal,
"Leave",
player,
Scenery(CastleWars.zamorakExitPortal, Location.create(2372, 3131, 1), 10, 0)
)
assertTrue(CastleWarsGameArea.saradominPlayers.contains(player))
runSceneryInteraction(
CastleWars.saradominExitPortal,
"Leave",
player,
Scenery(CastleWars.saradominExitPortal, Location.create(2427, 3076, 1), 10, 0)
)
assertFalse(CastleWarsGameArea.saradominPlayers.contains(player))
assertTrue(CastleWars.lobbyBankArea.insideBorder(player.properties.teleportLocation))
assertSubstitutionDialogue(waiting)
}
}
}
@Test
fun gameEndAwardsDrawTicketWithoutFivePerTeamOrTenMinuteGate() {
TestUtils.getMockPlayer("cwticket1").use { sara1 ->
TestUtils.getMockPlayer("cwticket2").use { sara2 ->
TestUtils.getMockPlayer("cwticket3").use { zam1 ->
TestUtils.getMockPlayer("cwticket4").use { zam2 ->
CastleWarsGameArea.saradominPlayers.addAll(listOf(sara1, sara2))
CastleWarsGameArea.zamorakPlayers.addAll(listOf(zam1, zam2))
CastleWarsGameState.endGame()
assertEquals(1, sara1.inventory.getAmount(CastleWars.cwTicketRewardCurrency))
assertEquals(1, sara2.inventory.getAmount(CastleWars.cwTicketRewardCurrency))
assertEquals(1, zam1.inventory.getAmount(CastleWars.cwTicketRewardCurrency))
assertEquals(1, zam2.inventory.getAmount(CastleWars.cwTicketRewardCurrency))
}
}
}
}
}
@Test
fun joinPortalRejectsFoodAndInventoryHelmets() {
withCastleWarsEnabled {
TestUtils.getMockPlayer("cwfood").use { player ->
player.inventory.add(Item(Items.SHARK_385))
runSceneryInteraction(
CastleWars.joinSaradominTeamPortal,
"Enter",
player,
Scenery(CastleWars.joinSaradominTeamPortal, Location.create(2440, 3089, 0), 10, 0)
)
assertNull(player.properties.teleportLocation)
player.inventory.clear()
player.inventory.add(Item(Items.BRONZE_FULL_HELM_1155))
runSceneryInteraction(
CastleWars.joinSaradominTeamPortal,
"Enter",
player,
Scenery(CastleWars.joinSaradominTeamPortal, Location.create(2440, 3089, 0), 10, 0)
)
assertNull(player.properties.teleportLocation)
}
}
}
@Test
fun joinPortalAllowsCombatEquipmentPotionsAndRunes() {
withCastleWarsEnabled {
TestUtils.getMockPlayer("cwallowed").use { player ->
player.inventory.add(Item(Items.BRONZE_SWORD_1277))
player.inventory.add(Item(Items.AIR_RUNE_556, 25))
player.inventory.add(Item(Items.ATTACK_POTION2_123))
runSceneryInteraction(
CastleWars.joinSaradominTeamPortal,
"Enter",
player,
Scenery(CastleWars.joinSaradominTeamPortal, Location.create(2440, 3089, 0), 10, 0)
)
assertTrue(CastleWarsWaitingArea.saradominWaitingRoom.insideBorder(player.properties.teleportLocation))
}
}
}
@Test
fun castleWarsPlayersCannotEquipHelmsAfterEntry() {
TestUtils.getMockPlayer("cwhelm").use { player ->
@ -377,8 +624,20 @@ class CastleWarsTests {
assertTrue(player.dialogueInterpreter.dialogue.file is CastleWarsSubstitutionDialogue)
}
@Suppress("DEPRECATION")
private fun setCombatLevel(player: Player, level: Int) {
player.properties.setCombatLevel(level)
private fun withCastleWarsEnabled(block: () -> Unit) {
val settings = GameWorld.settings!!
val original = settings.enable_castle_wars
settings.enable_castle_wars = true
try {
block()
} finally {
settings.enable_castle_wars = original
}
}
private fun runSceneryInteraction(id: Int, option: String, player: Player, scenery: Scenery): Boolean {
val handler = InteractionListeners.get(id, IntType.SCENERY.ordinal, option)
assertNotNull(handler)
return handler!!.invoke(player, scenery)
}
}