Implemented ContentInterface-based NPC Scripting

Obsoleted AbstractNPC
Implemented desert bandits
Rewrote a handful of existing NPCs into NPCBehaviors (e.g. rock slugs, nechryaels, water fiend and more)
This commit is contained in:
Ceikry 2023-03-01 06:42:25 +00:00 committed by Ryan
parent f0d7b82bf9
commit 9a4b933976
18 changed files with 490 additions and 563 deletions

View file

@ -0,0 +1,100 @@
package content.global.skill.slayer
import core.api.animate
import core.api.getAttribute
import core.api.setAttribute
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.combat.DeathTask
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.Player
import core.game.world.GameWorld
import core.tools.RandomFunction
import org.rs09.consts.NPCs
class NechryaelBehavior : NPCBehavior(*Tasks.NECHRYAELS.npcs) {
private val ATTR_SPAWNS = "deathSpawns"
private val ATTR_NEXTSPAWN = "deathSpawnNextTick"
override fun afterDamageReceived(self: NPC, attacker: Entity, state: BattleState) {
if (attacker !is Player) return
if (!canSpawnDeathspawn(self)) return
if (!RandomFunction.roll(5)) return
spawnDeathSpawn(self, attacker)
}
fun spawnDeathSpawn(self: NPC, player: Player) {
val npc = NPC.create(NPCs.DEATH_SPAWN_1614, self.location.transform(self.direction, 1))
setAttribute(npc, "parent", self)
setAttribute(npc, "target", player)
npc.isRespawn = false
npc.init()
addSpawn(self, npc)
setNextSpawn(self)
animate(self, 9491)
}
fun canSpawnDeathspawn(self: NPC) : Boolean {
if (getSpawns(self).size >= 2) {
setNextSpawn(self)
return false
}
return getNextSpawn(self) <= GameWorld.ticks
}
fun getNextSpawn(self: NPC) : Int {
return getAttribute(self, ATTR_NEXTSPAWN, 0)
}
fun setNextSpawn(self: NPC) {
setAttribute(self, ATTR_NEXTSPAWN, GameWorld.ticks + 50)
}
fun getSpawns(self: NPC) : ArrayList<NPC> {
return getAttribute(self, ATTR_SPAWNS, ArrayList())
}
fun addSpawn(self: NPC, spawn: NPC) {
val list = getSpawns(self)
list.add(spawn)
setAttribute(self, ATTR_SPAWNS, list)
}
fun removeSpawn(self: NPC, spawn: NPC) {
val list = getSpawns(self)
list.remove(spawn)
setAttribute(self, ATTR_SPAWNS, list)
}
}
class DeathspawnBehavior : NPCBehavior(NPCs.DEATH_SPAWN_1614) {
override fun onCreation(self: NPC) {
setAttribute(self, "despawn-time", GameWorld.ticks + 100)
val target = getAttribute<Player?>(self, "target", null) ?: return
self.attack(target)
}
override fun onRemoval(self: NPC) {
val parent = getAttribute<NPC?>(self, "parent", null) ?: return
if (parent.behavior !is NechryaelBehavior) return
parent.behavior.removeSpawn(parent, self)
}
override fun tick(self: NPC): Boolean {
val target = getAttribute<Player?>(self, "target", null) ?: return true
if (!target.isActive || DeathTask.isDead(target) || getAttribute(self, "despawn-time", 0) <= GameWorld.ticks)
self.clear()
return true
}
override fun shouldIgnoreMultiRestrictions(self: NPC, victim: Entity): Boolean {
return victim == getAttribute<Player?>(self, "target", null)
}
override fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean): Boolean {
return attacker == getAttribute<Player?>(self, "target", null)
}
}

View file

@ -1,193 +0,0 @@
package content.global.skill.slayer;
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.world.GameWorld;
import core.game.world.map.Location;
import core.game.world.update.flag.context.Animation;
import core.plugin.Initializable;
import core.tools.RandomFunction;
import java.util.ArrayList;
import java.util.List;
/**
* Handles the nechryael npc.
* @author Vexia
*/
@Initializable
public final class NechryaelNPC extends AbstractNPC {
/**
* The death spawn id.
*/
private static final int DEATH_SPAWN = 1614;
/**
* The death spawn npcs.
*/
private List<DeathSpawnNPC> spawns = new ArrayList<>(10);
/**
* The next spawn time.
*/
private int nextSpawn;
/**
* Constructs a new {@code NechryaelNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public NechryaelNPC(int id, Location location) {
super(id, location);
}
/**
* Constructs a new {@code NechryaelNPC} {@code Object}.
*/
public NechryaelNPC() {
super(0, null);
}
@Override
public void onImpact(Entity entity, final BattleState state) {
super.onImpact(entity, state);
if (entity instanceof Player && RandomFunction.random(5) == 1) {
spawn((Player) entity);
}
}
/**
* Spawns a death spawn.
* @param player the player.
*/
public void spawn(Player player) {
if (!canSpawn()) {
return;
}
final DeathSpawnNPC spawn = new DeathSpawnNPC(DEATH_SPAWN, getLocation().transform(getDirection(), 1), player, this);
spawn.init();
setSpawnTime();
spawns.add(spawn);
animate(Animation.create(9491));
spawn.getProperties().getCombatPulse().attack(player);
}
/**
* Sets the next spawn time.
*/
private void setSpawnTime() {
nextSpawn = GameWorld.getTicks() + 50;
}
/**
* Checks if a death spawn can spawn.
* @return {@code True} if so.
*/
private boolean canSpawn() {
if (spawns.size() >= 2) {
setSpawnTime();
return false;
}
return nextSpawn < GameWorld.getTicks();
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new NechryaelNPC(id, location);
}
@Override
public int[] getIds() {
return Tasks.NECHRYAELS.getNpcs();
}
/**
* Handles the death spawn npc.
* @author Vexia
*/
public static final class DeathSpawnNPC extends AbstractNPC {
/**
* The player binded with the death spawn.
*/
private final Player player;
/**
* The parent npc.
*/
private final NechryaelNPC parent;
/**
* The time until its cleared.
*/
private int time;
/**
* Constructs a new {@code DeathSpawnNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public DeathSpawnNPC(int id, Location location, final Player player, NechryaelNPC parent) {
super(id, location);
this.player = player;
this.parent = parent;
this.setRespawn(false);
this.time = GameWorld.getTicks() + 120;
}
@Override
public void handleTickActions() {
super.handleTickActions();
if (!inCombat() || !player.inCombat()) {
getProperties().getCombatPulse().attack(player);
}
if (time < GameWorld.getTicks() || !player.isActive() || player.getLocation().getDistance(getLocation()) > 15) {
clear();
}
}
@Override
public void clear() {
super.clear();
parent.spawns.remove(this);
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new DeathSpawnNPC(id, location, null, null);
}
@Override
public boolean isAttackable(Entity entity, CombatStyle style, boolean message) {
if (entity instanceof Player) {
final Player t = (Player) entity;
if (t != player) {
if(message) {
t.getPacketDispatch().sendMessage("This isn't spawned for you.");
}
return false;
}
}
return super.isAttackable(entity, style, message);
}
@Override
public boolean isIgnoreMultiBoundaries(Entity victim) {
return victim == player;
}
@Override
public boolean isPoisonImmune() {
return true;
}
@Override
public int[] getIds() {
return new int[] { DEATH_SPAWN };
}
}
}

View file

@ -0,0 +1,50 @@
package content.global.skill.slayer
import core.api.*
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.BattleState
import core.game.node.entity.combat.ImpactHandler
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.Player
import org.rs09.consts.Items
import java.lang.Integer.max
class RockSlug : NPCBehavior(*Tasks.ROCK_SLUGS.ids), InteractionListener {
override fun defineListeners() {
onUseWith(IntType.NPC, Items.BAG_OF_SALT_4161, *ids, handler = ::handleSaltUsage)
}
override fun beforeDamageReceived(self: NPC, attacker: Entity, state: BattleState) {
val lifepoints = self.skills.lifepoints
if (state.estimatedHit + max(state.secondaryHit, 0) > lifepoints - 1) {
state.estimatedHit = lifepoints - 1
state.secondaryHit = -1
setAttribute(self, "shouldRun", true)
}
}
override fun tick(self: NPC): Boolean {
if (getAttribute(self, "shouldRun", false)){
self.properties.combatPulse.stop()
forceWalk(self, self.properties.spawnLocation, "smart")
removeAttribute(self, "shouldRun")
}
return true
}
private fun handleSaltUsage(player: Player, used: Node, with: Node) : Boolean {
if (with !is NPC) return false
if (!removeItem(player, used.id)) return false
if (with.skills.lifepoints >= 5)
sendMessage(player, "Your bag of salt is ineffective. The Rockslug is not weak enough.")
else {
sendMessage(player, "The Rockslug shrivels up and dies.")
with.impactHandler.manualHit(player, with.skills.lifepoints, ImpactHandler.HitsplatType.NORMAL)
}
return true
}
}

View file

@ -1,148 +0,0 @@
package content.global.skill.slayer;
import core.game.interaction.NodeUsageEvent;
import core.game.interaction.UseWithHandler;
import core.game.node.entity.combat.BattleState;
import core.game.node.entity.combat.ImpactHandler.HitsplatType;
import core.game.node.entity.npc.AbstractNPC;
import core.game.node.entity.npc.NPC;
import core.game.node.entity.player.Player;
import core.game.node.item.Item;
import core.game.world.map.Location;
import core.game.world.map.path.Pathfinder;
import core.plugin.Initializable;
import core.plugin.Plugin;
import core.plugin.ClassScanner;
/**
* Handles the interactions of a rock slug.
* @author Vexia
*/
@Initializable
public final class RockSlugPlugin implements Plugin<Object> {
/**
* The bag of salt item.
*/
private static final Item SALT = new Item(4161, 1);
/**
* The rockslug npc ids.
*/
private static final int[] IDS = new int[] { 1631, 1632 };
@Override
public Plugin<Object> newInstance(Object arg) throws Throwable {
ClassScanner.definePlugin(new RockSlugNPC());
ClassScanner.definePlugin(new SaltBagHandler());
return this;
}
@Override
public Object fireEvent(String identifier, Object... args) {
return null;
}
/**
* The use with handler for the bag of salt on a rock slug.
* @author Vexia
*/
public final class SaltBagHandler extends UseWithHandler {
/**
* Constructs a new {@code SaltBagHandler} {@code Object}.
*/
public SaltBagHandler() {
super(SALT.getId());
}
@Override
public Plugin<Object> newInstance(Object arg) throws Throwable {
for (int id : IDS) {
addHandler(id, NPC_TYPE, this);
}
return this;
}
@Override
public boolean handle(NodeUsageEvent event) {
final Player player = event.getPlayer();
final NPC npc = (NPC) event.getUsedWith();
player.getInventory().remove(SALT);
if (npc.getSkills().getLifepoints() < 10) {
npc.getImpactHandler().manualHit(player, npc.getSkills().getLifepoints(), HitsplatType.NORMAL);
player.getPacketDispatch().sendMessage("The rockslug shrivels up and dies.");
} else {
player.sendMessage("Your bag of salt is ineffective. The Rockslug is not weak enough.");
}
return true;
}
}
/**
* The rock slug npc.
* @author Vexia
*/
public final class RockSlugNPC extends AbstractNPC {
/**
* Constructs a new {@code RockSlugNPC} {@code Object}.
*/
public RockSlugNPC() {
super(-1, null);
}
/**
* Constructs a new {@code RockSlugNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public RockSlugNPC(int id, Location location) {
super(id, location, true);
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new RockSlugNPC(id, location);
}
@Override
public void checkImpact(BattleState state) {
super.checkImpact(state);
int lifepoints = getSkills().getLifepoints();
boolean run = false;
if (state.getEstimatedHit() > -1) {
lifepoints -= state.getEstimatedHit();
if (lifepoints < 1) {
run = true;
state.setEstimatedHit(lifepoints - 1);
}
if (state.getEstimatedHit() < 0) {
state.setEstimatedHit(0);
}
}
if (state.getSecondaryHit() > -1) {
lifepoints -= state.getSecondaryHit();
if (lifepoints < 1) {
run = true;
state.setSecondaryHit(lifepoints - 1);
}
if (state.getSecondaryHit() < 0) {
state.setSecondaryHit(0);
}
}
if (run) {
getProperties().getCombatPulse().stop();
Pathfinder.find(getLocation(), getProperties().getSpawnLocation());
}
}
@Override
public int[] getIds() {
return IDS;
}
}
}

View file

@ -0,0 +1,56 @@
package content.global.skill.slayer
import content.global.handlers.item.equipment.special.DragonfireSwingHandler
import core.api.EquipmentSlot
import core.api.getItemFromEquipment
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.combat.CombatSwingHandler
import core.game.node.entity.combat.MultiSwingHandler
import core.game.node.entity.combat.equipment.SwitchAttack
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.Player
import core.game.node.entity.player.link.prayer.PrayerType
import core.game.world.update.flag.context.Animation
import core.game.world.update.flag.context.Graphics
import core.tools.RandomFunction
import org.rs09.consts.Items
class SkeletalWyvernBehavior : NPCBehavior(*Tasks.SKELETAL_WYVERN.ids) {
/**
* The combat swing handler.
*/
private val COMBAT_HANDLER = MultiSwingHandler(SwitchAttack(CombatStyle.MELEE.swingHandler, Animation(2985)), SwitchAttack(CombatStyle.RANGE.swingHandler, Animation(2989), Graphics(499)), DragonfireSwingHandler.get(false, 54, Animation(2988), Graphics(501), null, null, false))
/**
* The combat swing handler for far combat (5+ tile distance)
*/
private val COMBAT_HANDLER_FAR = MultiSwingHandler(SwitchAttack(CombatStyle.RANGE.swingHandler, Animation(2989), Graphics(499)))
private val SHIELDS = intArrayOf(Items.DRAGONFIRE_SHIELD_11283, Items.DRAGONFIRE_SHIELD_11285, Items.ELEMENTAL_SHIELD_2890, Items.MIND_SHIELD_9731)
override fun beforeAttackFinalized(self: NPC, victim: Entity, state: BattleState) {
if (victim !is Player) return
if (state.style != CombatStyle.MAGIC) return
val shield = getItemFromEquipment(victim, EquipmentSlot.SHIELD)
val hasShieldProtection = shield != null && shield.id in SHIELDS
if (!hasShieldProtection) return
state.estimatedHit = RandomFunction.random(11)
if (victim.location.getDistance(self.location) >= 5 && victim.prayer.get(PrayerType.PROTECT_FROM_MAGIC))
state.estimatedHit = 0
}
override fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler): CombatSwingHandler {
val victim = self.properties.combatPulse.getVictim() ?: return original
if (victim !is Player) return original
return if (victim.location.getDistance(self.location) >= 5)
COMBAT_HANDLER_FAR
else
COMBAT_HANDLER
}
}

View file

@ -1,90 +0,0 @@
package content.global.skill.slayer;
import core.game.container.impl.EquipmentContainer;
import core.game.node.entity.combat.BattleState;
import core.game.node.entity.combat.CombatStyle;
import core.game.node.entity.combat.equipment.SwitchAttack;
import content.global.handlers.item.equipment.special.DragonfireSwingHandler;
import core.game.node.entity.npc.AbstractNPC;
import core.game.node.entity.player.Player;
import core.game.node.entity.player.link.prayer.PrayerType;
import core.game.node.item.Item;
import core.game.world.map.Location;
import core.game.world.update.flag.context.Animation;
import core.game.world.update.flag.context.Graphics;
import core.plugin.Initializable;
import core.tools.RandomFunction;
import core.game.node.entity.combat.CombatSwingHandler;
import core.game.node.entity.combat.MultiSwingHandler;
/**
* Handles the skeletal wyvern npc.
* @author Vexia
* @author Splinter
* @version 1.0
*/
@Initializable
public final class SkeletalWyvernNPC extends AbstractNPC {
/**
* The combat swing handler.
*/
private static final MultiSwingHandler COMBAT_HANDLER = new MultiSwingHandler(new SwitchAttack(CombatStyle.MELEE.getSwingHandler(), new Animation(2985)), new SwitchAttack(CombatStyle.RANGE.getSwingHandler(), new Animation(2989), new Graphics(499)), DragonfireSwingHandler.get(false, 54, new Animation(2988), new Graphics(501), null, null, false));
/**
* The combat swing handler for far combat (5+ tile distance)
*/
private static final MultiSwingHandler COMBAT_HANDLER_FAR = new MultiSwingHandler(new SwitchAttack(CombatStyle.RANGE.getSwingHandler(), new Animation(2989), new Graphics(499)));
/**
* Constructs a new {@code SkeletalWyvernNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public SkeletalWyvernNPC(int id, Location location) {
super(id, location);
}
/**
* Constructs a new {@code SkeletalWyvernNPC} {@code Object}.
*/
public SkeletalWyvernNPC() {
super(0, null);
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new SkeletalWyvernNPC(id, location);
}
@Override
public void sendImpact(BattleState state) {
if (state.getStyle() == CombatStyle.MAGIC && state.getVictim() != null && state.getVictim().isPlayer()) {
Player p = state.getVictim().asPlayer();
Item item = p.getEquipment().get(EquipmentContainer.SLOT_SHIELD);
if (item != null && (item.getId() == 2890 || item.getId() == 9731 || item.getId() == 11283) && state.getEstimatedHit() > 10) {
state.setEstimatedHit(RandomFunction.random(10));
}
}
Player p = state.getVictim().asPlayer();
Item item = p.getEquipment().get(EquipmentContainer.SLOT_SHIELD);
if(state.getVictim().getLocation().getDistance(state.getAttacker().getLocation()) >= 5
&& state.getVictim().asPlayer().getPrayer().get(PrayerType.PROTECT_FROM_MAGIC) && (item.getId() == 2890 || item.getId() == 9731 || item.getId() == 11283)){
state.setEstimatedHit(0);
}
}
@Override
public CombatSwingHandler getSwingHandler(boolean swing) {
if (this.getProperties().getCombatPulse().getVictim() != null && this.getProperties().getCombatPulse().getVictim().getLocation().getDistance(this.getLocation()) >= 5){
return COMBAT_HANDLER_FAR;
}
return COMBAT_HANDLER;
}
@Override
public int[] getIds() {
return Tasks.SKELETAL_WYVERN.getNpcs();
}
}

View file

@ -0,0 +1,16 @@
package content.global.skill.slayer
import core.game.node.entity.Entity
import core.game.node.entity.combat.BattleState
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.Player
class TurothBehavior : NPCBehavior(*Tasks.TUROTHS.ids) {
override fun beforeDamageReceived(self: NPC, attacker: Entity, state: BattleState) {
if (attacker is Player) {
if (!SlayerUtils.hasBroadWeaponEquipped(attacker, state))
state.neutralizeHits()
}
}
}

View file

@ -1,58 +0,0 @@
package content.global.skill.slayer;
import core.game.node.entity.combat.BattleState;
import core.game.node.entity.npc.AbstractNPC;
import core.game.node.entity.player.Player;
import core.game.world.map.Location;
import core.plugin.Initializable;
/**
* Handles the turoth npc.
* @author Vexia
*/
@Initializable
public final class TurothNPC extends AbstractNPC {
/**
* Constructs a new {@code TurothNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public TurothNPC(int id, Location location) {
super(id, location);
}
/**
* Constructs a new {@code TurothNPC} {@code Object}.
*/
public TurothNPC() {
super(0, null);
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new TurothNPC(id, location);
}
@Override
public void checkImpact(final BattleState state) {
super.checkImpact(state);
boolean effective = false;
if (state.getAttacker() instanceof Player) {
final Player player = (Player) state.getAttacker();
effective = SlayerUtils.hasBroadWeaponEquipped(player, state);
}
if (!effective) {
state.setEstimatedHit(0);
if (state.getSecondaryHit() > 0) {
state.setSecondaryHit(0);
}
}
}
@Override
public int[] getIds() {
return new int[] { 1626, 1627, 1628, 1629, 1630 };
}
}

View file

@ -1,58 +0,0 @@
package content.global.skill.slayer;
import core.game.node.entity.Entity;
import core.game.node.entity.combat.CombatStyle;
import core.game.node.entity.combat.equipment.SwitchAttack;
import core.game.node.entity.impl.Animator.Priority;
import core.game.node.entity.impl.Projectile;
import core.game.node.entity.npc.AbstractNPC;
import core.game.world.map.Location;
import core.game.world.update.flag.context.Animation;
import core.plugin.Initializable;
import core.game.node.entity.combat.CombatSwingHandler;
import core.game.node.entity.combat.MultiSwingHandler;
/**
* Handles the water fiend npc.
* @author Vexia
*/
@Initializable
public final class WaterFiendNPC extends AbstractNPC {
/**
* Handles the combat.
*/
private final CombatSwingHandler combatAction = new MultiSwingHandler(true, new SwitchAttack(CombatStyle.MAGIC.getSwingHandler(), new Animation(1581, Priority.HIGH), null, null, Projectile.create((Entity) null, null, 500, 15, 30, 50, 50, 14, 255)), new SwitchAttack(CombatStyle.RANGE.getSwingHandler(), new Animation(1581, Priority.HIGH), null, null, Projectile.create((Entity) null, null, 16, 15, 30, 50, 50, 14, 255)));
/**
* Constructs a new {@code WaterFiendNPC} {@code Object}.
* @param id the id.
* @param location the location.
*/
public WaterFiendNPC(int id, Location location) {
super(id, location);
}
/**
* Constructs a new {@code WaterFiendNPC} {@code Object}.
*/
public WaterFiendNPC() {
super(0, null);
}
@Override
public AbstractNPC construct(int id, Location location, Object... objects) {
return new WaterFiendNPC(id, location);
}
@Override
public CombatSwingHandler getSwingHandler(boolean swing) {
return combatAction;
}
@Override
public int[] getIds() {
return Tasks.WATERFIENDS.getNpcs();
}
}

View file

@ -0,0 +1,55 @@
package content.global.skill.slayer
import core.game.node.entity.Entity
import core.game.node.entity.combat.CombatStyle
import core.game.node.entity.combat.CombatSwingHandler
import core.game.node.entity.combat.MultiSwingHandler
import core.game.node.entity.combat.equipment.SwitchAttack
import core.game.node.entity.impl.Animator.Priority
import core.game.node.entity.impl.Projectile
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.world.update.flag.context.Animation
class WaterfiendBehavior : NPCBehavior(*Tasks.WATERFIENDS.ids) {
private val combatHandler = MultiSwingHandler(
true,
SwitchAttack(
CombatStyle.MAGIC.swingHandler,
Animation(1581, Priority.HIGH),
null,
null,
Projectile.create(
null as Entity?,
null,
500,
15,
30,
50,
50,
14,
255
)
),
SwitchAttack(
CombatStyle.RANGE.swingHandler,
Animation(1581, Priority.HIGH),
null,
null,
Projectile.create(
null as Entity?,
null,
16,
15,
30,
50,
50,
14,
255
)
)
)
override fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler): CombatSwingHandler {
return combatHandler
}
}

View file

@ -229,7 +229,7 @@ public enum Pets {
// BABY_GOLD_CHINCHOMPA(14826, -1, -1, 8658, -1, -1, 0.0, 1),
// HERON(14827, -1, -1, 8647, -1, -1, 0.0, 1),
// TZREK_JAD(14828, -1, -1, 8650, -1, -1, 0.0, 1);
/**
* The baby pets mapping.
@ -451,12 +451,12 @@ public enum Pets {
*/
public int getNpcId(int stage) {
switch (stage) {
case 0:
return babyNpcId;
case 1:
return grownNpcId;
case 2:
return overgrownNpcId;
case 0:
return babyNpcId;
case 1:
return grownNpcId;
case 2:
return overgrownNpcId;
}
return 0;
}
@ -468,13 +468,13 @@ public enum Pets {
*/
public int getItemId(int stage) {
switch (stage) {
case 0:
return babyItemId;
case 1:
return grownItemId;
case 2:
return overgrownItemId;
case 0:
return babyItemId;
case 1:
return grownItemId;
case 2:
return overgrownItemId;
}
return 0;
}
}
}

View file

@ -0,0 +1,46 @@
package content.region.desert.bandits.handlers
import core.api.*
import core.game.node.entity.Entity
import core.game.node.entity.combat.BattleState
import core.game.node.entity.npc.NPC
import core.game.node.entity.npc.NPCBehavior
import core.game.world.map.RegionManager
import core.tools.RandomFunction
import org.rs09.consts.NPCs
class BanditBehavior : NPCBehavior(NPCs.BANDIT_1926) {
override fun tick(self: NPC): Boolean {
if (!self.inCombat() && RandomFunction.roll(3) && getWorldTicks() % 5 == 0) {
val players = RegionManager.getLocalPlayers(self, 5)
for (player in players) {
if (player.inCombat()) continue
if (hasGodItem(player, God.SARADOMIN)) {
sendChat(self, "Prepare to die, Saradominist scum!")
self.attack(player)
break
}
else if (hasGodItem(player, God.ZAMORAK)) {
sendChat(self, "Prepare to die, Zamorakian scum!")
self.attack(player)
break
}
}
}
return true
}
override fun afterDamageReceived(self: NPC, attacker: Entity, state: BattleState) {
if (getAttribute(self, "alerted-others", false)) return
val otherBandits = RegionManager.getLocalNpcs(self, 3).filter { it.id == self.id }
for (bandit in otherBandits) {
if (!bandit.inCombat())
bandit.attack(attacker)
}
setAttribute(self, "alerted-others", true)
}
override fun onDeathStarted(self: NPC, killer: Entity) {
removeAttribute(self, "alerted-others")
}
}

View file

@ -301,6 +301,9 @@ public abstract class Entity extends Node {
* combat zone.
*/
public boolean isIgnoreMultiBoundaries(Entity victim) {
if (this instanceof NPC) {
return ((NPC) this).behavior.shouldIgnoreMultiRestrictions((NPC) this, victim);
}
return false;
}
@ -337,6 +340,9 @@ public abstract class Entity extends Node {
public void onImpact(final Entity entity, BattleState state) {
if (DeathTask.isDead(this))
state.neutralizeHits();
if (this instanceof NPC) {
((NPC) this).behavior.afterDamageReceived((NPC) this, entity, state);
}
if (properties.isRetaliating() && !properties.getCombatPulse().isAttacking() && !getLocks().isInteractionLocked() && properties.getCombatPulse().getNextAttack() < GameWorld.getTicks()) {
if (!getWalkingQueue().hasPath() && !getPulseManager().isMovingPulse() || (this instanceof NPC)) {
properties.getCombatPulse().attack(entity);

View file

@ -142,6 +142,8 @@ public class NPC extends Entity {
*/
private String forceTalk;
public final NPCBehavior behavior;
/**
* Constructs a new {@code NPC} {@code Object}.
* @param id The NPC id.
@ -163,6 +165,7 @@ public class NPC extends Entity {
super.size = definition.size;
super.direction = direction;
super.interactPlugin = new InteractPlugin(this);
this.behavior = NPCBehavior.forId(id);
}
/**
@ -225,6 +228,7 @@ public class NPC extends Entity {
npc.size = size;
}
}
behavior.onCreation(this);
}
@Override
@ -233,6 +237,7 @@ public class NPC extends Entity {
Repository.removeRenderableNPC(this);
Repository.getNpcs().remove(this);
getViewport().setCurrentPlane(null);
behavior.onRemoval(this);
// getViewport().setRegion(null);
}
@ -360,6 +365,7 @@ public class NPC extends Entity {
public void checkImpact(BattleState state) {
super.checkImpact(state);
Entity entity = state.getAttacker();
behavior.beforeDamageReceived(this, entity, state);
if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getStaticLevel(Skills.SLAYER)) {
state.neutralizeHits();
}
@ -375,6 +381,8 @@ public class NPC extends Entity {
((Player) entity).getPacketDispatch().sendMessage("You need a higher slayer level to know how to wound this monster.");
}
}
if (!behavior.canBeAttackedBy(this, entity, style, message))
return false;
return super.isAttackable(entity, style, message);
}
@ -395,6 +403,7 @@ public class NPC extends Entity {
return;
}
if (respawnTick == GameWorld.getTicks()) {
behavior.onRespawn(this);
onRespawn();
}
handleTickActions();
@ -410,6 +419,8 @@ public class NPC extends Entity {
* Handles the automatic actions of the NPC.
*/
public void handleTickActions() {
if (!behavior.tick(this))
return;
if (!getLocks().isInteractionLocked()) {
if (!getLocks().isMovementLocked()) {
if (
@ -523,6 +534,12 @@ public class NPC extends Entity {
if (!isRespawn())
clear();
killer.dispatch(new NPCKillEvent(this));
behavior.onDeathFinished(this, killer);
}
@Override
public void commenceDeath(Entity killer) {
behavior.onDeathStarted(this, killer);
}
/**
@ -580,7 +597,8 @@ public class NPC extends Entity {
@Override
public CombatSwingHandler getSwingHandler(boolean swing) {
return getProperties().getCombatPulse().getStyle().getSwingHandler();
CombatSwingHandler original = getProperties().getCombatPulse().getStyle().getSwingHandler();
return behavior.getSwingHandlerOverride(this, original);
}
/**

View file

@ -0,0 +1,122 @@
package core.game.node.entity.npc
import core.api.ContentInterface
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.combat.CombatSwingHandler
import core.game.node.item.Item
open class NPCBehavior(vararg val ids: Int = intArrayOf()) : ContentInterface {
companion object {
private val idMap = HashMap<Int,NPCBehavior>()
private val defaultBehavior = NPCBehavior()
@JvmStatic fun forId(id: Int) : NPCBehavior {
return idMap[id] ?: defaultBehavior
}
fun register(ids: IntArray, behavior: NPCBehavior){
ids.forEach { idMap[it] = behavior }
}
}
/**
* Called every tick, before the base NPC tick() method.
* @param self the NPC instance this behavior belongs to
* @return whether we should proceed with the base NPC tick() method - e.g. returning false means we do not proceed with a normal NPC tick.
*/
open fun tick(self: NPC): Boolean {
return true
}
/**
* Called before this NPC receives damage, and allows you to adjust the battlestate if needed.
* @param self the NPC instance this behavior belongs to
* @param attacker the entity attacking this NPC
* @param state the current state of the combat between this NPC and the attacker.
*/
open fun beforeDamageReceived(self: NPC, attacker: Entity, state: BattleState) {}
/**
* Called after this NPC receives damage, and allows you to adjust the battlestate if needed.
* @param self the NPC instance this behavior belongs to
* @param attacker the entity attacking this NPC
* @param state the current state of the combat between this NPC and the attacker.
*/
open fun afterDamageReceived(self: NPC, attacker: Entity, state: BattleState) {}
/**
* Called after this NPC's basic attack has been calculated, but before it is finalized, so adjustments can be made.
* @param self the NPC instance this behavior belongs to
* @param victim the entity this NPC is attacking
* @param state the state of combat between this NPC and the victim.
*/
open fun beforeAttackFinalized(self: NPC, victim: Entity, state: BattleState) {}
/**
* Called when this NPC is being removed from the game world.
* Note: This is not the same as death. Death does not remove an NPC, unless that NPC cannot respawn.
* @param self the NPC instance this behavior belongs to
*/
open fun onRemoval(self: NPC) {}
/**
* Called when this NPC is first created and spawned into the game world.
* Note: This is not the same as respawning.
* @param self the NPC instance this behavior belongs to
*/
open fun onCreation(self: NPC) {}
/**
* Called when this NPC respawns after being killed.
* @param self the NPC instance this behavior belongs to
*/
open fun onRespawn(self: NPC) {}
/**
* Called immediately when the NPC first begins to die (on the same tick that the death animation begins)
* @param self the NPC instance this behavior belongs to
* @param killer the entity which killed this NPC.
*/
open fun onDeathStarted(self: NPC, killer: Entity) {}
/**
* Called immediately after the death animation of this NPC has finished, on the same tick that drop tables are rolled.
* @param self the NPC instance this behavior belongs to
* @param killer the entity which killed this NPC.
*/
open fun onDeathFinished(self: NPC, killer: Entity) {}
/**
* Called after this NPC's drop table is rolled, but before the items are actually dropped, so the list can be manipulated.
* @param self the NPC instance this behavior belongs to
* @param drops the generated list of drops for this roll of the table
*/
open fun onDropTableRolled(self: NPC, drops: ArrayList<Item>) {}
/**
* Called by combat-related code to check if this NPC can be attacked by the `attacker` entity.
* @param self the NPC instance this behavior belongs to
* @param attacker the entity attempting to attack this NPC
* @param style the combat style the attacker is attempting to use
* @param shouldSendMessage whether the core combat code believes you should send a message e.g. "You can't attack this NPC with that weapon"
* @return whether the attacker should be able to attack this NPC.
*/
open fun canBeAttackedBy(self: NPC, attacker: Entity, style: CombatStyle, shouldSendMessage: Boolean) : Boolean {return true}
/**
* Called by combat-related code to check if this NPC should ignore multi-combat rules when attempting to attack the given victim.
* @param self the NPC instance this behavior belongs to
* @param victim the entity that is being considered for attack.
* @return whether we should ignore the rules of multi-way combat for the given entity.
*/
open fun shouldIgnoreMultiRestrictions(self: NPC, victim: Entity) : Boolean {return false}
/**
* Called by combat-related code to allow the combat handler to be overridden
* @param self the NPC instance this behavior belongs to
* @param original the default swing handler this NPC would have used
* @return the SwingHandler instance to be used for this cycle of combat
*/
open fun getSwingHandlerOverride(self: NPC, original: CombatSwingHandler) : CombatSwingHandler {return original}
}

View file

@ -74,7 +74,9 @@ public final class NPCDropTables {
*/
public void drop(NPC npc, Entity looter) {
Player p = looter instanceof Player ? (Player) looter : null;
table.roll(looter).forEach(item -> createDrop(item,p,npc,npc.getDropLocation()));
ArrayList<Item> drops = table.roll(looter);
npc.behavior.onDropTableRolled(npc, drops);
drops.forEach(item -> createDrop(item,p,npc,npc.getDropLocation()));
}
/**

View file

@ -92,6 +92,7 @@ public final class MultiwayCombatZone extends MapZone {
registerRegion(14939);//Kalphite Stronghold Cave
registerRegion(9532); //Isle north of Jatizso
registerRegion(10810); //Eastern rock crabs
registerRegion(12590); //desert bandits
}
@Override

View file

@ -16,6 +16,7 @@ import io.github.classgraph.ScanResult
import core.game.bots.PlayerScripts
import core.game.interaction.InteractionListener
import core.game.interaction.InterfaceListener
import core.game.node.entity.npc.NPCBehavior
import core.game.node.entity.player.info.login.PlayerSaveParser
import core.game.node.entity.player.info.login.PlayerSaver
import core.tools.SystemLogger
@ -84,6 +85,7 @@ object ClassScanner {
if(clazz is InteractionListener) clazz.defineListeners().also { clazz.defineDestinationOverrides() }
if(clazz is InterfaceListener) clazz.defineInterfaceListeners()
if(clazz is Commands) clazz.defineCommands()
if(clazz is NPCBehavior) NPCBehavior.register(clazz.ids, clazz)
if(clazz is PersistPlayer) {
PlayerSaver.contentHooks.add(clazz)
PlayerSaveParser.contentHooks.add(clazz)