Rewrote impling system

Fixed static impling spawns
Fixed impling spawn ratio
Reworked Puro-Puro, much more authentic
Imp defenders now work properly
Crop circles now appear
Added admin command ::implings to list active implings (excludes implings from Puro-Puro)
Fixed bug where you could add to a finished compost bin
This commit is contained in:
Ceikry
2023-04-20 12:16:21 +00:00
committed by Ryan
parent 41c970efd4
commit f6096e3020
24 changed files with 915 additions and 457 deletions
@@ -1,6 +1,6 @@
package content.global.skill.farming
import core.api.toIntArray
import core.api.*
import core.game.node.item.Item
import core.game.system.task.Pulse
import core.game.world.update.flag.context.Animation
@@ -65,7 +65,7 @@ class UseWithBinHandler : InteractionListener {
if(bin.isFull()){
player.sendMessage("This compost bin is already full.")
return@onUseWith true
} else {
} else if (!bin.isFinished) {
player.pulseManager.run(object : Pulse(fillAnim.duration){
override fun pulse(): Boolean {
player.animator.animate(fillAnim)
@@ -75,6 +75,8 @@ class UseWithBinHandler : InteractionListener {
return bin.isFull() || player.inventory.getAmount(usedNode.asItem()) == 0
}
})
} else {
sendMessage (player, "You should empty the remaining compost first.")
}
}
return@onUseWith true
@@ -100,4 +102,4 @@ class UseWithBinHandler : InteractionListener {
if(this != 6476) return this + 2
else return Items.VIAL_229
}
}
}
@@ -1,152 +0,0 @@
package content.global.skill.hunter;
import content.global.skill.hunter.bnet.BNetTypes;
import content.global.skill.hunter.bnet.ImplingNode;
import core.api.StartupListener;
import core.game.world.GameWorld;
import core.game.world.map.Location;
import core.tools.RandomFunction;
import java.util.Random;
/**
* Handles the impetuous impulse minigame.
* @author Vexia
* @author Emperor
*/
public final class ImpetuousImpulses implements StartupListener {
/**
* The random instance used.
*/
private static final Random RANDOM = new Random();
/**
* The puro puro locations.
*/
private static final Location[] PURO_LOCATIONS = new Location[] { new Location(2575, 4314, 0), new Location(2572, 4302, 0), new Location(2575, 4297, 0), new Location(2589, 4297, 0), new Location(2603, 4297, 0), new Location(2612, 4301, 0), new Location(2619, 4296, 0), new Location(2616, 4297, 0), new Location(2620, 4308, 0), new Location(2617, 4313, 0), new Location(2620, 4317, 0), new Location(2611, 4312, 0), new Location(2607, 4305, 0), new Location(2593, 4303, 0), new Location(2578, 4318, 0), new Location(2578, 4330, 0), new Location(2581, 4338, 0), new Location(2586, 4342, 0), Location.create(2585, 4344, 0), Location.create(2573, 4335, 0), Location.create(2570, 4325, 0), Location.create(2567, 4313, 0), Location.create(2566, 4299, 0), Location.create(2573, 4292, 0), Location.create(2603, 4342, 0), Location.create(2616, 4318, 0), Location.create(2616, 4337, 0), Location.create(2584, 4297, 0), Location.create(2596, 4310, 0), Location.create(2596, 4310, 0), Location.create(2588, 4303, 0) };
/**
* The possible locations.
*/
public static final Location[] LOCATIONS = new Location[] {
/*
* Camelot
*/
Location.create(2718 + RANDOM.nextInt(50), 3441 + RANDOM.nextInt(20), 0),
/*
* Duel arena entrance
*/
Location.create(3300 + RANDOM.nextInt(10), 3250 + RANDOM.nextInt(10), 0),
/*
* Castle wars entrance
*/
Location.create(2478 + RANDOM.nextInt(20), 3068 + RANDOM.nextInt(20), 0),
/*
* Lumbridge
*/
Location.create(3175 + RANDOM.nextInt(10), 3261 + RANDOM.nextInt(10), 0),
/*
* Draynor
*/
Location.create(3092 + RANDOM.nextInt(5), 3227 + RANDOM.nextInt(5), 0),
/*
* Falador (east)
*/
Location.create(3071 + RANDOM.nextInt(10), 3355 + RANDOM.nextInt(10), 0),
/*
* Varrock (west)
*/
Location.create(3131 + RANDOM.nextInt(5), 3401 + RANDOM.nextInt(5), 0),
/*
* Ardougne (south)
*/
Location.create(2630 + RANDOM.nextInt(10), 3217 + RANDOM.nextInt(10), 0),
/*
* Yanille (north)
*/
Location.create(2594 + RANDOM.nextInt(20), 3121 + RANDOM.nextInt(20), 0),
/*
* Red salamander hunting place (ZMI altar)
*/
Location.create(2436 + RANDOM.nextInt(15), 3206 + RANDOM.nextInt(15), 0),
/*
* Shilo hunting area (graahk/larupia/..)
*/
Location.create(2763 + RANDOM.nextInt(20), 3003 + RANDOM.nextInt(20), 0),
/*
* Rimmington
*/
Location.create(2947 + RANDOM.nextInt(20), 3219 + RANDOM.nextInt(20), 0),
/*
* Karamja volcano
*/
Location.create(2856 + RANDOM.nextInt(15), 3150 + RANDOM.nextInt(15), 0),
/*
* Brimhaven dungeon entrance
*/
Location.create(2733 + RANDOM.nextInt(10), 3146 + RANDOM.nextInt(10), 0),
/*
* Sophanem
*/
Location.create(3299 + RANDOM.nextInt(5), 2784 + RANDOM.nextInt(5), 0),
/*
* Tree gnome stronghold
*/
Location.create(2438 + RANDOM.nextInt(20), 3420 + RANDOM.nextInt(20), 0),
/*
* Zanaris (center, just north of grain field)
*/
Location.create(2416 + RANDOM.nextInt(3), 4456 + RANDOM.nextInt(3), 0), };
/**
* Spawns the imps.
*/
private void spawnImps() {
int amount = BNetTypes.getImplings().size() + 2;
for (ImplingNode impling : BNetTypes.getImplings()) {
if (impling.getLevel() == 83) {
int rand = RandomFunction.random(1, 2);
for (int i = 0; i < rand; i++) {
createImp(impling, true);
}
int randAmt = RandomFunction.random(2, 5);
for (int i = 0; i < randAmt; i++) {
createImp(impling, false);
}
continue;
}
for (int i = 0; i < amount; i++) {
if (RANDOM.nextInt(10) < 4) {
createImp(impling, false);
continue;
}
createImp(impling, true);
}
amount--;
}
}
/**
* Creates the impling.
* @param impling the impling.
* @param puro the puro.
*/
private void createImp(ImplingNode impling, boolean puro) {
ImplingNPC npc = null;
if (puro) {
npc = new ImplingNPC(impling.getNpcs()[1], PURO_LOCATIONS[RANDOM.nextInt(PURO_LOCATIONS.length)], impling);
npc.setAttribute("puroPuro", true);
} else {
npc = new ImplingNPC(impling.getNpcs()[0], LOCATIONS[RANDOM.nextInt(LOCATIONS.length)], impling);
npc.setAttribute("nextTeleport", GameWorld.getTicks() + 600);
}
npc.init();
}
@Override
public void startup() {
spawnImps();
}
}
@@ -1,120 +0,0 @@
package content.global.skill.hunter;
import content.global.skill.hunter.bnet.ImplingNode;
import core.game.node.entity.Entity;
import core.game.node.entity.combat.BattleState;
import core.game.node.entity.combat.CombatStyle;
import core.game.node.entity.npc.AbstractNPC;
import core.game.node.entity.player.Player;
import core.game.system.task.Pulse;
import core.game.world.map.Location;
import core.game.world.map.path.Pathfinder;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Initializable;
import core.tools.RandomFunction;
import core.game.system.config.NPCConfigParser;
import core.game.world.GameWorld;
/**
* Handles an impling npc.
* @author Vexia
*/
@Initializable
public final class ImplingNPC extends AbstractNPC {
/**
* The impling node.
*/
private final ImplingNode impling;
/**
* Constructs a new {@code ImplingNPC} {@code Object}.
*/
public ImplingNPC() {
super(0, null);
this.impling = null;
this.setWalks(true);
this.setWalkRadius(20);
}
/**
* Constructs a new {@code ImplingNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public ImplingNPC(int id, Location location, ImplingNode impling) {
super(id, location);
this.impling = impling;
if (impling != null) {
this.getDefinition().getHandlers().put(NPCConfigParser.RESPAWN_DELAY, impling.getRespawnTime());
}
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new ImplingNPC(id, location, null);
}
@Override
public void handleTickActions() {
if (!getLocks().isMovementLocked()) {
if (isWalks() && !getPulseManager().hasPulseRunning() && nextWalk < GameWorld.getTicks()) {
setNextWalk();
Location l = getLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0);
if (canMove(l)) {
Pathfinder.find(this, l, true, Pathfinder.PROJECTILE).walk(this);
}
}
}
if (RandomFunction.random(100) < 4) {
sendChat("Tee hee!");
}
int nextTeleport = getAttribute("nextTeleport", -1);
if (nextTeleport > -1 && GameWorld.getTicks() > nextTeleport) {
setAttribute("nextTeleport", GameWorld.getTicks() + 600);
graphics(new Graphics(590));
GameWorld.getPulser().submit(new Pulse(1) {
@Override
public boolean pulse() {
teleport(ImpetuousImpulses.LOCATIONS[RandomFunction.random(ImpetuousImpulses.LOCATIONS.length)]);
return true;
}
});
return;
}
}
@Override
public boolean isAttackable(Entity entity, CombatStyle style, boolean message) {
if (style != CombatStyle.MAGIC) {
return false;
}
if (entity.getProperties().getSpell().getSpellId() == 12 || entity.getProperties().getSpell().getSpellId() == 30 || entity.getProperties().getSpell().getSpellId() == 56) {
return true;
}
return super.isAttackable(entity, style, message);
}
@Override
public void handleDrops(Player p, Entity killer) {
getProperties().setTeleportLocation(getProperties().getSpawnLocation());
}
@Override
public void checkImpact(BattleState state) {
}
@Override
public int[] getIds() {
return new int[] { 1028, 6055, 1029, 6056, 1030, 6057, 1031, 6058, 1032, 6059, 1033, 6060, 1034, 6061, 1035, 6062, 6053, 6063};
}
/**
* Gets the impling.
* @return The impling.
*/
public ImplingNode getImpling() {
return impling;
}
}
@@ -0,0 +1,84 @@
package content.global.skill.hunter.implings
import core.api.*
import core.tools.*
import core.game.world.map.RegionManager
import core.game.world.map.path.ClipMaskSupplier
import core.game.node.entity.npc.*
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.SpellBookManager;
import core.game.node.item.Item
import core.game.node.entity.combat.CombatStyle
import content.global.skill.magic.spellconsts.Modern
/**
* Manages the behavior for implings themselves.
*/
class ImplingBehavior : NPCBehavior (*Impling.getIds()) {
override fun onCreation (self: NPC) {
self.isWalks = true
self.isNeverWalks = false
self.properties.spawnLocation = self.location
}
override fun tick(self: NPC) : Boolean {
if (RandomFunction.roll(10))
sendChat(self, "Tee-hee!")
return true
}
override fun onRespawn(self: NPC) {
if (!isPuroImpling(self))
log (this::class.java, Log.ERR, "Non-puro impling has respawned!")
sendGraphics(1119, self.properties.teleportLocation)
}
override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean {
if (attacker !is Player) return false;
if (style != CombatStyle.MAGIC) {
if (shouldSendMessage)
sendMessage (attacker, "You can't do that.")
return false;
}
val spellBook = attacker.spellBookManager.spellBook
if (spellBook != SpellBookManager.SpellBook.MODERN.interfaceId) {
if (shouldSendMessage)
sendMessage (attacker, "The impling is too fast for that.")
return false;
}
val spellId = attacker.properties.spell.spellId
if (spellId != Modern.BIND && spellId != Modern.SNARE && spellId != Modern.ENTANGLE) {
if (shouldSendMessage)
sendMessage (attacker, "The impling is immune to that magic.")
return false
}
return true
}
override fun onDeathFinished (self: NPC, killer: Entity) {
if (!isPuroImpling(self))
ImplingController.deregister(self)
else if (self.originalId != self.id) { //if this is a spawner that transformed
self.reTransform() //turn back into spawner NPC
self.behavior = forId(self.id)
}
}
//manually clear any rolled drops so any drops set by JSON are properly ignored.
override fun onDropTableRolled (self: NPC, drops: ArrayList<Item>) {
drops.clear()
}
override fun getClippingSupplier (self: NPC) : ClipMaskSupplier {
return ImplingClipper
}
private fun isPuroImpling (self: NPC) : Boolean {
return self.id > 6054
}
}
@@ -0,0 +1,90 @@
package content.global.skill.hunter.implings
import core.api.*
import core.tools.*
import core.game.world.map.Location
import core.game.node.entity.npc.NPC
import core.game.system.command.*
import kotlin.math.*
/**
* Manages the high-level behavior of implings in the overworld: Spawning them, clearing them, etc.
* NOTE: This does not manage the spawns INSIDE puro-puro. Those are actually handled by JSON.
*/
class ImplingController : TickListener, Commands {
override fun tick() {
if (--nextCycle > getTicksBeforeNextCycleToDespawn())
return
if (activeImplings.size > 0) {
clearSomeImplings(implingsClearedPerTick)
return
}
generateSpawners()
nextCycle = secondsToTicks(60 * 30) // 30 minutes
}
override fun defineCommands() {
define ("implings", Privilege.ADMIN, "", "Lists the currently active implings/spawners") { player, _ ->
for (i in 0..310)
setInterfaceText(player, "", 275, i)
setInterfaceText(player, "Implings", 275, 2)
for ((index, impling) in activeImplings.withIndex()) {
var text = "This shouldn't be here -> ${impling.id}"
if (impling.id < 1028) {
val table = ImplingSpawner.forId(impling.id)
if (table != null)
text = table.name
}
else text = impling.name
setInterfaceText(player, "$text -> ${impling.location}", 275, index + 11)
}
openInterface(player, 275)
}
}
companion object {
val implingsClearedPerTick = 5
var nextCycle = 0
var activeImplings = ArrayList<NPC>()
fun clearSomeImplings (amount: Int) {
for (i in 0 until amount) {
val impling = activeImplings.removeAt(0)
poofClear(impling)
}
}
fun generateSpawners () {
val typeLocations = ImplingSpawnLocations.values()
for (set in typeLocations) {
val locations = set.locations
val type = set.type
locations.forEach { generateSpawnersAt(it, type) }
}
}
fun generateSpawnersAt(location: Location, type: ImplingSpawnTypes) {
for (i in 0 until type.spawnRolls) {
val spawner = type.table.roll() ?: continue
if (spawner == ImplingSpawner.Nothing) continue
val npc = NPC.create (spawner.npcId, location)
npc.init()
activeImplings.add(npc)
}
}
fun getTicksBeforeNextCycleToDespawn() : Int {
return ceil (activeImplings.size / implingsClearedPerTick.toDouble()).toInt()
}
fun deregister (impling: NPC, graceful: Boolean = false) : Boolean {
activeImplings.remove(impling)
if (graceful)
poofClear(impling)
else
impling.clear()
return true
}
}
}
@@ -0,0 +1,178 @@
package content.global.skill.hunter.implings
import core.api.utils.*
import core.game.world.map.*
import core.game.world.map.path.*
import core.game.world.map.build.*
enum class Impling (val npcId: Int, val puroId: Int) {
Baby (1028, 6055),
Young (1029, 6056),
Gourmet (1030, 6057),
Earth (1031, 6058),
Essence (1032, 6059),
Eclectic (1033, 6060),
Ninja (6053, 6063),
Nature (1034, 6061),
Magpie (1035, 6062),
Dragon (6054, 6064);
companion object {
fun getIds() : IntArray {
val list = ArrayList<Int>()
for (imp in values()){
list.add(imp.npcId)
list.add(imp.puroId)
}
return list.toIntArray()
}
}
}
enum class ImplingSpawner (val npcId: Int, val table: WeightedTable<Impling>) {
LowTier (npcId = 1024, WeightedTable.create
(
Pair (Impling.Baby, 20.0),
Pair (Impling.Young, 20.0),
Pair (Impling.Gourmet, 20.0),
Pair (Impling.Earth, 20.0),
Pair (Impling.Essence, 10.0),
Pair (Impling.Eclectic,10.0)
)),
MidTier (npcId = 1025, WeightedTable.create
(
Pair (Impling.Gourmet, 10.0),
Pair (Impling.Earth, 10.0),
Pair (Impling.Essence, 20.0),
Pair (Impling.Eclectic, 37.0),
Pair (Impling.Nature, 20.0),
Pair (Impling.Magpie, 2.0),
Pair (Impling.Ninja, 1.0)
)),
HighTier (npcId = 1026, WeightedTable.create
(
Pair (Impling.Nature, 10.0),
Pair (Impling.Magpie, 50.0),
Pair (Impling.Ninja, 30.0),
Pair (Impling.Dragon, 10.0)
)),
LowPuroTier (npcId = 6065, WeightedTable.create
(
Pair (Impling.Baby, 20.0),
Pair (Impling.Young, 20.0),
Pair (Impling.Gourmet, 20.0),
Pair (Impling.Earth, 20.0),
Pair (Impling.Essence, 10.0),
Pair (Impling.Eclectic,10.0)
)),
MidPuroTier (npcId = 6066, WeightedTable.create
(
Pair (Impling.Gourmet, 10.0),
Pair (Impling.Earth, 10.0),
Pair (Impling.Essence, 20.0),
Pair (Impling.Eclectic, 37.0),
Pair (Impling.Nature, 20.0),
Pair (Impling.Magpie, 2.0),
Pair (Impling.Ninja, 1.0)
)),
HighPuroTier (npcId = 6067, WeightedTable.create
(
Pair (Impling.Nature, 150.0),
Pair (Impling.Magpie, 114.0),
Pair (Impling.Ninja, 37.0),
Pair (Impling.Dragon, 10.0),
)),
Nothing (npcId = -1, WeightedTable<Impling>());
companion object {
private val idMap = values().map { it.npcId to it }.toMap()
fun forId (id: Int) : ImplingSpawner? {
return idMap[id]
}
fun getIds() : IntArray {
return idMap.keys.toIntArray()
}
}
}
enum class ImplingSpawnTypes (val table: WeightedTable<ImplingSpawner>, val spawnRolls: Int) {
Standard (WeightedTable.create
(
Pair (ImplingSpawner.LowTier, 14.0),
Pair (ImplingSpawner.MidTier, 7.0),
Pair (ImplingSpawner.HighTier, 4.0),
Pair (ImplingSpawner.Nothing, 75.0)
), spawnRolls = 3),
LowTierOnly (WeightedTable.create
(
Pair (ImplingSpawner.LowTier, 100.0)
), spawnRolls = 1),
MidTierOnly (WeightedTable.create
(
Pair (ImplingSpawner.MidTier, 100.0)
), spawnRolls = 1),
HighTierOnly (WeightedTable.create
(
Pair (ImplingSpawner.HighTier, 100.0)
), spawnRolls = 1),
HighPuroTierOnly (WeightedTable.create
(
Pair (ImplingSpawner.HighPuroTier, 100.0)
), spawnRolls = 1)
}
enum class ImplingSpawnLocations (val type: ImplingSpawnTypes, vararg val locations: Location) {
StandardSpawns (ImplingSpawnTypes.Standard,
Location.create(2204, 3232, 0),
Location.create(2582, 2974, 0),
Location.create(2522, 3105, 0),
Location.create(2470, 3221, 0),
Location.create(2593, 3251, 0),
Location.create(2735, 3354, 0),
Location.create(2646, 3424, 0),
Location.create(2462, 3429, 0),
Location.create(2386, 3513, 0),
Location.create(2335, 3649, 0),
Location.create(2740, 3536, 0),
Location.create(2654, 3609, 0),
Location.create(2724, 3769, 0),
Location.create(2817, 3513, 0),
Location.create(2844, 3154, 0),
Location.create(2844, 3033, 0),
Location.create(2841, 2926, 0),
Location.create(2907, 3491, 0),
Location.create(3020, 3525, 0),
Location.create(3021, 3424, 0),
Location.create(2981, 3276, 0),
Location.create(3135, 3377, 0),
Location.create(3149, 3233, 0),
Location.create(3170, 3004, 0),
Location.create(3239, 3289, 0),
Location.create(3287, 3271, 0),
Location.create(3418, 3124, 0),
Location.create(3356, 3010, 0),
Location.create(3550, 3529, 0),
Location.create(3449, 3488, 0),
Location.create(3441, 3352, 0)
),
LowTierOnlySpawns (ImplingSpawnTypes.LowTierOnly,
Location.create(2348, 3610, 0),
Location.create(2277, 3186, 0),
Location.create(2459, 3085, 0),
Location.create(2564, 3393, 0),
Location.create(2780, 3463, 0),
Location.create(2966, 3411, 0),
Location.create(3094, 3237, 0),
Location.create(3281, 3427, 0),
Location.create(3278, 3160, 0)
)
}
object ImplingClipper : ClipMaskSupplier {
override fun getClippingFlag (z: Int, x: Int, y: Int) : Int {
var flag = RegionManager.getClippingFlag(z, x, y);
return flag and (RegionFlags.SOLID_TILE.inv()) and (RegionFlags.TILE_OBJECT.inv()) //Allow walking on water and flying over small objects, but keep all other tile flags the same.
}
}
@@ -0,0 +1,52 @@
package content.global.skill.hunter.implings
import core.api.*
import core.tools.*
import core.game.node.entity.npc.*
import core.game.world.map.path.ClipMaskSupplier
/**
* Manages the behavior for impling spawners (the invisible NPCs that turn into random implings).
*/
class ImplingSpawnerBehavior : NPCBehavior (*ImplingSpawner.getIds()) {
override fun onCreation (self: NPC) {
val isPuro = isPuroSpawner(self)
val delay = if (isPuro) 120 else 180
setAttribute(self, "transformTime", getWorldTicks() + secondsToTicks(delay))
self.isRespawn = isPuro
self.walkRadius = if (isPuro) 20 else 100
self.isWalks = true
self.isNeverWalks = false
self.setInvisible(true)
self.properties.spawnLocation = self.location
}
override fun onRespawn (self: NPC) {
if (!isPuroSpawner(self))
log (this::class.java, Log.ERR, "Non-puro spawner has respawned!")
this.onCreation(self)
}
override fun tick (self: NPC) : Boolean {
var transformTime = getAttribute(self, "transformTime", 0)
if (transformTime != 0 && transformTime <= getWorldTicks()) {
val table = ImplingSpawner.forId(self.id)?.table ?: return ImplingController.deregister(self)
val impling = table.roll() ?: return ImplingController.deregister(self)
val targetId = if (isPuroSpawner(self)) impling.puroId else impling.npcId
self.transform(targetId)
self.behavior = forId(self.id)
self.setInvisible(false)
removeAttribute(self, "transformTime")
sendGraphics(1119, self.location)
}
return true
}
override fun getClippingSupplier (self: NPC) : ClipMaskSupplier {
return ImplingClipper
}
private fun isPuroSpawner(self: NPC) : Boolean {
return self.id > 6000
}
}
@@ -5,6 +5,7 @@ object Modern {
const val CONFUSE = 2
const val BONES_TO_BANANAS = 9
const val WEAKEN = 11
const val BIND = 12
const val LOW_ALCHEMY = 13
const val VARROCK_TELEPORT = 15
const val LUMBRIDGE_TELEPORT = 18
@@ -13,6 +14,7 @@ object Modern {
const val SUPERHEAT = 25
const val CAMELOT_TELEPORT = 26
const val TELEPORT_TO_HOUSE = 23
const val SNARE = 30
const val ARDOUGNE_TELEPORT = 32
const val HIGH_ALCHEMY = 34
const val WATCHTOWER_TELEPORT = 37
@@ -21,5 +23,6 @@ object Modern {
const val APE_ATOLL_TELEPORT = 47
const val VULNERABILITY = 50
const val ENFEEBLE = 53
const val ENTANGLE = 56
const val STUN = 57
}
}
@@ -83,8 +83,7 @@ class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) {
override fun onRemoval(self: NPC) {
val parent = getAttribute<NPC?>(self, "parent", null) ?: return
if (parent.behavior !is NechryaelBehavior) return
parent.behavior.removeSpawn(parent, self)
(parent.behavior as? NechryaelBehavior)?.let { it.removeSpawn (parent, self) }
}
override fun tick(self: NPC): Boolean {
@@ -102,4 +101,4 @@ class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) {
override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean): Boolean {
return attacker == getAttribute<Player?>(self, "target", null)
}
}
}