mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-01 14:39:13 -06:00
Merge branch 'castle-wars' into 'master'
Castle Wars See merge request 2009scape/2009scape!2267
This commit is contained in:
commit
161ca91d2b
49 changed files with 7737 additions and 395 deletions
|
|
@ -3080,5 +3080,29 @@
|
|||
"replaceId": "38142",
|
||||
"fence": "false",
|
||||
"metal": "false"
|
||||
},
|
||||
{
|
||||
"id": "4465",
|
||||
"replaceId": "4466",
|
||||
"fence": "false",
|
||||
"metal": "false"
|
||||
},
|
||||
{
|
||||
"id": "4466",
|
||||
"replaceId": "4465",
|
||||
"fence": "false",
|
||||
"metal": "false"
|
||||
},
|
||||
{
|
||||
"id": "4467",
|
||||
"replaceId": "4468",
|
||||
"fence": "false",
|
||||
"metal": "false"
|
||||
},
|
||||
{
|
||||
"id": "4468",
|
||||
"replaceId": "4467",
|
||||
"fence": "false",
|
||||
"metal": "false"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -34815,8 +34815,12 @@
|
|||
"name": "Saradomin banner",
|
||||
"examine": "The Holy Team Standard",
|
||||
"archery_ticket_price": "0",
|
||||
"attack_anims": "419,419,419,419",
|
||||
"attack_speed": "4",
|
||||
"defence_anim": "420",
|
||||
"durability": null,
|
||||
"equipment_slot": "3",
|
||||
"unequip_without_inventory_space": "true",
|
||||
"render_anim": "131",
|
||||
"run_anim": "1427",
|
||||
"stand_anim": "1421",
|
||||
|
|
@ -34827,7 +34831,7 @@
|
|||
"turn90cw_anim": "1424",
|
||||
"two_handed": "true",
|
||||
"walk_anim": "1422",
|
||||
"weapon_interface": "14",
|
||||
"weapon_interface": "1",
|
||||
"weight": "2"
|
||||
},
|
||||
{
|
||||
|
|
@ -34842,8 +34846,12 @@
|
|||
"name": "Zamorak banner",
|
||||
"examine": "The Zamorak team's flag.",
|
||||
"archery_ticket_price": "0",
|
||||
"attack_anims": "419,419,419,419",
|
||||
"attack_speed": "4",
|
||||
"defence_anim": "420",
|
||||
"durability": null,
|
||||
"equipment_slot": "3",
|
||||
"unequip_without_inventory_space": "true",
|
||||
"render_anim": "131",
|
||||
"run_anim": "1427",
|
||||
"stand_anim": "1421",
|
||||
|
|
@ -34854,7 +34862,7 @@
|
|||
"turn90cw_anim": "1424",
|
||||
"two_handed": "true",
|
||||
"walk_anim": "1422",
|
||||
"weapon_interface": "14"
|
||||
"weapon_interface": "1"
|
||||
},
|
||||
{
|
||||
"id": "4040",
|
||||
|
|
@ -125297,4 +125305,4 @@
|
|||
"tradeable": "false",
|
||||
"weight": "0.2"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2196,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}"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ class LunarListeners : SpellListener("lunar"), Commands {
|
|||
// Level 68
|
||||
onCast(Lunar.CURE_OTHER, PLAYER) { player, node ->
|
||||
requires(player, 68, arrayOf(Item(Items.ASTRAL_RUNE_9075, 1), Item(Items.LAW_RUNE_563), Item(Items.EARTH_RUNE_557, 10)))
|
||||
if (player.zoneMonitor.isInZone("CastleWarsGame")) {
|
||||
player.sendMessage("You can't use Cure Other in Castle Wars!")
|
||||
return@onCast
|
||||
}
|
||||
node?.let { cureOther(player, node) }
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +142,10 @@ class LunarListeners : SpellListener("lunar"), Commands {
|
|||
// Level 74
|
||||
onCast(Lunar.CURE_GROUP, NONE) { player, _ ->
|
||||
requires(player, 74, arrayOf(Item(Items.ASTRAL_RUNE_9075, 2), Item(Items.LAW_RUNE_563, 2), Item(Items.COSMIC_RUNE_564, 2)))
|
||||
if (player.zoneMonitor.isInZone("CastleWarsGame")) {
|
||||
player.sendMessage("You can't use Cure Group in Castle Wars!")
|
||||
return@onCast
|
||||
}
|
||||
cureGroup(player)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -234,6 +234,10 @@ class ModernListeners : SpellListener("modern"){
|
|||
player.sendMessage("You can only alch items from the cupboards!")
|
||||
return false
|
||||
}
|
||||
if (player.zoneMonitor.isInZone("CastleWarsGame")) {
|
||||
player.sendMessage("You can't use alchemy spells in Castle Wars!")
|
||||
return false
|
||||
}
|
||||
|
||||
val coins = Item(995, item.definition.getAlchemyValue(high))
|
||||
if (item.amount > 1 && coins.amount > 0 && !player.inventory.hasSpaceFor(coins)) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
package rs09.game.content.activity.castlewars
|
||||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CastleWarsConstants.GAME_COOLDOWN_MINUTES
|
||||
import content.minigame.castlewars.CastleWarsConstants.GAME_TIME_MINUTES
|
||||
import content.minigame.castlewars.CastleWarsConstants.ROPE_DURATION_TICKS
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.zone.ZoneBorders
|
||||
import org.rs09.consts.Items
|
||||
import org.rs09.consts.NPCs
|
||||
import org.rs09.consts.Scenery
|
||||
|
||||
object CastleWars {
|
||||
|
||||
// Scenery IDs
|
||||
const val joinSaradominTeamPortal: Int = Scenery.SARADOMIN_PORTAL_4387
|
||||
const val joinZamorakTeamPortal: Int = Scenery.ZAMORAK_PORTAL_4388
|
||||
|
|
@ -16,6 +22,115 @@ object CastleWars {
|
|||
const val castleWaterTap: Int = Scenery.TAP_4482
|
||||
const val cwSteppingStones: Int = Scenery.STEPPING_STONE_4411
|
||||
|
||||
private val steppingStoneLocations: Set<Location> by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val locations = mutableSetOf<Location>()
|
||||
for (x in 2368..2431) {
|
||||
for (y in 3072..3135) {
|
||||
val location = Location.create(x, y, 0)
|
||||
if (RegionManager.getObject(location)?.id == cwSteppingStones) {
|
||||
locations += location
|
||||
}
|
||||
}
|
||||
}
|
||||
locations
|
||||
}
|
||||
|
||||
// Stepping stone tile locations are cached so dropped flag scenery cannot mask the underlying stone tile.
|
||||
fun isSteppingStoneTile(location: Location): Boolean {
|
||||
return steppingStoneLocations.contains(Location.create(location))
|
||||
}
|
||||
|
||||
// Energy barriers
|
||||
const val saradominEnergyBarrier: Int = 4469 // "Only the Saradomin team may pass."
|
||||
const val zamorakEnergyBarrier: Int = 4470 // "Only the Zamorak team may pass."
|
||||
|
||||
// Exit portals - leave the game (in respawn room)
|
||||
const val saradominExitPortal: Int = 4406 // "A portal to leave the Saradomin team."
|
||||
const val zamorakExitPortal: Int = 4407 // "A portal to leave the Zamorak team."
|
||||
|
||||
// Flag stand scenery IDs
|
||||
const val saradominFlagStand: Int = 4902 // Saradomin Standard with flag (Capture option)
|
||||
const val zamorakFlagStand: Int = 4903 // Zamorak Standard with flag (Capture option)
|
||||
const val emptyFlagStand1: Int = 4377 // Empty stand (flag taken) - Saradomin side
|
||||
const val emptyFlagStand2: Int = 4378 // Empty stand (flag taken) - Zamorak side
|
||||
const val emptyFlagStand3: Int = 4379 // Empty stand variant (unused)
|
||||
|
||||
// Dropped flag scenery IDs (flags dropped on ground as interactable scenery)
|
||||
// 4900/4901 are standalone flag objects with "Take" option - used when flag is dropped
|
||||
const val saradominDroppedFlagScenery: Int = 4900 // Saradomin flag dropped on ground (Take option)
|
||||
const val zamorakDroppedFlagScenery: Int = 4901 // Zamorak flag dropped on ground (Take option)
|
||||
val droppedFlagSceneryIds: IntArray = intArrayOf(saradominDroppedFlagScenery, zamorakDroppedFlagScenery)
|
||||
|
||||
// All flag stand IDs for interaction handlers (stands at castle top)
|
||||
val allFlagStandIds: IntArray = intArrayOf(4902, 4903, 4377, 4378, 4379)
|
||||
|
||||
// Large doors - castle wall doors that can be attacked/repaired
|
||||
val largeDoorIds: IntArray = intArrayOf(4423, 4424, 4427, 4428) // Closed door variants
|
||||
val largeDoorOpenIds: IntArray = intArrayOf(4425, 4426, 4429, 4430) // Open door variants
|
||||
|
||||
// Small doors / Backdoors - can be locked/unlocked
|
||||
const val saradominBackdoor: Int = 4465
|
||||
const val zamorakBackdoor: Int = 4467
|
||||
val backdoorIds: IntArray = intArrayOf(saradominBackdoor, zamorakBackdoor)
|
||||
|
||||
// Backdoor open variants (for autowalk door handling)
|
||||
const val saradominBackdoorOpen: Int = 4466
|
||||
const val zamorakBackdoorOpen: Int = 4468
|
||||
val backdoorOpenIds: IntArray = intArrayOf(saradominBackdoorOpen, zamorakBackdoorOpen)
|
||||
|
||||
// Tunnel rocks/boulders - can be mined or collapsed
|
||||
const val tunnelRocks: Int = 4437 // Fully collapsed rocks (blocking)
|
||||
const val tunnelRocksWeakened: Int = 4438 // Weakened rocks (1 explosive hit, still blocking)
|
||||
const val caveWall: Int = 4448 // Cave wall for causing collapse
|
||||
|
||||
// Catapults - can be operated, set on fire, destroyed, and repaired.
|
||||
// 4381/4382 are the operational variants, 4904/4905 are the burning variants, 4385/4386 are damaged.
|
||||
const val catapult: Int = 4381 // Operational catapult variant 1
|
||||
const val catapultAlt: Int = 4382 // Operational catapult variant 2 (different orientation)
|
||||
const val catapultBurning1: Int = 4904 // Burning catapult variant 1
|
||||
const val catapultBurning2: Int = 4905 // Burning catapult variant 2
|
||||
const val catapultDamaged1: Int = 4385 // Damaged catapult variant 1
|
||||
const val catapultDamaged2: Int = 4386 // Damaged catapult variant 2
|
||||
val burningCatapultIds: IntArray = intArrayOf(catapultBurning1, catapultBurning2)
|
||||
val damagedCatapultIds: IntArray = intArrayOf(catapultDamaged1, catapultDamaged2)
|
||||
val allCatapultIds: IntArray = intArrayOf(
|
||||
catapult, catapultAlt, catapultBurning1, catapultBurning2, catapultDamaged1, catapultDamaged2
|
||||
)
|
||||
|
||||
fun getOperationalCatapultId(team: CastleWarsTeam): Int = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> catapultAlt
|
||||
CastleWarsTeam.ZAMORAK -> catapult
|
||||
}
|
||||
|
||||
fun getDamagedCatapultId(team: CastleWarsTeam): Int = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> catapultDamaged2
|
||||
CastleWarsTeam.ZAMORAK -> catapultDamaged1
|
||||
}
|
||||
|
||||
fun getBurningCatapultId(team: CastleWarsTeam): Int = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> catapultBurning2
|
||||
CastleWarsTeam.ZAMORAK -> catapultBurning1
|
||||
}
|
||||
|
||||
// Trapdoors - allow going down from 2nd floor to respawn room
|
||||
const val saradominTrapdoor: Int = 4471 // Saradomin castle trapdoor
|
||||
const val zamorakTrapdoor: Int = 4472 // Zamorak castle trapdoor
|
||||
|
||||
// Ladders and Staircases for vertical navigation
|
||||
// Ladders - climb down from respawn floor to ground floor (plane 1 -> plane 0)
|
||||
val ladderClimbDownIds: IntArray = intArrayOf(36349, 36365, 4911, 4912) // All Castle Wars ladders
|
||||
|
||||
// Staircases - climb down from respawn floor to ground floor (plane 1 -> plane 0)
|
||||
val staircaseClimbDownIds: IntArray = intArrayOf(4415, 4416, 4417, 4418, 4419, 4420)
|
||||
|
||||
// Steps - climb up from respawn floor to upper floors and flag floor
|
||||
// These go from plane 1 -> plane 2 -> plane 3 (flag floor)
|
||||
val stepsClimbUpIds: IntArray = intArrayOf(36480, 36481, 36484, 36495, 36521, 36523, 36532, 36540)
|
||||
|
||||
// All vertical navigation scenery IDs combined for easy searching
|
||||
val allClimbDownIds: IntArray = ladderClimbDownIds + staircaseClimbDownIds
|
||||
val allClimbUpIds: IntArray = stepsClimbUpIds
|
||||
|
||||
// Map of the item table scenery id to the item id they give
|
||||
val cwTableItemRewardMap = mapOf(
|
||||
Scenery.TABLE_36573 to Items.TOOLKIT_4051, // Saradomin Toolkit Table
|
||||
|
|
@ -33,10 +148,8 @@ object CastleWars {
|
|||
Scenery.TABLE_36579 to Items.BANDAGES_4049, // Saradomin Bandages Table
|
||||
Scenery.TABLE_36586 to Items.BANDAGES_4049 // Zamorak Bandages Table
|
||||
)
|
||||
|
||||
val cwCastleBattlementsMap = mapOf(
|
||||
Scenery.BATTLEMENTS_4446 to Scenery.BATTLEMENTS_36313,
|
||||
Scenery.BATTLEMENTS_4447 to Scenery.BATTLEMENTS_36314
|
||||
Scenery.BATTLEMENTS_4446 to Scenery.BATTLEMENTS_36313, Scenery.BATTLEMENTS_4447 to Scenery.BATTLEMENTS_36314
|
||||
)
|
||||
|
||||
// Item IDs
|
||||
|
|
@ -54,14 +167,33 @@ object CastleWars {
|
|||
const val cwManualBook: Int = Items.CASTLEWARS_MANUAL_4055
|
||||
const val cwTicketRewardCurrency: Int = Items.CASTLE_WARS_TICKET_4067
|
||||
|
||||
// Authoritative list of items to remove from players on game end or area exit.
|
||||
// Includes all CW-specific equipment, supplies, and items obtained from CW supply sources.
|
||||
// Does NOT include tinderbox. I think you can just steal them =)
|
||||
val CW_ITEMS_TO_REMOVE = intArrayOf(
|
||||
saradominFlag, // 4037 - Saradomin banner
|
||||
zamorakFlag, // 4039 - Zamorak banner
|
||||
saradominTeamHoodedCloak, // 4041 - Saradomin hooded cloak
|
||||
zamorakTeamHoodedCloak, // 4042 - Zamorak hooded cloak
|
||||
cwRock, // 4043 - Rock
|
||||
cwExplosivePotion, // 4045 - Explosive potion
|
||||
cwClimbingRope, // 4047 - Climbing rope
|
||||
cwBandages, // 4049 - Bandages
|
||||
cwToolkit, // 4051 - Toolkit
|
||||
cwBarricade, // 4053 - Barricade
|
||||
cwPickaxe, // 1265 - Bronze pickaxe
|
||||
Items.BUCKET_1925, // Bucket (obtained from CW water tap area)
|
||||
Items.BUCKET_OF_WATER_1929 // Bucket of water (filled at CW water tap)
|
||||
)
|
||||
|
||||
// NPC IDs
|
||||
const val sheep = NPCs.SHEEP_1529
|
||||
const val imp = NPCs.IMP_1531
|
||||
const val rabbit = NPCs.RABBIT_1530
|
||||
const val unknownCwarsBarricade1 = NPCs.BARRICADE_1532
|
||||
const val unknownCwarsBarricadeOnFire = NPCs.BARRICADE_1533
|
||||
const val unknownCwarsBarricade2 = NPCs.BARRICADE_1534
|
||||
const val unknownCwarsBarricade3 = NPCs.BARRICADE_1535
|
||||
const val saradominBarricadeNpc = NPCs.BARRICADE_1532 // Saradomin/Blue barricade
|
||||
const val burningBarricadeNpc = NPCs.BARRICADE_1533 // Burning barricade (either team)
|
||||
const val zamorakBarricadeNpc = NPCs.BARRICADE_1534 // Zamorak/Red barricade
|
||||
const val barricadeVariantNpc = NPCs.BARRICADE_1535 // Additional barricade variant
|
||||
|
||||
// Locations
|
||||
val lobbyBankArea: ZoneBorders = ZoneBorders(2440, 3092, 2444, 3086, 0)
|
||||
|
|
@ -74,8 +206,7 @@ object CastleWars {
|
|||
const val invFullMessage = "Your inventory is too full to hold any more "
|
||||
|
||||
// Ints
|
||||
const val gameTimeMinutes = 20
|
||||
const val gameCooldownMinutes = 5
|
||||
const val ropeAliveTicks = -1 // TODO: Find amount of time until wall reverts to non rope state
|
||||
|
||||
}
|
||||
const val gameTimeMinutes = GAME_TIME_MINUTES
|
||||
const val gameCooldownMinutes = GAME_COOLDOWN_MINUTES
|
||||
val ropeAliveTicks = ROPE_DURATION_TICKS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.npc.NPCBehavior
|
||||
import org.rs09.consts.NPCs
|
||||
|
||||
/**
|
||||
* NPCBehavior for Castle Wars barricades that prevents combat XP gain.
|
||||
* Authentic RS behavior: "manual combat, no XP gained" when attacking barricades.
|
||||
* This is registered for all barricade NPC IDs and returns 0.0 XP multiplier.
|
||||
*/
|
||||
class CastleWarsBarricadeBehavior : NPCBehavior(
|
||||
NPCs.BARRICADE_1532, // Saradomin/Blue barricade
|
||||
NPCs.BARRICADE_1533, // Burning barricade
|
||||
NPCs.BARRICADE_1534, // Zamorak/Red barricade
|
||||
NPCs.BARRICADE_1535 // Additional variant
|
||||
) {
|
||||
override fun getXpMultiplier(self: NPC, attacker: Entity): Double {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.game.node.Node
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.npc.AbstractNPC
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.world.map.Location
|
||||
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
|
||||
* - Never turn to face attackers or any direction
|
||||
* - Cannot attack or move
|
||||
*/
|
||||
class CastleWarsBarricadeNPC : AbstractNPC {
|
||||
|
||||
constructor() : super(NPCs.BARRICADE_1532, null)
|
||||
|
||||
constructor(id: Int, location: Location) : super(id, location)
|
||||
|
||||
override fun init() {
|
||||
super.init()
|
||||
isWalks = false
|
||||
isNeverWalks = true
|
||||
isRespawn = false
|
||||
properties.isRetaliating = false
|
||||
isAggressive = false
|
||||
}
|
||||
|
||||
override fun shouldPreventStacking(mover: Entity): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun faceLocation(location: Location?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun face(entity: Entity?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun faceTemporary(entity: Entity?, ticks: Int): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun attack(node: Node?) {}
|
||||
|
||||
override fun isPoisonImmune(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun construct(id: Int, location: Location, vararg objects: Any?): AbstractNPC {
|
||||
return CastleWarsBarricadeNPC(id, location)
|
||||
}
|
||||
|
||||
override fun getIds(): IntArray {
|
||||
return intArrayOf(
|
||||
NPCs.BARRICADE_1532, // Saradomin/Blue barricade
|
||||
NPCs.BARRICADE_1533, // Burning barricade
|
||||
NPCs.BARRICADE_1534, // Zamorak/Red barricade
|
||||
NPCs.BARRICADE_1535 // Additional variant
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Spawns a Castle Wars barricade at the given location for the specified team.
|
||||
*
|
||||
* @param location The location to spawn the barricade
|
||||
* @param team The team the barricade belongs to
|
||||
* @return The spawned barricade NPC
|
||||
*/
|
||||
fun spawn(location: Location, team: CastleWarsTeam): NPC {
|
||||
val barricadeId = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> NPCs.BARRICADE_1532
|
||||
CastleWarsTeam.ZAMORAK -> NPCs.BARRICADE_1534
|
||||
}
|
||||
val barricade = CastleWarsBarricadeNPC(barricadeId, location)
|
||||
barricade.init()
|
||||
return barricade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CastleWarsGameState.VARBIT_CATAPULT_X
|
||||
import content.minigame.castlewars.CastleWarsGameState.VARBIT_CATAPULT_Y
|
||||
import content.minigame.castlewars.CatapultConfig.ATTR_LOCATION
|
||||
import content.minigame.castlewars.CatapultConfig.INTERFACE_ID
|
||||
import core.api.*
|
||||
import core.game.interaction.InterfaceListener
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.system.task.Pulse
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
|
||||
class CastleWarsCatapultInterface : InterfaceListener {
|
||||
|
||||
private val renderer = CatapultInterfaceRenderer()
|
||||
private val firingHandler = CatapultFiringHandler()
|
||||
|
||||
override fun defineInterfaceListeners() {
|
||||
on(INTERFACE_ID) { player, _, _, buttonID, _, _ ->
|
||||
handleButtonClick(player, buttonID)
|
||||
}
|
||||
|
||||
onOpen(INTERFACE_ID) { player, _ ->
|
||||
renderCurrentCoordinate(player)
|
||||
submitWorldPulse(object : Pulse(1) {
|
||||
override fun pulse(): Boolean {
|
||||
renderCurrentCoordinate(player)
|
||||
return true
|
||||
}
|
||||
})
|
||||
true
|
||||
}
|
||||
|
||||
onClose(INTERFACE_ID) { player, _ ->
|
||||
player.removeAttribute(ATTR_LOCATION)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleButtonClick(player: Player, buttonId: Int): Boolean {
|
||||
val team = CastleWarsGameState.getActiveGameTeam(player) ?: return closeAndReturn(player, true)
|
||||
val currentCoord = getCurrentCoordinate(player)
|
||||
|
||||
return when (val button = CatapultButton.fromId(buttonId)) {
|
||||
CatapultButton.FIRE -> handleFire(player, currentCoord, team)
|
||||
CatapultButton.CLOSE -> closeAndReturn(player, true)
|
||||
CatapultButton.LEFT, CatapultButton.RIGHT, CatapultButton.UP, CatapultButton.DOWN -> {
|
||||
handleCoordinateChange(player, currentCoord, button)
|
||||
}
|
||||
|
||||
CatapultButton.UNKNOWN -> true // Ignore unknown buttons
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFire(player: Player, coord: Coordinate, team: CastleWarsTeam): Boolean {
|
||||
if (CatapultFiringHandler.hasShotInFlight(player)) {
|
||||
CatapultFiringHandler.sendShotInFlightMessage(player)
|
||||
return closeAndReturn(player, true)
|
||||
}
|
||||
|
||||
if (!hasRocks(player)) {
|
||||
sendMessage(player, "You don't have any rocks to fire!")
|
||||
return true
|
||||
}
|
||||
|
||||
val catapultLocation = getCatapultLocation(player) ?: run {
|
||||
sendMessage(player, "Error: Catapult location not found.")
|
||||
return true
|
||||
}
|
||||
val catapultTeam = getCatapultTeam(catapultLocation)
|
||||
if (!canFireCatapult(player, team, catapultTeam, catapultLocation)) {
|
||||
return closeAndReturn(player, true)
|
||||
}
|
||||
|
||||
if (!removeItem(player, CastleWars.cwRock)) {
|
||||
sendMessage(player, "You don't have any rocks to fire!")
|
||||
return true
|
||||
}
|
||||
|
||||
firingHandler.fire(player, coord, CatapultTeam.fromCastleWarsTeam(catapultTeam), catapultLocation)
|
||||
closeInterface(player)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun canFireCatapult(
|
||||
player: Player,
|
||||
playerTeam: CastleWarsTeam,
|
||||
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
|
||||
}
|
||||
|
||||
val currentCatapult = RegionManager.getObject(catapultLocation)
|
||||
if (currentCatapult == null || !currentCatapult.isActive || currentCatapult.id !in CastleWars.allCatapultIds) {
|
||||
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
|
||||
)
|
||||
) {
|
||||
sendMessage(player, "The catapult is destroyed! Use a toolkit to repair it.")
|
||||
return false
|
||||
}
|
||||
if (currentCatapult.id in CastleWars.burningCatapultIds || CastleWarsGameState.isCatapultOnFire(
|
||||
catapultTeam
|
||||
)
|
||||
) {
|
||||
sendMessage(player, "The catapult is on fire! Use a bucket of water to extinguish it.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getCatapultTeam(location: Location): CastleWarsTeam {
|
||||
return if (location.y < 3100) CastleWarsTeam.SARADOMIN else CastleWarsTeam.ZAMORAK
|
||||
}
|
||||
|
||||
private fun handleCoordinateChange(
|
||||
player: Player, currentCoord: Coordinate, button: CatapultButton
|
||||
): Boolean {
|
||||
val newCoord = currentCoord.withDelta(button.deltaX, button.deltaY).clamp()
|
||||
|
||||
setVarbit(player, VARBIT_CATAPULT_X, newCoord.x)
|
||||
setVarbit(player, VARBIT_CATAPULT_Y, newCoord.y)
|
||||
|
||||
renderer.renderCoordinate(player, newCoord)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getCurrentCoordinate(player: Player): Coordinate {
|
||||
val x = getVarbit(player, VARBIT_CATAPULT_X)
|
||||
val y = getVarbit(player, VARBIT_CATAPULT_Y)
|
||||
return Coordinate(x, y)
|
||||
}
|
||||
|
||||
private fun renderCurrentCoordinate(player: Player) {
|
||||
renderer.renderCoordinate(player, getCurrentCoordinate(player))
|
||||
}
|
||||
|
||||
private fun getCatapultLocation(player: Player): Location? {
|
||||
return getAttribute(player, ATTR_LOCATION, null)
|
||||
}
|
||||
|
||||
private fun hasRocks(player: Player): Boolean {
|
||||
return player.inventory.contains(CastleWars.cwRock, 1)
|
||||
}
|
||||
|
||||
private fun closeAndReturn(player: Player, result: Boolean): Boolean {
|
||||
closeInterface(player)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
import core.api.Commands
|
||||
import core.api.sendMessage
|
||||
import core.game.system.command.Privilege
|
||||
|
||||
/**
|
||||
* Admin commands for Castle Wars development and testing.
|
||||
*
|
||||
* Commands:
|
||||
* - ::cw start - Force starts a game if both teams have players
|
||||
* - ::cw end - Ends the current game
|
||||
*/
|
||||
class CastleWarsCommands : Commands {
|
||||
|
||||
override fun defineCommands() {
|
||||
define("cw", Privilege.ADMIN, "::cw <start|end>", "Castle Wars admin commands") { player, args ->
|
||||
if (args.size < 2) {
|
||||
sendMessage(player, "Usage: ::cw <start|end>")
|
||||
return@define
|
||||
}
|
||||
when (args[1].lowercase()) {
|
||||
"start" -> {
|
||||
if (CastleWarsGameArea.ticksLeftInGame > 0) {
|
||||
sendMessage(player, "A Castle Wars game is already in progress!")
|
||||
return@define
|
||||
}
|
||||
val saraCount = CastleWarsWaitingArea.waitingSaradominPlayers.size
|
||||
val zamCount = CastleWarsWaitingArea.waitingZamorakPlayers.size
|
||||
if (saraCount < CastleWarsConstants.MIN_PLAYERS_PER_TEAM || zamCount < CastleWarsConstants.MIN_PLAYERS_PER_TEAM) {
|
||||
sendMessage(
|
||||
player,
|
||||
"Both teams need at least ${CastleWarsConstants.MIN_PLAYERS_PER_TEAM} players. Saradomin: $saraCount, Zamorak: $zamCount"
|
||||
)
|
||||
return@define
|
||||
}
|
||||
CastleWarsGameArea.startGame()
|
||||
sendMessage(player, "Castle Wars game force started! Saradomin: $saraCount, Zamorak: $zamCount")
|
||||
}
|
||||
|
||||
"end" -> {
|
||||
if (CastleWarsGameArea.ticksLeftInGame <= 0) {
|
||||
sendMessage(player, "No Castle Wars game is currently running.")
|
||||
return@define
|
||||
}
|
||||
CastleWarsGameArea.endCurrentGame()
|
||||
sendMessage(player, "Castle Wars game ended by admin command.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendMessage(player, "Unknown subcommand. Use: start or end")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
/**
|
||||
* Centralized constants for Castle Wars minigame.
|
||||
* All magic numbers should be defined here with descriptive names.
|
||||
*/
|
||||
object CastleWarsConstants {
|
||||
|
||||
// ==================== Timing (in ticks) ====================
|
||||
/** Grace period after picking up own flag before castle entry check (prevents instant return) */
|
||||
const val FLAG_RETURN_GRACE_PERIOD_TICKS = 3
|
||||
|
||||
/** Ticks before a dropped flag automatically returns to its stand */
|
||||
const val FLAG_AUTO_RETURN_TICKS = 150
|
||||
|
||||
/** Duration of climbing ropes before despawning */
|
||||
const val ROPE_DURATION_TICKS = 75
|
||||
|
||||
/** Burning barricade pulse interval (3 ticks = 1.8 seconds) */
|
||||
const val BURNING_BARRICADE_PULSE_INTERVAL = 3
|
||||
|
||||
// ==================== Damage Values ====================
|
||||
/** Self-damage when dropping explosive potion */
|
||||
const val EXPLOSIVE_POTION_SELF_DAMAGE = 15
|
||||
|
||||
/** Door damage from explosive potion */
|
||||
const val EXPLOSIVE_POTION_DOOR_DAMAGE = 25
|
||||
|
||||
/** Door repair amount per toolkit use */
|
||||
const val TOOLKIT_DOOR_REPAIR = 10
|
||||
|
||||
/** Fire damage per tick to burning barricades */
|
||||
const val BURNING_BARRICADE_DAMAGE_PER_TICK = 4
|
||||
|
||||
/** Maximum ticks a barricade can burn before destruction */
|
||||
const val BURNING_BARRICADE_MAX_TICKS = 10
|
||||
|
||||
// ==================== Gameplay Limits ====================
|
||||
/** Maximum barricades per team */
|
||||
const val MAX_BARRICADES_PER_TEAM = 10
|
||||
|
||||
/** Minimum players per team required to start a game */
|
||||
const val MIN_PLAYERS_PER_TEAM = 2
|
||||
|
||||
/** Castle Wars timing uses the server's 600ms game tick: 100 ticks = 1 minute */
|
||||
const val TICKS_PER_MINUTE = 100
|
||||
|
||||
/** Catapult targeting inaccuracy radius */
|
||||
const val CATAPULT_INACCURACY_RADIUS = 2
|
||||
|
||||
/** Catapult impact damage radius */
|
||||
const val CATAPULT_IMPACT_RADIUS = 3
|
||||
|
||||
/** Catapult minimum damage */
|
||||
const val CATAPULT_MIN_DAMAGE = 10
|
||||
|
||||
/** Catapult maximum damage */
|
||||
const val CATAPULT_MAX_DAMAGE = 20
|
||||
|
||||
// ==================== Coordinate Defaults ====================
|
||||
/** Default X coordinate for catapult targeting */
|
||||
const val CATAPULT_DEFAULT_X = 8
|
||||
|
||||
/** Default Y coordinate for catapult targeting */
|
||||
const val CATAPULT_DEFAULT_Y = 13
|
||||
|
||||
/** Maximum catapult coordinate value */
|
||||
const val CATAPULT_MAX_COORDINATE = 30
|
||||
|
||||
// ==================== Bandage Healing ====================
|
||||
/** Bandage heal percentage of max HP (10%) */
|
||||
const val BANDAGE_HEAL_PERCENTAGE = 0.10
|
||||
|
||||
/** Bandage run energy restore percentage (30%) */
|
||||
const val BANDAGE_RUN_ENERGY_RESTORE = 30.0
|
||||
|
||||
/** Bandage cooldown in ticks (1 tick = faster than food's 2 ticks) */
|
||||
const val BANDAGE_COOLDOWN_TICKS = 1
|
||||
|
||||
// ==================== Castle Wars Bracelet ====================
|
||||
/** Bracelet damage bonus multiplier against flag carriers (20% extra) */
|
||||
const val BRACELET_FLAG_CARRIER_DAMAGE_BONUS = 0.20
|
||||
|
||||
/** Bracelet bandage heal percentage (15% instead of base 10%) */
|
||||
const val BRACELET_BANDAGE_HEAL_PERCENTAGE = 0.15
|
||||
|
||||
// ==================== Energy Barrier ====================
|
||||
/** Run energy drained when passing through an energy barrier */
|
||||
const val ENERGY_BARRIER_DRAIN = 4.0
|
||||
|
||||
// ==================== Backdoor Pick-Lock ====================
|
||||
/** Backdoor pick-lock success at Thieving level 1 */
|
||||
const val BACKDOOR_PICKLOCK_LEVEL_ONE_SUCCESS = 6.3
|
||||
|
||||
/** Backdoor pick-lock success at Thieving level 99 */
|
||||
const val BACKDOOR_PICKLOCK_LEVEL_NINETY_NINE_SUCCESS = 100.0
|
||||
|
||||
/** Highest Thieving level used by the linear backdoor pick-lock curve */
|
||||
const val BACKDOOR_PICKLOCK_MAX_LEVEL = 99
|
||||
|
||||
// ==================== Cleanup Intervals ====================
|
||||
/** How often to run inactive player cleanup (every 10 ticks = 6 seconds) */
|
||||
const val PLAYER_CLEANUP_INTERVAL_TICKS = 10
|
||||
|
||||
// ==================== Door Health ====================
|
||||
/** Initial door health percentage */
|
||||
const val DOOR_INITIAL_HEALTH = 100
|
||||
|
||||
/** Door destroyed threshold (0 or below) */
|
||||
const val DOOR_DESTROYED_THRESHOLD = 0
|
||||
|
||||
// ==================== Game Timing ====================
|
||||
/** Game duration in minutes */
|
||||
const val GAME_TIME_MINUTES = 20
|
||||
|
||||
/** Game cooldown in minutes */
|
||||
const val GAME_COOLDOWN_MINUTES = 5
|
||||
|
||||
// ==================== Waiting Room ====================
|
||||
/** Waiting-room cycle while no game is active: 2 minutes = 200 ticks */
|
||||
const val WAITING_ROOM_COUNTDOWN_TICKS = 2 * TICKS_PER_MINUTE
|
||||
|
||||
/** How long a substitution offer can block later vacancy offers */
|
||||
const val VACANCY_OFFER_TIMEOUT_TICKS = TICKS_PER_MINUTE
|
||||
|
||||
// ==================== Player Attribute Keys ====================
|
||||
/** Attribute key: world tick when player joined the current game */
|
||||
const val ATTR_JOIN_TICK = "cw:join_tick"
|
||||
|
||||
/** Attribute key: world tick when player logged out (for grace period) */
|
||||
const val ATTR_LOGOUT_TICK = "cw:logout_tick"
|
||||
|
||||
// ==================== Logout Grace Period ====================
|
||||
/** Grace period after logout before removing player from team (2 minutes = 200 ticks) */
|
||||
const val LOGOUT_GRACE_PERIOD_TICKS = 2 * TICKS_PER_MINUTE
|
||||
|
||||
/** Wall-clock logout grace for saved login state (2 minutes) */
|
||||
const val LOGOUT_GRACE_PERIOD_MILLIS = 120_000L
|
||||
|
||||
/** Saved attribute key: wall-clock millis when player logged out of an active match */
|
||||
const val ATTR_LOGOUT_TIME_MILLIS = "/save:cw:logout_time_millis"
|
||||
|
||||
/** Saved attribute key: team name for an active-match logout */
|
||||
const val ATTR_LOGOUT_TEAM = "/save:cw:logout_team"
|
||||
|
||||
/** Saved attribute key: exact active-match logout location */
|
||||
const val ATTR_LOGOUT_LOCATION = "/save:cw:logout_location"
|
||||
|
||||
// ==================== Flag Steal Timer ====================
|
||||
/** Time in ticks before teammates can steal the flag from a carrier (3 minutes = 300 ticks) */
|
||||
const val FLAG_STEAL_TIMER_TICKS = 3 * TICKS_PER_MINUTE
|
||||
|
||||
/** Player attribute key for tracking when enemy flag was picked up */
|
||||
const val ATTR_FLAG_PICKUP_TICK = "cw:flag_pickup_tick"
|
||||
|
||||
/** Player attribute key: team whose bracelet effect was activated when the match started */
|
||||
const val ATTR_ACTIVE_BRACELET_TEAM = "cw:active_bracelet_team"
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.animate
|
||||
import core.api.playAudio
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.node.scenery.Scenery
|
||||
import core.game.system.task.Pulse
|
||||
import core.tools.RandomFunction
|
||||
import org.rs09.consts.Sounds
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Handles attacking the Castle Wars front doors.
|
||||
* - Uses player's attack speed based on weapon
|
||||
* - Calculates damage based on combat stats
|
||||
* - Damages door health until destroyed or player stops
|
||||
*/
|
||||
class CastleWarsDoorAttackPulse(
|
||||
private val player: Player, private val door: Scenery, private val doorTeam: CastleWarsTeam
|
||||
) : Pulse() {
|
||||
|
||||
private var ticksUntilNextHit = 0
|
||||
|
||||
override fun pulse(): Boolean {
|
||||
if (!player.isActive || CastleWarsGameState.isDoorDestroyed(doorTeam) || CastleWarsGameState.getActiveGameTeam(
|
||||
player
|
||||
) == null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (player.location.getDistance(door.location) > 2.0) {
|
||||
return true
|
||||
}
|
||||
player.faceLocation(door.location)
|
||||
if (ticksUntilNextHit <= 0) {
|
||||
performAttack()
|
||||
ticksUntilNextHit = getAttackSpeed()
|
||||
}
|
||||
ticksUntilNextHit--
|
||||
return false
|
||||
}
|
||||
|
||||
private fun performAttack() {
|
||||
val animation = getAttackAnimation()
|
||||
animate(player, animation)
|
||||
val damage = calculateDamage()
|
||||
CastleWarsGameState.damageDoor(doorTeam, damage)
|
||||
playAudio(player, Sounds.WOODCUTTING_HIT_3038)
|
||||
}
|
||||
|
||||
private fun getAttackSpeed(): Int {
|
||||
val weapon = player.equipment.get(EquipmentContainer.SLOT_WEAPON)
|
||||
return weapon?.definition?.getConfiguration("attack_speed", 4) ?: 4
|
||||
}
|
||||
|
||||
private fun calculateDamage(): Int {
|
||||
var effectiveStrength = player.skills.getLevel(Skills.STRENGTH).toDouble()
|
||||
when (player.properties.attackStyle.style) {
|
||||
WeaponInterface.STYLE_AGGRESSIVE -> effectiveStrength += 3.0
|
||||
WeaponInterface.STYLE_CONTROLLED -> effectiveStrength += 1.0
|
||||
}
|
||||
effectiveStrength += 8.0
|
||||
|
||||
val strengthBonus = player.properties.bonuses[11] + 64
|
||||
val maxHit = floor(0.5 + (effectiveStrength * strengthBonus / 640.0)).toInt().coerceAtLeast(1)
|
||||
val bonusType = player.properties.attackStyle.bonusType
|
||||
val styleMultiplier = when (bonusType) {
|
||||
WeaponInterface.BONUS_CRUSH -> 1.25
|
||||
WeaponInterface.BONUS_SLASH -> 1.0
|
||||
WeaponInterface.BONUS_STAB -> 0.85
|
||||
else -> 0.75
|
||||
}
|
||||
val styleAdjustedMax = floor(maxHit * styleMultiplier).toInt().coerceAtLeast(1)
|
||||
return RandomFunction.random(styleAdjustedMax + 1).coerceIn(0, 25)
|
||||
}
|
||||
|
||||
private fun getAttackAnimation(): Int {
|
||||
val attackAnim = player.properties.attackAnimation
|
||||
return if (attackAnim != null && attackAnim.id > 0) attackAnim.id else 422
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsRespawnArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
import core.api.EquipmentSlot
|
||||
import core.api.sendMessage
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.item.GroundItemManager
|
||||
|
||||
object CastleWarsEquipmentRestrictions {
|
||||
private val restrictedSlots = intArrayOf(
|
||||
EquipmentSlot.HEAD.ordinal, EquipmentSlot.CAPE.ordinal
|
||||
)
|
||||
private val castleWarsTeamCapeIds = intArrayOf(
|
||||
CastleWars.saradominTeamHoodedCloak, CastleWars.zamorakTeamHoodedCloak
|
||||
)
|
||||
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
|
||||
sendMessage(player, "You can't wear hats, capes, or helms in the arena.")
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeInvalidEquipment(player: Player) {
|
||||
removeEquipment(player, EquipmentSlot.HEAD.ordinal)
|
||||
|
||||
val cape = player.equipment[EquipmentSlot.CAPE.ordinal] ?: return
|
||||
if (cape.id !in castleWarsTeamCapeIds) {
|
||||
removeEquipment(player, EquipmentSlot.CAPE.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (!player.inventory.add(item)) {
|
||||
GroundItemManager.create(item.copy(), player.location, player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.sendMessage
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.world.map.Location
|
||||
|
||||
/**
|
||||
* Handles flag interactions in Castle Wars:
|
||||
* - Taking the enemy flag from their stand
|
||||
* - Capturing a flag at your own stand
|
||||
* - Picking up dropped flags
|
||||
* - Returning your own dropped flag
|
||||
*/
|
||||
class CastleWarsFlagHandler : InteractionListener {
|
||||
|
||||
companion object {
|
||||
|
||||
// Zone borders for flag stand areas (top floor of each castle)
|
||||
private val saradominFlagArea =
|
||||
core.game.world.map.zone.ZoneBorders(Location.create(2426, 3071, 3), Location.create(2431, 3078, 3))
|
||||
private val zamorakFlagArea =
|
||||
core.game.world.map.zone.ZoneBorders(Location.create(2368, 3129, 3), Location.create(2373, 3136, 3))
|
||||
|
||||
/**
|
||||
* Check if a location is in the Saradomin flag area
|
||||
*/
|
||||
fun isInSaradominFlagArea(location: Location): Boolean {
|
||||
return saradominFlagArea.insideBorder(location)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a location is in the Zamorak flag area
|
||||
*/
|
||||
fun isInZamorakFlagArea(location: Location): Boolean {
|
||||
return zamorakFlagArea.insideBorder(location)
|
||||
}
|
||||
}
|
||||
|
||||
override fun defineListeners() { // Handle clicking on flag stands (both with and without flags)
|
||||
on(CastleWars.allFlagStandIds, IntType.SCENERY, "capture") { player, node ->
|
||||
if (!CastleWarsGameState.isGameActive) {
|
||||
sendMessage(player, "There is no game in progress.")
|
||||
return@on true
|
||||
}
|
||||
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
|
||||
if (playerTeam == null) {
|
||||
sendMessage(player, "You are not in a Castle Wars game.")
|
||||
return@on true
|
||||
}
|
||||
val sceneryId = node.id
|
||||
val location = node.location
|
||||
// 4902 = Saradomin flag stand with flag, 4377 = empty Saradomin stand
|
||||
// 4903 = Zamorak flag stand with flag, 4378 = empty Zamorak stand
|
||||
val isSaradominStand =
|
||||
sceneryId == CastleWars.saradominFlagStand || sceneryId == CastleWars.emptyFlagStand1 || isInSaradominFlagArea(
|
||||
location
|
||||
)
|
||||
val isZamorakStand =
|
||||
sceneryId == CastleWars.zamorakFlagStand || sceneryId == CastleWars.emptyFlagStand2 || isInZamorakFlagArea(
|
||||
location
|
||||
)
|
||||
val standTeam = when {
|
||||
isSaradominStand -> CastleWarsTeam.SARADOMIN
|
||||
isZamorakStand -> CastleWarsTeam.ZAMORAK
|
||||
else -> {
|
||||
sendMessage(player, "You can't interact with this.")
|
||||
return@on true
|
||||
}
|
||||
}
|
||||
val isEmptyStand = sceneryId in intArrayOf(
|
||||
CastleWars.emptyFlagStand1, CastleWars.emptyFlagStand2, CastleWars.emptyFlagStand3
|
||||
)
|
||||
if (CastleWarsGameState.isCarryingFlag(player)) {
|
||||
if (standTeam == playerTeam) {
|
||||
val ownFlagState = if (playerTeam == CastleWarsTeam.SARADOMIN) {
|
||||
CastleWarsGameState.saradominFlagState
|
||||
} else {
|
||||
CastleWarsGameState.zamorakFlagState
|
||||
}
|
||||
|
||||
if (ownFlagState != FlagState.SAFE) {
|
||||
sendMessage(player, "Your team's flag must be at its stand to capture!")
|
||||
return@on true
|
||||
}
|
||||
CastleWarsGameState.captureFlag(player)
|
||||
} else {
|
||||
sendMessage(player, "You need to take this flag to your own flag stand!")
|
||||
}
|
||||
return@on true
|
||||
}
|
||||
if (isEmptyStand) {
|
||||
sendMessage(player, "The flag isn't here!")
|
||||
return@on true
|
||||
}
|
||||
if (standTeam != playerTeam) {
|
||||
val flagState = if (standTeam == CastleWarsTeam.SARADOMIN) {
|
||||
CastleWarsGameState.saradominFlagState
|
||||
} else {
|
||||
CastleWarsGameState.zamorakFlagState
|
||||
}
|
||||
|
||||
if (flagState != FlagState.SAFE) {
|
||||
sendMessage(player, "The flag isn't here!")
|
||||
return@on true
|
||||
}
|
||||
CastleWarsGameState.pickUpFlag(player, standTeam)
|
||||
} else {
|
||||
sendMessage(player, "This is your team's flag stand.")
|
||||
}
|
||||
|
||||
return@on true
|
||||
}
|
||||
// Intercept flag unequip - drop on ground instead of adding to inventory
|
||||
// When player clicks "Remove" on the flag in equipment screen, or tries to equip another weapon,
|
||||
// the flag should drop at their feet instead of going to their inventory.
|
||||
onUnequip(intArrayOf(CastleWars.saradominFlag, CastleWars.zamorakFlag)) { player, _ ->
|
||||
if (CastleWarsGameState.isCarryingFlag(player)) {
|
||||
CastleWarsGameState.dropFlag(player)
|
||||
return@onUnequip false
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
return@onUnequip false
|
||||
}
|
||||
return@onUnequip true
|
||||
}
|
||||
on(CastleWars.droppedFlagSceneryIds, IntType.SCENERY, "take") { player, node ->
|
||||
if (!CastleWarsGameState.isGameActive) {
|
||||
sendMessage(player, "There is no game in progress.")
|
||||
return@on true
|
||||
}
|
||||
val playerTeam = CastleWarsGameState.getActiveGameTeam(player)
|
||||
if (playerTeam == null) {
|
||||
sendMessage(player, "You are not in a Castle Wars game.")
|
||||
return@on true
|
||||
}
|
||||
val scenery = node as? core.game.node.scenery.Scenery ?: return@on true
|
||||
val flagTeam = when (scenery.id) {
|
||||
CastleWars.saradominDroppedFlagScenery -> CastleWarsTeam.SARADOMIN
|
||||
CastleWars.zamorakDroppedFlagScenery -> CastleWarsTeam.ZAMORAK
|
||||
else -> return@on true
|
||||
}
|
||||
val isValidDroppedFlag = when (flagTeam) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsGameState.saradominDroppedFlagScenery?.location == scenery.location
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsGameState.zamorakDroppedFlagScenery?.location == scenery.location
|
||||
}
|
||||
if (!isValidDroppedFlag) {
|
||||
sendMessage(player, "The flag isn't here!")
|
||||
return@on true
|
||||
}
|
||||
if (flagTeam == playerTeam) {
|
||||
CastleWarsGameState.pickUpOwnFlag(player, flagTeam)
|
||||
} else {
|
||||
CastleWarsGameState.pickUpFlag(player, flagTeam)
|
||||
}
|
||||
|
||||
return@on true
|
||||
}
|
||||
}
|
||||
}
|
||||
1793
Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt
Normal file
1793
Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,114 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsRespawnArea
|
||||
import core.api.*
|
||||
import core.game.node.entity.Entity
|
||||
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.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.
|
||||
*/
|
||||
class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDOM_EVENTS), StartupListener {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Handle Castle Wars death with poison preservation.
|
||||
* 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.getActiveGameTeam(player) ?: return false
|
||||
|
||||
if (CastleWarsGameState.isCarryingFlag(player)) {
|
||||
CastleWarsGameState.dropFlag(player)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
}
|
||||
|
||||
PulseManager.cancelDeathTask(player)
|
||||
|
||||
val poisonTimer = getTimer<Poison>(player)
|
||||
val poisonSeverity: Int? = poisonTimer?.severity
|
||||
val poisonSource: Entity? = poisonTimer?.damageSource
|
||||
|
||||
player.fullRestore()
|
||||
|
||||
if (poisonSeverity != null && poisonSeverity > 0 && poisonSource != null) {
|
||||
registerTimer(player, spawnTimer<Poison>(poisonSource, poisonSeverity))
|
||||
}
|
||||
|
||||
val respawnLoc = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsRespawnArea.zamorakRespawnRoom.randomWalkableLoc
|
||||
}
|
||||
player.properties.teleportLocation = respawnLoc
|
||||
sendMessage(player, "You have been defeated and respawn at your team's base.")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun startup() {
|
||||
ZoneBuilder.configure(this)
|
||||
for (border in CastleWarsGameArea.areaBorders) {
|
||||
register(border)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configure() {
|
||||
}
|
||||
|
||||
override fun death(e: Entity, killer: Entity?): Boolean {
|
||||
if (e !is Player) return false
|
||||
return handleCastleWarsDeath(e)
|
||||
}
|
||||
|
||||
override fun move(e: Entity, from: Location, to: Location): Boolean {
|
||||
val dx = to.x - from.x
|
||||
val dy = to.y - from.y
|
||||
|
||||
val isMovingUp = to.z > from.z
|
||||
val isChangingZLevel = to.z != from.z
|
||||
|
||||
for (n in findLocalNPCs(e.location, 5)) {
|
||||
if (n.isInvisible || n === e) {
|
||||
continue
|
||||
}
|
||||
if (n.shouldPreventStacking(e)) {
|
||||
val s1 = e.size()
|
||||
val s2 = n.size()
|
||||
val l = n.location
|
||||
|
||||
if (Pathfinder.isStandingIn(to.x, to.y, s1, s1, l.x, l.y, s2, s2)) {
|
||||
if (isChangingZLevel && !isMovingUp) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (!isChangingZLevel && dx != 0 && dy != 0) {
|
||||
val intermediateXOverlap = Pathfinder.isStandingIn(from.x + dx, from.y, s1, s1, l.x, l.y, s2, s2)
|
||||
val intermediateYOverlap = Pathfinder.isStandingIn(
|
||||
from.x, from.y + dy, s1, s1, l.x, l.y, s2, s2
|
||||
)
|
||||
if (intermediateXOverlap || intermediateYOverlap) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e !is Player) return true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getZoneType(): Int {
|
||||
return ZoneType.CASTLE_WARS.id
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CastleWarsConstants.BANDAGE_COOLDOWN_TICKS
|
||||
import content.minigame.castlewars.CastleWarsConstants.BANDAGE_HEAL_PERCENTAGE
|
||||
import content.minigame.castlewars.CastleWarsConstants.BANDAGE_RUN_ENERGY_RESTORE
|
||||
import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_DAMAGE_PER_TICK
|
||||
import content.minigame.castlewars.CastleWarsConstants.BURNING_BARRICADE_MAX_TICKS
|
||||
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 core.api.*
|
||||
import core.game.interaction.Clocks
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.node.entity.combat.ImpactHandler
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.node.item.Item
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import org.rs09.consts.Animations
|
||||
import org.rs09.consts.Items
|
||||
import org.rs09.consts.NPCs
|
||||
|
||||
|
||||
/**
|
||||
* Handles Castle Wars item mechanics:
|
||||
* - Bandages: Heal 10% of max HP, restore 30% run energy, can heal teammates
|
||||
* - Barricades: Place barricade NPC with 50 HP, max 10 per team
|
||||
* - Explosive Potions: Instant-destroy barricades, 15% max HP self-damage on drop
|
||||
*/
|
||||
class CastleWarsItemHandler : InteractionListener {
|
||||
|
||||
companion object {
|
||||
|
||||
val saradominBarricades = mutableListOf<NPC>()
|
||||
val zamorakBarricades = mutableListOf<NPC>()
|
||||
|
||||
val burningBarricadePulses = mutableMapOf<NPC, core.game.system.task.Pulse>()
|
||||
|
||||
fun getBarricadeCount(team: CastleWarsTeam): Int {
|
||||
return when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> saradominBarricades.count { it.isActive }
|
||||
CastleWarsTeam.ZAMORAK -> zamorakBarricades.count { it.isActive }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllBarricades() {
|
||||
burningBarricadePulses.values.forEach { it.stop() }
|
||||
burningBarricadePulses.clear()
|
||||
|
||||
saradominBarricades.forEach { if (it.isActive) it.clear() }
|
||||
zamorakBarricades.forEach { if (it.isActive) it.clear() }
|
||||
saradominBarricades.clear()
|
||||
zamorakBarricades.clear()
|
||||
}
|
||||
|
||||
fun cleanupDeadBarricades() {
|
||||
saradominBarricades.removeAll { !it.isActive }
|
||||
zamorakBarricades.removeAll { !it.isActive }
|
||||
}
|
||||
|
||||
fun dealExplosiveAoeDamage(attacker: Player, location: Location, radius: Int = 1) {
|
||||
val attackerTeam = CastleWarsGameState.getActiveGameTeam(attacker) ?: return
|
||||
RegionManager.getLocalPlayers(location, radius).filter { target ->
|
||||
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(
|
||||
attacker, damage, ImpactHandler.HitsplatType.NORMAL
|
||||
)
|
||||
sendMessage(target, "You are caught in an explosion!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun defineListeners() {
|
||||
on(CastleWars.cwBandages, IntType.ITEM, "Heal") { player, node ->
|
||||
if (useBandage(player, player)) {
|
||||
removeItem(player, node.asItem())
|
||||
}
|
||||
return@on true
|
||||
}
|
||||
|
||||
onUseWithPlayer(CastleWars.cwBandages) { player, used, with ->
|
||||
val target = with as? Player ?: return@onUseWithPlayer false
|
||||
if (useBandage(player, target)) {
|
||||
removeItem(player, used.asItem())
|
||||
}
|
||||
return@onUseWithPlayer true
|
||||
}
|
||||
|
||||
on(CastleWars.cwBarricade, IntType.ITEM, "set-up") { player, node ->
|
||||
if (!CastleWarsGameState.isGameActive) {
|
||||
sendMessage(player, "You can only set up barricades during a game.")
|
||||
return@on false
|
||||
}
|
||||
|
||||
val team = CastleWarsGameState.getActiveGameTeam(player)
|
||||
if (team == null) {
|
||||
sendMessage(player, "You're not in a Castle Wars game.")
|
||||
return@on false
|
||||
}
|
||||
|
||||
cleanupDeadBarricades()
|
||||
if (getBarricadeCount(team) >= MAX_BARRICADES_PER_TEAM) {
|
||||
sendMessage(
|
||||
player, "Your team already has the maximum number of barricades ($MAX_BARRICADES_PER_TEAM)."
|
||||
)
|
||||
return@on false
|
||||
}
|
||||
|
||||
val nearbyBarricades = getLocalBarricades(player.location, 0)
|
||||
if (nearbyBarricades.isNotEmpty()) {
|
||||
sendMessage(player, "There's already a barricade here.")
|
||||
return@on false
|
||||
}
|
||||
|
||||
val barricade = CastleWarsBarricadeNPC.spawn(player.location, team)
|
||||
barricade.skills.setStaticLevel(Skills.HITPOINTS, 50)
|
||||
barricade.skills.lifepoints = 50
|
||||
|
||||
when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> saradominBarricades.add(barricade)
|
||||
CastleWarsTeam.ZAMORAK -> zamorakBarricades.add(barricade)
|
||||
}
|
||||
|
||||
removeItem(player, node.asItem())
|
||||
animate(player, 827) // Place animation
|
||||
sendMessage(player, "You set up a barricade.")
|
||||
|
||||
return@on true
|
||||
}
|
||||
|
||||
onUseWith(
|
||||
IntType.NPC, CastleWars.cwExplosivePotion, *intArrayOf(
|
||||
NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535
|
||||
)
|
||||
) { player, used, with ->
|
||||
val barricade = with as? NPC ?: return@onUseWith false
|
||||
|
||||
if (!isActiveGamePlayer(player)) {
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
if (barricade.id == NPCs.BARRICADE_1533) {
|
||||
sendMessage(player, "You cannot blow up a burning barricade.")
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
animate(player, Animations.HUMAN_BURYING_BONES_827)
|
||||
playAudio(player, 2599)
|
||||
|
||||
sendGraphics(157, barricade.location)
|
||||
|
||||
barricade.skills.lifepoints = 0
|
||||
barricade.clear()
|
||||
cleanupDeadBarricades()
|
||||
|
||||
removeItem(player, used.asItem())
|
||||
sendMessage(player, "The explosive potion destroys the barricade!")
|
||||
|
||||
dealExplosiveAoeDamage(player, barricade.location)
|
||||
|
||||
return@onUseWith true
|
||||
}
|
||||
|
||||
on(CastleWars.cwExplosivePotion, IntType.ITEM, "drop") { player, node ->
|
||||
if (!isActiveGamePlayer(player)) {
|
||||
return@on false
|
||||
}
|
||||
|
||||
player.impactHandler.manualHit(
|
||||
player,
|
||||
EXPLOSIVE_POTION_SELF_DAMAGE.coerceAtMost(player.skills.lifepoints),
|
||||
ImpactHandler.HitsplatType.NORMAL
|
||||
)
|
||||
animate(player, Animations.HUMAN_BURYING_BONES_827)
|
||||
playAudio(player, 2599) // Fire/explosion sound
|
||||
|
||||
removeItem(player, node.asItem())
|
||||
sendMessage(player, "The potion explodes! You take damage.")
|
||||
|
||||
val nearbyBarricades = getLocalBarricades(player.location, 1)
|
||||
for (barricade in nearbyBarricades) {
|
||||
if (barricade.id == NPCs.BARRICADE_1533) continue
|
||||
|
||||
sendGraphics(157, barricade.location)
|
||||
barricade.skills.lifepoints = 0
|
||||
barricade.clear()
|
||||
}
|
||||
cleanupDeadBarricades()
|
||||
|
||||
return@on true
|
||||
}
|
||||
|
||||
onUseWith(
|
||||
IntType.NPC, Items.TINDERBOX_590, *intArrayOf(
|
||||
NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535
|
||||
)
|
||||
) { player, _, with ->
|
||||
val barricade = with as? NPC ?: return@onUseWith false
|
||||
|
||||
if (!isActiveGamePlayer(player)) {
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
if (barricade.id == NPCs.BARRICADE_1533) {
|
||||
sendMessage(player, "The barricade is already on fire!")
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
val burningId = NPCs.BARRICADE_1533
|
||||
|
||||
playAudio(player, 2599)
|
||||
|
||||
burningBarricadePulses.remove(barricade)?.stop()
|
||||
barricade.transform(burningId)
|
||||
sendMessage(player, "You set the barricade on fire!")
|
||||
|
||||
val firePulse = object : core.game.system.task.Pulse(BURNING_BARRICADE_PULSE_INTERVAL) {
|
||||
var tickCount = 0
|
||||
override fun pulse(): Boolean {
|
||||
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
|
||||
barricade.clear()
|
||||
cleanupDeadBarricades()
|
||||
}
|
||||
return true
|
||||
}
|
||||
barricade.skills.lifepoints =
|
||||
(barricade.skills.lifepoints - BURNING_BARRICADE_DAMAGE_PER_TICK).coerceAtLeast(0)
|
||||
tickCount++
|
||||
return false
|
||||
}
|
||||
}
|
||||
burningBarricadePulses[barricade] = firePulse
|
||||
submitWorldPulse(firePulse)
|
||||
|
||||
return@onUseWith true
|
||||
}
|
||||
|
||||
onUseWith(IntType.NPC, Items.BUCKET_OF_WATER_1929, NPCs.BARRICADE_1533) { player, used, with ->
|
||||
val barricade = with as? NPC ?: return@onUseWith false
|
||||
|
||||
if (!isActiveGamePlayer(player)) {
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
val originalId = if (saradominBarricades.contains(barricade)) {
|
||||
NPCs.BARRICADE_1532
|
||||
} else {
|
||||
NPCs.BARRICADE_1534
|
||||
}
|
||||
|
||||
barricade.transform(originalId)
|
||||
burningBarricadePulses.remove(barricade)?.stop()
|
||||
|
||||
replaceSlot(player, used.asItem().slot, Item(Items.BUCKET_1925))
|
||||
|
||||
playAudio(player, 2401)
|
||||
sendMessage(player, "You extinguish the fire on the barricade.")
|
||||
|
||||
return@onUseWith true
|
||||
}
|
||||
}
|
||||
|
||||
private fun useBandage(user: Player, target: Player): Boolean {
|
||||
if (!CastleWarsGameState.isGameActive) {
|
||||
sendMessage(user, "You can only use bandages during a Castle Wars game.")
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
if (userTeam != targetTeam) {
|
||||
sendMessage(user, "You can only heal your teammates!")
|
||||
return false
|
||||
}
|
||||
|
||||
if (CastleWarsGameState.isCarryingOwnFlag(target)) {
|
||||
if (user == target) {
|
||||
sendMessage(user, "You can't heal while carrying your team's flag!")
|
||||
} else {
|
||||
sendMessage(user, "You can't heal a player carrying their team's flag!")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (target.clocks[Clocks.NEXT_EAT] > getWorldTicks()) {
|
||||
return false
|
||||
}
|
||||
target.clocks[Clocks.NEXT_EAT] = getWorldTicks() + BANDAGE_COOLDOWN_TICKS
|
||||
|
||||
val maxHp = target.skills.getStaticLevel(Skills.HITPOINTS)
|
||||
val healPercent = if (user.hasActiveCastleWarsBracelet()) {
|
||||
CastleWarsConstants.BRACELET_BANDAGE_HEAL_PERCENTAGE
|
||||
} else {
|
||||
BANDAGE_HEAL_PERCENTAGE
|
||||
}
|
||||
val healAmount = (maxHp * healPercent).toInt().coerceAtLeast(1)
|
||||
|
||||
val currentHp = target.skills.lifepoints
|
||||
val newHp = (currentHp + healAmount).coerceAtMost(maxHp)
|
||||
target.skills.lifepoints = newHp
|
||||
|
||||
// Restore run energy
|
||||
val currentEnergy = target.settings.runEnergy
|
||||
target.settings.runEnergy = (currentEnergy + BANDAGE_RUN_ENERGY_RESTORE).coerceAtMost(100.0)
|
||||
|
||||
curePoison(target)
|
||||
|
||||
if (user == target) {
|
||||
sendMessage(user, "You apply the bandage and feel better.")
|
||||
} else {
|
||||
sendMessage(user, "You apply the bandage to ${target.username}.")
|
||||
sendMessage(target, "${user.username} applies a bandage to you.")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun isActiveGamePlayer(player: Player): Boolean {
|
||||
return CastleWarsGameState.isGameActive && CastleWarsGameState.getActiveGameTeam(player) != null
|
||||
}
|
||||
|
||||
private fun getLocalBarricades(location: Location, radius: Int): List<NPC> {
|
||||
val barricadeIds = intArrayOf(
|
||||
NPCs.BARRICADE_1532, NPCs.BARRICADE_1533, NPCs.BARRICADE_1534, NPCs.BARRICADE_1535
|
||||
)
|
||||
|
||||
return findLocalNPCs(location, radius).filter { it.id in barricadeIds && it.isActive }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,6 @@
|
|||
import core.api.*
|
||||
package content.minigame.castlewars
|
||||
|
||||
import core.api.setVarp
|
||||
import core.game.node.entity.player.Player
|
||||
|
||||
/**
|
||||
|
|
@ -7,16 +9,7 @@ import core.game.node.entity.player.Player
|
|||
*/
|
||||
object CastleWarsOverlay {
|
||||
@JvmStatic
|
||||
fun sendLobbyUpdate(player: Player, bothTeamsHavePlayers: Boolean, gameStartMinutes: Int) {
|
||||
setVarp(player, 380, if (bothTeamsHavePlayers) gameStartMinutes else 0)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun sendGameUpdate(player: Player) {
|
||||
// Todo - Figure out underground mine/etc
|
||||
setVarbit(player, 143, 0) // Flag status - safe = 0, taken = 1, dropped = 2
|
||||
setVarbit(player, 145, 5) // Saradomin's score
|
||||
setVarbit(player, 153, 0) // Flag status - safe = 0, taken = 1, dropped = 2
|
||||
setVarbit(player, 155, 7) // Zamorak's score
|
||||
fun sendLobbyUpdate(player: Player, gameStartMinutes: Int) {
|
||||
setVarp(player, 380, gameStartMinutes)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsRespawnArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
import core.api.ShutdownListener
|
||||
import core.api.removeAttributes
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
|
||||
/**
|
||||
* Handles Castle Wars cleanup when the server shuts down.
|
||||
* Ensures all players in any Castle Wars area are properly removed
|
||||
* so they don't end up in a zombie match state when they log back in.
|
||||
*/
|
||||
class CastleWarsShutdownHandler : ShutdownListener {
|
||||
|
||||
override fun shutdown() {
|
||||
val allTrackedPlayers = mutableSetOf<Player>()
|
||||
allTrackedPlayers += CastleWarsGameArea.saradominPlayers
|
||||
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
|
||||
)
|
||||
for (border in respawnBorders) {
|
||||
val centerX = (border.southWestX + border.northEastX) / 2
|
||||
val centerY = (border.southWestY + border.northEastY) / 2
|
||||
val centerLocation = Location.create(centerX, centerY, border.plane)
|
||||
for (player in RegionManager.getLocalPlayers(centerLocation, 20)) {
|
||||
if (border.insideBorder(player.location)) {
|
||||
allTrackedPlayers.add(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
val centerLocation = Location.create(centerX, centerY, plane)
|
||||
for (player in RegionManager.getLocalPlayers(centerLocation, radius)) {
|
||||
if (border.insideBorder(player.location)) {
|
||||
allTrackedPlayers.add(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (player in allTrackedPlayers) {
|
||||
if (CastleWarsGameState.isCarryingFlag(player)) {
|
||||
CastleWarsGameState.dropFlag(player, broadcast = false)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player, broadcast = false)
|
||||
}
|
||||
CastleWarsGameState.restoreAndCleanupPlayer(player)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import core.api.sendMessage
|
||||
import core.game.dialogue.DialogueFile
|
||||
import core.game.dialogue.Topic
|
||||
import core.tools.END_DIALOGUE
|
||||
import core.tools.START_DIALOGUE
|
||||
|
||||
class CastleWarsSubstitutionDialogue(private val team: CastleWarsTeam) : DialogueFile() {
|
||||
|
||||
override fun handle(componentID: Int, buttonID: Int) {
|
||||
val currentPlayer = player ?: return
|
||||
when (stage) {
|
||||
START_DIALOGUE -> showTopics(
|
||||
Topic("Yes.", ACCEPT, true),
|
||||
Topic("No.", DECLINE, true),
|
||||
title = "Do you wish to join the current game?"
|
||||
)
|
||||
|
||||
ACCEPT -> {
|
||||
if (!CastleWarsGameArea.claimVacancy(currentPlayer, team)) {
|
||||
sendMessage(currentPlayer, "That Castle Wars vacancy is no longer available.")
|
||||
}
|
||||
stage = END_DIALOGUE
|
||||
}
|
||||
|
||||
DECLINE -> {
|
||||
CastleWarsGameArea.declineVacancyOffer(currentPlayer, team)
|
||||
stage = END_DIALOGUE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = 10
|
||||
private const val DECLINE = 20
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.data.skill.SkillingTool
|
||||
import core.api.*
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.node.scenery.Scenery
|
||||
import core.game.system.task.Pulse
|
||||
import core.game.world.update.flag.context.Animation
|
||||
|
||||
/**
|
||||
* Mining pulse for Castle Wars tunnel rocks.
|
||||
*
|
||||
* Uses coal ore difficulty parameters:
|
||||
* - Level requirement: None (authentic Castle Wars behavior)
|
||||
* - Rate: 0.40 (coal ore rate - difficulty factor for success chance)
|
||||
*
|
||||
* This pulse replaces the instant 3-tick delay with proper mining mechanics
|
||||
* where players must successfully "hit" the rock multiple times to clear it.
|
||||
*/
|
||||
class CastleWarsTunnelMiningPulse(
|
||||
private val player: Player,
|
||||
private val rocks: Scenery,
|
||||
private val boulderKey: String,
|
||||
private val isWeakened: Boolean
|
||||
) : Pulse(1, player, rocks) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val REQUIRED_LEVEL = 1
|
||||
private const val RATE = 0.40
|
||||
private const val EXPERIENCE = 0.0
|
||||
}
|
||||
|
||||
private var ticks = 0
|
||||
private var started = false
|
||||
|
||||
override fun start() {
|
||||
if (!checkRequirements()) {
|
||||
return
|
||||
}
|
||||
super.start()
|
||||
started = true
|
||||
sendMessage(player, "You swing your pickaxe at the rocks...")
|
||||
}
|
||||
|
||||
override fun pulse(): Boolean {
|
||||
if (!started) return true
|
||||
|
||||
if (!checkRequirements()) {
|
||||
return true
|
||||
}
|
||||
|
||||
animate()
|
||||
return checkReward()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (started) {
|
||||
animate(player, Animation(-1))
|
||||
}
|
||||
super.stop()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun animate() {
|
||||
val pickaxe = SkillingTool.getPickaxe(player) ?: return
|
||||
animate(player, pickaxe.animation)
|
||||
}
|
||||
|
||||
private fun checkReward(): Boolean {
|
||||
ticks++
|
||||
if (ticks % 4 != 0) {
|
||||
return false
|
||||
}
|
||||
val pickaxe = SkillingTool.getPickaxe(player) ?: return true
|
||||
val level = 1 + getDynLevel(player, Skills.MINING) + getFamiliarBoost(player, Skills.MINING)
|
||||
val hostRatio = Math.random() * (100.0 * RATE)
|
||||
val toolRatio = pickaxe.ratio
|
||||
val clientRatio = Math.random() * ((level - REQUIRED_LEVEL) * (1.0 + toolRatio))
|
||||
|
||||
if (hostRatio < clientRatio) {
|
||||
clearRocks()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun clearRocks() {
|
||||
rewardXP(player, Skills.MINING, EXPERIENCE)
|
||||
|
||||
if (isWeakened) {
|
||||
CastleWarsGameState.clearBoulder(boulderKey)
|
||||
sendMessage(player, "You manage to clear the rocks.")
|
||||
} else {
|
||||
CastleWarsGameState.weakenBoulder(boulderKey)
|
||||
sendMessage(player, "You manage to weaken the rocks.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CastleWarsCaveWallMiningPulse(
|
||||
private val player: Player, private val caveWall: Scenery, private val boulderKey: String
|
||||
) : Pulse(1, player, caveWall) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val REQUIRED_LEVEL = 1
|
||||
private const val RATE = 0.40
|
||||
private const val EXPERIENCE = 0.0
|
||||
}
|
||||
|
||||
private var ticks = 0
|
||||
private var started = false
|
||||
|
||||
override fun start() {
|
||||
if (!checkRequirements()) {
|
||||
return
|
||||
}
|
||||
super.start()
|
||||
started = true
|
||||
sendMessage(player, "You swing your pickaxe at the cave wall...")
|
||||
}
|
||||
|
||||
override fun pulse(): Boolean {
|
||||
if (!started) return true
|
||||
|
||||
if (!checkRequirements()) {
|
||||
return true
|
||||
}
|
||||
if (!CastleWarsGameState.isBoulderCleared(boulderKey)) {
|
||||
sendMessage(player, "The passage has already collapsed.")
|
||||
return true
|
||||
}
|
||||
|
||||
animate()
|
||||
return checkReward()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (started) {
|
||||
animate(player, Animation(-1))
|
||||
}
|
||||
super.stop()
|
||||
}
|
||||
|
||||
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.")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun animate() {
|
||||
val pickaxe = SkillingTool.getPickaxe(player) ?: return
|
||||
animate(player, pickaxe.animation)
|
||||
}
|
||||
|
||||
private fun checkReward(): Boolean {
|
||||
ticks++
|
||||
if (ticks % 4 != 0) {
|
||||
return false
|
||||
}
|
||||
val pickaxe = SkillingTool.getPickaxe(player) ?: return true
|
||||
val level = 1 + getDynLevel(player, Skills.MINING) + getFamiliarBoost(player, Skills.MINING)
|
||||
val hostRatio = Math.random() * (100.0 * RATE)
|
||||
val toolRatio = pickaxe.ratio
|
||||
val clientRatio = Math.random() * ((level - REQUIRED_LEVEL) * (1.0 + toolRatio))
|
||||
|
||||
if (hostRatio < clientRatio) {
|
||||
collapsePassage()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun collapsePassage() {
|
||||
rewardXP(player, Skills.MINING, EXPERIENCE)
|
||||
CastleWarsGameState.collapseBoulder(boulderKey)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -1,25 +1,18 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.data.Quests
|
||||
import content.global.handlers.iface.BookInterface
|
||||
import content.global.handlers.iface.BookLine
|
||||
import content.global.handlers.iface.Page
|
||||
import content.global.handlers.iface.PageSet
|
||||
import core.api.getAttribute
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.system.command.sets.ModelViewerCommandSet.Companion.ATTRIBUTE_MODEL_NUMBER
|
||||
import core.game.system.command.sets.ModelViewerCommandSet.Companion.ATTRIBUTE_PITCH
|
||||
import core.game.system.command.sets.ModelViewerCommandSet.Companion.ATTRIBUTE_YAW
|
||||
import core.game.system.command.sets.ModelViewerCommandSet.Companion.ATTRIBUTE_ZOOM
|
||||
import core.game.system.command.sets.ModelViewerCommandSet.Companion.DEF_BOOK
|
||||
import core.game.world.GameWorld
|
||||
import org.rs09.consts.Items
|
||||
|
||||
class CastlewarsBook : InteractionListener {
|
||||
|
||||
companion object {
|
||||
|
||||
private val TITLE = "Castle Wars Manual"
|
||||
private val CONTENTS = arrayOf(
|
||||
PageSet(
|
||||
|
|
@ -30,8 +23,7 @@ class CastlewarsBook : InteractionListener {
|
|||
BookLine("their team standard. Then", 41),
|
||||
BookLine("bring that back and capture", 42),
|
||||
BookLine("it on your teams standard. ", 43),
|
||||
),
|
||||
Page(
|
||||
), Page(
|
||||
BookLine("Toolkit:", 58),
|
||||
BookLine("This useful item allows you", 59),
|
||||
BookLine("to repair broken doors and", 60),
|
||||
|
|
@ -50,8 +42,7 @@ class CastlewarsBook : InteractionListener {
|
|||
BookLine("of your running energy.", 46),
|
||||
BookLine("You can also use them to", 47),
|
||||
BookLine("heal fellow players.", 48),
|
||||
),
|
||||
Page(
|
||||
), Page(
|
||||
BookLine("Explosive Potion:", 58),
|
||||
BookLine("A simple but effective item,", 59),
|
||||
BookLine("use it to blow up your", 60),
|
||||
|
|
@ -72,8 +63,7 @@ class CastlewarsBook : InteractionListener {
|
|||
BookLine("your castle. Each team can", 47),
|
||||
BookLine("only have 10 built at any one", 48),
|
||||
BookLine("time.", 49),
|
||||
),
|
||||
Page(
|
||||
), Page(
|
||||
BookLine("Bucket:", 58),
|
||||
BookLine("Fill a bucket with water and", 59),
|
||||
BookLine("you can use it to put out a", 60),
|
||||
|
|
@ -90,8 +80,7 @@ class CastlewarsBook : InteractionListener {
|
|||
BookLine("set light to your opponents", 46),
|
||||
BookLine("You can also use them to", 47),
|
||||
BookLine("catapult and barricades.", 48),
|
||||
),
|
||||
Page(
|
||||
), Page(
|
||||
BookLine("Pickaxe:", 58),
|
||||
BookLine("Use a pickaxe to mine your", 59),
|
||||
BookLine("way through the tunnels", 60),
|
||||
|
|
@ -111,8 +100,7 @@ class CastlewarsBook : InteractionListener {
|
|||
BookLine("coordinates and let the rock", 47),
|
||||
BookLine("fly, just be careful not to hit", 48),
|
||||
BookLine("your team with it!", 49),
|
||||
),
|
||||
Page(
|
||||
), Page(
|
||||
BookLine("Rock:", 58),
|
||||
BookLine("Used as ammo for the", 59),
|
||||
BookLine("catapult, and not much else.", 60),
|
||||
|
|
@ -121,18 +109,109 @@ class CastlewarsBook : InteractionListener {
|
|||
)
|
||||
),
|
||||
)
|
||||
private fun display(player: Player, pageNum: Int, buttonID: Int) : Boolean {
|
||||
|
||||
private fun display(player: Player, pageNum: Int, buttonID: Int): Boolean {
|
||||
BookInterface.pageSetup(player, BookInterface.FANCY_BOOK_2_27, TITLE, CONTENTS)
|
||||
BookInterface.clearModelsOnPage(player, BookInterface.FANCY_BOOK_2_27);
|
||||
BookInterface.setItemOnPage(player, 0, Items.TOOLKIT_4051, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 1, Items.BANDAGES_4049, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 1, Items.EXPLOSIVE_POTION_4045, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 2, Items.BARRICADE_4053, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 2, Items.BUCKET_OF_WATER_1929, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 3, Items.TINDERBOX_590, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2], 768, 192, 1792)
|
||||
BookInterface.setItemOnPage(player, 3, Items.BRONZE_PICKAXE_1265, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17], 768, 192, 1792)
|
||||
BookInterface.setModelOnPage(player, 4, 38202, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[4], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[4], 4048, 192, 768)
|
||||
BookInterface.setItemOnPage(player, 4, Items.ROCK_4043, BookInterface.FANCY_BOOK_2_27, BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17], BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17], 768, 192, 1792)
|
||||
BookInterface.clearModelsOnPage(player, BookInterface.FANCY_BOOK_2_27)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
0,
|
||||
Items.TOOLKIT_4051,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
1,
|
||||
Items.BANDAGES_4049,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
1,
|
||||
Items.EXPLOSIVE_POTION_4045,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
2,
|
||||
Items.BARRICADE_4053,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
2,
|
||||
Items.BUCKET_OF_WATER_1929,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
3,
|
||||
Items.TINDERBOX_590,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[2],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[2],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
3,
|
||||
Items.BRONZE_PICKAXE_1265,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
BookInterface.setModelOnPage(
|
||||
player,
|
||||
4,
|
||||
38202,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[4],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[4],
|
||||
4048,
|
||||
192,
|
||||
768
|
||||
)
|
||||
BookInterface.setItemOnPage(
|
||||
player,
|
||||
4,
|
||||
Items.ROCK_4043,
|
||||
BookInterface.FANCY_BOOK_2_27,
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_ENABLE_DRAW_IDS[17],
|
||||
BookInterface.FANCY_BOOK_2_27_IMAGE_DRAW_IDS[17],
|
||||
768,
|
||||
192,
|
||||
1792
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
124
Server/src/main/content/minigame/castlewars/CatapultConfig.kt
Normal file
124
Server/src/main/content/minigame/castlewars/CatapultConfig.kt
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import org.rs09.consts.Components
|
||||
|
||||
/**
|
||||
* Configuration constants for the Castle Wars catapult interface.
|
||||
* All magic numbers and hardcoded values are centralized here.
|
||||
*/
|
||||
object CatapultConfig {
|
||||
|
||||
/** Interface ID for the catapult targeting interface */
|
||||
const val INTERFACE_ID = Components.CASTLEWARS_CATAPULT_54
|
||||
|
||||
/** Child ID for the red X marker on the map */
|
||||
const val MARKER_CHILD_ID = 55
|
||||
|
||||
/** Native Castle Wars dummy items used to render digits on interface 54 */
|
||||
val DIGIT_ITEM_IDS = intArrayOf(4056, 4057, 4058, 4059, 4060, 4061, 4062, 4063, 4064, 4065)
|
||||
|
||||
const val BUTTON_FIRE = 51
|
||||
const val BUTTON_CLOSE = 56
|
||||
const val BUTTON_UP = 65
|
||||
const val BUTTON_DOWN = 66
|
||||
const val BUTTON_RIGHT = 67
|
||||
const val BUTTON_LEFT = 68
|
||||
|
||||
val DIGIT_CHILD_IDS = intArrayOf(61, 62, 63, 64)
|
||||
|
||||
const val MAP_BASE_X = 12
|
||||
const val MAP_BASE_Y = 20
|
||||
const val MAP_WIDTH = 485
|
||||
const val MAP_HEIGHT = 297
|
||||
|
||||
const val MAX_COORDINATE = CastleWarsConstants.CATAPULT_MAX_COORDINATE
|
||||
const val DEFAULT_X = CastleWarsConstants.CATAPULT_DEFAULT_X
|
||||
const val DEFAULT_Y = CastleWarsConstants.CATAPULT_DEFAULT_Y
|
||||
const val CENTER_OFFSET = 15
|
||||
const val TILE_SCALE = 2
|
||||
|
||||
const val SOUND_FIRE = 1440
|
||||
const val SOUND_IMPACT = 851
|
||||
|
||||
const val GFX_BOULDER_PROJECTILE = 857
|
||||
const val GFX_IMPACT = 406
|
||||
|
||||
const val PROJECTILE_START_HEIGHT = 80
|
||||
const val PROJECTILE_END_HEIGHT = 0
|
||||
const val PROJECTILE_DELAY = 0
|
||||
const val PROJECTILE_BASE_SPEED = 60
|
||||
const val PROJECTILE_SPEED_PER_TILE = 5
|
||||
const val PROJECTILE_ANGLE = 25
|
||||
|
||||
const val IMPACT_RADIUS = CastleWarsConstants.CATAPULT_IMPACT_RADIUS
|
||||
const val MIN_DAMAGE = CastleWarsConstants.CATAPULT_MIN_DAMAGE
|
||||
const val MAX_DAMAGE = CastleWarsConstants.CATAPULT_MAX_DAMAGE
|
||||
const val INACCURACY_RADIUS = CastleWarsConstants.CATAPULT_INACCURACY_RADIUS
|
||||
|
||||
const val ATTR_LOCATION = "catapult:location"
|
||||
const val ATTR_SHOT_IN_FLIGHT = "catapult:shot-in-flight"
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the buttons on the catapult interface.
|
||||
*/
|
||||
enum class CatapultButton(val buttonId: Int, val deltaX: Int, val deltaY: Int) {
|
||||
LEFT(CatapultConfig.BUTTON_LEFT, -1, 0), RIGHT(CatapultConfig.BUTTON_RIGHT, 1, 0), UP(
|
||||
CatapultConfig.BUTTON_UP, 0, 1
|
||||
),
|
||||
DOWN(CatapultConfig.BUTTON_DOWN, 0, -1), FIRE(CatapultConfig.BUTTON_FIRE, 0, 0), CLOSE(
|
||||
CatapultConfig.BUTTON_CLOSE, 0, 0
|
||||
),
|
||||
UNKNOWN(-1, 0, 0);
|
||||
|
||||
companion object {
|
||||
fun fromId(id: Int): CatapultButton = values().find { it.buttonId == id } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing team-specific catapult configurations.
|
||||
* Handles coordinate transformations based on team orientation.
|
||||
*/
|
||||
enum class CatapultTeam {
|
||||
SARADOMIN {
|
||||
override fun calculateTargetX(catapultX: Int, interfaceX: Int): Int {
|
||||
return catapultX + ((interfaceX - CatapultConfig.CENTER_OFFSET) * CatapultConfig.TILE_SCALE)
|
||||
}
|
||||
|
||||
override fun calculateTargetY(catapultY: Int, interfaceY: Int): Int {
|
||||
return catapultY + (interfaceY * CatapultConfig.TILE_SCALE)
|
||||
}
|
||||
},
|
||||
|
||||
ZAMORAK {
|
||||
override fun calculateTargetX(catapultX: Int, interfaceX: Int): Int {
|
||||
return catapultX - ((interfaceX - CatapultConfig.CENTER_OFFSET) * CatapultConfig.TILE_SCALE)
|
||||
}
|
||||
|
||||
override fun calculateTargetY(catapultY: Int, interfaceY: Int): Int {
|
||||
return catapultY - (interfaceY * CatapultConfig.TILE_SCALE)
|
||||
}
|
||||
};
|
||||
|
||||
abstract fun calculateTargetX(catapultX: Int, interfaceX: Int): Int
|
||||
abstract fun calculateTargetY(catapultY: Int, interfaceY: Int): Int
|
||||
|
||||
companion object {
|
||||
fun fromCastleWarsTeam(team: CastleWarsTeam): CatapultTeam = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> SARADOMIN
|
||||
CastleWarsTeam.ZAMORAK -> ZAMORAK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing X/Y coordinates on the catapult interface.
|
||||
*/
|
||||
data class Coordinate(val x: Int, val y: Int) {
|
||||
fun clamp(): Coordinate = Coordinate(
|
||||
x.coerceIn(0, CatapultConfig.MAX_COORDINATE), y.coerceIn(0, CatapultConfig.MAX_COORDINATE)
|
||||
)
|
||||
|
||||
fun withDelta(deltaX: Int, deltaY: Int): Coordinate = Coordinate(x + deltaX, y + deltaY)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CatapultConfig.ATTR_SHOT_IN_FLIGHT
|
||||
import content.minigame.castlewars.CatapultConfig.GFX_BOULDER_PROJECTILE
|
||||
import content.minigame.castlewars.CatapultConfig.GFX_IMPACT
|
||||
import content.minigame.castlewars.CatapultConfig.IMPACT_RADIUS
|
||||
import content.minigame.castlewars.CatapultConfig.INACCURACY_RADIUS
|
||||
import content.minigame.castlewars.CatapultConfig.MAX_DAMAGE
|
||||
import content.minigame.castlewars.CatapultConfig.MIN_DAMAGE
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_ANGLE
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_BASE_SPEED
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_DELAY
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_END_HEIGHT
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_SPEED_PER_TILE
|
||||
import content.minigame.castlewars.CatapultConfig.PROJECTILE_START_HEIGHT
|
||||
import content.minigame.castlewars.CatapultConfig.SOUND_FIRE
|
||||
import content.minigame.castlewars.CatapultConfig.SOUND_IMPACT
|
||||
import core.api.*
|
||||
import core.game.node.entity.combat.ImpactHandler
|
||||
import core.game.node.entity.impl.Projectile
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.system.task.Pulse
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.tools.RandomFunction
|
||||
|
||||
/**
|
||||
* Handles firing the catapult and dealing impact damage.
|
||||
*/
|
||||
class CatapultFiringHandler {
|
||||
|
||||
companion object {
|
||||
internal fun hasShotInFlight(player: Player): Boolean {
|
||||
return getAttribute(player, ATTR_SHOT_IN_FLIGHT, false)
|
||||
}
|
||||
|
||||
internal fun markShotInFlight(player: Player) {
|
||||
setAttribute(player, ATTR_SHOT_IN_FLIGHT, true)
|
||||
}
|
||||
|
||||
internal fun clearShotInFlight(player: Player) {
|
||||
removeAttribute(player, ATTR_SHOT_IN_FLIGHT)
|
||||
}
|
||||
|
||||
internal fun sendShotInFlightMessage(player: Player) {
|
||||
sendMessage(player, "You need to wait for your previous catapult shot to land.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the catapult at the specified coordinates.
|
||||
* @param player The player firing the catapult
|
||||
* @param coord The interface coordinates to fire at
|
||||
* @param team The catapult team configuration
|
||||
* @param catapultLocation The actual world location of the catapult
|
||||
*/
|
||||
fun fire(player: Player, coord: Coordinate, team: CatapultTeam, catapultLocation: Location) {
|
||||
if (hasShotInFlight(player)) {
|
||||
sendShotInFlightMessage(player)
|
||||
return
|
||||
}
|
||||
|
||||
markShotInFlight(player)
|
||||
|
||||
val targetLocation = calculateTargetLocation(team, catapultLocation, coord)
|
||||
val finalLocation = applyInaccuracy(targetLocation)
|
||||
|
||||
val distance = catapultLocation.getDistance(finalLocation).toInt()
|
||||
val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE)
|
||||
val impactDelayTicks = (speed / 30).coerceAtLeast(1)
|
||||
|
||||
sendProjectile(catapultLocation, finalLocation, distance, speed)
|
||||
playFireEffects(player, finalLocation, impactDelayTicks)
|
||||
scheduleImpact(player, finalLocation, impactDelayTicks)
|
||||
|
||||
sendMessage(player, "You fire the catapult!")
|
||||
}
|
||||
|
||||
internal fun calculateTargetLocation(team: CatapultTeam, catapultLocation: Location, coord: Coordinate): Location {
|
||||
val targetX = team.calculateTargetX(catapultLocation.x, coord.x)
|
||||
val targetY = team.calculateTargetY(catapultLocation.y, coord.y)
|
||||
return Location.create(targetX, targetY, 0)
|
||||
}
|
||||
|
||||
private fun applyInaccuracy(target: Location): Location {
|
||||
val finalX = target.x + RandomFunction.random(-INACCURACY_RADIUS, INACCURACY_RADIUS)
|
||||
val finalY = target.y + RandomFunction.random(-INACCURACY_RADIUS, INACCURACY_RADIUS)
|
||||
return Location.create(finalX, finalY, 0)
|
||||
}
|
||||
|
||||
private fun sendProjectile(from: Location, to: Location, distance: Int, speed: Int) {
|
||||
Projectile.create(
|
||||
from,
|
||||
to,
|
||||
GFX_BOULDER_PROJECTILE,
|
||||
PROJECTILE_START_HEIGHT,
|
||||
PROJECTILE_END_HEIGHT,
|
||||
PROJECTILE_DELAY,
|
||||
speed,
|
||||
PROJECTILE_ANGLE,
|
||||
distance
|
||||
).send()
|
||||
}
|
||||
|
||||
private fun playFireEffects(player: Player, targetLocation: Location, impactDelayTicks: Int) {
|
||||
playAudio(player, SOUND_FIRE)
|
||||
|
||||
submitWorldPulse(object : Pulse(impactDelayTicks) {
|
||||
override fun pulse(): Boolean {
|
||||
sendGraphics(GFX_IMPACT, targetLocation)
|
||||
playGlobalAudio(targetLocation, SOUND_IMPACT)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun scheduleImpact(player: Player, targetLocation: Location, impactDelayTicks: Int) {
|
||||
submitWorldPulse(object : Pulse(impactDelayTicks) {
|
||||
override fun pulse(): Boolean {
|
||||
try {
|
||||
dealImpactDamage(player, targetLocation)
|
||||
} finally {
|
||||
clearShotInFlight(player)
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun dealImpactDamage(attacker: Player, targetLocation: Location) {
|
||||
RegionManager.getLocalPlayers(targetLocation, IMPACT_RADIUS).filter { isValidTarget(it) }.forEach { target ->
|
||||
val damage = RandomFunction.random(MIN_DAMAGE, MAX_DAMAGE)
|
||||
target.impactHandler.manualHit(attacker, damage, ImpactHandler.HitsplatType.NORMAL)
|
||||
sendMessage(target, "You are hit by a catapult rock!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidTarget(target: Player): Boolean {
|
||||
return CastleWarsGameState.isGameActive && target.isActive &&
|
||||
CastleWarsGameState.getActiveGameTeam(target) != null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CatapultConfig.DIGIT_CHILD_IDS
|
||||
import content.minigame.castlewars.CatapultConfig.DIGIT_ITEM_IDS
|
||||
import content.minigame.castlewars.CatapultConfig.INTERFACE_ID
|
||||
import content.minigame.castlewars.CatapultConfig.MAP_BASE_X
|
||||
import content.minigame.castlewars.CatapultConfig.MAP_BASE_Y
|
||||
import content.minigame.castlewars.CatapultConfig.MAP_HEIGHT
|
||||
import content.minigame.castlewars.CatapultConfig.MAP_WIDTH
|
||||
import content.minigame.castlewars.CatapultConfig.MARKER_CHILD_ID
|
||||
import content.minigame.castlewars.CatapultConfig.MAX_COORDINATE
|
||||
import core.cache.def.impl.ComponentType
|
||||
import core.cache.def.impl.IfaceDefinition
|
||||
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.
|
||||
*/
|
||||
class CatapultInterfaceRenderer {
|
||||
|
||||
private val interfaceDefinition: IfaceDefinition by lazy { IfaceDefinition.forId(INTERFACE_ID) }
|
||||
|
||||
fun renderCoordinate(player: Player, coord: Coordinate) {
|
||||
updateMarkerPosition(player, coord)
|
||||
updateDigitDisplay(player, coord)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of the red X marker on the interface map.
|
||||
* @param player The player whose interface to update
|
||||
* @param coord The current X/Y coordinates
|
||||
*/
|
||||
fun updateMarkerPosition(player: Player, coord: Coordinate) {
|
||||
val screenX = calculateScreenX(coord.x)
|
||||
val screenY = calculateScreenY(coord.y)
|
||||
|
||||
PacketRepository.send(
|
||||
RepositionChild::class.java, ChildPositionContext(player, INTERFACE_ID, MARKER_CHILD_ID, screenX, screenY)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the digit displays showing the current X and Y coordinates.
|
||||
* @param player The player whose interface to update
|
||||
* @param coord The current X/Y coordinates
|
||||
*/
|
||||
fun updateDigitDisplay(player: Player, coord: Coordinate) {
|
||||
val xTens = coord.x / 10
|
||||
val xOnes = coord.x % 10
|
||||
val yTens = coord.y / 10
|
||||
val yOnes = coord.y % 10
|
||||
|
||||
sendDigitModel(player, DIGIT_CHILD_IDS[0], xTens)
|
||||
sendDigitModel(player, DIGIT_CHILD_IDS[1], xOnes)
|
||||
sendDigitModel(player, DIGIT_CHILD_IDS[2], yTens)
|
||||
sendDigitModel(player, DIGIT_CHILD_IDS[3], yOnes)
|
||||
}
|
||||
|
||||
internal fun calculateScreenX(coordX: Int): Int {
|
||||
return MAP_BASE_X + (coordX.toDouble() / MAX_COORDINATE * MAP_WIDTH).roundToInt()
|
||||
}
|
||||
|
||||
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).toDouble() / MAX_COORDINATE * MAP_HEIGHT).roundToInt()
|
||||
}
|
||||
|
||||
private fun sendDigitModel(player: Player, childId: Int, digit: Int) {
|
||||
if (!isValidDigitChild(childId)) {
|
||||
return
|
||||
}
|
||||
val child = interfaceDefinition.children[childId] ?: return
|
||||
val itemId = getDigitItemId(digit)
|
||||
if (itemId < 0) {
|
||||
return
|
||||
}
|
||||
player.packetDispatch.sendItemOnInterface(itemId, 1, INTERFACE_ID, childId)
|
||||
player.packetDispatch.sendAngleOnInterface(
|
||||
INTERFACE_ID, childId, child.modelZoom, child.modelXAngle, child.modelYAngle
|
||||
)
|
||||
}
|
||||
|
||||
internal fun isValidDigitChild(childId: Int): Boolean {
|
||||
if (childId !in interfaceDefinition.children.indices) {
|
||||
return false
|
||||
}
|
||||
return interfaceDefinition.children[childId]?.type == ComponentType.MODEL
|
||||
}
|
||||
|
||||
internal fun getDigitItemId(digit: Int): Int {
|
||||
return DIGIT_ITEM_IDS.getOrNull(digit) ?: -1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +1,64 @@
|
|||
package rs09.game.content.activity.castlewars
|
||||
package content.minigame.castlewars
|
||||
|
||||
import core.api.TickListener
|
||||
import core.api.openDialogue
|
||||
import core.api.sendMessage
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
import core.api.*
|
||||
import core.game.dialogue.DialogueFile
|
||||
import core.game.dialogue.FacialExpression
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.item.Item
|
||||
import core.tools.END_DIALOGUE
|
||||
import org.rs09.consts.Items
|
||||
import org.rs09.consts.NPCs
|
||||
|
||||
class Lanthus: InteractionListener, TickListener {
|
||||
class Lanthus : InteractionListener, TickListener {
|
||||
override fun defineListeners() {
|
||||
on(NPCs.LANTHUS_1526, IntType.NPC, "talk-to") { player, npcNode ->
|
||||
val lanthusNpc = npcNode.asNpc()
|
||||
openDialogue(player, object : DialogueFile(){
|
||||
openDialogue(player, object : DialogueFile() {
|
||||
override fun handle(componentID: Int, buttonID: Int) {
|
||||
when(stage) {
|
||||
when (stage) {
|
||||
0 -> npcl(FacialExpression.FRIENDLY, "Good day, how may I help you?").also { stage = 2 }
|
||||
2 -> options(
|
||||
"What is this place?",
|
||||
"What do you have for trade?",
|
||||
"Do you have a manual? I'd like to learn how to play!"
|
||||
).also { stage++ }
|
||||
|
||||
3 -> when (buttonID) {
|
||||
1 -> player("What is this place?").also { stage = 4 }
|
||||
2 -> lanthusNpc.openShop(player).also { stage = END_DIALOGUE }
|
||||
3 -> player("Do you have a manual? I'd like to learn how to play!").also { stage = 50 }
|
||||
}
|
||||
|
||||
4 -> npcl(
|
||||
FacialExpression.FRIENDLY,
|
||||
"This is the great Castle Wars arena! Here you can fight for the glory of Saradomin or Zamorak."
|
||||
).also { stage++ }
|
||||
|
||||
5 -> options(
|
||||
"Really, how do I do that?",
|
||||
"Are there any rules?",
|
||||
"What can I win?"
|
||||
"Really, how do I do that?", "Are there any rules?", "What can I win?"
|
||||
).also { stage++ }
|
||||
|
||||
6 -> when (buttonID) {
|
||||
1 -> player("Really, how do I do that?").also { stage = 7 }
|
||||
2 -> player("Are there any rules?").also { stage = 80 }
|
||||
3 -> player("What can I win?").also { stage = 90 }
|
||||
}
|
||||
|
||||
7 -> npcl(
|
||||
FacialExpression.FRIENDLY,
|
||||
"Easy, you just step through one of the three portals. To join Zamorak, pass through the red portal. To join Saradomin, pass through the blue portal. If you don't mind then pass through the green portal.",
|
||||
).also { stage ++ }
|
||||
).also { stage++ }
|
||||
|
||||
8 -> options(
|
||||
"Are there any rules?",
|
||||
"What can I win?",
|
||||
"What do you have for trade?",
|
||||
"Do you have a manual? I'd like to learn how to play!"
|
||||
).also { stage++ }
|
||||
|
||||
9 -> when (buttonID) {
|
||||
1 -> player("Are there any rules?").also { stage = 80 }
|
||||
2 -> player("What can I win?").also { stage = 90 }
|
||||
|
|
@ -63,31 +67,40 @@ class Lanthus: InteractionListener, TickListener {
|
|||
}
|
||||
|
||||
80 -> npcl(
|
||||
FacialExpression.FRIENDLY, "Of course, there are always rules. Firstly you can't wear a cape as you enter the portal, you'll be given your team colours to wear while in the arena. You're also prohibited from taking non-combat related items in")
|
||||
.also { stage ++ }
|
||||
FacialExpression.FRIENDLY,
|
||||
"Of course, there are always rules. Firstly you can't wear a cape as you enter the portal, you'll be given your team colours to wear while in the arena. You're also prohibited from taking non-combat related items in"
|
||||
).also { stage++ }
|
||||
|
||||
81 -> npcl(
|
||||
FacialExpression.FRIENDLY, " with you. So you should only have equipment, potions, and runes on you. Secondly, attacking your own team or your team's defences isn't allowed. You don't want to be angering"
|
||||
).also { stage ++ }
|
||||
FacialExpression.FRIENDLY,
|
||||
" with you. So you should only have equipment, potions, and runes on you. Secondly, attacking your own team or your team's defences isn't allowed. You don't want to be angering"
|
||||
).also { stage++ }
|
||||
|
||||
82 -> npcl(
|
||||
FacialExpression.FRIENDLY, "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)) }
|
||||
"your patron god, do you? Other than that, just have fun and enjoy it!"
|
||||
).also { stage++ }
|
||||
|
||||
83 -> player("Great! Oh, how do I win the game?").also { stage ++ }
|
||||
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(
|
||||
FacialExpression.FRIENDLY,
|
||||
"The aim is to get into your opponents' castle and take their team standard. Then bring that back and capture it on your team's standard."
|
||||
).also { stage ++ }
|
||||
).also { stage++ }
|
||||
|
||||
85 -> options(
|
||||
"What can I win?",
|
||||
"What do you have to trade?",
|
||||
"Do you have a manual? I'd like to learn how to play!"
|
||||
).also { stage ++ }
|
||||
).also { stage++ }
|
||||
|
||||
86 -> when (buttonID) {
|
||||
1 -> player("What can I win?").also { stage = 90 }
|
||||
2 -> lanthusNpc.openShop(player).also { stage = END_DIALOGUE }
|
||||
|
|
@ -96,13 +109,15 @@ class Lanthus: InteractionListener, TickListener {
|
|||
|
||||
90 -> npcl(
|
||||
FacialExpression.FRIENDLY,
|
||||
"Players on the winning team will receive 2 Castle Wars Tickets which you can trade back to me for other items. In the event of a draw every player will get 1 ticket."
|
||||
).also { stage ++ }
|
||||
"Players on the winning team will receive 2 Castle Wars Tickets which you can trade back to me for other items. In the event of a draw every player will get 1 ticket. Losers receive no tickets."
|
||||
).also { stage++ }
|
||||
|
||||
91 -> options(
|
||||
"Are there any rules?",
|
||||
"What do you have to trade?",
|
||||
"Do you have a manual? I'd like to learn how to play!"
|
||||
).also { stage ++ }
|
||||
).also { stage++ }
|
||||
|
||||
92 -> when (buttonID) {
|
||||
1 -> player("Are there any rules?").also { stage = 80 }
|
||||
2 -> lanthusNpc.openShop(player).also { stage = END_DIALOGUE }
|
||||
|
|
@ -122,10 +137,30 @@ class Lanthus: InteractionListener, TickListener {
|
|||
}
|
||||
}
|
||||
|
||||
override fun tick() {
|
||||
"The next game will start in 4 minutes"
|
||||
"The next game will start in 1 minute"
|
||||
// He also talks to Postie Pete sometimes?
|
||||
// Track the last announced minute to avoid spamming the same announcement every tick
|
||||
private var lastAnnouncedMinute = -1
|
||||
|
||||
override fun tick() {
|
||||
// Only announce when no game is active and a countdown is running
|
||||
if (CastleWarsGameArea.ticksLeftInGame > 0) {
|
||||
lastAnnouncedMinute = -1 // Reset for next countdown cycle
|
||||
return
|
||||
}
|
||||
|
||||
val countdown = CastleWarsWaitingArea.gameStartCountdown
|
||||
if (countdown < 0) {
|
||||
lastAnnouncedMinute = -1 // No countdown active
|
||||
return
|
||||
}
|
||||
|
||||
val minutesLeft = (countdown + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
|
||||
|
||||
// Announce at minute boundaries, only once per distinct minute value
|
||||
if (minutesLeft != lastAnnouncedMinute && minutesLeft in 1..5) {
|
||||
lastAnnouncedMinute = minutesLeft
|
||||
val lanthus = findNPC(NPCs.LANTHUS_1526) ?: return
|
||||
val minuteWord = if (minutesLeft == 1) "minute" else "minutes"
|
||||
sendChat(lanthus, "The next game will start in $minutesLeft $minuteWord!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,28 @@
|
|||
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 content.minigame.castlewars.CastleWarsEquipmentRestrictions
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
override fun areaLeave(entity: Entity, logout: Boolean) {
|
||||
super.areaLeave(entity, logout)
|
||||
exitArea(entity as? Player ?: return)
|
||||
|
|
@ -48,27 +62,18 @@ 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)
|
||||
|
||||
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 ->
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +1,446 @@
|
|||
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.container.impl.EquipmentContainer
|
||||
import core.game.event.EventHook
|
||||
import core.game.event.InterfaceCloseEvent
|
||||
import core.game.interaction.Option
|
||||
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.entity.player.info.login.PlayerParser
|
||||
import core.game.node.item.Item
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.zone.ZoneBorders
|
||||
import core.game.world.map.zone.ZoneType
|
||||
import core.tools.Log
|
||||
import core.tools.ticksPerMinute
|
||||
import org.rs09.consts.Components
|
||||
import rs09.game.content.activity.castlewars.CastleWars
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Handles the Castle Wars game map
|
||||
*/
|
||||
class CastleWarsGameArea : CastleWarsArea(), TickListener {
|
||||
class CastleWarsGameArea : CastleWarsArea(), TickListener, LoginListener {
|
||||
|
||||
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<Player>()
|
||||
val zamorakPlayers = mutableSetOf<Player>()
|
||||
|
||||
var ticksLeftInGame = 0
|
||||
data class LogoutRecord(
|
||||
val player: Player, val team: CastleWarsTeam, val logoutTick: Int, val logoutLocation: Location
|
||||
)
|
||||
|
||||
/** Players who have logged out but are within the grace period. Maps normalized username -> logout record. */
|
||||
val loggedOutPlayers = mutableMapOf<String, LogoutRecord>()
|
||||
|
||||
var ticksLeftInGame = -1
|
||||
|
||||
private val activeVacancyOffers = mutableMapOf<CastleWarsTeam, MutableSet<Player>>()
|
||||
private val activeVacancyOfferTicks = mutableMapOf<CastleWarsTeam, Int>()
|
||||
private val declinedSubstitutionOffers = mutableMapOf<CastleWarsTeam, MutableSet<Player>>()
|
||||
private val openVacancies = mutableMapOf<CastleWarsTeam, Int>()
|
||||
|
||||
fun startGame() {
|
||||
if (!CastleWarsWaitingArea.hasMinimumPlayers()) {
|
||||
CastleWarsWaitingArea.gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
|
||||
return
|
||||
}
|
||||
clearSubstitutionState()
|
||||
CastleWarsGameState.resetGame()
|
||||
CastleWarsItemHandler.clearAllBarricades()
|
||||
CastleWarsWaitingArea.gameStartCountdown = -1
|
||||
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
|
||||
ticksLeftInGame = CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE
|
||||
saradominPlayers.forEach { player ->
|
||||
player.properties.teleportLocation = CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
|
||||
CastleWarsGameState.activateBraceletIfEquipped(player, CastleWarsTeam.SARADOMIN)
|
||||
CastleWarsGameState.setActiveGamePlayerOptions(player)
|
||||
setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks())
|
||||
}
|
||||
zamorakPlayers.forEach { player ->
|
||||
player.properties.teleportLocation = CastleWarsRespawnArea.zamorakRespawnRoom.randomWalkableLoc
|
||||
CastleWarsGameState.activateBraceletIfEquipped(player, CastleWarsTeam.ZAMORAK)
|
||||
CastleWarsGameState.setActiveGamePlayerOptions(player)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun endGame() {
|
||||
saradominPlayers.forEach { player ->
|
||||
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
fun clearSubstitutionState() {
|
||||
activeVacancyOffers.clear()
|
||||
activeVacancyOfferTicks.clear()
|
||||
declinedSubstitutionOffers.clear()
|
||||
openVacancies.clear()
|
||||
}
|
||||
zamorakPlayers.forEach { player ->
|
||||
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
|
||||
fun selectGuthixTeam(): CastleWarsTeam {
|
||||
val saraTotal = CastleWarsWaitingArea.waitingSaradominPlayers.size + saradominPlayers.size
|
||||
val zamTotal = CastleWarsWaitingArea.waitingZamorakPlayers.size + zamorakPlayers.size
|
||||
return when {
|
||||
saraTotal < zamTotal -> CastleWarsTeam.SARADOMIN
|
||||
saraTotal > zamTotal -> CastleWarsTeam.ZAMORAK
|
||||
Math.random() < 0.5 -> CastleWarsTeam.SARADOMIN
|
||||
else -> CastleWarsTeam.ZAMORAK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer a vacancy on the given team to every eligible player in that team's waiting room.
|
||||
* Called when a player leaves an active game.
|
||||
* @param team The team that lost a player
|
||||
*/
|
||||
fun fillVacancy(team: CastleWarsTeam) {
|
||||
if (ticksLeftInGame <= 0) return
|
||||
if ((openVacancies[team] ?: 0) <= 0) return
|
||||
val waitingSet = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers
|
||||
}
|
||||
pruneVacancyOffer(team, waitingSet)
|
||||
if (activeVacancyOffers[team]?.isNotEmpty() == true) return
|
||||
val declined = declinedSubstitutionOffers[team].orEmpty()
|
||||
val candidates = waitingSet.filter { it !in declined }
|
||||
if (candidates.isEmpty()) return
|
||||
activeVacancyOffers[team] = candidates.toMutableSet()
|
||||
activeVacancyOfferTicks[team] = getWorldTicks()
|
||||
for (candidate in candidates) {
|
||||
openDialogue(candidate, CastleWarsSubstitutionDialogue(team))
|
||||
}
|
||||
}
|
||||
|
||||
fun claimVacancy(player: Player, team: CastleWarsTeam): Boolean {
|
||||
val offer = activeVacancyOffers[team] ?: return false
|
||||
if (!offer.contains(player)) return false
|
||||
val waitingSet = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers
|
||||
}
|
||||
if (!waitingSet.remove(player)) {
|
||||
offer.remove(player)
|
||||
if (offer.isEmpty()) {
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
}
|
||||
return false
|
||||
}
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
declinedSubstitutionOffers[team]?.remove(player)
|
||||
val remainingVacancies = ((openVacancies[team] ?: 0) - 1).coerceAtLeast(0)
|
||||
if (remainingVacancies > 0) {
|
||||
openVacancies[team] = remainingVacancies
|
||||
} else {
|
||||
openVacancies.remove(team)
|
||||
}
|
||||
val gameSet = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> saradominPlayers
|
||||
CastleWarsTeam.ZAMORAK -> zamorakPlayers
|
||||
}
|
||||
gameSet.add(player)
|
||||
CastleWarsGameState.activateBraceletIfEquipped(player, team)
|
||||
CastleWarsGameState.setActiveGamePlayerOptions(player)
|
||||
setAttribute(player, CastleWarsConstants.ATTR_JOIN_TICK, getWorldTicks())
|
||||
val respawnLoc = when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsRespawnArea.saradominRespawnRoom.randomWalkableLoc
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsRespawnArea.zamorakRespawnRoom.randomWalkableLoc
|
||||
}
|
||||
player.properties.teleportLocation = respawnLoc
|
||||
sendMessage(player, "A spot has opened up! You have been moved into the Castle Wars game.")
|
||||
if ((openVacancies[team] ?: 0) > 0) {
|
||||
fillVacancy(team)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun declineVacancyOffer(player: Player, team: CastleWarsTeam) {
|
||||
activeVacancyOffers[team]?.remove(player)
|
||||
declinedSubstitutionOffers.getOrPut(team) { mutableSetOf() }.add(player)
|
||||
if (activeVacancyOffers[team]?.isEmpty() == true) {
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
fillVacancy(team)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeVacancyOffer(player: Player) {
|
||||
val teamsToRefill = mutableSetOf<CastleWarsTeam>()
|
||||
for (team in CastleWarsTeam.values()) {
|
||||
activeVacancyOffers[team]?.remove(player)
|
||||
declinedSubstitutionOffers[team]?.remove(player)
|
||||
if (activeVacancyOffers[team]?.isEmpty() == true) {
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
teamsToRefill.add(team)
|
||||
}
|
||||
if (declinedSubstitutionOffers[team]?.isEmpty() == true) {
|
||||
declinedSubstitutionOffers.remove(team)
|
||||
}
|
||||
}
|
||||
for (team in teamsToRefill) {
|
||||
fillVacancy(team)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pruneVacancyOffer(team: CastleWarsTeam, waitingSet: Set<Player>) {
|
||||
val offer = activeVacancyOffers[team] ?: return
|
||||
val createdTick = activeVacancyOfferTicks[team] ?: getWorldTicks()
|
||||
if (getWorldTicks() - createdTick >= CastleWarsConstants.VACANCY_OFFER_TIMEOUT_TICKS) {
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
return
|
||||
}
|
||||
offer.removeAll { it !in waitingSet || !it.isActive }
|
||||
if (offer.isEmpty()) {
|
||||
activeVacancyOffers.remove(team)
|
||||
activeVacancyOfferTicks.remove(team)
|
||||
}
|
||||
}
|
||||
|
||||
fun removePlayerFromGame(player: Player, offerReplacement: Boolean = true): CastleWarsTeam? {
|
||||
val leavingTeam = when {
|
||||
saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN
|
||||
zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK
|
||||
else -> null
|
||||
}
|
||||
if (leavingTeam == null) return null
|
||||
|
||||
CastleWarsGameState.handlePlayerLeave(player)
|
||||
player.unhook(SaradominInterfaceCloseHook)
|
||||
player.unhook(ZamorakInterfaceCloseHook)
|
||||
clearLogoutState(player)
|
||||
saradominPlayers.remove(player)
|
||||
zamorakPlayers.remove(player)
|
||||
if (offerReplacement) {
|
||||
openVacancy(leavingTeam)
|
||||
fillVacancy(leavingTeam)
|
||||
}
|
||||
return leavingTeam
|
||||
}
|
||||
|
||||
private fun openVacancy(team: CastleWarsTeam) {
|
||||
openVacancies[team] = (openVacancies[team] ?: 0) + 1
|
||||
}
|
||||
|
||||
fun playerKey(player: Player): String {
|
||||
return player.username.lowercase(Locale.ROOT)
|
||||
}
|
||||
|
||||
private fun setTeam(player: Player, team: CastleWarsTeam) {
|
||||
when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> {
|
||||
zamorakPlayers.remove(player)
|
||||
saradominPlayers.add(player)
|
||||
}
|
||||
|
||||
CastleWarsTeam.ZAMORAK -> {
|
||||
saradominPlayers.remove(player)
|
||||
zamorakPlayers.add(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFromTeam(player: Player) {
|
||||
saradominPlayers.remove(player)
|
||||
zamorakPlayers.remove(player)
|
||||
}
|
||||
|
||||
private fun clearLogoutAttributes(player: Player) {
|
||||
removeAttributes(
|
||||
player,
|
||||
CastleWarsConstants.ATTR_LOGOUT_TICK,
|
||||
CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS,
|
||||
CastleWarsConstants.ATTR_LOGOUT_TEAM,
|
||||
CastleWarsConstants.ATTR_LOGOUT_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearLogoutState(player: Player) {
|
||||
loggedOutPlayers.remove(playerKey(player))
|
||||
clearLogoutAttributes(player)
|
||||
}
|
||||
|
||||
private fun waitingRoomFor(team: CastleWarsTeam): ZoneBorders {
|
||||
return when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.saradominWaitingRoom
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.zamorakWaitingRoom
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitingSetFor(team: CastleWarsTeam): MutableSet<Player> {
|
||||
return when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsWaitingArea.waitingSaradominPlayers
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsWaitingArea.waitingZamorakPlayers
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveLoggedOutPlayerToWaitingRoom(player: Player, team: CastleWarsTeam) {
|
||||
removeFromTeam(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
CastleWarsGameState.restoreAndCleanupPlayer(player)
|
||||
player.interaction.remove(Option._P_ATTACK)
|
||||
relocateImmediately(player, waitingRoomFor(team).randomWalkableLoc)
|
||||
player.equipment.replace(Item(team.cloakId), EquipmentContainer.SLOT_CAPE)
|
||||
waitingSetFor(team).add(player)
|
||||
clearLogoutAttributes(player)
|
||||
}
|
||||
|
||||
private fun moveLoggedOutPlayerToLobby(player: Player) {
|
||||
removeFromTeam(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
CastleWarsGameState.restoreAndCleanupPlayer(player)
|
||||
player.interaction.remove(Option._P_ATTACK)
|
||||
player.interaction.remove(Option._P_STEAL_FROM)
|
||||
val lobbyLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
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)
|
||||
if (savedTeamName.isEmpty() || savedLogoutTime <= 0L) return
|
||||
val team = CastleWarsTeam.values().firstOrNull { it.name == savedTeamName } ?: return
|
||||
val key = playerKey(player)
|
||||
val record = loggedOutPlayers.remove(key)
|
||||
val previousPlayer = record?.player
|
||||
if (previousPlayer != null && previousPlayer != player) {
|
||||
removeFromTeam(previousPlayer)
|
||||
}
|
||||
val elapsedMillis = System.currentTimeMillis() - savedLogoutTime
|
||||
val logoutLocation = getAttribute(
|
||||
player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, record?.logoutLocation ?: player.location
|
||||
)
|
||||
if (ticksLeftInGame <= 0) {
|
||||
moveLoggedOutPlayerToLobby(player)
|
||||
return
|
||||
}
|
||||
if (ticksLeftInGame > 0 && elapsedMillis < CastleWarsConstants.LOGOUT_GRACE_PERIOD_MILLIS) {
|
||||
relocateImmediately(player, Location.create(logoutLocation))
|
||||
setTeam(player, team)
|
||||
clearLogoutAttributes(player)
|
||||
return
|
||||
}
|
||||
moveLoggedOutPlayerToWaitingRoom(player, team)
|
||||
if (ticksLeftInGame > 0) {
|
||||
openVacancy(team)
|
||||
fillVacancy(team)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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<InterfaceCloseEvent> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun endCurrentGame(startCooldown: Boolean = true) {
|
||||
val saraSnapshot = saradominPlayers.toSet()
|
||||
val zamSnapshot = zamorakPlayers.toSet()
|
||||
val loggedOutSnapshot = loggedOutPlayers.values.toList()
|
||||
val loggedOutRewardRecipients = loggedOutSnapshot.associate { it.player to it.team }
|
||||
CastleWarsGameState.endGame(loggedOutRewardRecipients)
|
||||
for (record in loggedOutSnapshot) {
|
||||
moveLoggedOutPlayerToLobby(record.player)
|
||||
if (!record.player.isActive || record.player.session == null) {
|
||||
PlayerParser.saveImmediately(record.player)
|
||||
}
|
||||
}
|
||||
CastleWarsItemHandler.clearAllBarricades()
|
||||
CastleWarsGameState.clearCastleWarsGroundItems()
|
||||
clearSubstitutionState()
|
||||
ticksLeftInGame = -1
|
||||
CastleWarsWaitingArea.gameStartCountdown = if (startCooldown) {
|
||||
CastleWars.gameCooldownMinutes * CastleWarsConstants.TICKS_PER_MINUTE
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
saraSnapshot.forEach { player ->
|
||||
player.unhook(SaradominInterfaceCloseHook)
|
||||
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
}
|
||||
zamSnapshot.forEach { player ->
|
||||
player.unhook(ZamorakInterfaceCloseHook)
|
||||
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
}
|
||||
saradominPlayers.clear()
|
||||
zamorakPlayers.clear()
|
||||
CastleWarsRespawnArea.saradominPlayersInRespawnRoom.clear()
|
||||
CastleWarsRespawnArea.zamorakPlayersInRespawnRoom.clear()
|
||||
}
|
||||
saradominPlayers.clear()
|
||||
zamorakPlayers.clear()
|
||||
}
|
||||
|
||||
override fun defineAreaBorders(): Array<ZoneBorders> {
|
||||
|
|
@ -70,31 +449,193 @@ class CastleWarsGameArea : CastleWarsArea(), TickListener {
|
|||
|
||||
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)*60*2))
|
||||
registerTimer(
|
||||
player,
|
||||
spawnTimer("teleblock", CastleWars.gameTimeMinutes * CastleWarsConstants.TICKS_PER_MINUTE, false)
|
||||
)
|
||||
when (team) {
|
||||
CastleWarsTeam.SARADOMIN -> {
|
||||
player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_SARADOMIN_58))
|
||||
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), EquipmentContainer.SLOT_CAPE)
|
||||
player.hook(Event.InterfaceClosed, ZamorakInterfaceCloseHook)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
CastleWarsGameState.setActiveGamePlayerOptions(player)
|
||||
}
|
||||
|
||||
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) {
|
||||
removePlayerFromGame(player)
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout(player: Player) {
|
||||
val team = when {
|
||||
saradominPlayers.contains(player) -> CastleWarsTeam.SARADOMIN
|
||||
zamorakPlayers.contains(player) -> CastleWarsTeam.ZAMORAK
|
||||
else -> null
|
||||
}
|
||||
if (ticksLeftInGame > 0 && team != null) {
|
||||
val logoutTick = getWorldTicks()
|
||||
val logoutLocation = Location.create(player.location)
|
||||
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TICK, logoutTick)
|
||||
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TIME_MILLIS, System.currentTimeMillis())
|
||||
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_TEAM, team.name)
|
||||
setAttribute(player, CastleWarsConstants.ATTR_LOGOUT_LOCATION, logoutLocation)
|
||||
loggedOutPlayers[playerKey(player)] = LogoutRecord(player, team, logoutTick, logoutLocation)
|
||||
if (CastleWarsGameState.isCarryingFlag(player)) {
|
||||
CastleWarsGameState.dropFlag(player)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
}
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
return
|
||||
} else {
|
||||
CastleWarsGameState.handlePlayerLeave(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
saradominPlayers.remove(player)
|
||||
zamorakPlayers.remove(player)
|
||||
}
|
||||
super.logout(player)
|
||||
}
|
||||
|
||||
override fun login(player: Player) {
|
||||
restoreActiveLogoutOnLogin(player)
|
||||
}
|
||||
|
||||
override fun tick() {
|
||||
if (ticksLeftInGame < 0) {
|
||||
return
|
||||
}
|
||||
if (ticksLeftInGame == 0) {
|
||||
endCurrentGame()
|
||||
return
|
||||
}
|
||||
val previousMinutes =
|
||||
(ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
|
||||
ticksLeftInGame--
|
||||
val currentMinutes =
|
||||
(ticksLeftInGame + CastleWarsConstants.TICKS_PER_MINUTE - 1) / CastleWarsConstants.TICKS_PER_MINUTE
|
||||
if (ticksLeftInGame == 0) {
|
||||
endCurrentGame()
|
||||
return
|
||||
} else if (currentMinutes != previousMinutes) {
|
||||
CastleWarsGameState.updateAllOverlays()
|
||||
}
|
||||
CastleWarsGameState.checkFlagCarriers()
|
||||
CastleWarsGameState.checkDroppedFlagTimeouts()
|
||||
saradominPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.SARADOMIN) }
|
||||
zamorakPlayers.forEach { ensureGameOverlay(it, CastleWarsTeam.ZAMORAK) }
|
||||
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 { (_, record) ->
|
||||
currentTick - record.logoutTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
|
||||
}
|
||||
for ((key, record) in expiredLogouts) {
|
||||
val player = record.player
|
||||
loggedOutPlayers.remove(key)
|
||||
clearLogoutAttributes(player)
|
||||
removeFromTeam(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
CastleWarsGameState.restoreAndCleanupPlayer(player)
|
||||
openVacancy(record.team)
|
||||
fillVacancy(record.team)
|
||||
}
|
||||
// Remove truly inactive players (not in grace period)
|
||||
val inactiveSara = saradominPlayers.filter { player ->
|
||||
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(playerKey(player))
|
||||
}
|
||||
val inactiveZam = zamorakPlayers.filter { player ->
|
||||
(!player.isActive || player.session == null) && !loggedOutPlayers.containsKey(playerKey(player))
|
||||
}
|
||||
for (player in inactiveSara) {
|
||||
saradominPlayers.remove(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
openVacancy(CastleWarsTeam.SARADOMIN)
|
||||
fillVacancy(CastleWarsTeam.SARADOMIN)
|
||||
}
|
||||
for (player in inactiveZam) {
|
||||
zamorakPlayers.remove(player)
|
||||
CastleWarsGameState.clearHintIconForPlayer(player)
|
||||
openVacancy(CastleWarsTeam.ZAMORAK)
|
||||
fillVacancy(CastleWarsTeam.ZAMORAK)
|
||||
}
|
||||
// Also cleanup hint icons for any other inactive players
|
||||
CastleWarsGameState.cleanupInactiveHintIcons()
|
||||
}
|
||||
|
||||
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? {
|
||||
val attacker = entity as? Player ?: return null
|
||||
val victim = target as? Player ?: return null
|
||||
if (ticksLeftInGame <= 0) {
|
||||
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!")
|
||||
}
|
||||
return false
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
package rs09.game.content.activity.castlewars.areas
|
||||
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
|
||||
import rs09.game.content.activity.castlewars.CastleWars
|
||||
|
||||
/**
|
||||
* Handles the Castle Wars respawn rooms
|
||||
|
|
@ -14,11 +20,13 @@ 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<Player>()
|
||||
val saradominPlayersInRespawnRoom = mutableSetOf<Player>()
|
||||
val zamorakPlayersInRespawnRoom = mutableMapOf<Player, Int>()
|
||||
val saradominPlayersInRespawnRoom = mutableMapOf<Player, Int>()
|
||||
}
|
||||
|
||||
override fun defineAreaBorders(): Array<ZoneBorders> {
|
||||
|
|
@ -27,14 +35,84 @@ class CastleWarsRespawnArea : CastleWarsArea(), TickListener {
|
|||
|
||||
override fun areaEnter(entity: Entity) {
|
||||
val player = entity as? Player ?: return
|
||||
if (saradominRespawnRoom.insideBorder(player.location)) {
|
||||
player.equipment.replace(Item(CastleWars.saradominTeamHoodedCloak), 1)
|
||||
} else if (zamorakRespawnRoom.insideBorder(player.location)) {
|
||||
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1)
|
||||
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)) {
|
||||
CastleWarsGameState.dropFlag(player)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
}
|
||||
sendMessage(player, "You cannot take the flag into your respawn room!")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
override fun tick() {
|
||||
if (CastleWarsGameArea.ticksLeftInGame <= 0) {
|
||||
saradominPlayersInRespawnRoom.clear()
|
||||
zamorakPlayersInRespawnRoom.clear()
|
||||
return
|
||||
}
|
||||
removePlayersOverSpawnLimit(saradominPlayersInRespawnRoom)
|
||||
removePlayersOverSpawnLimit(zamorakPlayersInRespawnRoom)
|
||||
}
|
||||
|
||||
private fun removePlayersOverSpawnLimit(players: MutableMap<Player, Int>) {
|
||||
players.entries.removeIf { (player, _) -> !player.isActive || player.session == null }
|
||||
val currentTick = getWorldTicks()
|
||||
val expiredPlayers = players.filter { (player, entryTick) ->
|
||||
player.isActive && player.session != null && currentTick - entryTick >= CastleWarsConstants.LOGOUT_GRACE_PERIOD_TICKS
|
||||
}.keys
|
||||
for (player in expiredPlayers) {
|
||||
players.remove(player)
|
||||
CastleWarsGameArea.removePlayerFromGame(player)
|
||||
player.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
sendMessage(player, "You have been removed from Castle Wars for staying in the spawn room too long.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout(player: Player) {
|
||||
if (!defineAreaBorders().any { it.insideBorder(player.location) }) {
|
||||
return
|
||||
}
|
||||
saradominPlayersInRespawnRoom.remove(player)
|
||||
zamorakPlayersInRespawnRoom.remove(player)
|
||||
if (CastleWarsGameArea.ticksLeftInGame > 0 && CastleWarsGameState.getActiveGameTeam(player) != null) {
|
||||
CastleWarsGameArea().logout(player)
|
||||
return
|
||||
}
|
||||
super.logout(player)
|
||||
}
|
||||
|
||||
override fun exitArea(player: Player) {
|
||||
super.exitArea(player)
|
||||
// Remove player from respawn room tracking
|
||||
saradominPlayersInRespawnRoom.remove(player)
|
||||
zamorakPlayersInRespawnRoom.remove(player)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 content.minigame.castlewars.CastleWarsTeam
|
||||
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 core.tools.ticksPerMinute
|
||||
import org.rs09.consts.Components
|
||||
import rs09.game.content.activity.castlewars.CastleWars
|
||||
|
||||
/**
|
||||
* Handles the Castle Wars "waiting room"
|
||||
|
|
@ -24,6 +26,11 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
|
|||
|
||||
val waitingSaradominPlayers = mutableSetOf<Player>()
|
||||
val waitingZamorakPlayers = mutableSetOf<Player>()
|
||||
internal var gameStartCountdown = -1
|
||||
|
||||
fun hasMinimumPlayers(): Boolean {
|
||||
return waitingSaradominPlayers.size >= CastleWarsConstants.MIN_PLAYERS_PER_TEAM && waitingZamorakPlayers.size >= CastleWarsConstants.MIN_PLAYERS_PER_TEAM
|
||||
}
|
||||
}
|
||||
|
||||
override fun defineAreaBorders(): Array<ZoneBorders> {
|
||||
|
|
@ -33,45 +40,75 @@ 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) * CastleWarsConstants.TICKS_PER_MINUTE,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
// Set team attribute and equip the hooded cloak on the entity based on which waiting room they're in
|
||||
if (zamorakWaitingRoom.insideBorder(player.location)) {
|
||||
player.equipment.replace(Item(CastleWars.zamorakTeamHoodedCloak), 1)
|
||||
val joinedTeam = if (zamorakWaitingRoom.insideBorder(player.location)) {
|
||||
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 {
|
||||
null
|
||||
}
|
||||
if (joinedTeam != null && CastleWarsGameArea.ticksLeftInGame > 0) {
|
||||
CastleWarsGameArea.fillVacancy(joinedTeam)
|
||||
}
|
||||
|
||||
var transformed = false
|
||||
if (player.attributes[CastleWars.portalAttribute] == CastleWars.guthixName
|
||||
&& (hasGodItem(player, God.ZAMORAK) || hasGodItem(player, God.SARADOMIN))) {
|
||||
if (player.attributes[CastleWars.portalAttribute] == CastleWars.guthixName && (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))
|
||||
{
|
||||
if (player.attributes[CastleWars.portalAttribute] == CastleWars.zamorakName && (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))
|
||||
{
|
||||
if (player.attributes[CastleWars.portalAttribute] == CastleWars.saradominName && (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
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +124,7 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
|
|||
|
||||
// Open the white text overlay
|
||||
player.interfaceManager.openOverlay(Component(Components.CASTLEWARS_STATUS_OVERLAY_57))
|
||||
|
||||
}
|
||||
|
||||
override fun exitArea(player: Player) {
|
||||
|
|
@ -94,35 +132,40 @@ class CastleWarsWaitingArea : CastleWarsArea(), TickListener {
|
|||
// Remove player from waiting room set (whichever one that is)
|
||||
waitingSaradominPlayers.remove(player)
|
||||
waitingZamorakPlayers.remove(player)
|
||||
CastleWarsGameArea.removeVacancyOffer(player)
|
||||
}
|
||||
|
||||
override fun tick() {
|
||||
var nextStart = Int.MAX_VALUE
|
||||
// The game will start when there's enough players and CastleWarsGameArea.ticksLeftInGame = -5 minutes
|
||||
if (CastleWarsGameArea.ticksLeftInGame >= 0) {
|
||||
// A game is going on, display 5 minutes + remaining game time
|
||||
nextStart = CastleWarsGameArea.ticksLeftInGame + CastleWars.gameCooldownMinutes * ticksPerMinute
|
||||
val nextStart: Int
|
||||
if (CastleWarsGameArea.ticksLeftInGame > 0) {
|
||||
gameStartCountdown = -1
|
||||
nextStart =
|
||||
CastleWarsGameArea.ticksLeftInGame + CastleWars.gameCooldownMinutes * CastleWarsConstants.TICKS_PER_MINUTE
|
||||
} else {
|
||||
if (gameStartCountdown < 0) {
|
||||
gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
|
||||
}
|
||||
nextStart = gameStartCountdown
|
||||
gameStartCountdown--
|
||||
}
|
||||
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
|
||||
CastleWarsGameArea.ticksLeftInGame = -1
|
||||
}
|
||||
else {
|
||||
// There are enough players - Set nextStart to gameCooldownMinutes + ticksLeftInGame
|
||||
nextStart = CastleWars.gameCooldownMinutes * ticksPerMinute + CastleWarsGameArea.ticksLeftInGame
|
||||
}
|
||||
|
||||
for (player in waitingSaradominPlayers + waitingZamorakPlayers) {
|
||||
CastleWarsOverlay.sendLobbyUpdate(
|
||||
player,
|
||||
waitingSaradominPlayers.isNotEmpty() && waitingZamorakPlayers.isNotEmpty() || CastleWarsGameArea.ticksLeftInGame >= 0,
|
||||
(nextStart - 1) / ticksPerMinute + 1 // Displays max 5 minutes, min 1 minute, seems authentic
|
||||
player, ticksToDisplayMinutes(nextStart)
|
||||
)
|
||||
}
|
||||
|
||||
if (nextStart <= 0) {
|
||||
if (CastleWarsGameArea.ticksLeftInGame <= 0 && nextStart <= 0) {
|
||||
gameStartCountdown = -1
|
||||
if (!hasMinimumPlayers()) {
|
||||
gameStartCountdown = CastleWarsConstants.WAITING_ROOM_COUNTDOWN_TICKS
|
||||
return
|
||||
}
|
||||
CastleWarsGameArea.startGame()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ticksToDisplayMinutes(ticks: Int): Int {
|
||||
if (ticks <= 0) return 0
|
||||
return (ticks - 1) / CastleWarsConstants.TICKS_PER_MINUTE + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import core.game.world.map.Location
|
|||
import core.tools.LogLevel
|
||||
import core.tools.mysql.Database
|
||||
import core.tools.secondsToTicks
|
||||
import rs09.game.content.activity.castlewars.CastleWars
|
||||
import content.minigame.castlewars.CastleWars
|
||||
import java.math.BigInteger
|
||||
import java.util.*
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
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.RegionZone
|
||||
import core.game.world.map.zone.ZoneBorders
|
||||
import core.game.world.map.zone.ZoneRestriction
|
||||
|
||||
|
|
@ -25,6 +28,23 @@ interface MapArea : ContentInterface {
|
|||
fun areaEnter(entity: Entity) {}
|
||||
fun areaLeave(entity: Entity, logout: Boolean) {}
|
||||
fun entityStep(entity: Entity, location: Location, lastLocation: Location) {}
|
||||
/**
|
||||
* Extends MapZone to cater to pvp, team games and all other unique targeting systems.
|
||||
* Return true/false if you want to override the controls.
|
||||
* Return null if you want the default check in MapZone. (default)
|
||||
**/
|
||||
fun isPvpAllowed(entity: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean? { return null }
|
||||
|
||||
/**
|
||||
* Allows an area to reject equipment changes without coupling core equipment code
|
||||
* to a particular piece of content.
|
||||
**/
|
||||
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>()
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ public class AIPlayer extends Player {
|
|||
private ArrayList<Node> getNodeInRange(int range, int entry) {
|
||||
int meX = this.getLocation().getX();
|
||||
int meY = this.getLocation().getY();
|
||||
int meZ = this.getLocation().getZ();
|
||||
ArrayList<Node> nodes = new ArrayList<Node>();
|
||||
for (NPC npc : RegionManager.getLocalNpcs(this, range)) {
|
||||
if (npc.getId() == entry)
|
||||
|
|
@ -324,19 +325,19 @@ public class AIPlayer extends Player {
|
|||
}
|
||||
for (int x = 0; x < range; x++) {
|
||||
for (int y = 0; y < range - x; y++) {
|
||||
Node node = RegionManager.getObject(0, meX + x, meY + y);
|
||||
Node node = RegionManager.getObject(meZ, meX + x, meY + y);
|
||||
if (node != null)
|
||||
if (node.getId() == entry)
|
||||
nodes.add(node);
|
||||
Node node2 = RegionManager.getObject(0, meX + x, meY - y);
|
||||
Node node2 = RegionManager.getObject(meZ, meX + x, meY - y);
|
||||
if (node2 != null)
|
||||
if (node2.getId() == entry)
|
||||
nodes.add(node2);
|
||||
Node node3 = RegionManager.getObject(0, meX - x, meY + y);
|
||||
Node node3 = RegionManager.getObject(meZ, meX - x, meY + y);
|
||||
if (node3 != null)
|
||||
if (node3.getId() == entry)
|
||||
nodes.add(node3);
|
||||
Node node4 = RegionManager.getObject(0, meX - x, meY - y);
|
||||
Node node4 = RegionManager.getObject(meZ, meX - x, meY - y);
|
||||
if (node4 != null)
|
||||
if (node4.getId() == entry)
|
||||
nodes.add(node4);
|
||||
|
|
@ -348,7 +349,7 @@ public class AIPlayer extends Player {
|
|||
private ArrayList<Node> getNodeInRange(int range, List<Integer> entrys) {
|
||||
int meX = this.getLocation().getX();
|
||||
int meY = this.getLocation().getY();
|
||||
//int meX2 = this.getLocation().getX();
|
||||
int meZ = this.getLocation().getZ();
|
||||
|
||||
ArrayList<Node> nodes = new ArrayList<Node>();
|
||||
for (NPC npc : RegionManager.getLocalNpcs(this, range)) {
|
||||
|
|
@ -357,19 +358,19 @@ public class AIPlayer extends Player {
|
|||
}
|
||||
for (int x = 0; x < range; x++) {
|
||||
for (int y = 0; y < range - x; y++) {
|
||||
Node node = RegionManager.getObject(0, meX + x, meY + y);
|
||||
Node node = RegionManager.getObject(meZ, meX + x, meY + y);
|
||||
if (node != null)
|
||||
if (entrys.contains(node.getId()))
|
||||
nodes.add(node);
|
||||
Node node2 = RegionManager.getObject(0, meX + x, meY - y);
|
||||
Node node2 = RegionManager.getObject(meZ, meX + x, meY - y);
|
||||
if (node2 != null)
|
||||
if (entrys.contains(node2.getId()))
|
||||
nodes.add(node2);
|
||||
Node node3 = RegionManager.getObject(0, meX - x, meY + y);
|
||||
Node node3 = RegionManager.getObject(meZ, meX - x, meY + y);
|
||||
if (node3 != null)
|
||||
if (entrys.contains(node3.getId()))
|
||||
nodes.add(node3);
|
||||
Node node4 = RegionManager.getObject(0, meX - x, meY - y);
|
||||
Node node4 = RegionManager.getObject(meZ, meX - x, meY - y);
|
||||
if (node4 != null)
|
||||
if (entrys.contains(node4.getId()))
|
||||
nodes.add(node4);
|
||||
|
|
|
|||
|
|
@ -146,9 +146,17 @@ public final class EquipmentContainer extends Container {
|
|||
}
|
||||
|
||||
boolean listenersSayWeCanUnequip = runUnequipHooks(current, newItem);
|
||||
if(!listenersSayWeCanUnequip) {
|
||||
return false;
|
||||
}
|
||||
ArrayList<Item> itemsToMoveToInventory = new ArrayList<>(current);
|
||||
itemsToMoveToInventory.removeIf(item -> canUnequipWithoutInventorySpace(item) && !containsItem(item));
|
||||
if(itemsToMoveToInventory.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean allRemoved = true;
|
||||
for(Item item : current) {
|
||||
for(Item item : itemsToMoveToInventory) {
|
||||
if(!remove(item)) {
|
||||
allRemoved = false;
|
||||
break;
|
||||
|
|
@ -157,7 +165,7 @@ public final class EquipmentContainer extends Container {
|
|||
|
||||
boolean allAdded = allRemoved;
|
||||
if(allRemoved) {
|
||||
for (Item item : current) {
|
||||
for (Item item : itemsToMoveToInventory) {
|
||||
if (!player.getInventory().add(item, true, preferredSlot)) {
|
||||
allAdded = false;
|
||||
break;
|
||||
|
|
@ -168,7 +176,7 @@ public final class EquipmentContainer extends Container {
|
|||
if (listenersSayWeCanUnequip && allRemoved && allAdded) return true;
|
||||
else {
|
||||
//put things back if we couldn't remove everything
|
||||
for(Item item : current) {
|
||||
for(Item item : itemsToMoveToInventory) {
|
||||
if(!containsItem(item)) {
|
||||
add(item);
|
||||
}
|
||||
|
|
@ -182,6 +190,9 @@ public final class EquipmentContainer extends Container {
|
|||
int neededSlots = 0;
|
||||
|
||||
for(Item item : current) {
|
||||
if(canUnequipWithoutInventorySpace(item)) {
|
||||
continue;
|
||||
}
|
||||
if(!item.getDefinition().isStackable()) {
|
||||
neededSlots++;
|
||||
} else {
|
||||
|
|
@ -193,6 +204,10 @@ public final class EquipmentContainer extends Container {
|
|||
return neededSlots;
|
||||
}
|
||||
|
||||
private boolean canUnequipWithoutInventorySpace(Item item) {
|
||||
return item.getDefinition().getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Item getSecondaryEquipIfApplicable(Item newItem, int equipmentSlot) {
|
||||
Item secondaryEquipItem = null;
|
||||
|
|
@ -211,19 +226,28 @@ public final class EquipmentContainer extends Container {
|
|||
|
||||
for(Item currentItem : currentItems) {
|
||||
Plugin<Object> plugin = currentItem.getDefinition().getConfiguration("equipment", null);
|
||||
if (plugin != null) {
|
||||
Object object = plugin.fireEvent("unequip", player, currentItem);
|
||||
if (object != null && !((Boolean) object)) {
|
||||
canContinue = false;
|
||||
if (plugin != null) {
|
||||
Object object = plugin.fireEvent("unequip", player, currentItem);
|
||||
if (object != null && !((Boolean) object)) {
|
||||
if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) {
|
||||
continue;
|
||||
}
|
||||
canContinue = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false);
|
||||
|
||||
if(!canContinue) {
|
||||
if (canUnequipWithoutInventorySpace(currentItem) && !containsItem(currentItem)) {
|
||||
canContinue = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
canContinue = InteractionListeners.run(currentItem.getId(), player, currentItem, false);
|
||||
|
||||
if(!canContinue) break;
|
||||
}
|
||||
|
||||
return canContinue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,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.
|
||||
|
|
@ -35,14 +37,22 @@ class EquipHandler : InteractionListener {
|
|||
val item = node.asItem()
|
||||
val itemEquipmentSlot = item.definition.getConfiguration<Int>(ItemConfigParser.EQUIP_SLOT, -1)
|
||||
|
||||
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)){
|
||||
return
|
||||
val listenerRemovedItem =
|
||||
canUnequipWithoutInventorySpace(currentEquippedItem) && player.equipment[itemEquipmentSlot] == null
|
||||
if (!listenerRemovedItem) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,18 +128,31 @@ class EquipHandler : InteractionListener {
|
|||
if (slot == EquipmentContainer.SLOT_WEAPON) {
|
||||
player.packetDispatch.sendString("", 92, 0)
|
||||
}
|
||||
val maximumAdd = player.inventory.getMaximumAdd(item)
|
||||
if (maximumAdd < item.amount) {
|
||||
player.packetDispatch.sendMessage("Not enough free space in your inventory.")
|
||||
return
|
||||
if (!canUnequipWithoutInventorySpace(item)) {
|
||||
val maximumAdd = player.inventory.getMaximumAdd(item)
|
||||
if (maximumAdd < item.amount) {
|
||||
player.packetDispatch.sendMessage("Not enough free space in your inventory.")
|
||||
return
|
||||
}
|
||||
}
|
||||
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)
|
||||
if (maximumAdd < item.amount) {
|
||||
player.packetDispatch.sendMessage("Not enough free space in your inventory.")
|
||||
return
|
||||
}
|
||||
if (player.equipment.remove(item)) {
|
||||
|
|
@ -145,5 +168,9 @@ class EquipHandler : InteractionListener {
|
|||
SlayerEquipmentFlags.updateFlags(player)
|
||||
}
|
||||
}
|
||||
|
||||
private fun canUnequipWithoutInventorySpace(item: Item): Boolean {
|
||||
return item.definition.getConfiguration(ItemConfigParser.UNEQUIP_WITHOUT_INVENTORY_SPACE, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public enum SpecialLadders implements LadderAchievementCheck {
|
|||
|
||||
CASTLEWARS_ZAMORAK_TOP_FLOOR_DOWN(Location.create(2374, 3133, 3), Location.create(2374, 3130, 2)),
|
||||
CASTLEWARS_ZAMORAK_MAIN_FLOOR_STAIRS_UP(Location.create(2380, 3129, 0), Location.create(2379, 3127, 1)),
|
||||
CASTLEWARS_ZAMORAK_MAIN_FLOOR_STAIRS_DOWN(Location.create(2379, 3127, 1), Location.create(2380, 3129, 0)),
|
||||
CASTLEWARS_ZAMORAK_OUTERWALL_STAIRS_UP(Location.create(2382, 3130, 0), Location.create(2383, 3132, 0)),
|
||||
CASTLEWARS_ZAMOUTER_WALL_STAIRS_DOWN(Location.create(2382, 3132, 0), Location.create(2382, 3129, 0)),
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ public final class Option {
|
|||
|
||||
public static final Option _P_PICKPOCKET = new Option("Pickpocket", 4);
|
||||
|
||||
public static final Option _P_STEAL_FROM = new Option("Steal from", 4);
|
||||
|
||||
|
||||
public static final Option _P_EXAMINE = new Option("Examine", 7);
|
||||
|
||||
|
|
|
|||
|
|
@ -585,6 +585,9 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
if (player.equipment[3] != null && player.equipment[3].id == 14726 && state.style == CombatStyle.MAGIC) {
|
||||
hit += (hit.toDouble() * 0.15).toInt()
|
||||
}
|
||||
if (victim is Player) {
|
||||
hit = player.zoneMonitor.modifyOutgoingHit(player, victim, hit)
|
||||
}
|
||||
}
|
||||
if (attacker is Familiar && victim is Player) {
|
||||
if (victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]) {
|
||||
|
|
|
|||
26
Server/src/main/core/game/node/entity/combat/DeathContext.kt
Normal file
26
Server/src/main/core/game/node/entity/combat/DeathContext.kt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.zone.MapZone
|
||||
|
||||
/**
|
||||
* Stores the state at the moment death commenced.
|
||||
* Used to prevent zone-transition exploits during death animation.
|
||||
*
|
||||
* When a player dies, there's a delay between death commencement (when HP reaches 0)
|
||||
* and death finalization (after the death animation plays). During this time, a player
|
||||
* could theoretically transition between zones. By capturing the state at death start,
|
||||
* we ensure that item loss/keep decisions are made based on where the player actually died,
|
||||
* not where they ended up after the animation.
|
||||
*
|
||||
* @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 deathZones Snapshot of the zone handlers active when death commenced
|
||||
*/
|
||||
data class DeathContext(
|
||||
val deathLocation: Location,
|
||||
val zoneType: Int,
|
||||
val isSafeZone: Boolean,
|
||||
val deathZones: List<MapZone>
|
||||
)
|
||||
|
|
@ -49,6 +49,9 @@ 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.*;
|
||||
import core.net.IoSession;
|
||||
|
|
@ -86,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.*;
|
||||
|
|
@ -610,6 +614,16 @@ public class Player extends Entity {
|
|||
@Override
|
||||
public void commenceDeath(Entity killer) {
|
||||
if (!isPlaying()) return;
|
||||
DeathContext context = new DeathContext(
|
||||
Location.create(getLocation()),
|
||||
getZoneMonitor().getType(),
|
||||
getProperties().isSafeZone(),
|
||||
getZoneMonitor().getZones().stream()
|
||||
.map(RegionZone::getZone)
|
||||
.distinct()
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
setAttribute("death-context", context);
|
||||
super.commenceDeath(killer);
|
||||
if (prayer.get(PrayerType.RETRIBUTION)) {
|
||||
prayer.startRetribution(killer);
|
||||
|
|
@ -619,6 +633,11 @@ public class Player extends Entity {
|
|||
@Override
|
||||
public void finalizeDeath(Entity killer) {
|
||||
if (!isPlaying()) return; //if the player has already been full cleared, it has already disconnected. This code is probably getting called because something is maintaining a stale reference.
|
||||
DeathContext deathContext = getAttribute("death-context", null);
|
||||
removeAttribute("death-context");
|
||||
boolean wasSafeZone = deathContext != null ? deathContext.isSafeZone() : getProperties().isSafeZone();
|
||||
int deathZoneType = deathContext != null ? deathContext.getZoneType() : getZoneMonitor().getType();
|
||||
Location deathLocation = deathContext != null ? deathContext.getDeathLocation() : getLocation();
|
||||
GlobalStats.incrementDeathCount();
|
||||
settings.setSpecialEnergy(100);
|
||||
settings.updateRunEnergy(settings.getRunEnergy() - 100);
|
||||
|
|
@ -639,7 +658,10 @@ public class Player extends Entity {
|
|||
incrementAttribute("/save:"+STATS_BASE+":"+STATS_DEATHS);
|
||||
|
||||
packetDispatch.sendTempMusic(90);
|
||||
if (!getZoneMonitor().handleDeath(killer) && (!getProperties().isSafeZone() && getZoneMonitor().getType() != ZoneType.SAFE.getId()) && getDetails().getRights() != Rights.ADMINISTRATOR) {
|
||||
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
|
||||
String gender = this.isMale() ? "man " : "woman ";
|
||||
|
|
@ -649,7 +671,7 @@ public class Player extends Entity {
|
|||
return;
|
||||
}
|
||||
}
|
||||
GroundItemManager.create(new Item(BONES_526), this.getAttribute("/save:original-loc",location), k);
|
||||
GroundItemManager.create(new Item(BONES_526), this.getAttribute("/save:original-loc", deathLocation), k);
|
||||
final Container[] c = DeathTask.getContainers(this);
|
||||
|
||||
for (Item i : getEquipment().toArray()) {
|
||||
|
|
@ -662,7 +684,7 @@ public class Player extends Entity {
|
|||
boolean canCreateGrave = GraveController.allowGenerate(this);
|
||||
if (canCreateGrave) {
|
||||
Grave g = GraveController.produceGrave(GraveController.getGraveType(this));
|
||||
g.initialize(this, location, Arrays.stream(c[1].toArray()).filter(Objects::nonNull).toArray(Item[]::new)); //note: the amount of code required to filter nulls from an array in Java is atrocious.
|
||||
g.initialize(this, deathLocation, Arrays.stream(c[1].toArray()).filter(Objects::nonNull).toArray(Item[]::new)); //note: the amount of code required to filter nulls from an array in Java is atrocious.
|
||||
} else {
|
||||
StringBuilder itemsLost = new StringBuilder();
|
||||
int coins = 0;
|
||||
|
|
@ -693,11 +715,11 @@ public class Player extends Entity {
|
|||
} else {
|
||||
item = GraveController.checkTransform(item);
|
||||
}
|
||||
GroundItem gi = GroundItemManager.create(item, location, killer instanceof Player ? (Player) killer : this);
|
||||
GroundItem gi = GroundItemManager.create(item, deathLocation, killer instanceof Player ? (Player) killer : this);
|
||||
gi.setRemainPrivate(stayPrivate);
|
||||
}
|
||||
if (coins > 0) {
|
||||
GroundItemManager.create(new Item(Items.COINS_995, coins), location, (Player) killer);
|
||||
GroundItemManager.create(new Item(Items.COINS_995, coins), deathLocation, (Player) killer);
|
||||
}
|
||||
if (killer instanceof Player)
|
||||
PlayerMonitor.log((Player) killer, LogType.PK, "Killed " + name + ", who dropped: " + itemsLost);
|
||||
|
|
@ -719,7 +741,8 @@ public class Player extends Entity {
|
|||
setComponentVisibility(this, 746, 12, false); //reenable the logout button (HD)
|
||||
super.finalizeDeath(killer);
|
||||
appearance.sync();
|
||||
if (!getSavedData().getGlobalData().isDeathScreenDisabled()) {
|
||||
// Don't show death tutorial in safe minigames (zone handled death) or if player disabled it
|
||||
if (!zoneHandledDeath && !getSavedData().getGlobalData().isDeathScreenDisabled()) {
|
||||
getInterfaceManager().open(new Component(153));
|
||||
}
|
||||
}
|
||||
|
|
@ -771,16 +794,17 @@ public class Player extends Entity {
|
|||
return false;
|
||||
}
|
||||
if (entity instanceof Player) {
|
||||
Player p = (Player) entity;
|
||||
if (p.getSkullManager().isWilderness() && skullManager.isWilderness()) {
|
||||
Player attacker = (Player) entity;
|
||||
if (attacker.getSkullManager().isWilderness() && skullManager.isWilderness()) {
|
||||
if (!GameWorld.getSettings().getWild_pvp_enabled())
|
||||
return false;
|
||||
if (p.getSkullManager().hasWildernessProtection())
|
||||
if (attacker.getSkullManager().hasWildernessProtection())
|
||||
return false;
|
||||
if (skullManager.hasWildernessProtection())
|
||||
return false;
|
||||
return super.isAttackable(entity, style, message);
|
||||
} else return false;
|
||||
} else if (!getZoneMonitor().isPvPable(entity, this, style, message)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.isAttackable(entity, style, message);
|
||||
}
|
||||
|
|
@ -806,7 +830,7 @@ public class Player extends Entity {
|
|||
return false;
|
||||
}
|
||||
return !skullManager.hasWildernessProtection();
|
||||
} else {
|
||||
} else if (!getZoneMonitor().isPvPable(this, target, style, message)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import core.game.system.timer.PersistTimer
|
|||
import core.game.system.timer.RSTimer
|
||||
import core.game.system.timer.TimerFlag
|
||||
|
||||
class Teleblock : PersistTimer (1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) {
|
||||
class Teleblock(private val sendChatboxMessage: Boolean = true) :
|
||||
PersistTimer(1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) {
|
||||
override fun run (entity: Entity) : Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onRegister (entity: Entity) {
|
||||
if (entity !is Player) return
|
||||
if (!sendChatboxMessage) return
|
||||
sendMessage (entity, "You have been teleblocked.")
|
||||
}
|
||||
|
||||
override fun getTimer (vararg args: Any) : RSTimer {
|
||||
val t = Teleblock()
|
||||
val t = Teleblock(args.getOrNull(1) as? Boolean ?: true)
|
||||
t.runInterval = args.getOrNull(0) as? Int ?: 100
|
||||
return t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,21 @@ public abstract class MapZone implements Zone {
|
|||
public boolean actionButton(Player player, int interfaceId, int buttonId, int slot, int itemId, int opcode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity is able to continue attacking another player within this zone.
|
||||
* This is added like continueAttack for player vs player fighting.
|
||||
*
|
||||
* @param e the attacking entity
|
||||
* @param target the target node being attacked
|
||||
* @param style the combat style being used
|
||||
* @param message whether to send a message to the player explaining why the attack cannot continue
|
||||
* @return {@code true} if the zone allows the attack to continue
|
||||
*/
|
||||
public boolean continuePvp(Entity e, Node target, CombatStyle style, boolean message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entity is able to continue attacking the target within this zone.
|
||||
*
|
||||
|
|
@ -154,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.
|
||||
|
|
@ -500,4 +530,4 @@ public abstract class MapZone implements Zone {
|
|||
public int hashCode() {
|
||||
return Objects.hash(uid, name, overlappable, fireRandomEvents, restriction, zoneType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,81 +5,83 @@ 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.entity.player.link.music.MusicEntry;
|
||||
import core.game.node.entity.player.link.music.MusicZone;
|
||||
import core.game.node.entity.player.link.request.RequestType;
|
||||
import core.game.node.item.Item;
|
||||
import core.game.world.map.Location;
|
||||
import core.game.world.map.Region;
|
||||
import org.rs09.consts.Items;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import core.game.world.map.Region;
|
||||
import org.rs09.consts.Items;
|
||||
|
||||
/**
|
||||
* Handles the zones for an entity.
|
||||
*
|
||||
* @author Emperor
|
||||
*/
|
||||
public final class ZoneMonitor {
|
||||
|
||||
|
||||
/**
|
||||
* The set of jewellery which allow teleporting from up to and including level 30 wildy.
|
||||
* Used to check if a player can teleport from 20 < level <= 30 wildy, see canTeleportByJewellery.
|
||||
* Note: the check is based on the nextJewellery (see EnchantedJewellery.kt), so this list should not contain the (4) items, and should contain the empty ones.
|
||||
*
|
||||
* @author Player Name
|
||||
*/
|
||||
static final Set<Integer> MID_WILDY_TELEPORT_JEWELLERY = Set.of(
|
||||
Items.AMULET_OF_GLORY_1704,
|
||||
Items.AMULET_OF_GLORY1_1706,
|
||||
Items.AMULET_OF_GLORY2_1708,
|
||||
Items.AMULET_OF_GLORY3_1710,
|
||||
Items.AMULET_OF_GLORYT_10362,
|
||||
Items.AMULET_OF_GLORYT1_10360,
|
||||
Items.AMULET_OF_GLORYT2_10358,
|
||||
Items.AMULET_OF_GLORYT3_10356,
|
||||
Items.SKILLS_NECKLACE_11113,
|
||||
Items.SKILLS_NECKLACE1_11111,
|
||||
Items.SKILLS_NECKLACE2_11109,
|
||||
Items.SKILLS_NECKLACE3_11107,
|
||||
Items.COMBAT_BRACELET_11126,
|
||||
Items.COMBAT_BRACELET1_11124,
|
||||
Items.COMBAT_BRACELET2_11122,
|
||||
Items.COMBAT_BRACELET3_11120,
|
||||
Items.RING_OF_WEALTH_14638,
|
||||
Items.RING_OF_WEALTH1_14640,
|
||||
Items.RING_OF_WEALTH2_14642,
|
||||
Items.RING_OF_WEALTH3_14644,
|
||||
Items.RING_OF_LIFE_2570
|
||||
Items.AMULET_OF_GLORY_1704,
|
||||
Items.AMULET_OF_GLORY1_1706,
|
||||
Items.AMULET_OF_GLORY2_1708,
|
||||
Items.AMULET_OF_GLORY3_1710,
|
||||
Items.AMULET_OF_GLORYT_10362,
|
||||
Items.AMULET_OF_GLORYT1_10360,
|
||||
Items.AMULET_OF_GLORYT2_10358,
|
||||
Items.AMULET_OF_GLORYT3_10356,
|
||||
Items.SKILLS_NECKLACE_11113,
|
||||
Items.SKILLS_NECKLACE1_11111,
|
||||
Items.SKILLS_NECKLACE2_11109,
|
||||
Items.SKILLS_NECKLACE3_11107,
|
||||
Items.COMBAT_BRACELET_11126,
|
||||
Items.COMBAT_BRACELET1_11124,
|
||||
Items.COMBAT_BRACELET2_11122,
|
||||
Items.COMBAT_BRACELET3_11120,
|
||||
Items.RING_OF_WEALTH_14638,
|
||||
Items.RING_OF_WEALTH1_14640,
|
||||
Items.RING_OF_WEALTH2_14642,
|
||||
Items.RING_OF_WEALTH3_14644,
|
||||
Items.RING_OF_LIFE_2570
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* The entity.
|
||||
*/
|
||||
private final Entity entity;
|
||||
|
||||
|
||||
/**
|
||||
* The currently entered zones.
|
||||
*/
|
||||
private final List<RegionZone> zones = new ArrayList<>(20);
|
||||
|
||||
|
||||
/**
|
||||
* The currently entered music zones.
|
||||
*/
|
||||
private final List<MusicZone> musicZones = new ArrayList<>(20);
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ZoneMonitor} {@code Object}.
|
||||
*
|
||||
* @param entity The entity.
|
||||
*/
|
||||
public ZoneMonitor(Entity entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the zone type.
|
||||
*
|
||||
* @return The zone type.
|
||||
*/
|
||||
public int getType() {
|
||||
|
|
@ -90,9 +92,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the player can logout.
|
||||
*
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean canLogout() {
|
||||
|
|
@ -103,18 +106,20 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the restriction was flagged.
|
||||
*
|
||||
* @param restriction The restriction flag.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean isRestricted(ZoneRestriction restriction) {
|
||||
return isRestricted(restriction.getFlag());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the restriction was flagged.
|
||||
*
|
||||
* @param flag The restriction flag.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
|
|
@ -126,9 +131,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a death.
|
||||
*
|
||||
* @param killer The killer.
|
||||
* @return {@code True} if the death got handled.
|
||||
*/
|
||||
|
|
@ -140,13 +146,31 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity is able to continue attacking the target.
|
||||
* Checks whether any current zone explicitly allows PvP against the target.
|
||||
*
|
||||
* @param player The attacking entity.
|
||||
* @param target The target.
|
||||
* @param style The combat style used.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean isPvPable(Entity player, Node target, CombatStyle style, boolean message) {
|
||||
for (RegionZone z : zones) {
|
||||
if (z.getZone().continuePvp(player, target, style, message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entity is able to continue attacking the target.
|
||||
*
|
||||
* @param target The target.
|
||||
* @param style The combat style used.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean continueAttack(Node target, CombatStyle style, boolean message) {
|
||||
if (target instanceof Entity) {
|
||||
if (!entity.continueAttack((Entity) target, style, message)) {
|
||||
|
|
@ -158,22 +182,41 @@ public final class ZoneMonitor {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (entity instanceof Player && target instanceof Player) {
|
||||
if (!((Player) entity).getSkullManager().isWilderness() || !((Player) target).getSkullManager().isWilderness()) {
|
||||
if(message) {
|
||||
((Player) entity).getPacketDispatch().sendMessage("You can only attack other players in the wilderness.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (target instanceof Entity && !MapZone.checkMulti(entity, (Entity) target, message)) {
|
||||
return false;
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @param target The target to interact with.
|
||||
* @param option The option.
|
||||
* @return {@code True} if the option got handled.
|
||||
|
|
@ -186,26 +229,27 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a zone handles a useWith interaction
|
||||
*/
|
||||
public boolean useWith(Item used, Node with){
|
||||
public boolean useWith(Item used, Node with) {
|
||||
for (RegionZone z : zones) {
|
||||
if (z.getZone().handleUseWith(entity.asPlayer(), used,with)) {
|
||||
if (z.getZone().handleUseWith(entity.asPlayer(), used, with)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the player handled the reward button using a map zone.
|
||||
*
|
||||
* @param interfaceId The interface id.
|
||||
* @param buttonId The button id.
|
||||
* @param slot The slot.
|
||||
* @param itemId The item id.
|
||||
* @param opcode The packet opcode.
|
||||
* @param buttonId The button id.
|
||||
* @param slot The slot.
|
||||
* @param itemId The item id.
|
||||
* @param opcode The packet opcode.
|
||||
* @return {@code True} if the button got handled.
|
||||
*/
|
||||
public boolean clickButton(int interfaceId, int buttonId, int slot, int itemId, int opcode) {
|
||||
|
|
@ -216,9 +260,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if multiway combat zone rules should be ignored.
|
||||
*
|
||||
* @param victim The victim.
|
||||
* @return {@code True} if this entity can attack regardless of multiway
|
||||
* combat zone.
|
||||
|
|
@ -231,9 +276,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity can teleport.
|
||||
*
|
||||
* @param type The teleport type (0=spell, 1=item, 2=object, 3=npc -1= force)
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
|
|
@ -251,9 +297,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a player can teleport with a jewellery piece in >= 1 <= 30 wilderness level
|
||||
*
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
private boolean canTeleportByJewellery(int type, Node node) {
|
||||
|
|
@ -262,11 +309,11 @@ public final class ZoneMonitor {
|
|||
}
|
||||
if (entity.timers.getTimer("teleblock") != null)
|
||||
return false;
|
||||
|
||||
|
||||
if (entity.getZoneMonitor().isRestricted(ZoneRestriction.TELEPORT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (entity.getLocks().isTeleportLocked()) {
|
||||
if (entity.isPlayer()) {
|
||||
Player p = entity.asPlayer();
|
||||
|
|
@ -275,12 +322,13 @@ public final class ZoneMonitor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the death should start for an entity.
|
||||
*
|
||||
* @param entity the entity.
|
||||
* @param killer the killer.
|
||||
* @return {@code True} if so.
|
||||
|
|
@ -293,9 +341,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity can fire a random event.
|
||||
*
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean canFireRandomEvent() {
|
||||
|
|
@ -306,9 +355,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clears the zones.
|
||||
*
|
||||
* @return {@code True} if the entity successfully left all regions.
|
||||
*/
|
||||
public boolean clear() {
|
||||
|
|
@ -324,10 +374,11 @@ public final class ZoneMonitor {
|
|||
musicZones.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity can move.
|
||||
* @param location The destination location.
|
||||
*
|
||||
* @param location The destination location.
|
||||
* @param destination The destination location.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
|
|
@ -339,18 +390,19 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a location update.
|
||||
*
|
||||
* @param last The last location of the entity.
|
||||
* @return {@code false} If the entity could not enter/leave a region.
|
||||
*/
|
||||
public boolean updateLocation(Location last) {
|
||||
if(entity instanceof Player && !entity.asPlayer().isArtificial()) {
|
||||
if (entity instanceof Player && !entity.asPlayer().isArtificial()) {
|
||||
checkMusicZones();
|
||||
}
|
||||
entity.updateLocation(last);
|
||||
for (Iterator<RegionZone> it = zones.iterator(); it.hasNext();) {
|
||||
for (Iterator<RegionZone> it = zones.iterator(); it.hasNext(); ) {
|
||||
RegionZone zone = it.next();
|
||||
if (!zone.getBorders().insideBorder(entity)) {
|
||||
if (zone.getZone().isDynamicZone()) {
|
||||
|
|
@ -385,7 +437,7 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks the music zones.
|
||||
*/
|
||||
|
|
@ -395,7 +447,7 @@ public final class ZoneMonitor {
|
|||
}
|
||||
Player player = (Player) entity;
|
||||
Location l = player.getLocation();
|
||||
for (Iterator<MusicZone> it = musicZones.iterator(); it.hasNext();) {
|
||||
for (Iterator<MusicZone> it = musicZones.iterator(); it.hasNext(); ) {
|
||||
MusicZone zone = it.next();
|
||||
if (!zone.getBorders().insideBorder(l.getX(), l.getY())) {
|
||||
if (zone.leave(player, false)) {
|
||||
|
|
@ -419,11 +471,12 @@ public final class ZoneMonitor {
|
|||
player.getMusicPlayer().unlock(music, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses commands in a certain zone.
|
||||
* @param player the player.
|
||||
* @param name the name.
|
||||
*
|
||||
* @param player the player.
|
||||
* @param name the name.
|
||||
* @param arguments the arguments.
|
||||
* @return {@code True} if parsed.
|
||||
*/
|
||||
|
|
@ -435,10 +488,11 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a request can be made in this zone.
|
||||
* @param type the type.
|
||||
*
|
||||
* @param type the type.
|
||||
* @param target the target.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
|
|
@ -450,9 +504,10 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity is in a zone.
|
||||
*
|
||||
* @param name The name of the zone.
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
|
|
@ -465,34 +520,37 @@ public final class ZoneMonitor {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the proper region zone for the given map zone.
|
||||
*
|
||||
* @param zone The map zone.
|
||||
*/
|
||||
public void remove(MapZone zone) {
|
||||
for (Iterator<RegionZone> it = zones.iterator(); it.hasNext();) {
|
||||
for (Iterator<RegionZone> it = zones.iterator(); it.hasNext(); ) {
|
||||
if (it.next().getZone() == zone) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the zones list.
|
||||
*
|
||||
* @return The list of region zones the entity is in.
|
||||
*/
|
||||
public List<RegionZone> getZones() {
|
||||
return zones;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the musicZones.
|
||||
*
|
||||
* @return The musicZones.
|
||||
*/
|
||||
public List<MusicZone> getMusicZones() {
|
||||
return musicZones;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,17 @@ import core.game.activity.ActivityPlugin
|
|||
import core.game.bots.PlayerScripts
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.interaction.InterfaceListener
|
||||
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
|
||||
|
|
@ -128,6 +132,25 @@ object ClassScanner {
|
|||
if(e != null && from != null && to != null) clazz.entityStep(e, to, from)
|
||||
return super.move(e, from, to)
|
||||
}
|
||||
|
||||
// Additional hook for continueAttack for PVP area controls
|
||||
override fun continuePvp(e: Entity, target: Node?, style: CombatStyle?, message: Boolean): Boolean {
|
||||
val clazzAttack = clazz.isPvpAllowed(e, target, style, message)
|
||||
if (clazzAttack == null) { // If clazz returns null, continue the usual check.
|
||||
return super.continuePvp(e, target, style, message)
|
||||
} 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)
|
||||
ZoneBuilder.configure(zone)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ import core.game.node.entity.player.info.Rights
|
|||
|
||||
class EquipTests {
|
||||
companion object {
|
||||
init {TestUtils.preTestSetup(); EquipHandler().defineListeners()}
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
if (InteractionListeners.get("equip", IntType.ITEM.ordinal) == null) {
|
||||
EquipHandler().defineListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun equipShouldFireEquipListeners() {
|
||||
|
|
@ -52,6 +57,26 @@ class EquipTests {
|
|||
Assertions.assertEquals(true, didRun)
|
||||
}
|
||||
|
||||
@Test fun fullInventoryUnequipShouldNotFireUnequipListeners() {
|
||||
var didRun = false
|
||||
val listener = object : InteractionListener {
|
||||
override fun defineListeners() {
|
||||
onUnequip(Items.ABYSSAL_WHIP_4151) { _, _ -> didRun = true; return@onUnequip true }
|
||||
}
|
||||
}
|
||||
listener.defineListeners()
|
||||
|
||||
TestUtils.getMockPlayer("fullunequip").use { player ->
|
||||
player.equipment.replace(Item(Items.ABYSSAL_WHIP_4151), EquipmentSlot.WEAPON.ordinal)
|
||||
player.inventory.add(Item(Items.CUP_OF_TEA_1978, player.inventory.capacity()))
|
||||
|
||||
EquipHandler.unequip(player, EquipmentSlot.WEAPON.ordinal, Items.ABYSSAL_WHIP_4151)
|
||||
|
||||
Assertions.assertEquals(false, didRun)
|
||||
Assertions.assertEquals(Items.ABYSSAL_WHIP_4151, player.equipment[EquipmentSlot.WEAPON.ordinal].id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun equippingItemThatReplacesAnotherItemShouldCallUnequipListenersForTheReplacedItem() {
|
||||
var didRun = false
|
||||
val listener = object : InteractionListener {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -92,7 +92,7 @@ jad_practice_enabled = false
|
|||
enable_global_chat = false
|
||||
#minimum HA value for announcements of bots selling on ge
|
||||
ge_announcement_limit = 500
|
||||
enable_castle_wars = false
|
||||
enable_castle_wars = true
|
||||
personalized_shops = false
|
||||
bots_influence_ge_price = true
|
||||
#verbose cutscene logging (for cutscenes in the new system)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue