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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user