diff --git a/Server/src/main/content/minigame/barbassault/arena/BASession.kt b/Server/src/main/content/minigame/barbassault/arena/BASession.kt index 5d1b01361..43ec66b4d 100644 --- a/Server/src/main/content/minigame/barbassault/arena/BASession.kt +++ b/Server/src/main/content/minigame/barbassault/arena/BASession.kt @@ -57,6 +57,7 @@ class BarbassaultSession( val activity: BAActivity ? = null) :LogoutListener, M val sessionNpcs = mutableListOf() var currentNPCCount = SpawnCaps(0,0,0,0) var currentNPCDeathCount = SpawnCaps(0,0,0,0) + var currentNPCEscapeCount = SpawnCaps(0,0,0,0) var npcTunnel: List = listOf( PenanceNpcTunnel(PenanceType.RANGER,location(17,47,0),location(18,46,0),Direction.SOUTH), @@ -193,6 +194,18 @@ class BarbassaultSession( val activity: BAActivity ? = null) :LogoutListener, M fun isPenanceDead( type: PenanceType): Boolean = currentNPCDeathCount[type] >= currentNPCCount[type] fun isAllPenanceDead(): Boolean = PenanceType.values().all { isPenanceDead(it) } + fun runnerEscaped(runner: NPC) { + if (!sessionNpcs.remove(runner)) { + return + } + + currentNPCEscapeCount[PenanceType.RUNNER] = currentNPCEscapeCount[PenanceType.RUNNER] + 1 + val defenders = team.getPlayersByRole(BarbRole.DEFENDER) + val penalizedPlayers = if (defenders.isEmpty()) team.allPlayers() else defenders + penalizedPlayers.forEach { eventBus.emit(BarbAssEvent.RunnerEscaped(it)) } + runner.clear() + } + fun pennanceClearedMessage(npc: NPC) { val session = getBASession(npc) ?: return val type = npc.getAttribute("barbass-type") @@ -225,7 +238,7 @@ class BarbassaultSession( val activity: BAActivity ? = null) :LogoutListener, M } fun tryPenanceSpawn(type: PenanceType, force: Boolean = false): Boolean { - val current = currentNPCCount[type] + val current = currentNPCCount[type] - currentNPCEscapeCount[type] val cap = currentWaveDefinition.caps[type] val npcId = currentWaveDefinition.npcIds[type] ?: return false @@ -625,9 +638,9 @@ class BarbassaultSession( val activity: BAActivity ? = null) :LogoutListener, M } enum class LureFood(override val ui: String, val itemId:Int, override val hornShout:String):HasUIText { - TOFU( "Tofu", Items.WORMS_10515, "Defender: Drop Tofu!"), + TOFU( "Tofu", Items.TOFU_10514, "Defender: Drop Tofu!"), CRACKERS("Crackers",Items.CRACKERS_10513,"Defender: Drop Crackers!"), - WORMS( "Worms", Items.TOFU_10514, "Defender: Drop Worms!"); + WORMS( "Worms", Items.WORMS_10515, "Defender: Drop Worms!"); } class AttackerCall( override val required: AttackerStyle ) : RoleCall() diff --git a/Server/src/main/content/minigame/barbassault/arena/npcs/PenanceRunnerNPC.kt b/Server/src/main/content/minigame/barbassault/arena/npcs/PenanceRunnerNPC.kt index 025f5f641..26b370be5 100644 --- a/Server/src/main/content/minigame/barbassault/arena/npcs/PenanceRunnerNPC.kt +++ b/Server/src/main/content/minigame/barbassault/arena/npcs/PenanceRunnerNPC.kt @@ -2,17 +2,25 @@ package content.minigame.barbassault.arena.npcs import content.minigame.barbassault.BA_SESSION_KEY import content.minigame.barbassault.BarbRole +import content.minigame.barbassault.LureItems +import content.minigame.barbassault.getBALevels 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 import content.minigame.barbassault.arena.* +import content.minigame.barbassault.arena.scenery.DefenderTrap import content.minigame.barbassault.getBASession import core.api.forceWalk +import core.api.hasLineOfSight import core.api.sendChat +import core.game.node.item.GroundItemManager import core.game.node.entity.player.Player +import core.game.world.map.Location import core.game.world.map.RegionManager +import kotlin.math.abs +import kotlin.math.max val PENANCE_RUNNER_IDS = listOf( @@ -32,82 +40,292 @@ class PenanceRunnerNPC : NPCBehavior(*PENANCE_RUNNER_IDS.toIntArray()) { val BAD_EAT = "Blurghh" val TRAP_HIT = "Urghhh!" val ESCAPE = "Raaa!!" - var actionState = ACTION_STATE.SEEK_FOOD + private val defenderTrap = DefenderTrap() - enum class ACTION_STATE { SEEK_FOOD, SEEK_PLAYER, STALLED, SEEK_ESCAPE, EATING, THINKING } + private enum class RunnerMode { RANDOM_WALKING, HUNTING } + + private companion object { + const val MODE = "ba-runner:mode" + const val MODE_TICKS = "ba-runner:mode-ticks" + const val HUNT_TICKS = "ba-runner:hunt-ticks" + const val TARGET = "ba-runner:target" + const val TARGET_CYCLES = "ba-runner:target-cycles" + const val TARGET_LAST_LOC = "ba-runner:target-last-loc" + const val SKIP_NEXT_HUNT = "ba-runner:skip-next-hunt" + const val BAD_FOOD_WALK_TICKS = "ba-runner:bad-food-walk-ticks" + const val EAT_PAUSE_TICKS = "ba-runner:eat-pause-ticks" + const val ESCAPE_REMOVE_TICKS = "ba-runner:escape-remove-ticks" + + val HUNT_ZONE_ORDER = (0 downTo -5).flatMap { x -> + (0 downTo -5).map { y -> x to y } + } + } override fun getXpMultiplier(self: NPC, attacker: Entity): Double { return 0.0 } override fun onCreation(self: NPC) { - self.isWalks = true - self.walkRadius = 24 + self.isWalks = false + self.walkRadius = 0 self.destinationFlag self.setAttribute("barbass-type", PenanceType.RUNNER) - + self.setAttribute(MODE, RunnerMode.RANDOM_WALKING) + self.setAttribute(MODE_TICKS, (0..4).random()) + self.setAttribute(HUNT_TICKS, (0..2).random()) } - var stateTicks = 0 + override fun tick(self: NPC): Boolean { val session = self.getAttribute(BA_SESSION_KEY) ?: return false - stateTicks++ - - if (stateTicks >= 6) { - stateTicks = 0 - pickNewState() + if (defenderTrap.tryTriggerTrap(self)) { + return false } - when (actionState) { - ACTION_STATE.SEEK_FOOD -> seekFood(self, session) - ACTION_STATE.SEEK_PLAYER -> seekPlayers(self, session) - ACTION_STATE.SEEK_ESCAPE -> escape(self, session) - ACTION_STATE.STALLED -> stalled(self) - ACTION_STATE.EATING -> eating(self) - ACTION_STATE.THINKING -> self.resetWalk() + val escapeRemoveTicks = self.getAttribute(ESCAPE_REMOVE_TICKS, 0) + if (escapeRemoveTicks > 0) { + self.setAttribute(ESCAPE_REMOVE_TICKS, escapeRemoveTicks - 1) + self.resetWalk() + if (escapeRemoveTicks <= 1) { + session.runnerEscaped(self) + } + return false + } + + if (isAtEscapeTunnel(session, self.location)) { + beginEscape(self) + return false + } + + val eatPauseTicks = self.getAttribute(EAT_PAUSE_TICKS, 0) + if (eatPauseTicks > 0) { + self.setAttribute(EAT_PAUSE_TICKS, eatPauseTicks - 1) + self.resetWalk() + self.shouldPreventStacking(self) + return false + } + + val modeChanged = tickModeTimer(self) + tickHuntTimer(self, session) + + val badWalkTicks = self.getAttribute(BAD_FOOD_WALK_TICKS, 0) + if (badWalkTicks > 0) { + self.setAttribute(BAD_FOOD_WALK_TICKS, badWalkTicks - 1) + walkAfterBadFood(self, session) + self.shouldPreventStacking(self) + return false + } + + val target = self.getAttribute(TARGET, null) + if (target != null) { + chaseFood(self, session, target) + } else if (modeChanged && self.getAttribute(MODE, RunnerMode.RANDOM_WALKING) == RunnerMode.RANDOM_WALKING) { + randomWalk(self, session) } self.shouldPreventStacking(self) + return false + } + + private fun tickModeTimer(self: NPC): Boolean { + val modeTicks = self.getAttribute(MODE_TICKS, 0) + 1 + if (modeTicks < 5) { + self.setAttribute(MODE_TICKS, modeTicks) + return false + } + + self.setAttribute(MODE_TICKS, 0) + val nextMode = when (self.getAttribute(MODE, RunnerMode.RANDOM_WALKING)) { + RunnerMode.RANDOM_WALKING -> RunnerMode.HUNTING + RunnerMode.HUNTING -> RunnerMode.RANDOM_WALKING + } + + self.setAttribute(MODE, nextMode) + if (nextMode == RunnerMode.HUNTING) { + val targetCycles = self.getAttribute(TARGET_CYCLES, 0) + 1 + self.setAttribute(TARGET_CYCLES, targetCycles) + } return true } - - fun seekFood(self: NPC, session: BarbassaultSession) { - val groundItem = session.findClosestLure(self.location) ?: return - val target = groundItem.location - val occupied = RegionManager.getLocalEntitys(target, 0).any { it != self && it.location == target } - - if (occupied) { - val adjacent = target.getCardinalTiles().firstOrNull { it.isNextTo(self) } - if (adjacent != null) { forceWalk(self, adjacent, "DUMB") } - - self.resetWalk() - self.faceLocation(target) + private fun tickHuntTimer(self: NPC, session: BarbassaultSession) { + if (self.getAttribute(MODE, RunnerMode.RANDOM_WALKING) != RunnerMode.HUNTING) { return } - forceWalk(self, target, "DUMB") + val huntTicks = self.getAttribute(HUNT_TICKS, 0) + 1 + if (huntTicks < 3) { + self.setAttribute(HUNT_TICKS, huntTicks) + return + } - if (self.location == target) { - sendChat(self, if (groundItem.flag == true) GOOD_EAT else BAD_EAT) - session.destroyGroundItem(groundItem) - self.resetWalk() + self.setAttribute(HUNT_TICKS, 0) + if (self.getAttribute(SKIP_NEXT_HUNT, false)) { + self.setAttribute(SKIP_NEXT_HUNT, false) + clearTarget(self) + return + } + + val currentTarget = self.getAttribute(TARGET, null) + if (currentTarget != null && self.getAttribute(TARGET_CYCLES, 0) < 2 && isFoodStillActive(session, currentTarget)) { + return + } + + selectFood(self, session)?.let { food -> + self.setAttribute(TARGET, food) + self.setAttribute(TARGET_CYCLES, 0) + self.setAttribute(TARGET_LAST_LOC, food.location) } } - fun pickNewState() { - actionState = when ((0..2).random()) { - 0 -> ACTION_STATE.SEEK_FOOD - 1 -> ACTION_STATE.SEEK_PLAYER - else -> ACTION_STATE.SEEK_ESCAPE + + private fun chaseFood(self: NPC, session: BarbassaultSession, food: BAGroundSpawn) { + val foodLocation = food.location + if (!isFoodStillActive(session, food)) { + clearTarget(self) + forceWalk(self, foodLocation, "DUMB") + return } + + if (self.location == foodLocation) { + eatFood(self, session, food) + return + } + + val occupied = RegionManager.getLocalEntitys(foodLocation, 0).any { it != self && it.location == foodLocation } + if (occupied) { + if (!foodLocation.isNextTo(self)) { + val adjacent = foodLocation.cardinalTiles + .filter { RegionManager.isTeleportPermitted(it) } + .minByOrNull { it.getDistance(self.location) } + if (adjacent != null) { + forceWalk(self, adjacent, "DUMB") + } + } else { + self.resetWalk() + } + self.faceLocation(foodLocation) + return + } + + forceWalk(self, foodLocation, "DUMB") } - private fun escape(self: NPC, session1: BarbassaultSession) { } - private fun stalled(self: NPC) { } - private fun eating(self:NPC){ } + private fun eatFood(self: NPC, session: BarbassaultSession, food: BAGroundSpawn) { + val goodFood = food.flag == true + sendChat(self, if (goodFood) GOOD_EAT else BAD_EAT) + session.destroyGroundItem(food) + clearTarget(self) + self.resetWalk() - fun seekPlayers(self: NPC,session: BarbassaultSession) { - return + if (goodFood) { + self.setAttribute(EAT_PAUSE_TICKS, 5) + if (self.getAttribute(MODE, RunnerMode.RANDOM_WALKING) == RunnerMode.HUNTING) { + self.setAttribute(MODE, RunnerMode.RANDOM_WALKING) + self.setAttribute(MODE_TICKS, 0) + } + return + } + + self.setAttribute(SKIP_NEXT_HUNT, true) + self.setAttribute(BAD_FOOD_WALK_TICKS, 5) + walkAfterBadFood(self, session) + } + + private fun selectFood(self: NPC, session: BarbassaultSession): BAGroundSpawn? { + val radius = lureRadius(session) + val allVisibleFood = session.BAgroundItems + .filter { isFoodStillActive(session, it) } + .filter { it.id in LureItems } + .filter { hasLineOfSight(self, it) } + + if (allVisibleFood.none { tileRadius(self.location, it.location) <= radius }) { + return null + } + + for (zone in HUNT_ZONE_ORDER) { + val food = allVisibleFood.lastOrNull { zoneFor(session, it.location) == zone } + if (food != null) { + return food + } + } + + return null + } + + private fun randomWalk(self: NPC, session: BarbassaultSession) { + val location = self.location + val destination = if (isAtSouthernWall(session, location)) { + furthestWalkableStraight(location, 0, -1, 5) ?: escapeLocation(session) + } else { + when ((0..5).random()) { + 0 -> furthestWalkableStraight(location, -1, 0, 5) + 1 -> furthestWalkableStraight(location, 1, 0, 5) + else -> furthestWalkableStraight(location, 0, -1, 5) + } + } ?: return + + forceWalk(self, destination, "DUMB") + } + + private fun walkAfterBadFood(self: NPC, session: BarbassaultSession) { + val directionY = if (self.location.y >= session.base.y + 42) -1 else 1 + val destination = furthestWalkableStraight(self.location, 0, directionY, 5) ?: return + forceWalk(self, destination, "DUMB") + } + + private fun lureRadius(session: BarbassaultSession): Int { + return if (session.team.getPlayersByRole(BarbRole.DEFENDER).any { getBALevels(it).def >= 2 }) 5 else 4 + } + + private fun isFoodStillActive(session: BarbassaultSession, food: BAGroundSpawn): Boolean { + return food.isActive && !food.isRemoved && session.BAgroundItems.contains(food) && GroundItemManager.getItems().contains(food) + } + + private fun clearTarget(self: NPC) { + self.removeAttribute(TARGET) + self.removeAttribute(TARGET_CYCLES) + self.removeAttribute(TARGET_LAST_LOC) + } + + private fun zoneFor(session: BarbassaultSession, location: Location): Pair { + val relativeX = location.x - session.base.x + val relativeY = location.y - session.base.y + return Math.floorDiv(relativeX - 40, 8) to Math.floorDiv(relativeY - 40, 8) + } + + private fun tileRadius(a: Location, b: Location): Int { + return max(abs(a.x - b.x), abs(a.y - b.y)) + } + + private fun furthestWalkableStraight(start: Location, dx: Int, dy: Int, maxSteps: Int): Location? { + var destination: Location? = null + for (step in 1..maxSteps) { + val next = start.transform(dx * step, dy * step, 0) + if (!RegionManager.isTeleportPermitted(next)) { + break + } + destination = next + } + return destination + } + + private fun isAtSouthernWall(session: BarbassaultSession, location: Location): Boolean { + return location.y <= session.base.y + 16 + } + + private fun isAtEscapeTunnel(session: BarbassaultSession, location: Location): Boolean { + return location.y <= escapeLocation(session).y + } + + private fun beginEscape(self: NPC) { + sendChat(self, ESCAPE) + clearTarget(self) + self.resetWalk() + self.setAttribute(ESCAPE_REMOVE_TICKS, 1) + } + + private fun escapeLocation(session: BarbassaultSession): Location { + return session.base.transform(35, 15, 0) } override fun onDeathFinished(self: NPC, killer: Entity) { @@ -130,4 +348,4 @@ Runner emote runner_hit_5101 runner_Move_5100 runner_Idle 5099 - */ \ No newline at end of file + */ diff --git a/Server/src/main/content/minigame/barbassault/arena/scenery/DefenderTrap.kt b/Server/src/main/content/minigame/barbassault/arena/scenery/DefenderTrap.kt index cc9fda70a..a7c96f10a 100644 --- a/Server/src/main/content/minigame/barbassault/arena/scenery/DefenderTrap.kt +++ b/Server/src/main/content/minigame/barbassault/arena/scenery/DefenderTrap.kt @@ -1,8 +1,14 @@ package content.minigame.barbassault.arena.scenery +import content.minigame.barbassault.LureItems +import content.minigame.barbassault.BarbRole +import content.minigame.barbassault.arena.BAGroundSpawn +import content.minigame.barbassault.arena.BarbassaultSession import content.minigame.barbassault.arena.npcs.PENANCE_RUNNER_IDS +import content.minigame.barbassault.getBASession import core.api.* import core.game.node.entity.Entity +import core.game.node.entity.combat.DeathTask import core.game.node.entity.player.Player import core.game.system.task.Pulse import core.game.world.GameWorld @@ -28,23 +34,35 @@ class DefenderTrap : MapArea { val GOOD_EAT = "Chomp, chomp" val BAD_EAT = "Blurghh" val TRAP_HIT = "Urghhh!" - val TRAP_EAST_ZONE: ZoneBorders = ZoneBorders(1900, 5475, 1902, 5473, 0) val TRAP_EAST = Location.create(1901, 5474, 0) - val TRAP_WEST_ZONE: ZoneBorders = ZoneBorders(1870, 5474, 1872, 5472, 0) val TRAP_WEST = Location.create(1871, 5473, 0) + val TRAP_EAST_ZONE: ZoneBorders = zoneFrom3x3(TRAP_EAST) + val TRAP_WEST_ZONE: ZoneBorders = zoneFrom3x3(TRAP_WEST) val TRAPS_ZONE = arrayOf(TRAP_EAST_ZONE,TRAP_WEST_ZONE) val Fix_trap_IDs = intArrayOf( Scenery.RUNNER_TRAP1_20230, Scenery.BROKEN_TRAP0_20231 ) val TRAPS = arrayOf(TRAP_WEST,TRAP_EAST) + val TRAP_EAST_OFFSET = Location.create(45, 34, 0) + val TRAP_WEST_OFFSET = Location.create(15, 33, 0) val playersInArea = mutableListOf() override fun defineAreaBorders(): Array { return TRAPS_ZONE } + + private fun zoneFrom3x3(center: Location): ZoneBorders { + val tiles = center.get3x3Tiles() + return ZoneBorders( + tiles.minOf { it.x }, + tiles.minOf { it.y }, + tiles.maxOf { it.x }, + tiles.maxOf { it.y }, + center.z + ) + } + override fun areaEnter(entity: Entity) { if (PENANCE_RUNNER_IDS.contains(entity.id)) { - killRunner(entity) - updateTrap(entity.location) - sendMessageArea() + tryTriggerTrap(entity) } val player = entity as? Player ?: return @@ -58,37 +76,75 @@ class DefenderTrap : MapArea { playersInArea.remove(player) } + fun tryTriggerTrap(entity: Entity): Boolean { + if (DeathTask.isDead(entity)) { + return false + } + + val session = getBASession(entity) ?: return false + val trapLocation = trapLocationFor(session, entity.location) ?: return false + val scenery = getScenery(trapLocation) ?: return false + if (scenery.id != Scenery.RUNNER_TRAP2_20135 && scenery.id != Scenery.RUNNER_TRAP1_20230) { + return false + } + + val bait = goodTrapBait(session, trapLocation) ?: return false + session.destroyGroundItem(bait) + entity.sendChat(GOOD_EAT) + killRunner(entity) + updateTrap(scenery, trapLocation) + session.broadcastToRole(BarbRole.DEFENDER, "A Penance Runner broke a trap to the ${trapDirectionFor(session, trapLocation)}!") + sendMessageArea() + return true + } + fun killRunner(entity: Entity){ entity.sendChat(TRAP_HIT) entity.startDeath(entity) - val thistrap = Location.create(1901, 5474, 0) - val scenery = getScenery(thistrap) - //animateScenery(scenery!!, 5076)//5076 - } //step on to trap area, if good food "chomp chomp" stay in location //set trap off - fun updateTrap(location: Location) { - var thistrap = Location.create(1901, 5474, 0) - val scenery = getScenery(thistrap) - animateScenery(scenery!!, 5076)//5076 + fun updateTrap(scenery: core.game.node.scenery.Scenery, trapLocation: Location) { + val nextTrap = when (scenery.id) { + Scenery.RUNNER_TRAP2_20135 -> Scenery.RUNNER_TRAP1_20230 + Scenery.RUNNER_TRAP1_20230 -> Scenery.BROKEN_TRAP0_20231 + else -> return + } + + animateScenery(scenery, 5076)//5076 GameWorld.Pulser.submit(object : Pulse(3, scenery) { override fun pulse(): Boolean { - replaceScenery(scenery,Scenery.RUNNER_TRAP1_20230,-1,thistrap) + replaceScenery(scenery, nextTrap, -1, trapLocation) return true } }) + } - //replaceScenery(getScenery(thistrap)!!,Scenery.RUNNER_TRAP1_20230,-1,thistrap) - if (TRAPS.contains(location)){ - // replaceScenery(getScenery(location)!!,Scenery.RUNNER_TRAP1_20230,40,location) - + private fun goodTrapBait(session: BarbassaultSession, trapLocation: Location): BAGroundSpawn? { + val trapTiles = trapLocation.get3x3Tiles() + return session.BAgroundItems.firstOrNull { + it.isActive && + !it.isRemoved && + it.id in LureItems && + it.flag == true && + trapTiles.contains(it.location) } } + private fun trapLocationFor(session: BarbassaultSession, location: Location): Location? { + return listOf( + session.base.transform(TRAP_EAST_OFFSET), + session.base.transform(TRAP_WEST_OFFSET) + ).firstOrNull { trapLocation -> trapLocation.get3x3Tiles().contains(location) } + } + + private fun trapDirectionFor(session: BarbassaultSession, trapLocation: Location): String { + return if (trapLocation == session.base.transform(TRAP_EAST_OFFSET)) "east" else "west" + } + fun sendMessageArea(){ playersInArea.forEach { player -> diff --git a/Server/src/main/content/minigame/barbassault/bots/BABotContext.kt b/Server/src/main/content/minigame/barbassault/bots/BABotContext.kt index 25e67e80c..0e3a18814 100644 --- a/Server/src/main/content/minigame/barbassault/bots/BABotContext.kt +++ b/Server/src/main/content/minigame/barbassault/bots/BABotContext.kt @@ -150,7 +150,66 @@ class BABotContext( val bot: Player,val api: ScriptAPI ) { "Get szued", "I've seen better defense from a broken gate", "Do you even see the runners?", - "My grandma can do better then that!" + "My grandma can do better then that!", + + "Runner escaped. Again.", + "Nice lure, shame it did nothing.", + "Did you place the food in Lumbridge?", + "The runners thank you for your service.", + "Wrong food, wrong place, classic.", + "You had one job.", + "The runners are speedrunning because of you.", + "I've seen level 5s defend better.", + "Did you forget to pick a call?", + "The runners are literally laughing at you.", + "Blink twice if you're AFK.", + "That runner just waved as it passed.", + "Maybe try feeding them the right bait?", + "You're defending their right to escape.", + "At this rate we'll finish next week.", + "The healer is doing your job too.", + "Even the collector is judging you.", + "Attacker called. Wants their role back.", + "No wonder nobody picks Defender.", + "This is why Defender is always last.", + "You make runners look smart.", + "The trap isn't decorative, you know.", + "Another runner escaped. Impressive.", + "The runners have achieved freedom.", + "I think the runners own the arena now.", + "Outstanding performance. For the runners.", + "The queen will hatch before you finish.", + "Did you learn Defender from YouTube shorts?", + "You lure like a drunk goblin.", + "Food goes on the ground, not in your pocket.", + "The runners aren't supposed to reach the cave.", + "This wave is sponsored by missed runners.", + "Your lure path looks like modern art.", + "The runners are farming YOU for points.", + "Defender? More like Spectator.", + "I've seen bots defend better.", + "The horn gave a call. Did you miss it?", + "Wrong bait any% speedrun.", + "You're boosting the runners' confidence.", + "Defender is not a cosmetic role.", + "Every escaped runner adds 5 minutes.", + "The runners have filed for citizenship.", + "You couldn't defend a sandwich from seagulls.", + "The cave entrance misses you.", + "Maybe the runners are the defenders now.", + "The runners are following YOU.", + "I can hear the healers crying.", + "This is why quick-starts fail.", + "The runner path is not a suggestion.", + "Congratulations, you've unlocked free-range runners.", + "One more escape and they get a pension.", + "Your trap placement scares nobody.", + "Did the runners bribe you?", + "The runners are playing on easy mode.", + "You're the reason Defender guides exist.", + "At least you're consistently bad.", + "The runners have stopped respecting you.", + "Even the penance know you're lost." ) api.sendChat(defenderInsults.random()) defenderInsultNextTick = tick + (8..20).random()