more defender insults,

attempt at better runner logic.
defender trap,
This commit is contained in:
Tooze 2026-06-09 21:42:33 -04:00
parent 35738bb170
commit 174ae95e8b
4 changed files with 414 additions and 68 deletions

View file

@ -57,6 +57,7 @@ class BarbassaultSession( val activity: BAActivity ? = null) :LogoutListener, M
val sessionNpcs = mutableListOf<NPC>()
var currentNPCCount = SpawnCaps(0,0,0,0)
var currentNPCDeathCount = SpawnCaps(0,0,0,0)
var currentNPCEscapeCount = SpawnCaps(0,0,0,0)
var npcTunnel: List<PenanceNpcTunnel> = 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<PenanceType>("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<AttackerStyle>()

View file

@ -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<BarbassaultSession>(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<BAGroundSpawn?>(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<BAGroundSpawn?>(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<Int, Int> {
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
*/
*/

View file

@ -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<Player>()
override fun defineAreaBorders(): Array<ZoneBorders> {
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 ->

View file

@ -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()