From 6dee7082632f0e87197049ed68613d8dd7f8b0ce Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Wed, 1 Apr 2026 17:02:38 +0300 Subject: [PATCH] Fixed edge cases introduced by recent combat changes --- .../skill/summoning/familiar/Familiar.java | 158 ++++++++++++++---- .../main/core/game/node/entity/Entity.java | 3 + .../entity/combat/equipment/BoltEffect.java | 12 +- .../main/core/game/node/entity/npc/NPC.java | 32 ++-- .../core/game/node/entity/player/Player.java | 2 +- .../core/game/system/timer/impl/Poison.kt | 34 +++- .../core/game/world/map/zone/MapZone.java | 13 +- .../world/map/zone/impl/WildernessZone.java | 17 ++ 8 files changed, 207 insertions(+), 64 deletions(-) diff --git a/Server/src/main/content/global/skill/summoning/familiar/Familiar.java b/Server/src/main/content/global/skill/summoning/familiar/Familiar.java index b386a54d6..b561a5b30 100644 --- a/Server/src/main/content/global/skill/summoning/familiar/Familiar.java +++ b/Server/src/main/content/global/skill/summoning/familiar/Familiar.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.List; import static core.api.ContentAPIKt.*; +import static core.api.ContentAPIKt.sendMessage; /** * Represents a familiar. @@ -258,7 +259,10 @@ public abstract class Familiar extends NPC implements Plugin { if (victim == null) { victim = owner.getAttribute("combat-attacker"); } - if (combat.getVictim() != this && victim != null && !victim.isInvisible() && getProperties().isMultiZone() && owner.getProperties().isMultiZone() && isCombatFamiliar() && !isBurdenBeast() && !isPeacefulFamiliar()) { + if (combat.getVictim() != this && victim != null && !victim.isInvisible() + && getProperties().isMultiZone() && owner.getProperties().isMultiZone() + && victim.getProperties().isMultiZone() && isCombatFamiliar() + && !isBurdenBeast() && !isPeacefulFamiliar()) { // Are you serious my brotha? getProperties().getCombatPulse().attack(victim); } } @@ -271,58 +275,120 @@ public abstract class Familiar extends NPC implements Plugin { } handleFamiliarTick(); } - + @Override public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { - if (isInvalidTarget(entity, style, message)) return false; + if (isAttackableBy(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); + if (getProperties().getCombatPulse().getVictim() == target) { + return canAttackTarget(target, style, message); + } + if (target == owner) { + if (message) { + sendMessage(owner, "You can't just betray your own familiar like that!"); + } + return false; + } + return true; } - - private boolean isInvalidTarget(Entity entity, CombatStyle style, boolean message) { - if (entity == owner) { + + /** + * Checks if the familiar can attack the target (familiar as attacker). + * Called from continueAttack() when familiar initiates combat. + * + * @param target The target to attack. + * @param style The combat style. + * @param message Whether to send messages. + * + * @return true if the familiar can attack the target. + */ + private boolean canAttackTarget(Entity target, CombatStyle style, boolean message) { + if (target == owner) { + if (message) { + sendMessage(owner, "You can't just betray your own familiar like that!"); + } + return false; + } + if (target instanceof Player) { + Player targetPlayer = (Player) target; + if (!targetPlayer.getSkullManager().isWilderness()) { + if (message) { + sendMessage(owner, "You have to be in the wilderness to attack a player's familiar."); + } + return false; + } + if (!owner.getSkullManager().isWilderness()) { + if (message) { + sendMessage(owner, "This familiar's owner is not in the wilderness."); + } + return false; + } + if (!owner.isAttackable(target, style, false)) { + return false; + } + if (!owner.getProperties().isMultiZone()) { + if (message) { + sendMessage(owner, "You have to be in a multicombat zone to attack another player with your familiar."); + } + return false; + } + } + + return true; + } + + /** + * Checks if the entity can attack this familiar (familiar as target). + * Called from isAttackable() when someone attacks the familiar. + * + * @param attacker The entity trying to attack the familiar. + * @param style The combat style. + * @param message Whether to send messages. + * + * @return true if the attacker cannot attack the familiar (invalid target). + */ + private boolean isAttackableBy(Entity attacker, CombatStyle style, boolean message) { + if (attacker == owner) { if (message) { sendMessage(owner, "You can't just betray your own familiar like that!"); } return true; } - if (entity instanceof Player) { - if (!owner.isAttackable(entity, style, message)) { - return true; - } - } - if (!getProperties().isMultiZone()) { - if (entity instanceof Player && !((Player) entity).getProperties().isMultiZone()) { + if (attacker instanceof Player) { + Player attackingPlayer = (Player) attacker; + if (!attackingPlayer.getSkullManager().isWilderness()) { if (message) { - sendMessage((Player) entity, "You have to be in multicombat to attack a player's familiar."); - } - return true; - } - if (entity instanceof Player) { - if (message) { - sendMessage((Player) entity, "This familiar is not in the a multicombat zone."); - } - } - return true; - } - if (entity instanceof Player) { - if (!((Player) entity).getSkullManager().isWilderness()) { - if (message) { - sendMessage((Player) entity, "You have to be in the wilderness to attack a player's familiar."); + sendMessage(attackingPlayer, "You have to be in the wilderness to attack a player's familiar."); } return true; } if (!owner.getSkullManager().isWilderness()) { if (message) { - sendMessage((Player) entity, "This familiar's owner is not in the wilderness."); + sendMessage(attackingPlayer, "This familiar's owner is not in the wilderness."); + } + return true; + } + if (!owner.isAttackable(attackingPlayer, style, false)) { + return true; + } + if (!attackingPlayer.getProperties().isMultiZone()) { + if (message) { + sendMessage(attackingPlayer, "You have to be in a multicombat zone to attack a player's familiar."); + } + return true; + } + if (!this.getProperties().isMultiZone()) { + if (message) { + sendMessage(attackingPlayer, "This familiar is not in a multicombat zone."); } return true; } } + return false; } @@ -440,9 +506,10 @@ public abstract class Familiar extends NPC implements Plugin { public void visualizeSpecialMove() { owner.visualize(Animation.create(7660), Graphics.create(1316)); } - + /** * Sends a familiar hit. + * * @param target the target. * @param maxHit the max hit. * @param graphics the graphics. @@ -459,6 +526,13 @@ public abstract class Familiar extends NPC implements Plugin { hit = RandomFunction.randomize(maxHit); } state.setEstimatedHit(hit); + if (target instanceof NPC && ((NPC) target).getTask() != null && ((NPC) target).getTask().levelReq > owner.getSkills().getLevel(Skills.SLAYER)) { + state.neutralizeHits(); + state.setPoisonDamage(-1); + if (state.getStyle() == CombatStyle.MAGIC) { + state.setEstimatedHit(-1); + } + } target.getImpactHandler().handleImpact(owner, hit, CombatStyle.MELEE, state); if (graphics != null) { target.graphics(graphics); @@ -484,7 +558,7 @@ public abstract class Familiar extends NPC implements Plugin { public void sendFamiliarHit(final Entity target, final int maxHit) { sendFamiliarHit(target, maxHit, null); } - + /** * Checks if this familiar can attack the target (used mainly for special * moves). @@ -495,11 +569,17 @@ public abstract class Familiar extends NPC implements Plugin { } if (target.getLocation().getDistance(getLocation()) > 8) { if (message) { - owner.getPacketDispatch().sendMessage("That target is too far."); + sendMessage(owner, "That target is too far."); } return false; } - if (target.getLocks().isInteractionLocked() || !target.isAttackable(this, CombatStyle.MAGIC, true)) { + if (target instanceof Player && !owner.getProperties().isMultiZone()) { + if (message) { + sendMessage(owner, "You have to be in a multicombat zone to attack another player with your familiar."); + } + return false; + } + if (target.getLocks().isInteractionLocked() || !target.isAttackable(this, CombatStyle.MAGIC, message)) { return false; } return isCombatFamiliar(); @@ -509,11 +589,13 @@ public abstract class Familiar extends NPC implements Plugin { public boolean canAttack(Entity target) { return canAttack(target, true); } - + /** * Checks if a familiar can perform a combat special attack. + * * @param target the target. * @param message show message. + * * @return {@code True} if so. */ public boolean canCombatSpecial(Entity target, boolean message) { @@ -526,6 +608,10 @@ public abstract class Familiar extends NPC implements Plugin { if (getAttribute("special-delay", 0) > GameWorld.getTicks()) { return false; } + // The special move probably should go through but with all harmful effects neutralized, but for now just block the damn thing + if (target instanceof NPC && ((NPC) target).getTask() != null && ((NPC) target).getTask().levelReq > owner.getSkills().getLevel(Skills.SLAYER)) { + return false; + } return true; } diff --git a/Server/src/main/core/game/node/entity/Entity.java b/Server/src/main/core/game/node/entity/Entity.java index 3576742f7..ffaa8f461 100644 --- a/Server/src/main/core/game/node/entity/Entity.java +++ b/Server/src/main/core/game/node/entity/Entity.java @@ -450,6 +450,9 @@ public abstract class Entity extends Node { if (!entity.getZoneMonitor().continueAttack(this, style, message)) { return false; } + if (!this.getZoneMonitor().continueAttack(entity, style, message)) { + return false; + } return true; } diff --git a/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java b/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java index 7fb24f1a5..06ab0edf2 100644 --- a/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java +++ b/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java @@ -226,14 +226,22 @@ public enum BoltEffect { victim.graphics(graphics); } } - + /** * Checks if the effect can fire. + * * @param state the state. + * * @return {@code True} if so. */ public boolean canFire(BattleState state) { - return RandomFunction.random(13) == 5; + // Never fires on slayer monster that player lacks level requirement for. Inauthentic(?) + // and should be considered a TODO: temporary abuse prevention measure (bolt effect on slayer monsters) + boolean rollSuccess = RandomFunction.random(13) == 5; + if (!(state.getVictim() instanceof NPC)) return rollSuccess; + if (!(state.getAttacker() instanceof Player) && ((NPC) state.getVictim()).getTask() == null) return rollSuccess; + if (state.getVictim().asNpc().getTask().levelReq > state.getAttacker().asPlayer().getSkills().getLevel(Skills.SLAYER)) return false; + return rollSuccess; } /** diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index bcd5c4f70..b8139e66a 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -383,7 +383,7 @@ public class NPC extends Entity { this.movementIndex = 0; this.pathBoundMovement = true; } - + @Override public void checkImpact(BattleState state) { super.checkImpact(state); @@ -391,21 +391,31 @@ public class NPC extends Entity { behavior.beforeDamageReceived(this, entity, state); if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getLevel(Skills.SLAYER)) { state.neutralizeHits(); + state.setPoisonDamage(-1); + if (state.getStyle() == CombatStyle.MAGIC) { + state.setEstimatedHit(-1); + } + } + if (task != null && entity instanceof Familiar && task.levelReq > ((Familiar) entity).getOwner().getSkills().getLevel(Skills.SLAYER)) { + state.neutralizeHits(); + state.setPoisonDamage(-1); + if (state.getStyle() == CombatStyle.MAGIC) { + state.setEstimatedHit(-1); + } } } - + @Override public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { if (isInvisible()) { return false; } if (task != null && entity instanceof Player && task.levelReq > entity.getSkills().getLevel(Skills.SLAYER)) { - if(message) { - ((Player) entity).getPacketDispatch().sendMessage("You need a higher slayer level to know how to wound this monster."); - } + if (message) { + ((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; + if (!behavior.canBeAttackedBy(this, entity, style, message)) return false; return super.isAttackable(entity, style, message); } @@ -414,13 +424,7 @@ public class NPC extends Entity { 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); + return true; } @Override diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index a4dc0b4bc..6b7bca07c 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -762,7 +762,7 @@ public class Player extends Entity { return false; if (skullManager.hasWildernessProtection()) return false; - return true; + return super.isAttackable(entity, style, message); } else return false; } return super.isAttackable(entity, style, message); diff --git a/Server/src/main/core/game/system/timer/impl/Poison.kt b/Server/src/main/core/game/system/timer/impl/Poison.kt index 7dbf9f2b0..a2f4aa78f 100644 --- a/Server/src/main/core/game/system/timer/impl/Poison.kt +++ b/Server/src/main/core/game/system/timer/impl/Poison.kt @@ -1,10 +1,13 @@ package core.game.system.timer.impl +import content.global.skill.summoning.familiar.Familiar import core.game.system.timer.* import core.api.* import core.game.node.entity.Entity import core.game.node.entity.player.Player import core.game.node.entity.combat.ImpactHandler +import core.game.node.entity.npc.NPC +import core.game.node.entity.skill.Skills import core.game.world.repository.Repository import org.json.simple.* @@ -30,15 +33,28 @@ class Poison : PersistTimer (30, "poison", flags = arrayOf(TimerFlag.ClearOnDeat damageSource = Repository.getPlayerByUid (uid) ?: entity severity = root["severity"].toString().toInt() } - - override fun onRegister (entity: Entity) { - if (entity is Player) { - sendMessage(entity, "You have been poisoned.") - entity.debug ("[Poison] -> Received for $severity severity.") - } - if (damageSource is Player) - (damageSource as? Player)?.debug ("[Poison] -> Applied for $severity severity.") - } + + override fun onRegister(entity : Entity) { + if (entity is Player) { + sendMessage(entity, "You have been poisoned.") + entity.debug("[Poison] -> Received for $severity severity.") + } + if (damageSource is Player) { + (damageSource as? Player)?.debug("[Poison] -> Applied for $severity severity.") + // this is ass, and preventing poisoning slayer monster player lacks lv req for should be done but prob not here & not like this. same for familiars + if (entity is NPC && entity.task != null && entity.task.levelReq > damageSource.skills.getLevel(Skills.SLAYER)) { + (damageSource as? Player)?.debug("[Poison] -> Removed poison from ${entity.name} ${entity.location} due to Slayer reqs!") + removeTimer(entity, this) + } + } + if (damageSource is Familiar) { + val owner : Player? = (damageSource as Familiar).owner + if (owner != null && entity is NPC && entity.task != null && entity.task.levelReq > owner.skills.getLevel(Skills.SLAYER)) { + owner.debug("[Poison] -> Removed poison from ${entity.name} ${entity.location} due to Slayer reqs!") + removeTimer(entity, this) + } + } + } override fun run (entity: Entity) : Boolean { entity.impactHandler.manualHit ( diff --git a/Server/src/main/core/game/world/map/zone/MapZone.java b/Server/src/main/core/game/world/map/zone/MapZone.java index de20b4b08..7adb16dfc 100644 --- a/Server/src/main/core/game/world/map/zone/MapZone.java +++ b/Server/src/main/core/game/world/map/zone/MapZone.java @@ -177,14 +177,23 @@ public abstract class MapZone implements Zone { if (multi || e.isIgnoreMultiBoundaries(t) || e.getZoneMonitor().isIgnoreMultiBoundaries(t)) { return true; } + Entity attacker = e instanceof Familiar ? ((Familiar) e).getOwner() : e; Entity target = t.getAttribute("combat-attacker", e); - if (t.getAttribute("combat-time", -1L) > time && target != e && target.isActive()) { + if (target instanceof Familiar) { + target = ((Familiar) target).getOwner(); + } + if (t.getAttribute("combat-time", -1L) > time && target != attacker && target.isActive()) { if (message && e instanceof Player) { ((Player) e).getPacketDispatch().sendMessage("Someone else is already fighting this" + (t instanceof Player ? " player." : ".")); } return false; } - if (e.getAttribute("combat-time", -1L) > time && (target = e.getAttribute("combat-attacker", t)) != t && target.isActive()) { + Entity attackerOnE = e.getAttribute("combat-attacker", t); + if (attackerOnE instanceof Familiar) { + attackerOnE = ((Familiar) attackerOnE).getOwner(); + } + Entity tResolved = t instanceof Familiar ? ((Familiar) t).getOwner() : t; + if (e.getAttribute("combat-time", -1L) > time && attackerOnE != tResolved && attackerOnE.isActive()) { if (t.getId() == 1614 || t.getId() == 1613) { return true; } diff --git a/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java b/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java index 623f2f080..cce6ab1d4 100644 --- a/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java +++ b/Server/src/main/core/game/world/map/zone/impl/WildernessZone.java @@ -263,6 +263,23 @@ public final class WildernessZone extends MapZone { return false; } } + if (e instanceof Player && target instanceof NPC && ((NPC) target).getName().contains("Revenant")) { + Player p = (Player) e; + NPC npc = (NPC) target; + int level = p.getSkullManager().getLevel(); + int npcWildLevel = getWilderness(npc); + if (npcWildLevel < level) { + level = npcWildLevel; + } + int combat = p.getProperties().getCurrentCombatLevel(); + int npcCombat = npc.getDefinition().getCombatLevel(); + if (combat - level > npcCombat || combat + level < npcCombat) { + if (message) { + p.getPacketDispatch().sendMessage("The level difference between you and your opponent is too great."); + } + return false; + } + } return true; }