mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-24 20:05:13 -06:00
And then you commit the rest of the Castle War
This commit is contained in:
parent
357160befe
commit
5f34e94c96
20 changed files with 4717 additions and 118 deletions
|
|
@ -1,11 +1,16 @@
|
|||
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.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 +21,85 @@ object CastleWars {
|
|||
const val castleWaterTap: Int = Scenery.TAP_4482
|
||||
const val cwSteppingStones: Int = Scenery.STEPPING_STONE_4411
|
||||
|
||||
// Stepping stone tile locations for flag auto-return check.
|
||||
// If a flag is dropped on any of these tiles, it immediately returns to base.
|
||||
fun isSteppingStoneTile(location: Location): Boolean {
|
||||
val obj = core.game.world.map.RegionManager.getObject(location)
|
||||
return obj != null && obj.id == cwSteppingStones
|
||||
}
|
||||
|
||||
// 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, destroyed, and repaired
|
||||
// Both 4381 and 4382 are operational catapults (different orientations in the map)
|
||||
// Both 4385 and 4386 are damaged/destroyed catapults (have "Repair" option instead of "Operate")
|
||||
const val catapult: Int = 4381 // Operational catapult variant 1
|
||||
const val catapultAlt: Int = 4382 // Operational catapult variant 2 (different orientation)
|
||||
const val catapultDamaged1: Int = 4385 // Damaged catapult variant 1
|
||||
const val catapultDamaged2: Int = 4386 // Damaged catapult variant 2
|
||||
val damagedCatapultIds: IntArray = intArrayOf(catapultDamaged1, catapultDamaged2)
|
||||
val allCatapultIds: IntArray = intArrayOf(catapult, catapultAlt, catapultDamaged1, catapultDamaged2)
|
||||
|
||||
// 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,7 +117,6 @@ 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
|
||||
|
|
@ -51,17 +134,36 @@ object CastleWars {
|
|||
const val cwBandages: Int = Items.BANDAGES_4049
|
||||
const val cwToolkit: Int = Items.TOOLKIT_4051
|
||||
const val cwBarricade: Int = Items.BARRICADE_4053
|
||||
const val cwManualBook: Int = Items.CASTLEWARS_MANUAL_4055
|
||||
const val cwManualBook: Int = Items.CASTLEWARS_MANUAL_4055 // TODO: unused? what happened
|
||||
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 +176,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,85 @@
|
|||
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) TODO: so... it sucks? shit
|
||||
* - Never retaliate when attacked (this was scary man they squared up)
|
||||
* - 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?) {
|
||||
return
|
||||
}
|
||||
|
||||
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,108 @@
|
|||
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.world.map.Location
|
||||
|
||||
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, _ ->
|
||||
renderer.resetToDefaults(player)
|
||||
true
|
||||
}
|
||||
|
||||
onClose(INTERFACE_ID) { player, _ ->
|
||||
player.removeAttribute(ATTR_LOCATION)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleButtonClick(player: Player, buttonId: Int): Boolean {
|
||||
val team = getPlayerTeam(player) ?: return closeAndReturn(player, false)
|
||||
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 (!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
|
||||
}
|
||||
|
||||
if (!removeItem(player, CastleWars.cwRock)) {
|
||||
sendMessage(player, "You don't have any rocks to fire!")
|
||||
return true
|
||||
}
|
||||
|
||||
val catapultTeam = CatapultTeam.fromCastleWarsTeam(team)
|
||||
firingHandler.fire(player, coord, catapultTeam, catapultLocation)
|
||||
closeInterface(player)
|
||||
return true
|
||||
}
|
||||
|
||||
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.updateMarkerPosition(player, newCoord)
|
||||
renderer.updateDigitDisplay(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 getPlayerTeam(player: Player): CastleWarsTeam? {
|
||||
return CastleWarsGameState.getPlayerTeam(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,106 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.bots.CastleWarsBotSpawner
|
||||
import core.api.Commands
|
||||
import core.api.sendMessage
|
||||
import core.game.interaction.Option
|
||||
import core.game.system.command.Privilege
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea.Companion.saradominPlayers
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea.Companion.zamorakPlayers
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
|
||||
/**
|
||||
* 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
|
||||
* - ::cw addbot <sara|zammy> [amount] - Spawns bots for the specified team
|
||||
*/
|
||||
class CastleWarsCommands : Commands {
|
||||
|
||||
override fun defineCommands() {
|
||||
define("cw", Privilege.ADMIN, "::cw <start|end|addbot>", "Castle Wars admin commands") { player, args ->
|
||||
if (args.size < 2) {
|
||||
sendMessage(player, "Usage: ::cw <start|end|addbot [sara|zammy] [amount]>")
|
||||
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 == 0 || zamCount == 0) {
|
||||
sendMessage(player, "Both teams need at least 1 player. 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
|
||||
}
|
||||
// Snapshot the sets before endGame() modifies them via teleport callbacks
|
||||
val saraSnapshot = saradominPlayers.toSet()
|
||||
val zamSnapshot = zamorakPlayers.toSet()
|
||||
CastleWarsGameState.endGame()
|
||||
CastleWarsItemHandler.clearAllBarricades()
|
||||
for (p in saraSnapshot) {
|
||||
p.interaction.remove(Option._P_ATTACK)
|
||||
p.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
}
|
||||
for (p in zamSnapshot) {
|
||||
p.interaction.remove(Option._P_ATTACK)
|
||||
p.properties.teleportLocation = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
}
|
||||
saradominPlayers.clear()
|
||||
zamorakPlayers.clear()
|
||||
sendMessage(player, "Castle Wars game ended by admin command.")
|
||||
}
|
||||
|
||||
"addbot" -> {
|
||||
if (args.size < 3) {
|
||||
sendMessage(player, "Usage: ::cw addbot <sara|zammy> [amount]")
|
||||
return@define
|
||||
}
|
||||
val teamArg = args[2].lowercase()
|
||||
val team = when {
|
||||
teamArg.startsWith("sara") -> CastleWarsTeam.SARADOMIN
|
||||
teamArg.startsWith("zam") -> CastleWarsTeam.ZAMORAK
|
||||
else -> {
|
||||
sendMessage(player, "Invalid team. Use 'sara' or 'zammy'.")
|
||||
return@define
|
||||
}
|
||||
}
|
||||
val amount = if (args.size >= 4) {
|
||||
args[3].toIntOrNull() ?: run {
|
||||
sendMessage(player, "Invalid amount. Must be a number.")
|
||||
return@define
|
||||
}
|
||||
} else {
|
||||
1
|
||||
}
|
||||
if (amount !in 1..20) {
|
||||
sendMessage(player, "Amount must be between 1 and 20.")
|
||||
return@define
|
||||
}
|
||||
for (i in 0 until amount) {
|
||||
CastleWarsBotSpawner.spawnBot(team)
|
||||
}
|
||||
sendMessage(player, "Spawned $amount ${team.teamName} bot(s) at the Castle Wars lobby.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendMessage(player, "Unknown subcommand. Use: start, end, or addbot")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
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 (~90 seconds) */
|
||||
const val FLAG_AUTO_RETURN_TICKS = 150
|
||||
|
||||
/** Duration of climbing ropes before despawning (45 seconds at 600ms/tick) */
|
||||
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
|
||||
|
||||
/** Maximum players per team */
|
||||
const val MAX_PLAYERS_PER_TEAM = 25 // TODO: I dont believe this, where? Says who
|
||||
|
||||
/** Minimum players per team required to start a game */
|
||||
const val MIN_PLAYERS_PER_TEAM = 2
|
||||
|
||||
/** Catapult targeting inaccuracy radius */
|
||||
const val CATAPULT_INACCURACY_RADIUS = 2 // TODO: says who
|
||||
|
||||
/** Catapult impact damage radius */
|
||||
const val CATAPULT_IMPACT_RADIUS = 3 // TODO: says who
|
||||
|
||||
/** Catapult minimum damage */
|
||||
const val CATAPULT_MIN_DAMAGE = 10 // TODO: says who
|
||||
|
||||
/** Catapult maximum damage */
|
||||
const val CATAPULT_MAX_DAMAGE = 20 // TODO: says who
|
||||
|
||||
// ==================== Coordinate Defaults ====================
|
||||
/** Default X/Y coordinate for catapult targeting (center of 0-30 range) */
|
||||
const val CATAPULT_DEFAULT_COORDINATE = 15 // TODO: says who
|
||||
|
||||
/** 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 // TODO: says who
|
||||
|
||||
// ==================== 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 ==================== // TODO: says who
|
||||
/** Thieving level required for pick-lock (1 = no real requirement, all players can attempt) */
|
||||
const val BACKDOOR_PICKLOCK_LEVEL = 1
|
||||
|
||||
/** Base pick-lock success chance (out of 100) at minimum level */
|
||||
const val BACKDOOR_PICKLOCK_BASE_CHANCE = 50
|
||||
|
||||
/** Maximum pick-lock success chance (out of 100) */
|
||||
const val BACKDOOR_PICKLOCK_MAX_CHANCE = 95
|
||||
|
||||
/** Ticks to lock player during pick-lock attempt */
|
||||
const val BACKDOOR_PICKLOCK_TICKS = 3
|
||||
|
||||
// ==================== 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 ====================
|
||||
/** Authentic waiting room countdown: GAME_COOLDOWN_MINUTES in ticks (5 min = 500 ticks) */
|
||||
const val WAITING_ROOM_COUNTDOWN_TICKS = GAME_COOLDOWN_MINUTES * 100 // ticksPerMinute = 100
|
||||
|
||||
/** Dev-mode fast countdown for testing (~10 seconds) */
|
||||
const val DEV_COUNTDOWN_TICKS = 17
|
||||
|
||||
/** Post-game shortened countdown in ticks (~2 minutes) for quick re-start */
|
||||
const val POST_GAME_COUNTDOWN_TICKS = 200 // 2 minutes = 200 ticks
|
||||
|
||||
// ==================== Ticket Reward Requirements ====================
|
||||
/** Minimum play time in minutes to be eligible for tickets */
|
||||
const val MIN_PLAY_TIME_MINUTES = 10
|
||||
|
||||
/** Minimum team size for tickets to be awarded */
|
||||
const val MIN_TEAM_SIZE_FOR_TICKETS = 5
|
||||
|
||||
// ==================== 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 = 200
|
||||
|
||||
// ==================== Flag Steal Timer ====================
|
||||
/** Time in ticks before teammates can steal the flag from a carrier (3 minutes = 300 ticks) */
|
||||
const val FLAG_STEAL_TIMER_TICKS = 300
|
||||
|
||||
/** Player attribute key for tracking when enemy flag was picked up */
|
||||
const val ATTR_FLAG_PICKUP_TICK = "cw:flag_pickup_tick"
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.animate
|
||||
import core.api.playAudio
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
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 org.rs09.consts.Sounds
|
||||
|
||||
/**
|
||||
* 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
|
||||
* TODO: 100% wild guesses and needs a fat refactoring
|
||||
*/
|
||||
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.getPlayerTeam(
|
||||
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 {
|
||||
// Doors are weak to crush > slash > stab
|
||||
val strengthLevel = player.skills.getLevel(Skills.STRENGTH)
|
||||
val bonusType = player.properties.attackStyle.bonusType
|
||||
val styleBonus = when (bonusType) {
|
||||
2 -> 10 // Crush
|
||||
1 -> 4 // Slash
|
||||
0 -> 1 // Stab
|
||||
else -> 3 // Default
|
||||
} // Calculate damage: base from strength + style bonus
|
||||
val damage =
|
||||
(strengthLevel / 12) + styleBonus // Clamp to 1-25 range (max 25% of door health per hit) TODO: 100% inauthentic
|
||||
return damage.coerceIn(1, 25)
|
||||
}
|
||||
|
||||
private fun getAttackAnimation(): Int {
|
||||
val attackAnim = player.properties.attackAnimation
|
||||
return if (attackAnim != null && attackAnim.id > 0) attackAnim.id else 422
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.sendMessage
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListener
|
||||
import core.game.node.item.GroundItem
|
||||
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.getPlayerTeam(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)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
}
|
||||
return@onUnequip false
|
||||
}
|
||||
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.getPlayerTeam(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) {
|
||||
if (CastleWarsGameState.pickUpOwnFlag(player, flagTeam)) { // Message already sent by pickUpOwnFlag
|
||||
}
|
||||
} else {
|
||||
if (CastleWarsGameState.pickUpFlag(player, flagTeam)) { // Message already sent by pickUpFlag
|
||||
}
|
||||
}
|
||||
|
||||
return@on true
|
||||
}
|
||||
// Legacy: Handle picking up dropped flags from the ground (ground items)
|
||||
// This is kept for backwards compatibility but scenery is the primary method now
|
||||
// TODO: cut
|
||||
on(intArrayOf(CastleWars.saradominFlag, CastleWars.zamorakFlag), IntType.GROUNDITEM, "take") { player, node ->
|
||||
if (!CastleWarsGameState.isGameActive) {
|
||||
return@on false
|
||||
}
|
||||
val playerTeam = CastleWarsGameState.getPlayerTeam(player) ?: return@on false
|
||||
val groundItem = node as? GroundItem ?: return@on false
|
||||
val flagTeam = when (groundItem.id) {
|
||||
CastleWars.saradominFlag -> CastleWarsTeam.SARADOMIN
|
||||
CastleWars.zamorakFlag -> CastleWarsTeam.ZAMORAK
|
||||
else -> return@on false
|
||||
}
|
||||
val isDroppedFlag = when (flagTeam) {
|
||||
CastleWarsTeam.SARADOMIN -> CastleWarsGameState.saradominFlagState == FlagState.DROPPED
|
||||
CastleWarsTeam.ZAMORAK -> CastleWarsGameState.zamorakFlagState == FlagState.DROPPED
|
||||
}
|
||||
if (!isDroppedFlag) {
|
||||
return@on false
|
||||
}
|
||||
if (flagTeam == playerTeam) {
|
||||
if (CastleWarsGameState.pickUpOwnFlag(player, flagTeam)) {
|
||||
return@on true
|
||||
}
|
||||
} else {
|
||||
if (CastleWarsGameState.pickUpFlag(player, flagTeam)) {
|
||||
return@on true
|
||||
}
|
||||
}
|
||||
|
||||
return@on true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1694
Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt
Normal file
1694
Server/src/main/content/minigame/castlewars/CastleWarsGameState.kt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,141 @@
|
|||
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.RegionManager.getLocalNpcs
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.zone.*
|
||||
|
||||
/**
|
||||
* MapZone for Castle Wars game area that handles death and respawn.
|
||||
* This is registered in addition to the MapArea to provide death handling,
|
||||
* since MapArea doesn't support overriding the death method.
|
||||
*/
|
||||
class CastleWarsGameZone : MapZone("CastleWarsGame", true, ZoneRestriction.RANDOM_EVENTS), StartupListener {
|
||||
|
||||
companion object {
|
||||
|
||||
private val saradominStandardFloor = ZoneBorders(Location.create(2426, 3073, 3), Location.create(2430, 3077, 3))
|
||||
private val zamorakStandardFloor = ZoneBorders(Location.create(2373, 3134, 3), Location.create(2369, 3130, 3))
|
||||
private val saradominUpperFloor = ZoneBorders(Location.create(2431, 3080, 2), Location.create(2423, 3072, 2))
|
||||
private val zamorakUpperFloor = ZoneBorders(Location.create(2368, 3127, 2), Location.create(2376, 3135, 2))
|
||||
private val saradominFloor = ZoneBorders(Location.create(2420, 3072, 1), Location.create(2431, 3083, 1))
|
||||
private val zamorakFloor = ZoneBorders(Location.create(2379, 3135, 1), Location.create(2368, 3124, 1))
|
||||
private val battleField = ZoneBorders(Location.create(2368, 3135, 0), Location.create(2431, 3072, 0))
|
||||
private val saradominTunnels = ZoneBorders(Location.create(2430, 9481, 0), Location.create(2385, 9504, 0))
|
||||
private val zamorakTunnels = ZoneBorders(Location.create(2401, 9503, 0), Location.create(2366, 9529, 0))
|
||||
private val centerTunnels = ZoneBorders(Location.create(2396, 9493, 0), Location.create(2405, 9515, 0))
|
||||
val allBorders = arrayOf(
|
||||
saradominStandardFloor,
|
||||
zamorakStandardFloor,
|
||||
saradominUpperFloor,
|
||||
zamorakUpperFloor,
|
||||
saradominFloor,
|
||||
zamorakFloor,
|
||||
battleField,
|
||||
saradominTunnels,
|
||||
zamorakTunnels,
|
||||
centerTunnels
|
||||
)
|
||||
|
||||
/**
|
||||
* Handle Castle Wars death with poison preservation.
|
||||
* Shared between CastleWarsGameZone.death() and CastleWarsGameArea.handleDeath().
|
||||
* In Castle Wars, poison persists through death unlike normal safe deaths.
|
||||
*/
|
||||
fun handleCastleWarsDeath(player: Player): Boolean {
|
||||
if (CastleWarsGameArea.ticksLeftInGame <= 0) return false
|
||||
val team = CastleWarsGameState.getPlayerTeam(player) ?: return false
|
||||
|
||||
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 allBorders) {
|
||||
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 getLocalNpcs(e, 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,351 @@
|
|||
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 5000 HP, max 10 per team TODO: 5000 HP BARRICADE HAHHAH FAT CHANCE. keeping that in for now
|
||||
* - 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.getPlayerTeam(attacker) ?: return
|
||||
RegionManager.getLocalPlayers(location, radius)
|
||||
.filter { target ->
|
||||
target != attacker &&
|
||||
target.isActive &&
|
||||
CastleWarsGameState.getPlayerTeam(target) != null &&
|
||||
CastleWarsGameState.getPlayerTeam(target) != 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
|
||||
}
|
||||
|
||||
onUseWith(IntType.PLAYER, CastleWars.cwBandages, -1) { player, used, with ->
|
||||
val target = with as? Player ?: return@onUseWith false
|
||||
|
||||
val playerTeam = CastleWarsGameState.getPlayerTeam(player)
|
||||
val targetTeam = CastleWarsGameState.getPlayerTeam(target)
|
||||
|
||||
if (playerTeam == null || targetTeam == null) {
|
||||
sendMessage(player, "You can only use bandages in Castle Wars.")
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
if (playerTeam != targetTeam) {
|
||||
sendMessage(player, "You can only heal your teammates!")
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
if (CastleWarsGameState.isCarryingFlag(target) && CastleWarsGameState.canTeammateStealFlag(target)) {
|
||||
CastleWarsGameState.transferFlag(target, player)
|
||||
return@onUseWith true
|
||||
}
|
||||
|
||||
if (useBandage(player, target)) {
|
||||
removeItem(player, used.asItem())
|
||||
}
|
||||
return@onUseWith 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.getPlayerTeam(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 (!CastleWarsGameState.isGameActive) {
|
||||
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 (!CastleWarsGameState.isGameActive) {
|
||||
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()
|
||||
|
||||
dealExplosiveAoeDamage(player, player.location)
|
||||
|
||||
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 (!CastleWarsGameState.isGameActive) {
|
||||
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)
|
||||
|
||||
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 (!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 (!CastleWarsGameState.isGameActive) {
|
||||
return@onUseWith false
|
||||
}
|
||||
|
||||
val originalId = if (saradominBarricades.contains(barricade)) {
|
||||
NPCs.BARRICADE_1532
|
||||
} else {
|
||||
NPCs.BARRICADE_1534
|
||||
}
|
||||
|
||||
barricade.transform(originalId)
|
||||
|
||||
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.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.hasCastleWarsBracelet()) {
|
||||
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 getLocalBarricades(location: Location, radius: Int): List<NPC> {
|
||||
val barricadeIds = intArrayOf(
|
||||
NPCs.BARRICADE_1532, NPCs.BARRICADE_1533,
|
||||
NPCs.BARRICADE_1534, NPCs.BARRICADE_1535
|
||||
)
|
||||
|
||||
return RegionManager.getLocalNpcs(location, radius)
|
||||
.filter { it.id in barricadeIds && it.isActive }
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,5 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.*
|
||||
import core.game.node.entity.player.Player
|
||||
|
||||
|
|
@ -7,16 +9,8 @@ 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, enoughPlayers: Boolean, gameStartMinutes: Int) {
|
||||
// When not enough players, send -1 to hide the timer display
|
||||
setVarp(player, 380, if (enoughPlayers) gameStartMinutes else -1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.api.ShutdownListener
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsRespawnArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
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)
|
||||
} else if (CastleWarsGameState.isCarryingOwnFlag(player)) {
|
||||
CastleWarsGameState.dropOwnFlag(player)
|
||||
}
|
||||
CastleWarsGameState.restoreAndCleanupPlayer(player)
|
||||
player.location = CastleWars.lobbyBankArea.randomWalkableLoc
|
||||
}
|
||||
|
||||
CastleWarsItemHandler.clearAllBarricades()
|
||||
|
||||
CastleWarsGameState.resetGame()
|
||||
|
||||
CastleWarsGameArea.saradominPlayers.clear()
|
||||
CastleWarsGameArea.zamorakPlayers.clear()
|
||||
CastleWarsGameArea.loggedOutPlayers.clear()
|
||||
CastleWarsWaitingArea.waitingSaradominPlayers.clear()
|
||||
CastleWarsWaitingArea.waitingZamorakPlayers.clear()
|
||||
|
||||
CastleWarsGameArea.ticksLeftInGame = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
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( // TODO: tunnels in general are working great. but this... needs a thorough check from start to finish
|
||||
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 MINING_ANIMATION = 625
|
||||
|
||||
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 {
|
||||
val pickaxe = SkillingTool.getPickaxe(player)
|
||||
if (pickaxe == null) {
|
||||
sendMessage(player, "You need a pickaxe to mine these rocks.")
|
||||
return false
|
||||
}
|
||||
if (!rocks.isActive) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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 {
|
||||
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!")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
126
Server/src/main/content/minigame/castlewars/CatapultConfig.kt
Normal file
126
Server/src/main/content/minigame/castlewars/CatapultConfig.kt
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import core.game.world.map.Location
|
||||
import org.rs09.consts.Components
|
||||
|
||||
/**
|
||||
* Configuration constants for the Castle Wars catapult interface.
|
||||
* All magic numbers and hardcoded values are centralized here.
|
||||
*/
|
||||
object CatapultConfig { // TODO: this is definitely fucked somehow
|
||||
|
||||
/** 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
|
||||
|
||||
/** Enum ID for digit models (maps 0-9 to model IDs) */
|
||||
const val DIGIT_MODEL_ENUM = 1595
|
||||
|
||||
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(69, 70, 71, 72)
|
||||
|
||||
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_COORDINATE = CastleWarsConstants.CATAPULT_DEFAULT_COORDINATE
|
||||
const val CENTER_OFFSET = 15
|
||||
|
||||
val SARADOMIN_BASE = Location.create(2426, 3073, 0)
|
||||
val ZAMORAK_BASE = Location.create(2373, 3134, 0)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(val baseLocation: Location) {
|
||||
SARADOMIN(CatapultConfig.SARADOMIN_BASE) {
|
||||
override fun calculateTargetX(catapultX: Int, interfaceX: Int): Int {
|
||||
return catapultX + (interfaceX - CatapultConfig.CENTER_OFFSET)
|
||||
}
|
||||
|
||||
override fun calculateTargetY(catapultY: Int, interfaceY: Int): Int {
|
||||
return catapultY + interfaceY
|
||||
}
|
||||
},
|
||||
|
||||
ZAMORAK(CatapultConfig.ZAMORAK_BASE) {
|
||||
override fun calculateTargetX(catapultX: Int, interfaceX: Int): Int {
|
||||
return catapultX - (interfaceX - CatapultConfig.CENTER_OFFSET)
|
||||
}
|
||||
|
||||
override fun calculateTargetY(catapultY: Int, interfaceY: Int): Int {
|
||||
return catapultY - interfaceY
|
||||
}
|
||||
};
|
||||
|
||||
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,116 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
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 { // TODO: i dont think this even works.. at all even
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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)
|
||||
playFireEffects(player, finalLocation, impactDelayTicks)
|
||||
scheduleImpact(player, finalLocation, impactDelayTicks)
|
||||
|
||||
sendMessage(player, "You fire the catapult!")
|
||||
}
|
||||
|
||||
private 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) {
|
||||
val distance = from.getDistance(to).toInt()
|
||||
val speed = PROJECTILE_BASE_SPEED + (distance * PROJECTILE_SPEED_PER_TILE)
|
||||
|
||||
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 {
|
||||
dealImpactDamage(player, targetLocation)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun dealImpactDamage(attacker: Player, targetLocation: Location) {
|
||||
RegionManager.getLocalPlayers(targetLocation, IMPACT_RADIUS)
|
||||
.filter { isValidTarget(it, attacker) }
|
||||
.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, attacker: Player): Boolean {
|
||||
return CastleWarsGameState.getPlayerTeam(target) != null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.CatapultConfig.DEFAULT_COORDINATE
|
||||
import content.minigame.castlewars.CatapultConfig.DIGIT_CHILD_IDS
|
||||
import content.minigame.castlewars.CatapultConfig.DIGIT_MODEL_ENUM
|
||||
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.DataMap
|
||||
import core.game.node.entity.player.Player
|
||||
import core.net.packet.PacketRepository
|
||||
import core.net.packet.context.ChildPositionContext
|
||||
import core.net.packet.context.DisplayModelContext
|
||||
import core.net.packet.out.DisplayModel
|
||||
import core.net.packet.out.RepositionChild
|
||||
|
||||
/**
|
||||
* Handles rendering the catapult interface including marker position and digit displays.
|
||||
*/
|
||||
class CatapultInterfaceRenderer { // TODO: this is getting very close to 100%. still fucked but on the right track for sure
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the interface to default coordinates.
|
||||
* @param player The player whose interface to reset
|
||||
*/
|
||||
fun resetToDefaults(player: Player) {
|
||||
val defaultCoord = Coordinate(DEFAULT_COORDINATE, DEFAULT_COORDINATE)
|
||||
updateMarkerPosition(player, defaultCoord)
|
||||
updateDigitDisplay(player, defaultCoord)
|
||||
}
|
||||
|
||||
private fun calculateScreenX(coordX: Int): Int {
|
||||
val pixelsPerUnit = MAP_WIDTH / MAX_COORDINATE
|
||||
return MAP_BASE_X + (coordX * pixelsPerUnit)
|
||||
}
|
||||
|
||||
private fun calculateScreenY(coordY: Int): Int {
|
||||
val pixelsPerUnit = MAP_HEIGHT / MAX_COORDINATE
|
||||
// Y is inverted - 0 is at the bottom, MAX is at the top
|
||||
return MAP_BASE_Y + ((MAX_COORDINATE - coordY) * pixelsPerUnit)
|
||||
}
|
||||
|
||||
private fun sendDigitModel(player: Player, childId: Int, digit: Int) {
|
||||
val modelId = getDigitModelId(digit)
|
||||
PacketRepository.send(
|
||||
DisplayModel::class.java,
|
||||
DisplayModelContext(
|
||||
player,
|
||||
DisplayModelContext.ModelType.MODEL,
|
||||
modelId,
|
||||
0,
|
||||
INTERFACE_ID,
|
||||
childId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDigitModelId(digit: Int): Int {
|
||||
return DataMap.get(DIGIT_MODEL_ENUM).getInt(digit)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
package rs09.game.content.activity.castlewars
|
||||
package content.minigame.castlewars
|
||||
|
||||
import content.minigame.castlewars.areas.CastleWarsGameArea
|
||||
import content.minigame.castlewars.areas.CastleWarsWaitingArea
|
||||
import core.api.TickListener
|
||||
import core.api.findNPC
|
||||
import core.api.openDialogue
|
||||
import core.api.sendChat
|
||||
import core.api.sendMessage
|
||||
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 core.tools.ticksPerMinute
|
||||
import org.rs09.consts.Items
|
||||
import org.rs09.consts.NPCs
|
||||
|
||||
|
|
@ -96,7 +100,7 @@ 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."
|
||||
"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?",
|
||||
|
|
@ -122,10 +126,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 + ticksPerMinute - 1) / ticksPerMinute
|
||||
|
||||
// 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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue