Big fix batch, return to non-MobyDick2

This commit is contained in:
dam 2026-07-23 14:28:52 +03:00
parent 5b4545ea4b
commit 4ba22ba1a7
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
32 changed files with 504 additions and 388 deletions

View file

@ -34820,6 +34820,7 @@
"defence_anim": "420",
"durability": null,
"equipment_slot": "3",
"unequip_without_inventory_space": "true",
"render_anim": "131",
"run_anim": "1427",
"stand_anim": "1421",
@ -34850,6 +34851,7 @@
"defence_anim": "420",
"durability": null,
"equipment_slot": "3",
"unequip_without_inventory_space": "true",
"render_anim": "131",
"run_anim": "1427",
"stand_anim": "1421",

View file

@ -2178,6 +2178,15 @@
"title": "Castle Wars Ticket Exchange",
"stock": "{4068,1,100}-{4069,1,100}-{4070,1,100}-{4071,1,100}-{4072,1,100}-{4503,1,100}-{4504,1,100}-{4505,1,100}-{4506,1,100}-{4507,1,100}-{4508,1,100}-{4509,1,100}-{4510,1,100}-{4511,1,100}-{4512,1,100}-{4513,1,100}-{4514,1,100}-{4515,1,100}-{4516,1,100}"
},
{
"npcs": "5111",
"high_alch": "0",
"currency": "995",
"general_store": "false",
"id": "256",
"title": "Leon's Prototype Crossbow",
"stock": "{10156,2,100}"
},
{
"npcs": "2039",
"high_alch": "0",
@ -2187,4 +2196,4 @@
"title": "Uglug's Stuffsies",
"stock": "{4844,100,10}-{10927,0,100}-{2862,100,100}-{1777,10,200}-{2876,0,500}-{2878,10,100}-{4850,0,100}-{946,5,100}-{4773,0,1000}-{4778,0,1000}-{4783,0,1000}-{4788,0,1500}-{4793,0,2000}-{4798,0,3000}-{4803,0,4000}-{4827,0,2000}"
}
]
]

View file

@ -11,7 +11,7 @@ import org.rs09.consts.NPCs
* Custom NPC class for Castle Wars barricades.
*
* - Block all entity movement through them (like Pest Control Brawlers)
* - Never retaliate when attacked (this was scary man they squared up)
* - Never retaliate when attacked
* - Never turn to face attackers or any direction
* - Cannot attack or move
*/
@ -46,9 +46,7 @@ class CastleWarsBarricadeNPC : AbstractNPC {
return false
}
override fun attack(node: Node?) {
return
}
override fun attack(node: Node?) {}
override fun isPoisonImmune(): Boolean {
return true

View file

@ -39,7 +39,7 @@ class CastleWarsCatapultInterface : InterfaceListener {
}
private fun handleButtonClick(player: Player, buttonId: Int): Boolean {
val team = getPlayerTeam(player) ?: return closeAndReturn(player, false)
val team = CastleWarsGameState.getActiveGameTeam(player) ?: return closeAndReturn(player, true)
val currentCoord = getCurrentCoordinate(player)
return when (val button = CatapultButton.fromId(buttonId)) {
@ -89,6 +89,10 @@ class CastleWarsCatapultInterface : InterfaceListener {
catapultTeam: CastleWarsTeam,
catapultLocation: Location
): Boolean {
if (!CastleWarsGameState.isGameActive || CastleWarsGameState.getActiveGameTeam(player) != playerTeam) {
sendMessage(player, "The Castle Wars game is no longer active.")
return false
}
if (playerTeam != catapultTeam) {
sendMessage(player, "You can't use the enemy's catapult!")
return false
@ -99,6 +103,10 @@ class CastleWarsCatapultInterface : InterfaceListener {
sendMessage(player, "The catapult is no longer there.")
return false
}
if (player.location.z != catapultLocation.z || player.location.getDistance(catapultLocation) > 2.0) {
sendMessage(player, "You have moved too far away from the catapult.")
return false
}
if (currentCatapult.id in CastleWars.damagedCatapultIds || !CastleWarsGameState.isCatapultOperational(
catapultTeam
)
@ -143,10 +151,6 @@ class CastleWarsCatapultInterface : InterfaceListener {
renderer.renderCoordinate(player, getCurrentCoordinate(player))
}
private fun getPlayerTeam(player: Player): CastleWarsTeam? {
return CastleWarsGameState.getPlayerTeam(player)
}
private fun getCatapultLocation(player: Player): Location? {
return getAttribute(player, ATTR_LOCATION, null)
}

View file

@ -25,7 +25,7 @@ class CastleWarsDoorAttackPulse(
private var ticksUntilNextHit = 0
override fun pulse(): Boolean {
if (!player.isActive || CastleWarsGameState.isDoorDestroyed(doorTeam) || CastleWarsGameState.getPlayerTeam(
if (!player.isActive || CastleWarsGameState.isDoorDestroyed(doorTeam) || CastleWarsGameState.getActiveGameTeam(
player
) == null
) {

View file

@ -16,8 +16,15 @@ object CastleWarsEquipmentRestrictions {
CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak
)
private val castleWarsBorders by lazy {
CastleWarsGameArea.areaBorders.toList() + CastleWarsWaitingArea.areaBorders.toList() + listOf(
CastleWarsRespawnArea.saradominRespawnRoom,
CastleWarsRespawnArea.zamorakRespawnRoom
)
}
fun blocksEquip(player: Player, equipmentSlot: Int): Boolean {
if (equipmentSlot !in restrictedSlots || !isInCastleWarsArea(player)) {
if (equipmentSlot !in restrictedSlots) {
return false
}
@ -34,6 +41,10 @@ object CastleWarsEquipmentRestrictions {
}
}
fun isInCastleWarsArea(player: Player): Boolean {
return castleWarsBorders.any { it.insideBorder(player.location) }
}
private fun removeEquipment(player: Player, slot: Int) {
val item = player.equipment[slot] ?: return
player.equipment.remove(item)
@ -41,13 +52,4 @@ object CastleWarsEquipmentRestrictions {
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

@ -43,7 +43,7 @@ class CastleWarsFlagHandler : InteractionListener {
sendMessage(player, "There is no game in progress.")
return@on true
}
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
sendMessage(player, "You are not in a Castle Wars game.")
return@on true
@ -129,7 +129,7 @@ class CastleWarsFlagHandler : InteractionListener {
sendMessage(player, "There is no game in progress.")
return@on true
}
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
sendMessage(player, "You are not in a Castle Wars game.")
return@on true
@ -149,11 +149,9 @@ class CastleWarsFlagHandler : InteractionListener {
return@on true
}
if (flagTeam == playerTeam) {
if (CastleWarsGameState.pickUpOwnFlag(player, flagTeam)) { // Message already sent by pickUpOwnFlag
}
CastleWarsGameState.pickUpOwnFlag(player, flagTeam)
} else {
if (CastleWarsGameState.pickUpFlag(player, flagTeam)) { // Message already sent by pickUpFlag
}
CastleWarsGameState.pickUpFlag(player, flagTeam)
}
return@on true

View file

@ -19,7 +19,6 @@ import core.game.node.scenery.SceneryBuilder
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 org.rs09.consts.Items
/**
@ -64,7 +63,9 @@ enum class BoulderState {
}
data class ActiveClimbingRope(
val rope: Scenery, val originalWall: Scenery
val rope: Scenery,
val originalWall: Scenery,
val ropeWall: Scenery
)
/**
@ -275,12 +276,12 @@ object CastleWarsGameState {
resetBattlefield()
}
fun registerClimbingRope(rope: Scenery, originalWall: Scenery) {
fun registerClimbingRope(rope: Scenery, originalWall: Scenery, ropeWall: Scenery) {
val key = Location.create(rope.location)
rope.isActive = true
rope.isRenderable = true
activeClimbingRopes[key] = ActiveClimbingRope(rope, Scenery(originalWall))
activeClimbingRopes[key] = ActiveClimbingRope(rope, Scenery(originalWall), Scenery(ropeWall))
SceneryBuilder.add(rope)
}
@ -288,18 +289,21 @@ object CastleWarsGameState {
return activeClimbingRopes[Location.create(location)]?.rope?.takeIf { it.isActive }
}
fun removeClimbingRope(location: Location, sendUpdate: Boolean = true): Boolean {
val activeRope = activeClimbingRopes.remove(Location.create(location)) ?: return false
fun removeClimbingRope(rope: Scenery): Boolean {
val key = Location.create(rope.location)
val activeRope = activeClimbingRopes[key] ?: return false
if (activeRope.rope !== rope) return false
activeClimbingRopes.remove(key)
activeRope.rope.isActive = false
SceneryBuilder.replace(activeRope.rope, activeRope.originalWall)
SceneryBuilder.remove(activeRope.rope)
SceneryBuilder.replace(activeRope.ropeWall, activeRope.originalWall, false, false)
return true
}
fun clearClimbingRopes(sendUpdate: Boolean = true) {
val ropeLocations = activeClimbingRopes.keys.map { Location.create(it) }
for (location in ropeLocations) {
removeClimbingRope(location, sendUpdate)
fun clearClimbingRopes() {
val ropes = activeClimbingRopes.values.map { it.rope }
for (rope in ropes) {
removeClimbingRope(rope)
}
}
@ -385,33 +389,9 @@ object CastleWarsGameState {
CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder(location)
}
/**
* Get the team a player is on.
* Includes a self-healing mechanism if the player is in the area but not in the sets.
*/
fun getPlayerTeam(player: Player): CastleWarsTeam? {
fun getActiveGameTeam(player: Player): CastleWarsTeam? {
if (CastleWarsGameArea.saradominPlayers.contains(player)) return CastleWarsTeam.SARADOMIN
if (CastleWarsGameArea.zamorakPlayers.contains(player)) return CastleWarsTeam.ZAMORAK
if (CastleWarsWaitingArea.waitingSaradominPlayers.contains(player)) return CastleWarsTeam.SARADOMIN
if (CastleWarsWaitingArea.waitingZamorakPlayers.contains(player)) return CastleWarsTeam.ZAMORAK
if (isGameActive) {
val inCWArea =
CastleWarsGameArea.areaBorders.any { it.insideBorder(player.location) } || CastleWarsRespawnArea.saradominRespawnRoom.insideBorder(
player.location
) || CastleWarsRespawnArea.zamorakRespawnRoom.insideBorder(
player.location
)
if (inCWArea) {
val capeId = player.equipment.get(EquipmentContainer.SLOT_CAPE)?.id ?: -1
if (capeId == CastleWars.saradominTeamHoodedCloak) {
CastleWarsGameArea.saradominPlayers.add(player)
return CastleWarsTeam.SARADOMIN
} else if (capeId == CastleWars.zamorakTeamHoodedCloak) {
CastleWarsGameArea.zamorakPlayers.add(player)
return CastleWarsTeam.ZAMORAK
}
}
}
return null
}
@ -422,7 +402,7 @@ object CastleWarsGameState {
*/
fun checkFlagCarriers() {
saradominFlagCarrier?.let { player ->
if (!player.isActive || getPlayerTeam(player) == null) {
if (!player.isActive || getActiveGameTeam(player) == null) {
dropFlag(player)
} else {
val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON)
@ -432,7 +412,7 @@ object CastleWarsGameState {
}
}
zamorakFlagCarrier?.let { player ->
if (!player.isActive || getPlayerTeam(player) == null) {
if (!player.isActive || getActiveGameTeam(player) == null) {
dropFlag(player)
} else {
val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON)
@ -442,7 +422,7 @@ object CastleWarsGameState {
}
}
saradominOwnFlagCarrier?.let { player ->
if (!player.isActive || getPlayerTeam(player) == null) {
if (!player.isActive || getActiveGameTeam(player) == null) {
dropOwnFlag(player)
} else {
val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON)
@ -454,7 +434,7 @@ object CastleWarsGameState {
}
}
zamorakOwnFlagCarrier?.let { player ->
if (!player.isActive || getPlayerTeam(player) == null) {
if (!player.isActive || getActiveGameTeam(player) == null) {
dropOwnFlag(player)
} else {
val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON)
@ -465,7 +445,6 @@ object CastleWarsGameState {
}
}
}
updateAllHintIcons()
}
/**
@ -539,7 +518,7 @@ object CastleWarsGameState {
* @return true if the flag was successfully picked up
*/
fun pickUpFlag(player: Player, flagTeam: CastleWarsTeam): Boolean {
val playerTeam = getPlayerTeam(player) ?: return false
val playerTeam = getActiveGameTeam(player) ?: return false
if (playerTeam == flagTeam) {
sendMessage(player, "You can't pick up your own team's flag!")
return false
@ -572,7 +551,7 @@ object CastleWarsGameState {
if (currentWeapon != null) {
if (!player.inventory.add(currentWeapon)) {
GroundItemManager.create(currentWeapon, player.location, player)
sendMessage(player, "Your weapon was dropped because your inventory is full.") // TODO: nah?
sendMessage(player, "Your weapon was dropped because your inventory is full.")
} else {
sendMessage(player, "Your weapon has been unequipped.")
}
@ -582,7 +561,7 @@ object CastleWarsGameState {
player.equipment.remove(shield)
if (!player.inventory.add(shield)) {
GroundItemManager.create(shield, player.location, player)
sendMessage(player, "Your shield was dropped because your inventory is full.") // TODO: nah?
sendMessage(player, "Your shield was dropped because your inventory is full.")
} else {
sendMessage(player, "Your shield has been unequipped.")
}
@ -776,7 +755,7 @@ object CastleWarsGameState {
* @return true if the flag was successfully picked up
*/
fun pickUpOwnFlag(player: Player, flagTeam: CastleWarsTeam): Boolean {
val playerTeam = getPlayerTeam(player) ?: return false // Must be your own team's flag
val playerTeam = getActiveGameTeam(player) ?: return false // Must be your own team's flag
if (playerTeam != flagTeam) {
sendMessage(player, "That's not your team's flag!")
return false
@ -842,7 +821,7 @@ object CastleWarsGameState {
* A grace period prevents instant return if the flag is picked up while already inside the castle.
*/
private fun checkOwnFlagReturn(player: Player) {
val playerTeam = getPlayerTeam(player) ?: return
val playerTeam = getActiveGameTeam(player) ?: return
val carriedFlag = getOwnCarriedFlag(player) ?: return
if (carriedFlag != playerTeam) return
val pickupTick = when (playerTeam) {
@ -924,7 +903,7 @@ object CastleWarsGameState {
* @return true if the flag was successfully captured
*/
fun captureFlag(player: Player): Boolean {
val playerTeam = getPlayerTeam(player) ?: return false
val playerTeam = getActiveGameTeam(player) ?: return false
val carriedFlag = getCarriedFlag(player) ?: return false
if (carriedFlag == playerTeam) {
return false
@ -957,6 +936,7 @@ object CastleWarsGameState {
announceToAllPlayers("${playerTeam.teamName} has captured the ${carriedFlag.teamName} flag!")
announceToAllPlayers("Score: Saradomin $saradominScore - $zamorakScore Zamorak")
updateAllOverlays()
updateAllHintIcons()
return true
}
@ -1034,7 +1014,7 @@ object CastleWarsGameState {
* Update the hint icon for a single player
*/
private fun updateHintIconForPlayer(player: Player) {
val playerTeam = getPlayerTeam(player) ?: return
val playerTeam = getActiveGameTeam(player) ?: return
val enemyTeam = playerTeam.opposite()
playerHintIconSlots[player]?.let { slot ->
HintIconManager.removeHintIcon(player, slot)
@ -1046,8 +1026,7 @@ object CastleWarsGameState {
val carrier = if (enemyTeam == CastleWarsTeam.SARADOMIN) saradominFlagCarrier ?: saradominOwnFlagCarrier
else zamorakFlagCarrier ?: zamorakOwnFlagCarrier
carrier?.let {
if (it == player) {
} else {
if (it != player) {
val slot = HintIconManager.registerHintIcon(player, it)
if (slot >= 0) {
playerHintIconSlots[player] = slot
@ -1123,7 +1102,7 @@ object CastleWarsGameState {
fun endGame(extraRewardRecipients: Map<Player, CastleWarsTeam> = emptyMap()) {
val rewardRecipients = linkedMapOf<Player, CastleWarsTeam>()
for (player in getAllGamePlayers()) {
val team = getPlayerTeam(player) ?: continue
val team = getActiveGameTeam(player) ?: continue
rewardRecipients[player] = team
}
for ((player, team) in extraRewardRecipients) {
@ -1190,6 +1169,7 @@ object CastleWarsGameState {
* @param damage Amount of damage (0-100)
*/
fun damageDoor(team: CastleWarsTeam, damage: Int) {
require(damage >= 0) { "Door damage cannot be negative" }
val previousHealth = getDoorHealth(team)
when (team) {
CastleWarsTeam.SARADOMIN -> {
@ -1252,6 +1232,7 @@ object CastleWarsGameState {
* @param repair Amount to repair (0-100)
*/
fun repairDoor(team: CastleWarsTeam, repair: Int) {
require(repair >= 0) { "Door repair amount cannot be negative" }
when (team) {
CastleWarsTeam.SARADOMIN -> {
saradominDoorHealth = (saradominDoorHealth + repair).coerceAtMost(100)
@ -1314,7 +1295,7 @@ object CastleWarsGameState {
"saradomin2" -> saradominBoulder2State
"zamorak1" -> zamorakBoulder1State
"zamorak2" -> zamorakBoulder2State
else -> BoulderState.COLLAPSED
else -> throw IllegalArgumentException("Unknown Castle Wars boulder key: $boulderKey")
}
}
@ -1327,6 +1308,7 @@ object CastleWarsGameState {
"saradomin2" -> saradominBoulder2State = state
"zamorak1" -> zamorakBoulder1State = state
"zamorak2" -> zamorakBoulder2State = state
else -> throw IllegalArgumentException("Unknown Castle Wars boulder key: $boulderKey")
}
}
@ -1561,7 +1543,7 @@ object CastleWarsGameState {
player, CASTLE_WARS_TIMER_VARP, remainingMinutes
)
// Boulder varbits: 0 = collapsed/blocked (green/secure), 1 = cleared (red/open)
val team = getPlayerTeam(player) ?: return
val team = getActiveGameTeam(player) ?: return
when (team) {
CastleWarsTeam.SARADOMIN -> {
setVarbit(player, SARA_DOOR_HEALTH_VARBIT, saradominDoorHealth)
@ -1581,48 +1563,11 @@ object CastleWarsGameState {
}
}
/**
* Get all players currently in the Castle Wars game area zones.
* Uses RegionManager to find all players in the zone borders,
* ensuring we reach players even if they weren't properly added to the tracked sets.
* @return Set of all players in the game area who have a valid team
*/
/** Get all connected players tracked as active participants. */
private fun getAllGamePlayers(): Set<Player> {
val players = mutableSetOf<Player>()
players.addAll((CastleWarsGameArea.saradominPlayers + CastleWarsGameArea.zamorakPlayers).filter {
return (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
val plane = border.plane
val radius = maxOf(
kotlin.math.abs(border.northEastX - border.southWestX),
kotlin.math.abs(border.northEastY - border.southWestY)
) / 2 + 10 // Add buffer
val centerLocation = Location.create(centerX, centerY, plane)
for (player in RegionManager.getLocalPlayers(
centerLocation, radius
)) {
if (border.insideBorder(player.location) && getPlayerTeam(player) != null) {
players.add(player)
}
}
}
val saraRespawn = CastleWarsRespawnArea.saradominRespawnRoom
val zamRespawn = CastleWarsRespawnArea.zamorakRespawnRoom
for (respawnBorder in arrayOf(saraRespawn, zamRespawn)) {
val centerX = (respawnBorder.southWestX + respawnBorder.northEastX) / 2
val centerY = (respawnBorder.southWestY + respawnBorder.northEastY) / 2
val plane = respawnBorder.plane
val centerLocation = Location.create(centerX, centerY, plane)
for (player in RegionManager.getLocalPlayers(centerLocation, 20)) {
if (respawnBorder.insideBorder(player.location) && getPlayerTeam(player) != null) {
players.add(player)
}
}
}
return players
}.toSet()
}
/**
@ -1631,7 +1576,7 @@ object CastleWarsGameState {
* @return Set of all players on the specified team
*/
private fun getTeamPlayers(team: CastleWarsTeam): Set<Player> {
return getAllGamePlayers().filter { getPlayerTeam(it) == team }.toSet()
return getAllGamePlayers().filter { getActiveGameTeam(it) == team }.toSet()
}
/**
@ -1743,8 +1688,8 @@ object CastleWarsGameState {
sendMessage(player, "You can't steal the flag from yourself.")
return false
}
val playerTeam = getPlayerTeam(player)
val carrierTeam = getPlayerTeam(carrier)
val playerTeam = getActiveGameTeam(player)
val carrierTeam = getActiveGameTeam(carrier)
if (playerTeam == null || carrierTeam == null) {
sendMessage(player, "You can only steal flags in Castle Wars.")
return false

View file

@ -8,50 +8,24 @@ import core.game.node.entity.impl.PulseManager
import core.game.node.entity.player.Player
import core.game.system.timer.impl.Poison
import core.game.world.map.Location
import core.game.world.map.RegionManager.getLocalNPCs
import core.game.world.map.path.Pathfinder
import core.game.world.map.zone.*
/**
* MapZone for Castle Wars game area that handles death and respawn.
* This is registered in addition to the MapArea to provide death handling,
* since MapArea doesn't support overriding the death method.
* This is registered in addition to the MapArea to provide death handling.
*/
class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDOM_EVENTS), StartupListener {
companion object {
private val saradominStandardFloor = ZoneBorders(Location.create(2426, 3073, 3), Location.create(2430, 3077, 3))
private val zamorakStandardFloor = ZoneBorders(Location.create(2373, 3134, 3), Location.create(2369, 3130, 3))
private val saradominUpperFloor = ZoneBorders(Location.create(2431, 3080, 2), Location.create(2423, 3072, 2))
private val zamorakUpperFloor = ZoneBorders(Location.create(2368, 3127, 2), Location.create(2376, 3135, 2))
private val saradominFloor = ZoneBorders(Location.create(2420, 3072, 1), Location.create(2431, 3083, 1))
private val zamorakFloor = ZoneBorders(Location.create(2379, 3135, 1), Location.create(2368, 3124, 1))
private val battleField = ZoneBorders(Location.create(2368, 3135, 0), Location.create(2431, 3072, 0))
private val saradominTunnels = ZoneBorders(Location.create(2430, 9481, 0), Location.create(2385, 9504, 0))
private val zamorakTunnels = ZoneBorders(Location.create(2401, 9503, 0), Location.create(2366, 9529, 0))
private val centerTunnels = ZoneBorders(Location.create(2396, 9493, 0), Location.create(2405, 9515, 0))
val allBorders = arrayOf(
saradominStandardFloor,
zamorakStandardFloor,
saradominUpperFloor,
zamorakUpperFloor,
saradominFloor,
zamorakFloor,
battleField,
saradominTunnels,
zamorakTunnels,
centerTunnels
)
/**
* Handle Castle Wars death with poison preservation.
* Shared between CastleWarsGameZone.death() and CastleWarsGameArea.handleDeath().
* In Castle Wars, poison persists through death unlike normal safe deaths.
*/
fun handleCastleWarsDeath(player: Player): Boolean {
if (CastleWarsGameArea.ticksLeftInGame <= 0) return false
val team = CastleWarsGameState.getPlayerTeam(player) ?: return false
val team = CastleWarsGameState.getActiveGameTeam(player) ?: return false
if (CastleWarsGameState.isCarryingFlag(player)) {
CastleWarsGameState.dropFlag(player)
@ -83,7 +57,7 @@ class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDO
override fun startup() {
ZoneBuilder.configure(this)
for (border in allBorders) {
for (border in CastleWarsGameArea.areaBorders) {
register(border)
}
}
@ -103,7 +77,7 @@ class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDO
val isMovingUp = to.z > from.z
val isChangingZLevel = to.z != from.z
for (n in getLocalNPCs(e.location, 5)) {
for (n in findLocalNPCs(e.location, 5)) {
if (n.isInvisible || n === e) {
continue
}

View file

@ -8,7 +8,6 @@ import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_MAX_TIC
import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_PULSE_INTERVAL
import content.minigame.castlewars.CastleWarsConstants.EXPLOSIVE_POTION_SELF_DAMAGE
import content.minigame.castlewars.CastleWarsConstants.MAX_BARRICADES_PER_TEAM
import content.minigame.castlewars.areas.CastleWarsGameArea
import core.api.*
import core.game.interaction.Clocks
import core.game.interaction.IntType
@ -63,11 +62,10 @@ class CastleWarsItemHandler : InteractionListener {
}
fun dealExplosiveAoeDamage(attacker: Player, location: Location, radius: Int = 1) {
val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return
val attackerTeam = CastleWarsGameState.getActiveGameTeam(attacker) ?: return
RegionManager.getLocalPlayers(location, radius).filter { target ->
target != attacker && target.isActive && CastleWarsGameState.getPlayerTeam(target) != null && CastleWarsGameState.getPlayerTeam(
target
) != attackerTeam
val targetTeam = CastleWarsGameState.getActiveGameTeam(target)
target != attacker && target.isActive && targetTeam != null && targetTeam != attackerTeam
}.forEach { target ->
val damage = CastleWarsConstants.EXPLOSIVE_POTION_SELF_DAMAGE.coerceAtMost(target.skills.lifepoints)
target.impactHandler.manualHit(
@ -100,7 +98,7 @@ class CastleWarsItemHandler : InteractionListener {
return@on false
}
val team = CastleWarsGameState.getPlayerTeam(player)
val team = CastleWarsGameState.getActiveGameTeam(player)
if (team == null) {
sendMessage(player, "You're not in a Castle Wars game.")
return@on false
@ -143,7 +141,7 @@ class CastleWarsItemHandler : InteractionListener {
) { player, used, with ->
val barricade = with as? NPC ?: return@onUseWith false
if (!CastleWarsGameState.isGameActive) {
if (!isActiveGamePlayer(player)) {
return@onUseWith false
}
@ -170,7 +168,7 @@ class CastleWarsItemHandler : InteractionListener {
}
on(CastleWars.cwExplosivePotion, IntType.ITEM, "drop") { player, node ->
if (!CastleWarsGameState.isGameActive) {
if (!isActiveGamePlayer(player)) {
return@on false
}
@ -205,7 +203,7 @@ class CastleWarsItemHandler : InteractionListener {
) { player, _, with ->
val barricade = with as? NPC ?: return@onUseWith false
if (!CastleWarsGameState.isGameActive) {
if (!isActiveGamePlayer(player)) {
return@onUseWith false
}
@ -225,7 +223,9 @@ class CastleWarsItemHandler : InteractionListener {
val firePulse = object : core.game.system.task.Pulse(BURNING_BARRICADE_PULSE_INTERVAL) {
var tickCount = 0
override fun pulse(): Boolean {
if (!barricade.isActive || barricade.id != NPCs.BARRICADE_1533 || tickCount >= BURNING_BARRICADE_MAX_TICKS) {
if (!CastleWarsGameState.isGameActive || !barricade.isActive ||
barricade.id != NPCs.BARRICADE_1533 || tickCount >= BURNING_BARRICADE_MAX_TICKS
) {
burningBarricadePulses.remove(barricade)
if (barricade.isActive && barricade.id == NPCs.BARRICADE_1533) {
barricade.skills.lifepoints = 0
@ -249,7 +249,7 @@ class CastleWarsItemHandler : InteractionListener {
onUseWith(IntType.NPC, Items.BUCKET_OF_WATER_1929, NPCs.BARRICADE_1533) { player, used, with ->
val barricade = with as? NPC ?: return@onUseWith false
if (!CastleWarsGameState.isGameActive) {
if (!isActiveGamePlayer(player)) {
return@onUseWith false
}
@ -276,8 +276,8 @@ class CastleWarsItemHandler : InteractionListener {
sendMessage(user, "You can only use bandages during a Castle Wars game.")
return false
}
val userTeam = getActiveGameTeam(user)
val targetTeam = getActiveGameTeam(target)
val userTeam = CastleWarsGameState.getActiveGameTeam(user)
val targetTeam = CastleWarsGameState.getActiveGameTeam(target)
if (userTeam == null || targetTeam == null) {
sendMessage(user, "You can only use bandages during a Castle Wars game.")
return false
@ -328,12 +328,8 @@ class CastleWarsItemHandler : InteractionListener {
return true
}
private fun getActiveGameTeam(player: Player): CastleWarsTeam? {
return when {
CastleWarsGameArea.saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN
CastleWarsGameArea.zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK
else -> null
}
private fun isActiveGamePlayer(player: Player): Boolean {
return CastleWarsGameState.isGameActive && CastleWarsGameState.getActiveGameTeam(player) != null
}
private fun getLocalBarricades(location: Location, radius: Int): List<NPC> {
@ -341,6 +337,6 @@ class CastleWarsItemHandler : InteractionListener {
NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535
)
return RegionManager.getLocalNPCs(location, radius).filter { it.id in barricadeIds && it.isActive }
return findLocalNPCs(location, radius).filter { it.id in barricadeIds && it.isActive }
}
}

View file

@ -25,7 +25,6 @@ import core.game.world.GameWorld
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.update.flag.chunk.ObjectUpdateFlag
import core.game.world.update.flag.context.Animation
import core.tools.Log
import core.tools.RandomFunction
@ -246,20 +245,20 @@ class CastleWarsListeners : InteractionListener {
onUseWith(
SCENERY, CastleWars.cwClimbingRope, *CastleWars.cwCastleBattlementsMap.keys.toIntArray()
) { player, rope, wall ->
if (!requireActiveGamePlayer(player)) return@onUseWith false
if (CastleWarsGameState.getClimbingRope(wall.location) != null) {
sendDialogue(player, "There is already a rope tied to the battlements.")
return@onUseWith true
}
removeItem(player, rope)
val wallScenery = wall.asScenery()
val ropeWall = wallScenery.transform(CastleWars.cwCastleBattlementsMap.getValue(wall.id))
SceneryBuilder.replace(wallScenery, ropeWall, CastleWars.ropeAliveTicks, false)
SceneryBuilder.replace(wallScenery, ropeWall, false, false)
val hangingRope = Scenery(CastleWars.cwCastleClimbingRope, wall.location, 4, wall.direction.toInteger())
CastleWarsGameState.registerClimbingRope(hangingRope, wallScenery)
RegionManager.getRegionChunk(hangingRope.location).flag(ObjectUpdateFlag(hangingRope, false))
CastleWarsGameState.registerClimbingRope(hangingRope, wallScenery, ropeWall)
removeItem(player, rope)
GameWorld.Pulser.submit(object : Pulse(CastleWars.ropeAliveTicks) {
override fun pulse(): Boolean {
CastleWarsGameState.removeClimbingRope(hangingRope.location)
CastleWarsGameState.removeClimbingRope(hangingRope)
return true
}
})
@ -301,7 +300,7 @@ class CastleWarsListeners : InteractionListener {
}
on(CastleWars.saradominEnergyBarrier, IntType.SCENERY, "Pass") { player, barrier ->
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam != CastleWarsTeam.SARADOMIN) {
sendMessage(player, "Only the Saradomin team may pass through this barrier.")
return@on true
@ -330,7 +329,7 @@ class CastleWarsListeners : InteractionListener {
return@on true
}
on(CastleWars.zamorakEnergyBarrier, IntType.SCENERY, "Pass") { player, barrier ->
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam != CastleWarsTeam.ZAMORAK) {
sendMessage(player, "Only the Zamorak team may pass through this barrier.")
return@on true
@ -370,8 +369,8 @@ class CastleWarsListeners : InteractionListener {
on(CastleWars.largeDoorIds, IntType.SCENERY, "Open") { player, node ->
val door = node.asScenery()
val doorTeam = getDoorTeam(door.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val doorTeam = getCastleTeam(door.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
// Not in Castle Wars game - shouldn't happen in game area
@ -404,8 +403,8 @@ class CastleWarsListeners : InteractionListener {
on(CastleWars.largeDoorOpenIds, IntType.SCENERY, "Close") { player, node ->
val door = node.asScenery()
val doorTeam = getDoorTeam(door.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val doorTeam = getCastleTeam(door.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
return@on false
@ -429,8 +428,8 @@ class CastleWarsListeners : InteractionListener {
on(CastleWars.largeDoorIds, IntType.SCENERY, "Attack") { player, node ->
val door = node.asScenery()
val doorTeam = getDoorTeam(door.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val doorTeam = getCastleTeam(door.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
return@on false
@ -454,8 +453,8 @@ class CastleWarsListeners : InteractionListener {
SCENERY, CastleWars.cwExplosivePotion, *(CastleWars.largeDoorIds + CastleWars.largeDoorOpenIds)
) { player, used, with ->
val door = with.asScenery()
val doorTeam = getDoorTeam(door.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val doorTeam = getCastleTeam(door.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
sendMessage(player, "You can only use this in Castle Wars.")
@ -488,10 +487,10 @@ class CastleWarsListeners : InteractionListener {
onUseWith(
SCENERY, CastleWars.cwToolkit, *(CastleWars.largeDoorIds + CastleWars.largeDoorOpenIds)
) { player, used, with ->
) { player, _, with ->
val door = with.asScenery()
val doorTeam = getDoorTeam(door.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val doorTeam = getCastleTeam(door.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
sendMessage(player, "You can only use this in Castle Wars.")
@ -517,7 +516,7 @@ class CastleWarsListeners : InteractionListener {
val door = node.asScenery()
val doorTeam =
if (door.id == CastleWars.saradominBackdoor) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false
if (playerTeam == doorTeam) {
openBackdoor(player, door)
} else {
@ -535,7 +534,7 @@ class CastleWarsListeners : InteractionListener {
val door = node.asScenery()
val doorTeam =
if (door.id == CastleWars.saradominBackdoor) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false
if (playerTeam == doorTeam) {
sendMessage(player, "You don't need to pick the lock on your own door.")
return@on true
@ -568,7 +567,7 @@ class CastleWarsListeners : InteractionListener {
val door = node.asScenery()
val doorTeam = if (door.id == CastleWars.saradominBackdoorOpen) CastleWarsTeam.SARADOMIN
else CastleWarsTeam.ZAMORAK
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@on false
if (playerTeam != doorTeam) {
sendMessage(player, "You can't close the enemy's door!")
return@on true
@ -579,6 +578,7 @@ class CastleWarsListeners : InteractionListener {
}
on(CastleWars.tunnelRocks, IntType.SCENERY, "Mine") { player, node ->
if (!requireActiveGamePlayer(player)) return@on true
val rocks = node.asScenery()
val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location)
if (boulderKey == null) {
@ -590,6 +590,7 @@ class CastleWarsListeners : InteractionListener {
}
on(CastleWars.tunnelRocksWeakened, IntType.SCENERY, "Mine") { player, node ->
if (!requireActiveGamePlayer(player)) return@on true
val rocks = node.asScenery()
val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location)
if (boulderKey == null) {
@ -601,6 +602,7 @@ class CastleWarsListeners : InteractionListener {
}
onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.tunnelRocks) { player, used, with ->
if (!requireActiveGamePlayer(player)) return@onUseWith false
val rocks = with.asScenery()
val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location)
if (boulderKey == null) {
@ -616,6 +618,7 @@ class CastleWarsListeners : InteractionListener {
}
onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.tunnelRocksWeakened) { player, used, with ->
if (!requireActiveGamePlayer(player)) return@onUseWith false
val rocks = with.asScenery()
val boulderKey = CastleWarsGameState.getBoulderKey(rocks.location)
if (boulderKey == null) {
@ -631,6 +634,7 @@ class CastleWarsListeners : InteractionListener {
}
onUseWith(SCENERY, CastleWars.cwExplosivePotion, CastleWars.caveWall) { player, used, with ->
if (!requireActiveGamePlayer(player)) return@onUseWith false
val wall = with.asScenery()
val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location)
if (boulderKey == null) {
@ -649,7 +653,8 @@ class CastleWarsListeners : InteractionListener {
return@onUseWith true
}
onUseWith(SCENERY, pickaxeIds, CastleWars.caveWall) { player, used, with ->
onUseWith(SCENERY, pickaxeIds, CastleWars.caveWall) { player, _, with ->
if (!requireActiveGamePlayer(player)) return@onUseWith false
val wall = with.asScenery()
val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location)
if (boulderKey == null) {
@ -665,6 +670,7 @@ class CastleWarsListeners : InteractionListener {
}
on(CastleWars.caveWall, IntType.SCENERY, "Collapse") { player, node ->
if (!requireActiveGamePlayer(player)) return@on true
val wall = node.asScenery()
val boulderKey = CastleWarsGameState.getNearestBoulderKey(wall.location)
if (boulderKey == null) {
@ -681,8 +687,8 @@ class CastleWarsListeners : InteractionListener {
on(CastleWars.allCatapultIds, IntType.SCENERY, "Operate") { player, node ->
val catapult = node.asScenery()
val catapultTeam = getCatapultTeam(catapult.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val catapultTeam = getCastleTeam(catapult.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
sendMessage(player, "You need to be in a Castle Wars game to use this!")
return@on true
@ -713,15 +719,15 @@ class CastleWarsListeners : InteractionListener {
}
setVarbit(player, CastleWarsGameState.VARBIT_CATAPULT_X, CatapultConfig.DEFAULT_X)
setVarbit(player, CastleWarsGameState.VARBIT_CATAPULT_Y, CatapultConfig.DEFAULT_Y)
player.setAttribute("catapult:location", catapult.location)
player.setAttribute(CatapultConfig.ATTR_LOCATION, catapult.location)
player.interfaceManager.open(Component(Components.CASTLEWARS_CATAPULT_54))
return@on true
}
onUseWith(SCENERY, org.rs09.consts.Items.TINDERBOX_590, *CastleWars.allCatapultIds) { player, _, with ->
val catapult = with.asScenery()
val catapultTeam = getCatapultTeam(catapult.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false
val catapultTeam = getCastleTeam(catapult.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false
if (playerTeam == catapultTeam) {
sendMessage(player, "You can't destroy your own catapult!")
return@onUseWith false
@ -753,8 +759,8 @@ class CastleWarsListeners : InteractionListener {
onUseWith(SCENERY, CastleWars.cwExplosivePotion, *CastleWars.allCatapultIds) { player, used, with ->
val catapult = with.asScenery()
val catapultTeam = getCatapultTeam(catapult.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false
val catapultTeam = getCastleTeam(catapult.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false
if (playerTeam == catapultTeam) {
sendMessage(player, "You can't destroy your own catapult!")
return@onUseWith false
@ -772,8 +778,8 @@ class CastleWarsListeners : InteractionListener {
onUseWith(SCENERY, Items.BUCKET_OF_WATER_1929, *CastleWars.allCatapultIds) { player, used, with ->
val catapult = with.asScenery()
val catapultTeam = getCatapultTeam(catapult.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false
val catapultTeam = getCastleTeam(catapult.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false
if (playerTeam != catapultTeam) {
sendMessage(player, "You can't help the enemy's catapult!")
return@onUseWith false
@ -789,10 +795,10 @@ class CastleWarsListeners : InteractionListener {
return@onUseWith true
}
onUseWith(SCENERY, CastleWars.cwToolkit, *CastleWars.allCatapultIds) { player, used, with ->
onUseWith(SCENERY, CastleWars.cwToolkit, *CastleWars.allCatapultIds) { player, _, with ->
val catapult = with.asScenery()
val catapultTeam = getCatapultTeam(catapult.location)
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@onUseWith false
val catapultTeam = getCastleTeam(catapult.location)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player) ?: return@onUseWith false
if (playerTeam != catapultTeam) {
sendMessage(player, "You can't repair the enemy's catapult!")
return@onUseWith false
@ -806,8 +812,8 @@ class CastleWarsListeners : InteractionListener {
return@onUseWith true
}
on(CastleWars.saradominTrapdoor, IntType.SCENERY, "Go-down") { player, node ->
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
on(CastleWars.saradominTrapdoor, IntType.SCENERY, "Go-down") { player, _ ->
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam != CastleWarsTeam.SARADOMIN) {
sendMessage(player, "Only the Saradomin team may use this trapdoor.")
return@on true
@ -819,8 +825,8 @@ class CastleWarsListeners : InteractionListener {
teleport(player, saradominTrapdoorDestination, TeleportManager.TeleportType.INSTANT)
return@on true
}
on(CastleWars.zamorakTrapdoor, IntType.SCENERY, "Go-down") { player, node ->
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
on(CastleWars.zamorakTrapdoor, IntType.SCENERY, "Go-down") { player, _ ->
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam != CastleWarsTeam.ZAMORAK) {
sendMessage(player, "Only the Zamorak team may use this trapdoor.")
return@on true
@ -840,7 +846,7 @@ class CastleWarsListeners : InteractionListener {
player.properties.teleportLocation = destination
return@on true
}
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
if (playerTeam == null) {
player.properties.teleportLocation = destination
return@on true
@ -917,12 +923,16 @@ class CastleWarsListeners : InteractionListener {
AgilityHandler.forceWalk(player, -1, startTile, endTile, Animation.create(819), 12, 0.0, null)
}
private fun getDoorTeam(location: Location): CastleWarsTeam {
private fun getCastleTeam(location: Location): CastleWarsTeam {
return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK
}
private fun getCatapultTeam(location: Location): CastleWarsTeam {
return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK
private fun requireActiveGamePlayer(player: Player): Boolean {
if (CastleWarsGameState.isGameActive && CastleWarsGameState.getActiveGameTeam(player) != null) {
return true
}
sendMessage(player, "You can only do that during a Castle Wars game.")
return false
}
private fun getOpenDoorVariant(closedId: Int): Int? = when (closedId) {
@ -933,14 +943,6 @@ class CastleWarsListeners : InteractionListener {
else -> null
}
private fun getClosedDoorVariant(openId: Int): Int? = when (openId) {
4425 -> 4423
4426 -> 4424
4429 -> 4427
4430 -> 4428
else -> null
}
private fun isPlayerOutsideFrontDoor(player: Player, door: Scenery, doorTeam: CastleWarsTeam): Boolean {
val playerY = player.location.y
val doorY = door.location.y
@ -994,7 +996,7 @@ class CastleWarsListeners : InteractionListener {
}
private fun leaveGameThroughPortal(player: Player, portalTeam: CastleWarsTeam) {
if (CastleWarsGameState.getPlayerTeam(player) != portalTeam) {
if (CastleWarsGameState.getActiveGameTeam(player) != portalTeam) {
sendMessage(player, "Only the ${portalTeam.teamName} team may use this portal.")
return
}

View file

@ -4,6 +4,7 @@ import content.minigame.castlewars.areas.CastleWarsGameArea
import content.minigame.castlewars.areas.CastleWarsRespawnArea
import content.minigame.castlewars.areas.CastleWarsWaitingArea
import core.api.ShutdownListener
import core.api.removeAttributes
import core.game.node.entity.player.Player
import core.game.world.map.Location
import core.game.world.map.RegionManager
@ -21,6 +22,9 @@ class CastleWarsShutdownHandler : ShutdownListener {
allTrackedPlayers += CastleWarsGameArea.zamorakPlayers
allTrackedPlayers += CastleWarsWaitingArea.waitingSaradominPlayers
allTrackedPlayers += CastleWarsWaitingArea.waitingZamorakPlayers
allTrackedPlayers += CastleWarsRespawnArea.saradominPlayersInRespawnRoom.keys
allTrackedPlayers += CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.keys
allTrackedPlayers += CastleWarsGameArea.loggedOutPlayers.values.map { it.player }
val respawnBorders = arrayOf(
CastleWarsRespawnArea.saradominRespawnRoom, CastleWarsRespawnArea.zamorakRespawnRoom
@ -54,24 +58,38 @@ class CastleWarsShutdownHandler : ShutdownListener {
for (player in allTrackedPlayers) {
if (CastleWarsGameState.isCarryingFlag(player)) {
CastleWarsGameState.dropFlag(player)
CastleWarsGameState.dropFlag(player, broadcast = false)
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
CastleWarsGameState.dropOwnFlag(player)
CastleWarsGameState.dropOwnFlag(player, broadcast = false)
}
CastleWarsGameState.restoreAndCleanupPlayer(player)
player.location = CastleWars.lobbyBankArea.randomWalkableLoc
val lobbyLocation = CastleWars.lobbyBankArea.randomWalkableLoc
player.location = lobbyLocation
player.properties.teleportLocation = lobbyLocation
removeAttributes(
player,
CastleWarsConstants.ATTR_LOGOUT_TICK,
CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS,
CastleWarsConstants.ATTR_LOGOUT_TEAM,
CastleWarsConstants.ATTR_LOGOUT_LOCATION,
CastleWarsConstants.ATTR_JOIN_TICK
)
}
CastleWarsItemHandler.clearAllBarricades()
CastleWarsGameState.clearCastleWarsGroundItems()
CastleWarsGameState.resetGame()
CastleWarsGameArea.clearSubstitutionState()
CastleWarsGameArea.saradominPlayers.clear()
CastleWarsGameArea.zamorakPlayers.clear()
CastleWarsGameArea.loggedOutPlayers.clear()
CastleWarsWaitingArea.waitingSaradominPlayers.clear()
CastleWarsWaitingArea.waitingZamorakPlayers.clear()
CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear()
CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear()
CastleWarsGameArea.ticksLeftInGame = -1
CastleWarsWaitingArea.gameStartCountdown = -1
}
}

View file

@ -10,6 +10,7 @@ import core.tools.START_DIALOGUE
class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : DialogueFile() {
override fun handle(componentID: Int, buttonID: Int) {
val currentPlayer = player ?: return
when (stage) {
START_DIALOGUE -> showTopics(
Topic("Yes.", ACCEPT, true),
@ -18,14 +19,14 @@ class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : Dialogu
)
ACCEPT -> {
if (!CastleWarsGameArea.claimVacancy(player!!, team)) {
sendMessage(player!!, "That Castle Wars vacancy is no longer available.")
if (!CastleWarsGameArea.claimVacancy(currentPlayer, team)) {
sendMessage(currentPlayer, "That Castle Wars vacancy is no longer available.")
}
stage = END_DIALOGUE
}
DECLINE -> {
CastleWarsGameArea.declineVacancyOffer(player!!, team)
CastleWarsGameArea.declineVacancyOffer(currentPlayer, team)
stage = END_DIALOGUE
}
}

View file

@ -29,8 +29,6 @@ class CastleWarsTunnelMiningPulse(
private const val REQUIRED_LEVEL = 1
private const val RATE = 0.40
private const val MINING_ANIMATION = 625
private const val EXPERIENCE = 0.0
}
@ -65,15 +63,18 @@ class CastleWarsTunnelMiningPulse(
}
private fun checkRequirements(): Boolean {
if (!canContinueCastleWarsMining(player, rocks)) {
return false
}
val expectedState = if (isWeakened) BoulderState.WEAKENED else BoulderState.COLLAPSED
if (CastleWarsGameState.getBoulderState(boulderKey) != expectedState) {
return false
}
val pickaxe = SkillingTool.getPickaxe(player)
if (pickaxe == null) {
sendMessage(player, "You need a pickaxe to mine these rocks.")
return false
}
if (!rocks.isActive) {
return false
}
return true
}
@ -159,6 +160,12 @@ class CastleWarsCaveWallMiningPulse(
}
private fun checkRequirements(): Boolean {
if (!canContinueCastleWarsMining(player, caveWall)) {
return false
}
if (!CastleWarsGameState.isBoulderCleared(boulderKey)) {
return false
}
val pickaxe = SkillingTool.getPickaxe(player)
if (pickaxe == null) {
sendMessage(player, "You need a pickaxe to mine the cave wall.")
@ -198,3 +205,9 @@ class CastleWarsCaveWallMiningPulse(
sendMessage(player, "You collapse the tunnel!")
}
}
private fun canContinueCastleWarsMining(player: Player, scenery: Scenery): Boolean {
return player.isActive && CastleWarsGameState.isGameActive &&
CastleWarsGameState.getActiveGameTeam(player) != null && scenery.isActive &&
player.location.z == scenery.location.z && player.location.getDistance(scenery.location) <= 2.0
}

View file

@ -69,7 +69,7 @@ class CatapultFiringHandler {
val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE)
val impactDelayTicks = (speed / 30).coerceAtLeast(1)
sendProjectile(catapultLocation, finalLocation)
sendProjectile(catapultLocation, finalLocation, distance, speed)
playFireEffects(player, finalLocation, impactDelayTicks)
scheduleImpact(player, finalLocation, impactDelayTicks)
@ -88,10 +88,7 @@ class CatapultFiringHandler {
return Location.create(finalX, finalY, 0)
}
private fun sendProjectile(from: Location, to: Location) {
val distance = from.getDistance(to).toInt()
val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE)
private fun sendProjectile(from: Location, to: Location, distance: Int, speed: Int) {
Projectile.create(
from,
to,
@ -139,6 +136,7 @@ class CatapultFiringHandler {
}
private fun isValidTarget(target: Player): Boolean {
return CastleWarsGameState.getPlayerTeam(target) != null
return CastleWarsGameState.isGameActive && target.isActive &&
CastleWarsGameState.getActiveGameTeam(target) != null
}
}

View file

@ -1,7 +1,5 @@
package content.minigame.castlewars
import content.minigame.castlewars.CatapultConfig.DEFAULT_X
import content.minigame.castlewars.CatapultConfig.DEFAULT_Y
import content.minigame.castlewars.CatapultConfig.DIGIT_CHILD_IDS
import content.minigame.castlewars.CatapultConfig.DIGIT_ITEM_IDS
import content.minigame.castlewars.CatapultConfig.INTERFACE_ID
@ -17,6 +15,7 @@ import core.game.node.entity.player.Player
import core.net.packet.PacketRepository
import core.net.packet.context.ChildPositionContext
import core.net.packet.out.RepositionChild
import kotlin.math.roundToInt
/**
* Handles rendering the catapult interface including marker position and digit displays.
@ -61,24 +60,13 @@ class CatapultInterfaceRenderer {
sendDigitModel(player, DIGIT_CHILD_IDS[3], yOnes)
}
/**
* Resets the interface to default coordinates.
* @param player The player whose interface to reset
*/
fun resetToDefaults(player: Player) {
val defaultCoord = Coordinate(DEFAULT_X, DEFAULT_Y)
renderCoordinate(player, defaultCoord)
internal fun calculateScreenX(coordX: Int): Int {
return MAP_BASE_X + (coordX.toDouble() / MAX_COORDINATE * MAP_WIDTH).roundToInt()
}
private fun calculateScreenX(coordX: Int): Int {
val pixelsPerUnit = MAP_WIDTH / MAX_COORDINATE
return MAP_BASE_X + (coordX * pixelsPerUnit)
}
private fun calculateScreenY(coordY: Int): Int {
val pixelsPerUnit = MAP_HEIGHT / MAX_COORDINATE
internal fun calculateScreenY(coordY: Int): Int {
// Y is inverted - 0 is at the bottom, MAX is at the top
return MAP_BASE_Y + ((MAX_COORDINATE - coordY) * pixelsPerUnit)
return MAP_BASE_Y + ((MAX_COORDINATE - coordY).toDouble() / MAX_COORDINATE * MAP_HEIGHT).roundToInt()
}
private fun sendDigitModel(player: Player, childId: Int, digit: Int) {

View file

@ -81,10 +81,13 @@ class Lanthus : InteractionListener, TickListener {
"your patron god, do you? Other than that, just have fun and enjoy it!"
).also { stage++ }
50 -> npcl(
FacialExpression.FRIENDLY, "Sure, here you go."
).also { stage = END_DIALOGUE }
.also { player.inventory.add(Item(Items.CASTLEWARS_MANUAL_4055)) }
50 -> {
if (addItem(player, Items.CASTLEWARS_MANUAL_4055)) {
npcl(FacialExpression.FRIENDLY, "Sure, here you go.")
} else {
npcl(FacialExpression.FRIENDLY, "You don't have enough inventory space.")
}.also { stage = END_DIALOGUE }
}
83 -> player("Great! Oh, how do I win the game?").also { stage++ }
84 -> npcl(

View file

@ -10,9 +10,14 @@ import core.api.sendMessage
import core.game.interaction.InteractionListener
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.node.item.Item
abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener {
override fun canEquipItem(player: Player, item: Item, equipmentSlot: Int): Boolean? {
return !CastleWarsEquipmentRestrictions.blocksEquip(player, equipmentSlot)
}
override fun areaEnter(entity: Entity) {
val player = entity as? Player ?: return
CastleWarsEquipmentRestrictions.removeInvalidEquipment(player)
@ -64,18 +69,11 @@ abstract class CastleWarsArea : MapArea, LogoutListener, InteractionListener {
override fun defineListeners() {
onUnequip(intArrayOf(CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak)) { player, _ ->
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
}
if (CastleWarsEquipmentRestrictions.isInCastleWarsArea(player)) {
sendMessage(player, "You can't remove your team's colours")
return@onUnequip false
}
return@onUnequip true
true
}
}

View file

@ -4,6 +4,7 @@ import content.minigame.castlewars.*
import content.minigame.castlewars.CastleWarsConstants.PLAYER_CLEANUP_INTERVAL_TICKS
import core.api.*
import core.game.component.Component
import core.game.container.impl.EquipmentContainer
import core.game.event.EventHook
import core.game.event.InterfaceCloseEvent
import core.game.interaction.Option
@ -18,6 +19,7 @@ import core.game.world.map.zone.ZoneBorders
import core.game.world.map.zone.ZoneType
import core.tools.Log
import org.rs09.consts.Components
import java.util.Locale
/**
* Handles the Castle Wars game map
@ -111,14 +113,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
}
}
/**
* 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)
}
fun clearSubstitutionState() {
activeVacancyOffers.clear()
activeVacancyOfferTicks.clear()
@ -263,9 +257,6 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
clearLogoutState(player)
saradominPlayers.remove(player)
zamorakPlayers.remove(player)
CastleWarsGameState.clearHintIconForPlayer(player)
player.interaction.remove(Option._P_ATTACK)
player.interaction.remove(Option._P_STEAL_FROM)
if (offerReplacement) {
openVacancy(leavingTeam)
fillVacancy(leavingTeam)
@ -278,7 +269,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
}
fun playerKey(player: Player): String {
return player.username.lowercase()
return player.username.lowercase(Locale.ROOT)
}
private fun setTeam(player: Player, team: CastleWarsTeam) {
@ -334,8 +325,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
CastleWarsGameState.clearHintIconForPlayer(player)
CastleWarsGameState.restoreAndCleanupPlayer(player)
player.interaction.remove(Option._P_ATTACK)
player.location = waitingRoomFor(team).randomWalkableLoc
player.equipment.replace(Item(team.cloakId), 1)
relocateImmediately(player, waitingRoomFor(team).randomWalkableLoc)
player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE)
waitingSetFor(team).add(player)
clearLogoutAttributes(player)
}
@ -347,11 +338,17 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
player.interaction.remove(Option._P_ATTACK)
player.interaction.remove(Option._P_STEAL_FROM)
val lobbyLocation = CastleWars.lobbyBankArea.randomWalkableLoc
player.location = lobbyLocation
player.properties.teleportLocation = lobbyLocation
relocateImmediately(player, lobbyLocation)
clearLogoutState(player)
}
@Suppress("DEPRECATION")
private fun relocateImmediately(player: Player, destination: Location) {
// Logout restoration may run before the player can process a normal teleport.
player.location = destination
player.properties.teleportLocation = destination
}
fun restoreActiveLogoutOnLogin(player: Player) {
val savedTeamName = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, "")
val savedLogoutTime = getAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, 0L)
@ -372,7 +369,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
return
}
if (ticksLeftInGame > 0 && elapsedMillis < CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS) {
player.location = Location.create(logoutLocation)
relocateImmediately(player, Location.create(logoutLocation))
setTeam(player, team)
clearLogoutAttributes(player)
return
@ -384,7 +381,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
}
}
object SaradominInterfaceCloseHook : EventHook<InterfaceCloseEvent> { // TODO: this might be useless.
object SaradominInterfaceCloseHook : EventHook<InterfaceCloseEvent> {
override fun process(entity: Entity, event: InterfaceCloseEvent) {
val player = entity as? Player ?: return
// Only restore if player is still in the game
@ -398,7 +395,7 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
}
}
object ZamorakInterfaceCloseHook : EventHook<InterfaceCloseEvent> { // TODO: this might be useless.
object ZamorakInterfaceCloseHook : EventHook<InterfaceCloseEvent> {
override fun process(entity: Entity, event: InterfaceCloseEvent) {
val player = entity as? Player ?: return
if (!zamorakPlayers.contains(player)) return
@ -453,29 +450,36 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
override fun areaEnter(entity: Entity) {
val player = entity as? Player ?: return
restoreActiveLogoutOnLogin(player)
if (areaBorders.none { it.insideBorder(player.location) }) {
return
}
val team = CastleWarsGameState.getActiveGameTeam(player)
if (team == null || ticksLeftInGame <= 0) {
log(this::class.java, Log.WARN, "${player.name} entered Castle Wars without an active match membership")
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
return
}
super.areaEnter(player)
zone.zoneType = ZoneType.CASTLE_WARS.id
registerTimer(
player,
spawnTimer("teleblock", CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE, false)
)
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.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), EquipmentContainer.SLOT_CAPE)
player.hook(Event.InterfaceClosed, SaradominInterfaceCloseHook)
}
CastleWarsTeam.ZAMORAK -> {
player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59))
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1)
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), EquipmentContainer.SLOT_CAPE)
player.hook(Event.InterfaceClosed, ZamorakInterfaceCloseHook)
}
null -> {
log(this::class.java, Log.WARN, "${player.name} entered Castle Wars but is NOT on a team?!")
}
}
CastleWarsGameState.setActiveGamePlayerOptions(player)
}
@ -546,22 +550,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
}
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)
}
saradominPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.SARADOMIN) }
zamorakPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.ZAMORAK) }
if (ticksLeftInGame % PLAYER_CLEANUP_INTERVAL_TICKS == 0) {
cleanupInactivePlayers()
}
@ -609,12 +599,15 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
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)
private fun ensureGameOverlay(player: Player, team: CastleWarsTeam) {
val overlayId = when (team) {
CastleWarsTeam.SARADOMIN -> Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58
CastleWarsTeam.ZAMORAK -> Components.CASTLEWARS_STATUS_OVERLAY_ZAMORAK_59
}
if (player.interfaceManager.overlay?.id != overlayId) {
player.interfaceManager.openOverlay(Component(overlayId))
CastleWarsGameState.updateOverlay(player)
}
}
override fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? {
@ -623,8 +616,8 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
if (ticksLeftInGame <= 0) {
return false
}
val attackerTeam = CastleWarsGameState.getPlayerTeam(attacker) ?: return false
val victimTeam = CastleWarsGameState.getPlayerTeam(victim) ?: return false
val attackerTeam = CastleWarsGameState.getActiveGameTeam(attacker) ?: return false
val victimTeam = CastleWarsGameState.getActiveGameTeam(victim) ?: return false
if (attackerTeam == victimTeam) {
if (message) {
sendMessage(attacker, "You can only attack players on the opposite team!")
@ -634,4 +627,15 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
return true
}
override fun modifyOutgoingHitInArea(attacker: Entity, target: Entity, hit: Int): Int? {
val attackingPlayer = attacker as? Player ?: return null
val victim = target as? Player ?: return null
if (!CastleWarsGameState.isGameActive || CastleWarsGameState.getActiveGameTeam(attackingPlayer) == null ||
CastleWarsGameState.getActiveGameTeam(victim) == null
) {
return null
}
return CastleWarsGameState.applyBraceletDamageBoost(attackingPlayer, victim, hit)
}
}

View file

@ -3,11 +3,13 @@ package content.minigame.castlewars.areas
import content.minigame.castlewars.CastleWars
import content.minigame.castlewars.CastleWarsConstants
import content.minigame.castlewars.CastleWarsGameState
import content.minigame.castlewars.CastleWarsTeam
import core.api.TickListener
import core.api.getWorldTicks
import core.api.sendMessage
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.container.impl.EquipmentContainer
import core.game.node.item.Item
import core.game.world.map.Location
import core.game.world.map.zone.ZoneBorders
@ -33,6 +35,11 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
override fun areaEnter(entity: Entity) {
val player = entity as? Player ?: return
val team = CastleWarsGameState.getActiveGameTeam(player)
if (team == null || CastleWarsGameArea.ticksLeftInGame <= 0) {
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
return
}
super.areaEnter(player)
if (CastleWarsGameState.isCarryingAnyFlag(player)) {
if (CastleWarsGameState.isCarryingFlag(player)) {
@ -42,12 +49,24 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
}
sendMessage(player, "You cannot take the flag into your respawn room!")
}
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()
when (team) {
CastleWarsTeam.SARADOMIN -> {
if (!saradominRespawnRoom.insideBorder(player.location)) {
player.properties.teleportLocation = saradominRespawnRoom.randomWalkableLoc
return
}
player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE)
saradominPlayersInRespawnRoom[player] = getWorldTicks()
}
CastleWarsTeam.ZAMORAK -> {
if (!zamorakRespawnRoom.insideBorder(player.location)) {
player.properties.teleportLocation = zamorakRespawnRoom.randomWalkableLoc
return
}
player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE)
zamorakPlayersInRespawnRoom[player] = getWorldTicks()
}
}
CastleWarsGameState.setActiveGamePlayerOptions(player)
}
@ -82,7 +101,7 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
}
saradominPlayersInRespawnRoom.remove(player)
zamorakPlayersInRespawnRoom.remove(player)
if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getPlayerTeam(player) != null) {
if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getActiveGameTeam(player) != null) {
CastleWarsGameArea().logout(player)
return
}

View file

@ -8,6 +8,7 @@ import core.api.*
import core.game.component.Component
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.container.impl.EquipmentContainer
import core.game.node.item.Item
import core.game.world.map.zone.ZoneBorders
import org.rs09.consts.Components
@ -50,11 +51,11 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
// Set team attribute and equip the hooded cloak on the entity based on which waiting room they're in
val joinedTeam = if (zamorakWaitingRoom.insideBorder(player.location)) {
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1)
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), EquipmentContainer.SLOT_CAPE)
waitingZamorakPlayers.add(player)
CastleWarsTeam.ZAMORAK
} else if (saradominWaitingRoom.insideBorder(player.location)) {
player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1)
player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), EquipmentContainer.SLOT_CAPE)
waitingSaradominPlayers.add(player)
CastleWarsTeam.SARADOMIN
} else {

View file

@ -3,6 +3,8 @@ package core.api
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.CombatStyle
import core.game.node.entity.player.Player
import core.game.node.item.Item
import core.game.world.map.Location
import core.game.world.map.zone.MapZone
import core.game.world.map.zone.ZoneBorders
@ -32,12 +34,17 @@ interface MapArea : ContentInterface {
* Return null if you want the default check in MapZone. (default)
**/
fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { return null }
/**
* Extends MapZone to cater to pvp, team games and all other unique targeting systems.
* Return true/false if you want to allow the deaths, especially false for "safe" areas.
* Return null if you want the default check in MapZone. (default)
* Allows an area to reject equipment changes without coupling core equipment code
* to a particular piece of content.
**/
fun canStartDeath(entity: Entity, killer: Entity?): Boolean? { return null }
fun canEquipItem(player: Player, item: Item, equipmentSlot: Int): Boolean? { return null }
/**
* Allows an area to modify damage dealt by an entity inside it.
*/
fun modifyOutgoingHitInArea(attacker: Entity, target: Entity, hit: Int): Int? { return null }
companion object {
val zoneMaps = HashMap<String, MapZone>()

View file

@ -2,7 +2,6 @@ package core.game.container.impl;
import content.global.skill.skillcapeperks.SkillcapeEquipmentPlugin;
import content.global.skill.skillcapeperks.SkillcapePerks;
import content.minigame.castlewars.CastleWars;
import core.game.container.Container;
import core.game.container.ContainerEvent;
import core.game.container.ContainerListener;
@ -151,7 +150,7 @@ public final class EquipmentContainer extends Container {
return false;
}
ArrayList<Item> itemsToMoveToInventory = new ArrayList<>(current);
itemsToMoveToInventory.removeIf(item -> isCastleWarsFlag(item) && !containsItem(item));
itemsToMoveToInventory.removeIf(item -> canUnequipWithoutInventorySpace(item) && !containsItem(item));
if(itemsToMoveToInventory.isEmpty()) {
return true;
}
@ -191,7 +190,7 @@ public final class EquipmentContainer extends Container {
int neededSlots = 0;
for(Item item : current) {
if(isCastleWarsFlag(item)) {
if(canUnequipWithoutInventorySpace(item)) {
continue;
}
if(!item.getDefinition().isStackable()) {
@ -205,8 +204,8 @@ public final class EquipmentContainer extends Container {
return neededSlots;
}
private boolean isCastleWarsFlag(Item item) {
return item.getId() == CastleWars.saradominFlag || item.getId() == CastleWars.zamorakFlag;
private boolean canUnequipWithoutInventorySpace(Item item) {
return item.getDefinition().getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false);
}
@Nullable
@ -230,7 +229,7 @@ public final class EquipmentContainer extends Container {
if (plugin != null) {
Object object = plugin.fireEvent("unequip", player, currentItem);
if (object != null && !((Boolean) object)) {
if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) {
if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) {
continue;
}
canContinue = false;
@ -241,7 +240,7 @@ public final class EquipmentContainer extends Container {
canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false);
if(!canContinue) {
if (isCastleWarsFlag(currentItem) && !containsItem(currentItem)) {
if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) {
canContinue = true;
continue;
}

View file

@ -1,7 +1,5 @@
package core.game.global.action
import content.minigame.castlewars.CastleWars
import content.minigame.castlewars.CastleWarsEquipmentRestrictions
import core.game.event.ItemEquipEvent
import core.game.event.ItemUnequipEvent
import core.game.container.impl.EquipmentContainer
@ -12,12 +10,14 @@ import core.game.node.Node
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.audio.Audio
import core.game.node.item.Item
import core.plugin.Plugin
import core.game.interaction.InteractionListener
import core.game.interaction.IntType
import core.game.interaction.InteractionListeners
import core.game.system.config.ItemConfigParser
import org.rs09.consts.Sounds
import java.util.Locale
/**
* Represents the equipment equipping handler plugin.
@ -37,20 +37,20 @@ class EquipHandler : InteractionListener {
val item = node.asItem()
val itemEquipmentSlot = item.definition.getConfiguration<Int>(ItemConfigParser.EQUIP_SLOT, -1)
if (CastleWarsEquipmentRestrictions.blocksEquip(player, itemEquipmentSlot)) {
if (!player.zoneMonitor.canEquip(player, item, itemEquipmentSlot)) {
return
}
val currentEquippedItem = player.equipment[itemEquipmentSlot]
if (item == null || currentEquippedItem == item || item.name.toLowerCase().contains("goblin mail")) {
if (currentEquippedItem == item || item.name.lowercase(Locale.ROOT).contains("goblin mail")) {
return
}
if(currentEquippedItem != null){
if(!InteractionListeners.run(currentEquippedItem.id, player, currentEquippedItem, false)){
val droppedCastleWarsFlag =
isCastleWarsFlag(currentEquippedItem.id) && player.equipment[itemEquipmentSlot] == null
if (!droppedCastleWarsFlag) {
val listenerRemovedItem =
canUnequipWithoutInventorySpace(currentEquippedItem) && player.equipment[itemEquipmentSlot] == null
if (!listenerRemovedItem) {
return
}
}
@ -128,7 +128,7 @@ class EquipHandler : InteractionListener {
if (slot == EquipmentContainer.SLOT_WEAPON) {
player.packetDispatch.sendString("", 92, 0)
}
if (!isCastleWarsFlag(item.id)) {
if (!canUnequipWithoutInventorySpace(item)) {
val maximumAdd = player.inventory.getMaximumAdd(item)
if (maximumAdd < item.amount) {
player.packetDispatch.sendMessage("Not enough free space in your inventory.")
@ -138,10 +138,16 @@ class EquipHandler : InteractionListener {
val plugin = item.definition.getConfiguration<Plugin<Any>>("equipment", null)
if (plugin != null) {
if (!(plugin.fireEvent("unequip", player, item) as Boolean)) {
if (canUnequipWithoutInventorySpace(item) && player.equipment[slot] == null) {
return
}
return
}
}
if (!InteractionListeners.run(itemId, player, item, false)) {
if (canUnequipWithoutInventorySpace(item) && player.equipment[slot] == null) {
return
}
return
}
val maximumAdd = player.inventory.getMaximumAdd(item)
@ -163,8 +169,8 @@ class EquipHandler : InteractionListener {
}
}
private fun isCastleWarsFlag(itemId: Int): Boolean {
return itemId == CastleWars.saradominFlag || itemId == CastleWars.zamorakFlag
private fun canUnequipWithoutInventorySpace(item: Item): Boolean {
return item.definition.getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false)
}
}
}

View file

@ -23,7 +23,6 @@ import core.game.world.update.flag.context.Animation
import core.tools.RandomFunction
import core.game.system.config.ItemConfigParser
import core.tools.Log
import content.minigame.castlewars.CastleWarsGameState
import org.rs09.consts.Sounds
import java.util.*
import kotlin.math.floor
@ -587,7 +586,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
hit += (hit.toDouble() * 0.15).toInt()
}
if (victim is Player) {
hit = CastleWarsGameState.applyBraceletDamageBoost(player, victim.asPlayer(), hit)
hit = player.zoneMonitor.modifyOutgoingHit(player, victim, hit)
}
}
if (attacker is Familiar && victim is Player) {

View file

@ -1,7 +1,7 @@
package core.game.node.entity.combat
import core.game.node.entity.Entity
import core.game.world.map.Location
import core.game.world.map.zone.MapZone
/**
* Stores the state at the moment death commenced.
@ -16,18 +16,11 @@ import core.game.world.map.Location
* @param deathLocation The location where death commenced
* @param zoneType The zone type ID at death time (from ZoneMonitor.getType())
* @param isSafeZone Whether the player was in a safe zone at death time
* @param wasInWilderness Whether the player was in wilderness at death time
* @param wasSkulled Whether the player was skulled at death time
* @param killer The entity that killed the player (may be null for environmental deaths)
* @param deathTick The game tick when death commenced
* @param deathZones Snapshot of the zone handlers active when death commenced
*/
data class DeathContext(
val deathLocation: Location,
val zoneType: Int,
val isSafeZone: Boolean,
val wasInWilderness: Boolean,
val wasSkulled: Boolean,
val killer: Entity?,
val deathTick: Int
val deathZones: List<MapZone>
)

View file

@ -49,6 +49,8 @@ import core.game.world.map.build.DynamicRegion;
import core.game.world.map.path.Pathfinder;
import core.game.world.map.zone.ZoneRestriction;
import core.game.world.map.zone.ZoneType;
import core.game.world.map.zone.MapZone;
import core.game.world.map.zone.RegionZone;
import core.game.node.entity.combat.DeathContext;
import core.game.world.update.flag.PlayerFlags;
import core.game.world.update.flag.*;
@ -87,6 +89,7 @@ import core.worker.ManagementEvents;
import core.game.world.update.flag.context.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import static core.api.ContentAPIKt.*;
@ -612,13 +615,13 @@ public class Player extends Entity {
public void commenceDeath(Entity killer) {
if (!isPlaying()) return;
DeathContext context = new DeathContext(
getLocation(),
Location.create(getLocation()),
getZoneMonitor().getType(),
getProperties().isSafeZone(),
getSkullManager().isWilderness(),
getSkullManager().isSkulled(),
killer,
GameWorld.getTicks()
getZoneMonitor().getZones().stream()
.map(RegionZone::getZone)
.distinct()
.collect(Collectors.toList())
);
setAttribute("death-context", context);
super.commenceDeath(killer);
@ -655,7 +658,9 @@ public class Player extends Entity {
incrementAttribute("/save:"+STATS_BASE+":"+STATS_DEATHS);
packetDispatch.sendTempMusic(90);
boolean zoneHandledDeath = getZoneMonitor().handleDeath(killer);
boolean zoneHandledDeath = deathContext != null
? deathContext.getDeathZones().stream().anyMatch(zone -> zone.death(this, killer))
: getZoneMonitor().handleDeath(killer);
if (!zoneHandledDeath && (!wasSafeZone && deathZoneType != ZoneType.SAFE.getId()) && getDetails().getRights() != Rights.ADMINISTRATOR) {
//If player was a Hardcore Ironman, announce that they died
if (this.getIronmanManager().getMode().equals(IronmanMode.HARDCORE)) { //if this was checkRestriction, ultimate irons would be moved to HARDCORE_DEAD as well

View file

@ -227,6 +227,12 @@ class ItemConfigParser {
*/
const val CASTLE_WARS_TICKET_PRICE = "castle_wars_ticket_price"
/**
* Whether an unequip listener may remove the item without first requiring
* enough inventory space to hold it.
*/
const val UNEQUIP_WITHOUT_INVENTORY_SPACE = "unequip_without_inventory_space"
}
val parser = JSONParser()
@ -264,6 +270,7 @@ class ItemConfigParser {
"fun_weapon",
"rare_item",
"bankable",
UNEQUIP_WITHOUT_INVENTORY_SPACE,
"two_handed",
"has_special",
"remove_sleeves",
@ -311,4 +318,4 @@ class ItemConfigParser {
}
log(this::class.java, Log.FINE, "Parsed $count item configs.")
}
}
}

View file

@ -168,6 +168,22 @@ public abstract class MapZone implements Zone {
return true;
}
/**
* Checks whether an item may be equipped in this zone.
*/
public boolean canEquip(Player player, Item item, int equipmentSlot) {
return true;
}
/**
* Applies zone-specific outgoing damage modifiers.
*
* @return the modified hit, or {@code null} when this zone has no modifier.
*/
public Integer modifyOutgoingHit(Entity attacker, Entity target, int hit) {
return null;
}
/**
* If multi-zone boundaries should be ignored.
* @param attacker The attacker.
@ -514,4 +530,4 @@ public abstract class MapZone implements Zone {
public int hashCode() {
return Objects.hash(uid, name, overlappable, fireRandomEvents, restriction, zoneType);
}
}
}

View file

@ -53,7 +53,7 @@ public final class ZoneMonitor {
Items.RING_OF_WEALTH2_14642,
Items.RING_OF_WEALTH3_14644,
Items.RING_OF_LIFE_2570
);
);
/**
* The entity.
@ -187,6 +187,32 @@ public final class ZoneMonitor {
}
return true;
}
/**
* Checks whether all current zones permit an equipment change.
*/
public boolean canEquip(Player player, Item item, int equipmentSlot) {
for (RegionZone z : zones) {
if (!z.getZone().canEquip(player, item, equipmentSlot)) {
return false;
}
}
return true;
}
/**
* Applies outgoing damage modifiers from the entity's current zones.
*/
public int modifyOutgoingHit(Entity attacker, Entity target, int hit) {
int modifiedHit = hit;
for (RegionZone z : zones) {
Integer zoneHit = z.getZone().modifyOutgoingHit(attacker, target, modifiedHit);
if (zoneHit != null) {
modifiedHit = zoneHit;
}
}
return modifiedHit;
}
/**
* Checks if the entity can interact with the target.

View file

@ -11,11 +11,13 @@ import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.CombatStyle
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.Player
import core.game.node.entity.player.info.login.LoginConfiguration
import core.game.node.entity.player.info.login.PlayerSaveParser
import core.game.node.entity.player.info.login.PlayerSaver
import core.game.node.entity.player.link.quest.Quest
import core.game.node.entity.player.link.quest.QuestRepository
import core.game.node.item.Item
import core.game.system.timer.RSTimer
import core.game.system.timer.TimerRegistry
import core.game.world.GameWorld
@ -141,17 +143,13 @@ object ClassScanner {
}
}
// Additional hook for startDeath for PVP area controls
override fun startDeath(e: Entity?, killer: Entity?): Boolean {
if (e == null) {
return super.startDeath(null, killer)
}
val clazzAttack = clazz.canStartDeath(e, killer)
if (clazzAttack == null) { // If clazz returns null, continue the usual check.
return super.startDeath(e, killer)
} else {
return clazzAttack // Otherwise return whatever function.
}
override fun canEquip(player: Player, item: Item, equipmentSlot: Int): Boolean {
return clazz.canEquipItem(player, item, equipmentSlot)
?: super.canEquip(player, item, equipmentSlot)
}
override fun modifyOutgoingHit(attacker: Entity, target: Entity, hit: Int): Int? {
return clazz.modifyOutgoingHitInArea(attacker, target, hit)
}
}
for(border in clazz.defineAreaBorders()) zone.register(border)

View file

@ -21,6 +21,9 @@ import core.game.system.timer.impl.Teleblock
import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.zone.MapZone
import core.game.world.map.zone.RegionZone
import core.game.world.map.zone.ZoneBorders
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
@ -28,6 +31,16 @@ import org.rs09.consts.Items
class CastleWarsTests {
private fun addCastleWarsEquipmentZone(player: Player) {
val area = CastleWarsGameArea()
val zone = object : MapZone("Castle Wars equipment test", true) {
override fun canEquip(player: Player, item: Item, equipmentSlot: Int): Boolean {
return area.canEquipItem(player, item, equipmentSlot) ?: true
}
}
player.zoneMonitor.zones.add(RegionZone(zone, ZoneBorders(player.location, player.location)))
}
companion object {
init {
TestUtils.preTestSetup()
@ -82,6 +95,26 @@ class CastleWarsTests {
}
}
@Test
fun waitingRoomMembershipDoesNotGrantActiveGamePrivileges() {
TestUtils.getMockPlayer("cwwaitingonly").use { player ->
CastleWarsGameArea.ticksLeftInGame = 100
CastleWarsWaitingArea.waitingSaradominPlayers.add(player)
assertTrue(CastleWarsWaitingArea.waitingSaradominPlayers.contains(player))
assertNull(CastleWarsGameState.getActiveGameTeam(player))
player.inventory.add(Item(CastleWars.cwBarricade))
val setUp = requireNotNull(
InteractionListeners.get(CastleWars.cwBarricade, IntType.ITEM.ordinal, "set-up")
)
setUp.invoke(player, player.inventory[0])
assertEquals(0, CastleWarsItemHandler.getBarricadeCount(CastleWarsTeam.SARADOMIN))
assertTrue(player.inventory.contains(CastleWars.cwBarricade, 1))
}
}
@Test
fun spawnRoomPortalOffersVacancyAndFirstAcceptanceWins() {
TestUtils.getMockPlayer("cwleaver").use { leaver ->
@ -802,6 +835,7 @@ class CastleWarsTests {
fun castleWarsPlayersCannotEquipHelmsAfterEntry() {
TestUtils.getMockPlayer("cwhelm").use { player ->
player.location = Location.create(2380, 9490, 0)
addCastleWarsEquipmentZone(player)
player.inventory.add(Item(Items.BRONZE_FULL_HELM_1155))
InteractionListeners.run(
@ -817,6 +851,7 @@ class CastleWarsTests {
fun castleWarsPlayersCannotManuallyEquipTeamCloaksAfterEntry() {
TestUtils.getMockPlayer("cwcloak").use { player ->
player.location = Location.create(2380, 9490, 0)
addCastleWarsEquipmentZone(player)
player.inventory.add(Item(Items.HOODED_CLOAK_4041))
InteractionListeners.run(
@ -980,6 +1015,16 @@ class CastleWarsTests {
assertEquals(-2, zamorakNorth.y - zamorakBase.y)
}
@Test
fun catapultMarkerUsesEntireInterfaceMap() {
val renderer = CatapultInterfaceRenderer()
assertEquals(CatapultConfig.MAP_BASE_X, renderer.calculateScreenX(0))
assertEquals(CatapultConfig.MAP_BASE_X + CatapultConfig.MAP_WIDTH, renderer.calculateScreenX(30))
assertEquals(CatapultConfig.MAP_BASE_Y + CatapultConfig.MAP_HEIGHT, renderer.calculateScreenY(0))
assertEquals(CatapultConfig.MAP_BASE_Y, renderer.calculateScreenY(30))
}
@Test
fun catapultShotInFlightBlocksOnlyShooterUntilLanding() {
TestUtils.getMockPlayer("cwcatlock1").use { shooter ->
@ -996,6 +1041,9 @@ class CastleWarsTests {
)!!
CastleWarsGameArea.saradominPlayers.addAll(listOf(shooter, other))
CastleWarsGameArea.ticksLeftInGame = 100
shooter.location = catapult.location
other.location = catapult.location
shooter.inventory.add(Item(CastleWars.cwRock, 2))
other.inventory.add(Item(CastleWars.cwRock))
@ -1048,6 +1096,8 @@ class CastleWarsTests {
catapult.id, IntType.SCENERY.ordinal, "operate"
)!!
CastleWarsGameArea.saradominPlayers.add(shooter)
CastleWarsGameArea.ticksLeftInGame = 100
shooter.location = catapult.location
shooter.inventory.add(Item(CastleWars.cwRock))
operateCatapult(shooter, catapult)
@ -1064,10 +1114,39 @@ class CastleWarsTests {
}
}
@Test
fun staleCatapultInterfaceCannotFireAfterPlayerMovesAway() {
TestUtils.getMockPlayer("cwcatmoved").use { shooter ->
val catapult = Scenery(
CastleWars.getOperationalCatapultId(CastleWarsTeam.SARADOMIN),
CastleWarsGameState.saradominCatapultLocation,
10,
0
)
ensureCatapultObject(catapult)
CastleWarsGameArea.saradominPlayers.add(shooter)
CastleWarsGameArea.ticksLeftInGame = 100
shooter.location = catapult.location
shooter.inventory.add(Item(CastleWars.cwRock))
shooter.setAttribute(CatapultConfig.ATTR_LOCATION, catapult.location)
shooter.interfaceManager.open(Component(CatapultConfig.INTERFACE_ID))
shooter.location = Location.create(catapult.location.x + 10, catapult.location.y, catapult.location.z)
InterfaceListeners.run(
shooter, Component(CatapultConfig.INTERFACE_ID), 0, CatapultConfig.BUTTON_FIRE, 0, 0
)
assertEquals(1, shooter.inventory.getAmount(CastleWars.cwRock))
assertFalse(CatapultFiringHandler.hasShotInFlight(shooter))
assertFalse(shooter.interfaceManager.hasMainComponent(CatapultConfig.INTERFACE_ID))
}
}
@Test
fun staleCatapultInterfaceCannotFireWhenCatapultObjectIsMissing() {
TestUtils.getMockPlayer("cwcatmissing").use { shooter ->
CastleWarsGameArea.saradominPlayers.add(shooter)
CastleWarsGameArea.ticksLeftInGame = 100
shooter.inventory.add(Item(CastleWars.cwRock))
shooter.setAttribute(CatapultConfig.ATTR_LOCATION, Location.create(2400, 3090, 0))
shooter.interfaceManager.open(Component(CatapultConfig.INTERFACE_ID))
@ -1120,6 +1199,13 @@ class CastleWarsTests {
assertTrue(canClimbUpClimbingRope(rope.location, rope))
}
@Test
fun invalidBoulderKeysFailFast() {
assertThrows(IllegalArgumentException::class.java) {
CastleWarsGameState.getBoulderState("not-a-boulder")
}
}
@Test
fun respawnEnergyBarrierPassesAcrossOneWallTile() {
val barrierTile = Location.create(2426, 3080, 1)
@ -1171,6 +1257,7 @@ class CastleWarsTests {
TestUtils.getMockPlayer("cwbarrfire").use { player ->
val barricade = CastleWarsBarricadeNPC.spawn(Location.create(2400, 3100, 0), CastleWarsTeam.SARADOMIN)
CastleWarsGameArea.ticksLeftInGame = 100
CastleWarsGameArea.saradominPlayers.add(player)
CastleWarsItemHandler.saradominBarricades.add(barricade)
player.inventory.add(Item(Items.BUCKET_OF_WATER_1929))