diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt index c6b336c55..0d1ed3d76 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsArea.kt @@ -1,11 +1,14 @@ -package rs09.game.content.activity.castlewars.areas +package content.minigame.castlewars.areas import content.global.skill.summoning.familiar.BurdenBeast -import core.api.* +import content.minigame.castlewars.CastleWars +import core.api.LogoutListener +import core.api.MapArea +import core.api.removeTimer +import core.api.sendMessage import core.game.interaction.InteractionListener import core.game.node.entity.Entity import core.game.node.entity.player.Player -import rs09.game.content.activity.castlewars.CastleWars abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { @@ -48,21 +51,21 @@ abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener { // Remove teleblock removeTimer(player, "teleblock") - // Remove any Castle Wars items - // Todo Remove any tinderboxes or other castle wars items - See Jan 2018 update: https://oldschool.runescape.wiki/w/Castle_Wars - val cwarsItems = intArrayOf(CastleWars.saradominTeamHoodedCloak, - CastleWars.zamorakTeamHoodedCloak, - CastleWars.saradominFlag, - CastleWars.zamorakFlag) + // TODO: PvPStateAudit blows, has gotta go and actual safe handling of PvP generalized to cover current inauthentic deep wildy, + // future authentic minigames and that wont result in 7000 exploits that ruin the server (yes. thats a good idea. who said that) + // PvPStateAudit.cleanupPvPState(player) - player.equipment.removeAll(cwarsItems) - player.inventory.removeAll(cwarsItems) - (player.familiarManager.familiar as? BurdenBeast)?.container?.removeAll(cwarsItems) + player.equipment.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) + player.inventory.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) + (player.familiarManager.familiar as? BurdenBeast)?.container?.removeAll(CastleWars.CW_ITEMS_TO_REMOVE) } override fun defineListeners() { onUnequip(intArrayOf(CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak)) { player, _ -> - defineAreaBorders().forEach { border -> + val allCastleWarsBorders = CastleWarsGameArea.areaBorders.toList() + + CastleWarsWaitingArea.areaBorders.toList() + + listOf(CastleWarsRespawnArea.saradominRespawnRoom, CastleWarsRespawnArea.zamorakRespawnRoom) + for (border in allCastleWarsBorders) { if (border.insideBorder(player)) { sendMessage(player, "You can't remove your team's colours") return@onUnequip false diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt index fb44ea7d9..3db8b03f2 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsGameArea.kt @@ -1,7 +1,12 @@ -package rs09.game.content.activity.castlewars.areas +package content.minigame.castlewars.areas +import content.minigame.castlewars.* +import content.minigame.castlewars.CastleWarsConstants.PLAYER_CLEANUP_INTERVAL_TICKS import core.api.* import core.game.component.Component +import core.game.event.EventHook +import core.game.event.InterfaceCloseEvent +import core.game.interaction.Option import core.game.node.entity.Entity import core.game.node.entity.player.Player import core.game.node.item.Item @@ -11,57 +16,159 @@ import core.game.world.map.zone.ZoneType import core.tools.Log import core.tools.ticksPerMinute import org.rs09.consts.Components -import rs09.game.content.activity.castlewars.CastleWars /** * Handles the Castle Wars game map */ -class CastleWarsGameArea : CastleWarsArea(), TickListener { +class CastleWarsGameArea : CastleWarsArea(), TickListener, PvPZone { companion object { - private val saradominStandardFloor: ZoneBorders = ZoneBorders(Location.create(2426, 3073, 3), Location.create(2430, 3077, 3)) - private val zamorakStandardFloor: ZoneBorders = ZoneBorders(Location.create(2373, 3134, 3), Location.create(2369, 3130, 3)) - private val saradominUpperFloor: ZoneBorders = ZoneBorders(Location.create(2431, 3080, 2), Location.create(2423, 3072, 2)) - private val zamorakUpperFloor: ZoneBorders = ZoneBorders(Location.create(2368, 3127, 2), Location.create(2376, 3135, 2)) - private val saradominFloor: ZoneBorders = ZoneBorders(Location.create(2420, 3072, 1), Location.create(2431, 3083, 1)) - private val zamorakFloor: ZoneBorders = ZoneBorders(Location.create(2379, 3135, 1), Location.create(2368, 3124, 1)) - private val battleField: ZoneBorders = ZoneBorders(Location.create(2368, 3135, 0), Location.create(2431, 3072, 0)) - private val saradominTunnels: ZoneBorders = ZoneBorders(Location.create(2430, 9481, 0), Location.create(2400, 9504, 0)) - private val zamorakTunnels: ZoneBorders = ZoneBorders(Location.create(2401, 9503, 0), Location.create(2366, 9529, 0)) + private val saradominStandardFloor: ZoneBorders = + ZoneBorders(Location.create(2426, 3073, 3), Location.create(2430, 3077, 3)) + private val zamorakStandardFloor: ZoneBorders = + ZoneBorders(Location.create(2373, 3134, 3), Location.create(2369, 3130, 3)) + private val saradominUpperFloor: ZoneBorders = + ZoneBorders(Location.create(2431, 3080, 2), Location.create(2423, 3072, 2)) + private val zamorakUpperFloor: ZoneBorders = + ZoneBorders(Location.create(2368, 3127, 2), Location.create(2376, 3135, 2)) + private val saradominFloor: ZoneBorders = + ZoneBorders(Location.create(2420, 3072, 1), Location.create(2431, 3083, 1)) + private val zamorakFloor: ZoneBorders = + ZoneBorders(Location.create(2379, 3135, 1), Location.create(2368, 3124, 1)) + private val battleField: ZoneBorders = + ZoneBorders(Location.create(2368, 3135, 0), Location.create(2431, 3072, 0)) + private val saradominTunnels: ZoneBorders = + ZoneBorders(Location.create(2430, 9481, 0), Location.create(2385, 9504, 0)) + private val zamorakTunnels: ZoneBorders = + ZoneBorders(Location.create(2401, 9503, 0), Location.create(2366, 9529, 0)) - val areaBorders = arrayOf(saradominStandardFloor, zamorakStandardFloor, saradominUpperFloor, zamorakUpperFloor, saradominFloor, zamorakFloor, battleField, saradominTunnels, zamorakTunnels) + // Center tunnel zone to cover the gap where ladders are located (2399, 9499) and (2400, 9508) + private val centerTunnels: ZoneBorders = + ZoneBorders(Location.create(2396, 9493, 0), Location.create(2405, 9515, 0)) + + val areaBorders = arrayOf( + saradominStandardFloor, + zamorakStandardFloor, + saradominUpperFloor, + zamorakUpperFloor, + saradominFloor, + zamorakFloor, + battleField, + saradominTunnels, + zamorakTunnels, + centerTunnels + ) val saradominPlayers = mutableSetOf() val zamorakPlayers = mutableSetOf() + /** Players who have logged out but are within the grace period. Maps Player -> logout tick. */ + val loggedOutPlayers = mutableMapOf() + var ticksLeftInGame = 0 fun startGame() { + CastleWarsGameState.resetGame() + CastleWarsItemHandler.clearAllBarricades() saradominPlayers.addAll(CastleWarsWaitingArea.waitingSaradominPlayers) zamorakPlayers.addAll(CastleWarsWaitingArea.waitingZamorakPlayers) CastleWarsWaitingArea.waitingSaradominPlayers.clear() CastleWarsWaitingArea.waitingZamorakPlayers.clear() ticksLeftInGame = CastleWars.gameTimeMinutes * ticksPerMinute - - // Put all the players in their respawn area saradominPlayers.forEach { player -> player.properties.teleportLocation = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc + setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks()) } zamorakPlayers.forEach { player -> player.properties.teleportLocation = CastleWarsRespawnArea.zamorakRespawnRoom.randomWalkableLoc + setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks()) + } + val allPlayers = saradominPlayers + zamorakPlayers + allPlayers.forEach { player -> + sendMessage(player, "The Castle Wars game has begun!") + CastleWarsGameState.updateOverlay(player) + } + } + + /** + * Handle player death in Castle Wars - delegates to CastleWarsGameZone. + * This ensures consistent death behavior including poison preservation. + */ + fun handleDeath(player: Player, killer: Entity?): Boolean { + return CastleWarsGameZone.handleCastleWarsDeath(player) + } + + /** + * Try to fill a vacancy on the given team by pulling a player from the waiting room. + * Called when a player leaves an active game. + * @param team The team that lost a player + */ + fun fillVacancy(team: CastleWarsTeam) { + if (ticksLeftInGame <= 0) return + val waitingSet = when (team) { + CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers + CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers + } + val gameSet = when (team) { + CastleWarsTeam.SARADOMIN -> saradominPlayers + CastleWarsTeam.ZAMORAK -> zamorakPlayers + } + if (waitingSet.isEmpty()) return + if (gameSet.size >= CastleWarsConstants.MAX_PLAYERS_PER_TEAM) return + val replacement = waitingSet.first() + waitingSet.remove(replacement) + gameSet.add(replacement) + setAttribute(replacement, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks()) + val respawnLoc = when (team) { + CastleWarsTeam.SARADOMIN -> CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc + CastleWarsTeam.ZAMORAK -> CastleWarsRespawnArea.zamorakRespawnRoom.randomWalkableLoc + } + replacement.properties.teleportLocation = respawnLoc + sendMessage(replacement, "A spot has opened up! You have been moved into the Castle Wars game.") + } + + object SaradominInterfaceCloseHook : EventHook { // TODO: this might be useless. + override fun process(entity: Entity, event: InterfaceCloseEvent) { + val player = entity as? Player ?: return + // Only restore if player is still in the game + if (!saradominPlayers.contains(player)) return + + val currentOverlayId = player.interfaceManager.overlay?.id + if (currentOverlayId == null || currentOverlayId != 58) { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) + CastleWarsGameState.updateOverlay(player) + } + } + } + + object ZamorakInterfaceCloseHook : EventHook { // TODO: this might be useless. + override fun process(entity: Entity, event: InterfaceCloseEvent) { + val player = entity as? Player ?: return + if (!zamorakPlayers.contains(player)) return + val currentOverlayId = player.interfaceManager.overlay?.id + if (currentOverlayId == null || currentOverlayId != 59) { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) + CastleWarsGameState.updateOverlay(player) + } } } } - private fun endGame() { + private fun endGame() { // TODO: ropes, rocks in tunnels, ground items, castle doors, catapults + CastleWarsWaitingArea.postGameCooldown = true + CastleWarsGameState.endGame() + CastleWarsItemHandler.clearAllBarricades() saradominPlayers.forEach { player -> + player.unhook(SaradominInterfaceCloseHook) player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc } zamorakPlayers.forEach { player -> + player.unhook(ZamorakInterfaceCloseHook) player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc } saradominPlayers.clear() zamorakPlayers.clear() + loggedOutPlayers.clear() } override fun defineAreaBorders(): Array { @@ -70,31 +177,211 @@ 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)) + registerTimer(player, spawnTimer("teleblock", (CastleWars.gameTimeMinutes) * 60 * 2)) + val team = CastleWarsGameState.getPlayerTeam(player) + when (team) { + CastleWarsTeam.SARADOMIN -> { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) + player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) + player.hook(Event.InterfaceClosed, SaradominInterfaceCloseHook) + } - if (saradominPlayers.contains(player)) { - player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) - player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) - } else if (zamorakPlayers.contains(player)) { - player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) - player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) + CastleWarsTeam.ZAMORAK -> { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) + player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1) + player.hook(Event.InterfaceClosed, ZamorakInterfaceCloseHook) + } + + null -> { + log(this::class.java, Log.WARN, "${player.name} entered Castle Wars but is NOT on a team?!") + } } + player.interaction.set(Option._P_ATTACK) } override fun exitArea(player: Player) { super.exitArea(player) - // Remove player from the players set (whichever one that is) - saradominPlayers.remove(player) - zamorakPlayers.remove(player) - } - - override fun tick() { - ticksLeftInGame-- - if (ticksLeftInGame == 0) { - endGame() + val stillInCastleWars = areaBorders.any { it.insideBorder(player.location) } || + CastleWarsRespawnArea.saradominRespawnRoom.insideBorder(player.location) || + CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder(player.location) + if (!stillInCastleWars) { + val leavingTeam = CastleWarsGameState.getPlayerTeam(player) + CastleWarsGameState.handlePlayerLeave(player) + player.unhook(SaradominInterfaceCloseHook) + player.unhook(ZamorakInterfaceCloseHook) + saradominPlayers.remove(player) + zamorakPlayers.remove(player) + CastleWarsGameState.clearHintIconForPlayer(player) + player.interaction.remove(Option._P_ATTACK) + if (leavingTeam != null) { + fillVacancy(leavingTeam) + } } } + 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() + 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 + } else { + // No active game or not on a team -- full cleanup + CastleWarsGameState.handlePlayerLeave(player) + CastleWarsGameState.clearHintIconForPlayer(player) + saradominPlayers.remove(player) + zamorakPlayers.remove(player) + } + super.logout(player) + } + + override fun tick() { + val previousMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute + ticksLeftInGame-- + val currentMinutes = (ticksLeftInGame + ticksPerMinute - 1) / ticksPerMinute + if (ticksLeftInGame > 0) { + val saraEmpty = saradominPlayers.isEmpty() + val zamEmpty = zamorakPlayers.isEmpty() + if (saraEmpty || zamEmpty) { + endGame() + return + } + } + if (ticksLeftInGame == 0) { + endGame() + } else if (currentMinutes != previousMinutes) { + CastleWarsGameState.updateAllOverlays() + } + CastleWarsGameState.checkFlagCarriers() + CastleWarsGameState.checkDroppedFlagTimeouts() + for (player in saradominPlayers) { + val currentOverlayId = player.interfaceManager.overlay?.id + if (currentOverlayId == null || currentOverlayId != 58) { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58)) + } + // TODO: this... should not be done lmfao CRITICAL TODO: PROPER CW MATCH UI UPDATING THATS NOT SHIT + CastleWarsGameState.updateOverlay(player) + } + for (player in zamorakPlayers) { + val currentOverlayId = player.interfaceManager.overlay?.id + if (currentOverlayId == null || currentOverlayId != 59) { + player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59)) + } + // TODO: this... should not be done lmfao CRITICAL TODO: PROPER CW MATCH UI UPDATING THATS NOT SHIT + CastleWarsGameState.updateOverlay(player) + } + if (ticksLeftInGame % PLAYER_CLEANUP_INTERVAL_TICKS == 0) { + cleanupInactivePlayers() + } + } + + /** + * Remove inactive players from team tracking sets. + */ + 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 + } + 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) + CastleWarsGameState.clearHintIconForPlayer(player) + CastleWarsGameState.restoreAndCleanupPlayer(player) + if (team != null) fillVacancy(team) + } + // Remove truly inactive players (not in grace period) + val inactiveSara = saradominPlayers.filter { player -> + (!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(player) + } + val inactiveZam = zamorakPlayers.filter { player -> + (!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(player) + } + for (player in inactiveSara) { + saradominPlayers.remove(player) + CastleWarsGameState.clearHintIconForPlayer(player) + fillVacancy(CastleWarsTeam.SARADOMIN) + } + for (player in inactiveZam) { + zamorakPlayers.remove(player) + CastleWarsGameState.clearHintIconForPlayer(player) + fillVacancy(CastleWarsTeam.ZAMORAK) + } + // Also cleanup hint icons for any other inactive players + CastleWarsGameState.cleanupInactiveHintIcons() + } + + /** + * Handle death in Castle Wars - respawn at team base instead of normal death + */ + fun death(entity: Entity, killer: Entity?): Boolean { + if (entity !is Player) return false + return handleDeath(entity, killer) + } + + // ==================== PvPZone Implementation ==================== TODO: PvPZone general review... it might be good but need to triple-check + + /** + * Check if attacker can attack victim in this zone. + * Only allows attacks between players on opposite teams. + * Uses self-healing team detection based on equipped cloak to ensure + * players are properly tracked even if they weren't added to sets correctly. + * @param attacker The player initiating the attack + * @param victim The player being attacked + * @return true if players are on opposite teams, false otherwise + */ + override fun canAttackPlayer(attacker: Player, victim: Player): Boolean { + if (ticksLeftInGame <= 0) return false + val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return false + val victimTeam = CastleWarsGameState.getPlayerTeam(victim) ?: return false + return attackerTeam != victimTeam + } + + /** + * Check combat level restrictions. + * Castle Wars has no combat level restrictions. + */ + override fun checkCombatLevel(attacker: Player, victim: Player): Boolean { + return true + } + + /** + * Get the message to display when attack is blocked. + * @return The message string + */ + override fun getBlockedAttackMessage(): String { + return "You can only attack players on the opposite team!" // TODO: pretty sure this is inauthentic. seen the real one in vids, needs correction here (also does this even get called huh) + } + + /** + * Get the message to display when attack is blocked by combat level check. + * @return The message string + */ + override fun getCombatLevelBlockedMessage(): String { + return PvPZone.DEFAULT_COMBAT_LEVEL_MESSAGE + } + } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt index 671564342..f0a306e50 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsRespawnArea.kt @@ -1,12 +1,14 @@ -package rs09.game.content.activity.castlewars.areas +package content.minigame.castlewars.areas +import content.minigame.castlewars.CastleWars +import content.minigame.castlewars.CastleWarsGameState import core.api.TickListener +import core.api.sendMessage import core.game.node.entity.Entity import core.game.node.entity.player.Player import core.game.node.item.Item import core.game.world.map.Location import core.game.world.map.zone.ZoneBorders -import rs09.game.content.activity.castlewars.CastleWars /** * Handles the Castle Wars respawn rooms @@ -14,8 +16,10 @@ import rs09.game.content.activity.castlewars.CastleWars class CastleWarsRespawnArea : CastleWarsArea(), TickListener { companion object { - val zamorakRespawnRoom: ZoneBorders = ZoneBorders(Location.create(2376, 3127, 1), Location.create(2368, 3135, 1)) - val saradominRespawnRoom: ZoneBorders = ZoneBorders(Location.create(2423, 3080, 1), Location.create(2431, 3072, 1)) + val zamorakRespawnRoom: ZoneBorders = + ZoneBorders(Location.create(2376, 3127, 1), Location.create(2368, 3135, 1)) + val saradominRespawnRoom: ZoneBorders = + ZoneBorders(Location.create(2423, 3080, 1), Location.create(2431, 3072, 1)) val zamorakPlayersInRespawnRoom = mutableSetOf() val saradominPlayersInRespawnRoom = mutableSetOf() @@ -27,6 +31,14 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return + if (CastleWarsGameState.isCarryingAnyFlag(player)) { + if (CastleWarsGameState.isCarryingFlag(player)) { + CastleWarsGameState.dropFlag(player) + } else if (CastleWarsGameState.isCarryingOwnFlag(player)) { + CastleWarsGameState.dropOwnFlag(player) + } + sendMessage(player, "You cannot take the flag into your respawn room!") + } if (saradominRespawnRoom.insideBorder(player.location)) { player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1) } else if (zamorakRespawnRoom.insideBorder(player.location)) { @@ -37,4 +49,11 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener { override fun tick() { } + override fun exitArea(player: Player) { + super.exitArea(player) + // Remove player from respawn room tracking + saradominPlayersInRespawnRoom.remove(player) + zamorakPlayersInRespawnRoom.remove(player) + } + } diff --git a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt index 3bc53ddd2..eb8f2a93e 100644 --- a/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt +++ b/Server/src/main/content/minigame/castlewars/areas/CastleWarsWaitingArea.kt @@ -1,15 +1,17 @@ -package rs09.game.content.activity.castlewars.areas +package content.minigame.castlewars.areas -import CastleWarsOverlay +import content.minigame.castlewars.CastleWars +import content.minigame.castlewars.CastleWarsConstants +import content.minigame.castlewars.CastleWarsOverlay import core.api.* 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 -import rs09.game.content.activity.castlewars.CastleWars /** * Handles the Castle Wars "waiting room" @@ -24,6 +26,8 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { val waitingSaradominPlayers = mutableSetOf() val waitingZamorakPlayers = mutableSetOf() + internal var gameStartCountdown = -1 + var postGameCooldown = false } override fun defineAreaBorders(): Array { @@ -33,7 +37,10 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { override fun areaEnter(entity: Entity) { val player = entity as? Player ?: return super.areaEnter(player) - registerTimer(player, spawnTimer("teleblock", (CastleWars.gameCooldownMinutes + CastleWars.gameTimeMinutes)*60*2)) + registerTimer( + player, + spawnTimer("teleblock", (CastleWars.gameCooldownMinutes + CastleWars.gameTimeMinutes) * 60 * 2) + ) // Set team attribute and equip the hooded cloak on the entity based on which waiting room they're in if (zamorakWaitingRoom.insideBorder(player.location)) { @@ -46,32 +53,42 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { var transformed = false if (player.attributes[CastleWars.portalAttribute] == CastleWars.guthixName - && (hasGodItem(player, God.ZAMORAK) || hasGodItem(player, God.SARADOMIN))) { + && (hasGodItem(player, God.ZAMORAK) || hasGodItem(player, God.SARADOMIN)) + ) { // https://www.youtube.com/watch?v=Vg3Om-jZNIY // Turn the player into a sheep player.appearance.transformNPC(CastleWars.sheep) // Send the player a chat dialog - sendDialogue(player, "I pity your faith in my brothers. I shall bless you with some time in the most holy of forms; maybe its wisdom will rub off on you and you'll see the error of your ways.") + sendDialogue( + player, + "I pity your faith in my brothers. I shall bless you with some time in the most holy of forms; maybe its wisdom will rub off on you and you'll see the error of your ways." + ) transformed = true } if (player.attributes[CastleWars.portalAttribute] == CastleWars.zamorakName - && hasGodItem(player, God.SARADOMIN) || hasGodItem(player, God.GUTHIX)) - { + && (hasGodItem(player, God.SARADOMIN) || hasGodItem(player, God.GUTHIX)) + ) { // https://youtu.be/hmF-CU9OEY4?t=63 // Turn the player into an imp! player.appearance.transformNPC(CastleWars.imp) // Send the player a chat dialog - sendDialogue(player, "You're wearing objects of my ignorant brothers and you come to me? Such treachery must be rewarded! Enjoy some time in the most mischievous of forms.") + sendDialogue( + player, + "You're wearing objects of my ignorant brothers and you come to me? Such treachery must be rewarded! Enjoy some time in the most mischievous of forms." + ) transformed = true } if (player.attributes[CastleWars.portalAttribute] == CastleWars.saradominName - && hasGodItem(player, God.ZAMORAK) || hasGodItem(player, God.GUTHIX)) - { + && (hasGodItem(player, God.ZAMORAK) || hasGodItem(player, God.GUTHIX)) + ) { // https://www.youtube.com/watch?v=Vg3Om-jZNIY // Turn the player into a rabbit! player.appearance.transformNPC(CastleWars.rabbit) // Send the player a chat dialog - sendDialogue(player, "You wear objects of my foolish brothers? Perhaps some time spent as the lowliest of forms will help you appreciate the gifts that I can bestow upon my followers.") + sendDialogue( + player, + "You wear objects of my foolish brothers? Perhaps some time spent as the lowliest of forms will help you appreciate the gifts that I can bestow upon my followers." + ) transformed = true } @@ -101,26 +118,38 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener { // The game will start when there's enough players and CastleWarsGameArea.ticksLeftInGame = -5 minutes 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.isEmpty() || waitingZamorakPlayers.isEmpty()) { - // A game is not going and there aren't enough players - Keep the game over time to the current value + } 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 + } 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 + } + nextStart = gameStartCountdown + gameStartCountdown-- } - else { - // There are enough players - Set nextStart to gameCooldownMinutes + ticksLeftInGame - nextStart = CastleWars.gameCooldownMinutes * ticksPerMinute + CastleWarsGameArea.ticksLeftInGame - } - + val minPlayers = if (GameWorld.settings?.isDevMode == true) 1 else CastleWarsConstants.MIN_PLAYERS_PER_TEAM for (player in waitingSaradominPlayers + waitingZamorakPlayers) { CastleWarsOverlay.sendLobbyUpdate( player, - waitingSaradominPlayers.isNotEmpty() && waitingZamorakPlayers.isNotEmpty() || 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 ) } - if (nextStart <= 0) { + gameStartCountdown = -1 // Reset countdown CastleWarsGameArea.startGame() } }