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