Added target validity checking for combat interactions and special cases

This commit is contained in:
dam
2026-01-22 13:27:17 +02:00
committed by Ryan
parent 0f29ae93ff
commit 7276590373
10 changed files with 127 additions and 45 deletions
@@ -13,6 +13,7 @@ import core.game.world.map.RegionManager;
import core.game.world.update.flag.context.Graphics; import core.game.world.update.flag.context.Graphics;
import core.tools.RandomFunction; import core.tools.RandomFunction;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@@ -54,13 +55,15 @@ public final class ChinchompaSwingHandler extends RangeSwingHandler {
entity.getProperties().getCombatPulse().stop(); entity.getProperties().getCombatPulse().stop();
return -1; return -1;
} }
@SuppressWarnings("rawtypes")
List list = victim instanceof NPC ? RegionManager.getSurroundingNPCs(victim, 14, entity) : RegionManager.getSurroundingPlayers(victim, 14, entity); // ! Initial capacity of 14, but has dynamic size? i.e. we are not restricting to a max of 14 targets hit here now
BattleState[] targets = new BattleState[list.size()]; List<Entity> targetCandidates = new ArrayList<>(14);
targetCandidates.addAll(RegionManager.getSurroundingNPCs(victim, 14, entity));
targetCandidates.addAll(RegionManager.getSurroundingPlayers(victim, 14, entity));
BattleState[] targets = new BattleState[targetCandidates.size()];
int count = 0; int count = 0;
for (Object o : list) { for (Entity e : targetCandidates) {
Entity e = (Entity) o; if (canSwing(entity, e) != InteractionType.NO_INTERACT && e.isAttackable(entity, CombatStyle.RANGE, false)) {
if (canSwing(entity, e) != InteractionType.NO_INTERACT) {
BattleState s = targets[count++] = new BattleState(entity, e); BattleState s = targets[count++] = new BattleState(entity, e);
s.setStyle(CombatStyle.RANGE); s.setStyle(CombatStyle.RANGE);
int hit = 0; int hit = 0;
@@ -49,7 +49,7 @@ public final class PowerstabSpecialHandler extends MeleeSwingHandler implements
@Override @Override
public void impact(Entity entity, Entity victim, BattleState state) { public void impact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s); s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s);
@@ -81,13 +81,13 @@ public final class PowerstabSpecialHandler extends MeleeSwingHandler implements
int count = 0; int count = 0;
for (Object o : list) { for (Object o : list) {
Entity e = (Entity) o; Entity e = (Entity) o;
if (CombatStyle.RANGE.getSwingHandler().canSwing(entity, e) != InteractionType.NO_INTERACT) { if (CombatStyle.RANGE.getSwingHandler().canSwing(entity, e) != InteractionType.NO_INTERACT && e.isAttackable(entity, CombatStyle.RANGE, false)) {
BattleState s = targets[count++] = new BattleState(entity, e); BattleState s = targets[count++] = new BattleState(entity, e);
int hit = 0; int hit = 0;
if (isAccurateImpact(entity, e)) { if (isAccurateImpact(entity, e)) {
hit = RandomFunction.random(calculateHit(entity, e, 1.0) + 1); hit = RandomFunction.random(calculateHit(entity, e, 1.0) + 1);
} }
s.setStyle(CombatStyle.MELEE); s.setStyle(CombatStyle.MELEE);
s.setEstimatedHit(hit); s.setEstimatedHit(hit);
} }
} }
@@ -103,7 +103,7 @@ public final class PowerstabSpecialHandler extends MeleeSwingHandler implements
@Override @Override
public void visualizeImpact(Entity entity, Entity victim, BattleState state) { public void visualizeImpact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().animate(victim.getProperties().getDefenceAnimation()); s.getVictim().animate(victim.getProperties().getDefenceAnimation());
@@ -49,7 +49,7 @@ public final class SpearWallSpecialHandler extends MeleeSwingHandler implements
@Override @Override
public void impact(Entity entity, Entity victim, BattleState state) { public void impact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s); s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s);
@@ -82,11 +82,11 @@ public final class SpearWallSpecialHandler extends MeleeSwingHandler implements
int count = 0; int count = 0;
for (Object o : list) { for (Object o : list) {
Entity e = (Entity) o; Entity e = (Entity) o;
if (CombatStyle.RANGE.getSwingHandler().canSwing(entity, e) != InteractionType.NO_INTERACT) { if (CombatStyle.RANGE.getSwingHandler().canSwing(entity, e) != InteractionType.NO_INTERACT && e.isAttackable(entity, CombatStyle.RANGE, false)) {
BattleState s = targets[count++] = new BattleState(entity, e); BattleState s = targets[count++] = new BattleState(entity, e);
int hit = 0; int hit = 0;
hit = RandomFunction.random(calculateHit(entity, e, 1.0) + 1); hit = RandomFunction.random(calculateHit(entity, e, 1.0) + 1);
s.setStyle(CombatStyle.MELEE); s.setStyle(CombatStyle.MELEE);
s.setEstimatedHit(hit); s.setEstimatedHit(hit);
} }
} }
@@ -96,13 +96,13 @@ public final class SpearWallSpecialHandler extends MeleeSwingHandler implements
@Override @Override
public void visualize(Entity entity, Entity victim, BattleState state) { public void visualize(Entity entity, Entity victim, BattleState state) {
playGlobalAudio(entity.getLocation(), Sounds.CLEAVE_2529); playGlobalAudio(entity.getLocation(), Sounds.CLEAVE_2529);
entity.visualize(ANIMATION, GRAPHIC); entity.visualize(ANIMATION, GRAPHIC);
} }
@Override @Override
public void visualizeImpact(Entity entity, Entity victim, BattleState state) { public void visualizeImpact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().animate(victim.getProperties().getDefenceAnimation()); s.getVictim().animate(victim.getProperties().getDefenceAnimation());
@@ -64,7 +64,7 @@ public final class SweepSpecialHandler extends MeleeSwingHandler implements Plug
BattleState[] targets = getTargets(entity, victim, state); BattleState[] targets = getTargets(entity, victim, state);
state.setTargets(targets); state.setTargets(targets);
for (BattleState s : targets) { for (BattleState s : targets) {
s.setStyle(CombatStyle.MELEE); s.setStyle(CombatStyle.MELEE);
int hit = 0; int hit = 0;
if (isAccurateImpact(entity, s.getVictim(), CombatStyle.MELEE)) { if (isAccurateImpact(entity, s.getVictim(), CombatStyle.MELEE)) {
hit = RandomFunction.random(calculateHit(entity, s.getVictim(), 1.1) + 1); hit = RandomFunction.random(calculateHit(entity, s.getVictim(), 1.1) + 1);
@@ -90,7 +90,7 @@ public final class SweepSpecialHandler extends MeleeSwingHandler implements Plug
*/ */
private BattleState[] getTargets(Entity entity, Entity victim, BattleState state) { private BattleState[] getTargets(Entity entity, Entity victim, BattleState state) {
if (!entity.getProperties().isMultiZone() || !victim.getProperties().isMultiZone()) { if (!entity.getProperties().isMultiZone() || !victim.getProperties().isMultiZone()) {
return new BattleState[] { state }; return new BattleState[]{state};
} }
Location vl = victim.getLocation(); Location vl = victim.getLocation();
int x = vl.getX(); int x = vl.getX();
@@ -102,6 +102,9 @@ public final class SweepSpecialHandler extends MeleeSwingHandler implements Plug
if (n instanceof Familiar) { if (n instanceof Familiar) {
continue; continue;
} }
if (!n.isAttackable(entity, CombatStyle.MELEE, false)) {
continue;
}
if (n.getLocation().equals(vl.transform(dir.getStepY(), dir.getStepX(), 0)) || n.getLocation().equals(vl.transform(-dir.getStepY(), -dir.getStepX(), 0))) { if (n.getLocation().equals(vl.transform(dir.getStepY(), dir.getStepX(), 0)) || n.getLocation().equals(vl.transform(-dir.getStepY(), -dir.getStepX(), 0))) {
l.add(new BattleState(entity, n)); l.add(new BattleState(entity, n));
if (l.size() >= 3) { if (l.size() >= 3) {
@@ -127,7 +130,7 @@ public final class SweepSpecialHandler extends MeleeSwingHandler implements Plug
@Override @Override
public void impact(Entity entity, Entity victim, BattleState state) { public void impact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s); s.getVictim().getImpactHandler().handleImpact(entity, s.getEstimatedHit(), CombatStyle.MELEE, s);
@@ -146,7 +149,7 @@ public final class SweepSpecialHandler extends MeleeSwingHandler implements Plug
@Override @Override
public void visualizeImpact(Entity entity, Entity victim, BattleState state) { public void visualizeImpact(Entity entity, Entity victim, BattleState state) {
if (state.getTargets() != null) { if (state != null && state.getTargets() != null) {
for (BattleState s : state.getTargets()) { for (BattleState s : state.getTargets()) {
if (s != null) { if (s != null) {
s.getVictim().animate(victim.getProperties().getDefenceAnimation()); s.getVictim().animate(victim.getProperties().getDefenceAnimation());
@@ -274,46 +274,56 @@ public abstract class Familiar extends NPC implements Plugin<Object> {
@Override @Override
public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { public boolean isAttackable(Entity entity, CombatStyle style, boolean message) {
if (isInvalidTarget(entity, style, message)) return false;
return super.isAttackable(entity, style, message);
}
@Override
public boolean continueAttack(Entity target, CombatStyle style, boolean message) {
return !isInvalidTarget(target, style, message);
}
private boolean isInvalidTarget(Entity entity, CombatStyle style, boolean message) {
if (entity == owner) { if (entity == owner) {
if (message) { if (message) {
owner.getPacketDispatch().sendMessage("You can't just betray your own familiar like that!"); sendMessage(owner, "You can't just betray your own familiar like that!");
} }
return false; return true;
} }
if (entity instanceof Player) { if (entity instanceof Player) {
if (!owner.isAttackable(entity, style, message)) { if (!owner.isAttackable(entity, style, message)) {
return false; return true;
} }
} }
if (!getProperties().isMultiZone()) { if (!getProperties().isMultiZone()) {
if (entity instanceof Player && !((Player) entity).getProperties().isMultiZone()) { if (entity instanceof Player && !((Player) entity).getProperties().isMultiZone()) {
if (message) { if (message) {
((Player) entity).getPacketDispatch().sendMessage("You have to be in multicombat to attack a player's familiar."); sendMessage((Player) entity, "You have to be in multicombat to attack a player's familiar.");
} }
return false; return true;
} }
if (entity instanceof Player) { if (entity instanceof Player) {
if (message) { if (message) {
((Player) entity).getPacketDispatch().sendMessage("This familiar is not in the a multicombat zone."); sendMessage((Player) entity, "This familiar is not in the a multicombat zone.");
} }
} }
return false; return true;
} }
if (entity instanceof Player) { if (entity instanceof Player) {
if (!((Player) entity).getSkullManager().isWilderness()) { if (!((Player) entity).getSkullManager().isWilderness()) {
if (message) { if (message) {
((Player) entity).getPacketDispatch().sendMessage("You have to be in the wilderness to attack a player's familiar."); sendMessage((Player) entity, "You have to be in the wilderness to attack a player's familiar.");
} }
return false; return true;
} }
if (!owner.getSkullManager().isWilderness()) { if (!owner.getSkullManager().isWilderness()) {
if (message) { if (message) {
((Player) entity).getPacketDispatch().sendMessage("This familiar's owner is not in the wilderness."); sendMessage((Player) entity, "This familiar's owner is not in the wilderness.");
} }
return false; return true;
} }
} }
return super.isAttackable(entity, style, message); return false;
} }
@Override @Override
@@ -490,9 +490,24 @@ public abstract class Entity extends Node {
/** /**
* Checks if an entity can continue its attack. * Checks if an entity can continue its attack.
* @param target the target. *
* @param style the style. * <p>This method is called during attack validation to determine if the attacking entity should be
* @return {@code True} if so. * allowed to continue attacking the target. It's invoked by
* {@link core.game.world.map.zone.ZoneMonitor#continueAttack(Node, CombatStyle, boolean)}
* as part of the attack validation chain.</p>
*
* <p>The default implementation returns {@code true}, allowing the attack to proceed.
* <b>Subclasses should override this method to implement entity-specific attack restrictions.</b></p>
*
* <p>This method is distinct from {@link #isAttackable(Entity, CombatStyle, boolean)}, which
* checks if this entity can be attacked by another entity. The {@code continueAttack} method
* checks if this entity can attack the given target.</p>
*
* @param target the target entity being attacked
* @param style the combat style being used
* @param message whether to send a message to the player explaining why the attack cannot continue
*
* @return {@code true} if the entity can continue attacking the target
*/ */
public boolean continueAttack(Entity target, CombatStyle style, boolean message) { public boolean continueAttack(Entity target, CombatStyle style, boolean message) {
return true; return true;
@@ -127,7 +127,7 @@ public abstract class CombatSpell extends MagicSpell {
if (e == target || e == entity) { if (e == target || e == entity) {
continue; continue;
} }
if (CombatStyle.MAGIC.getSwingHandler().canSwing(entity, e) == InteractionType.NO_INTERACT) { if (CombatStyle.MAGIC.getSwingHandler().canSwing(entity, e) == InteractionType.NO_INTERACT || !e.isAttackable(entity, CombatStyle.MAGIC, false)) {
continue; continue;
} }
if (e instanceof Familiar) { if (e instanceof Familiar) {
@@ -400,15 +400,29 @@ public class NPC extends Entity {
return false; return false;
} }
if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getLevel(Skills.SLAYER)) { if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getLevel(Skills.SLAYER)) {
if(message) { if(message) {
((Player) entity).getPacketDispatch().sendMessage("You need a higher slayer level to know how to wound this monster."); ((Player) entity).getPacketDispatch().sendMessage("You need a higher slayer level to know how to wound this monster.");
} }
} }
if (!behavior.canBeAttackedBy(this, entity, style, message)) if (!behavior.canBeAttackedBy(this, entity, style, message))
return false; return false;
return super.isAttackable(entity, style, message); return super.isAttackable(entity, style, message);
} }
@Override
public boolean continueAttack(Entity target, CombatStyle style, boolean message) {
if (isInvisible()) {
return false;
}
if (task != null && target instanceof Player && task.levelReq > target.getSkills().getLevel(Skills.SLAYER)) {
if (message) {
((Player) target).getPacketDispatch().sendMessage("You need a higher slayer level to know how to wound this monster.");
}
return false;
}
return behavior.canBeAttackedBy(this, target, style, message);
}
@Override @Override
public int getDragonfireProtection(boolean fire) { public int getDragonfireProtection(boolean fire) {
return 0; return 0;
@@ -766,6 +766,30 @@ public class Player extends Entity {
return super.isAttackable(entity, style, message); return super.isAttackable(entity, style, message);
} }
@Override
public boolean continueAttack(Entity target, CombatStyle style, boolean message) {
if (target instanceof NPC
&& !((NPC) target).getDefinition().hasAction("attack")
&& !((NPC) target).isIgnoreAttackRestrictions(this)) {
return false;
}
if (target instanceof Player) {
Player p = (Player) target;
if (p.getSkullManager().isWilderness() && skullManager.isWilderness()) {
if (GameWorld.getSettings() == null || !GameWorld.getSettings().getWild_pvp_enabled()) {
return false;
}
if (p.getSkullManager().hasWildernessProtection()) {
return false;
}
return !skullManager.hasWildernessProtection();
} else {
return false;
}
}
return true;
}
@Override @Override
public boolean isPoisonImmune() { public boolean isPoisonImmune() {
return timers.getTimer("poison:immunity") != null; return timers.getTimer("poison:immunity") != null;
@@ -130,12 +130,25 @@ public abstract class MapZone implements Zone {
} }
/** /**
* Checks if the entity is able to continue attacking the target. * Checks if the entity is able to continue attacking the target within this zone.
* @param e the attacking entity. *
* @param target The target. * <p>This method is called during attack validation to determine if zone-specific rules
* @param style The combat style used. * allow the attack to proceed. It's invoked by
* @param message If a message should be send. * {@link core.game.world.map.zone.ZoneMonitor#continueAttack(Node, CombatStyle, boolean)}
* @return {@code True} if so. * for each zone the attacking entity is in.</p>
*
* <p>The default implementation returns {@code true}, allowing the attack to proceed.
* <b>Subclasses should override this method to implement zone-specific attack restrictions.</b></p>
* <p>This method is part of the attack validation chain:
* {@link Entity#isAttackable(Entity, CombatStyle, boolean)} ->
* {@link core.game.world.map.zone.ZoneMonitor#continueAttack(Node, CombatStyle, boolean)} ->
* {@code MapZone.continueAttack()} + {@link Entity#continueAttack(Entity, CombatStyle, boolean)}</p>
*
* @param e the attacking entity
* @param target the target node being attacked
* @param style the combat style being used
* @param message whether to send a message to the player explaining why the attack cannot continue
* @return {@code true} if the zone allows the attack to continue
*/ */
public boolean continueAttack(Entity e, Node target, CombatStyle style, boolean message) { public boolean continueAttack(Entity e, Node target, CombatStyle style, boolean message) {
return true; return true;