From 6627efd7809ee818ae971afa041406a57a371f93 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Thu, 2 Jul 2026 05:32:57 +0300 Subject: [PATCH] Melee clipping fix --- .../entity/combat/CombatMovementIntents.kt | 23 +- .../game/node/entity/combat/CombatReach.kt | 24 ++ .../node/entity/combat/CombatSwingHandler.kt | 194 ++++++++---- .../node/entity/combat/MeleeSwingHandler.kt | 293 ++++++++++++------ .../kotlin/content/CombatMovementTests.kt | 173 ++++++++++- 5 files changed, 544 insertions(+), 163 deletions(-) diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt index 98914d7d4..cf9f076d6 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -565,6 +565,9 @@ object CombatMovementIntents { val attackable = candidates.filter { canAttackFrom(attacker, target, it, targetLocation, trace) } + if (attackable.isEmpty() && !CombatMovementPlanner.hasMovementStepThisTick(target)) { + return emptyList() + } return attackable .ifEmpty { candidates } .sortedWith( @@ -728,13 +731,20 @@ object CombatMovementIntents { ) { return false } - return hasProjectileLineOfSight( + if (CombatReach.isUsingHalberd(attacker)) { + return hasProjectileLineOfSight( + attackerLocation, + attacker.size(), + target, + targetLocation, + trace = trace, + ) + } + return CombatReach.hasMeleeReach( attackerLocation, attacker.size(), - target, targetLocation, - checkClose = !CombatReach.isUsingHalberd(attacker), - trace = trace, + target.size(), ) } @@ -937,6 +947,11 @@ object CombatMovementIntents { return movingTargetPath } for (directDestination in preferredMeleeDestinations(attacker, target, targetLocation)) { + // Walking to a side the target can't be attacked from (e.g. behind a fence) would + // tug the attacker back and forth against the candidate routing below. + if (!canAttackFrom(attacker, target, directDestination, targetLocation, trace)) { + continue + } val directPath = directPathTo(attacker, directDestination) if (directPath != null) { trace.directPathHits++ diff --git a/Server/src/main/core/game/node/entity/combat/CombatReach.kt b/Server/src/main/core/game/node/entity/combat/CombatReach.kt index 65891b95d..f384b2c9f 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatReach.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatReach.kt @@ -9,6 +9,7 @@ import core.game.world.map.Location import core.game.world.map.RegionManager.getClippingFlag import core.game.world.map.path.Pathfinder import core.game.world.map.path.Pathfinder.* +import core.game.world.map.path.RsmodPathfinder /** Shared combat reach calculations. */ object CombatReach { @@ -69,6 +70,29 @@ object CombatReach { ) } + @JvmStatic + fun hasMeleeReach( + attackerLocation: Location, + attackerSize: Int, + targetLocation: Location, + targetSize: Int, + ): Boolean { + return RsmodPathfinder.canReach( + attackerLocation.x, + attackerLocation.y, + attackerSize, + targetLocation.x, + targetLocation.y, + targetSize, + targetSize, + 0, + -1, + 0, + attackerLocation.z, + null, + ) + } + private fun occupiedAreasOverlap(first: Entity, second: Entity): Boolean { return Pathfinder.isStandingIn( first.location.x, diff --git a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt index 58790729a..bc09dd687 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatSwingHandler.kt @@ -1,46 +1,47 @@ package core.game.node.entity.combat +import content.global.skill.summoning.familiar.Familiar +import core.api.log +import core.api.playGlobalAudio import core.game.component.Component import core.game.container.impl.EquipmentContainer import core.game.node.Node import core.game.node.entity.Entity -import core.game.node.entity.combat.equipment.* +import core.game.node.entity.combat.equipment.ArmourSet +import core.game.node.entity.combat.equipment.DegradableEquipment +import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.node.entity.player.link.audio.Audio import core.game.node.entity.player.link.prayer.PrayerType import core.game.node.entity.skill.Skills -import content.global.skill.summoning.familiar.Familiar -import core.api.log -import core.api.playGlobalAudio +import core.game.system.config.ItemConfigParser import core.game.world.map.path.RsmodPathfinder import core.game.world.update.flag.context.Animation -import core.tools.RandomFunction -import core.game.system.config.ItemConfigParser import core.tools.Log +import core.tools.RandomFunction import org.rs09.consts.Sounds -import java.util.* -import kotlin.math.floor /** * Handles a combat swing. + * * @author Emperor * @author Ceikry - Kotlin refactoring, general cleanup * @author Player Name - converted `flags` to ArrayList */ abstract class CombatSwingHandler(var type: CombatStyle?) { var flags: ArrayList = ArrayList(SwingHandlerFlag.values().size) + constructor(type: CombatStyle?, vararg flags: SwingHandlerFlag) : this(type) { this.flags = arrayListOf(*flags) } - /** - * The mapping of the special attack handlers. - */ + /** The mapping of the special attack handlers. */ private var specialHandlers: MutableMap? = null /** * Starts the combat swing. + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state instance. @@ -50,6 +51,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Handles the impact of the combat swing (victim getting hit). + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state instance. @@ -58,6 +60,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Visualizes the impact itself (end animation, end GFX, ...) + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state instance. @@ -66,6 +69,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Calculates the maximum accuracy of the entity. + * * @param entity The entity. * @return The maximum accuracy value. */ @@ -73,6 +77,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Calculates the maximum strength of the entity. + * * @param entity The entity. * @param victim The victim. * @param modifier The modifier. @@ -82,6 +87,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Calculates the maximum defence of the entity. + * * @param victim The entity. * @param attacker The entity to defend against. * @return The maximum defence value. @@ -90,6 +96,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the void set multiplier. + * * @param e The entity. * @param skillId The skill id. * @return The multiplier. @@ -98,6 +105,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Visualizes the combat swing (start animation, GFX, projectile, ...) + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state instance. @@ -108,6 +116,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Method called when the impact method got called. + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state. @@ -134,6 +143,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the currently worn armour set, if any. + * * @param e The entity. * @return The armour set worn. */ @@ -143,6 +153,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Checks if the hit will be accurate. + * * @param entity The entity. * @param victim The victim. * @return `True` if the hit is accurate. @@ -153,6 +164,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Checks if the hit will be accurate. + * * @param entity The entity. * @param victim The victim. * @param style The combat style used. @@ -164,6 +176,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Checks if the hit will be accurate. + * * @param entity The entity. * @param victim The victim. * @param style The combat style (null to ignore prayers). @@ -171,26 +184,38 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { * @param defenceMod The defence modifier. * @return `True` if the hit is accurate. */ - fun isAccurateImpact(entity: Entity?, victim: Entity?, style: CombatStyle?, accuracyMod: Double, defenceMod: Double): Boolean { + fun isAccurateImpact( + entity: Entity?, + victim: Entity?, + style: CombatStyle?, + accuracyMod: Double, + defenceMod: Double, + ): Boolean { var mod = 1.0 if (victim == null || style == null) { return false } - if (victim is Player && entity is Familiar && victim.prayer[PrayerType.PROTECT_FROM_SUMMONING]) { + if ( + victim is Player && + entity is Familiar && + victim.prayer[PrayerType.PROTECT_FROM_SUMMONING] + ) { mod = 0.0 } val attack = calculateAccuracy(entity) * accuracyMod * mod val defence = calculateDefence(victim, entity) * defenceMod - val chance: Double = if (attack > defence) { - 1 - ((defence + 2) / (2 * (attack + 1))) - } else { - attack / (2 * (defence + 1)) - } + val chance: Double = + if (attack > defence) { + 1 - ((defence + 2) / (2 * (attack + 1))) + } else { + attack / (2 * (defence + 1)) + } return Math.random() < chance } /** * Checks if the entity can execute a combat swing at the victim. + * * @param entity The entity. * @param victim The victim. * @return `True` if so. @@ -201,6 +226,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Checks if the victim can be attacked by the entity. + * * @param entity The attacking entity. * @param victim The entity being attacked. * @return `True` if so. @@ -210,15 +236,20 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { return InteractionType.NO_INTERACT } - if (type == CombatStyle.MELEE) { + if (type == CombatStyle.MELEE && !CombatReach.isUsingHalberd(entity)) { val stepType = canStepTowards(entity, victim) if (stepType != InteractionType.STILL_INTERACT) return stepType } - val comp = entity.getAttribute("autocast_component",null) as Component? - if((comp != null || type == CombatStyle.MAGIC) && (entity.properties.autocastSpell == null || entity.properties.autocastSpell.spellId == 0) && entity is Player){ + val comp = entity.getAttribute("autocast_component", null) as Component? + if ( + (comp != null || type == CombatStyle.MAGIC) && + (entity.properties.autocastSpell == null || + entity.properties.autocastSpell.spellId == 0) && + entity is Player + ) { val weapEx = entity.getExtension(WeaponInterface::class.java) as WeaponInterface? - if(comp != null){ + if (comp != null) { entity.interfaceManager.close(comp) entity.interfaceManager.openTab(weapEx) entity.properties.combatPulse.stop() @@ -229,7 +260,12 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { entity.debug("Adjusting attack style") } if (entity.location == victim.location) { - return if (entity is Player && victim is Player && entity.clientIndex < victim.clientIndex && victim.properties.combatPulse.getVictim() === entity) { + return if ( + entity is Player && + victim is Player && + entity.clientIndex < victim.clientIndex && + victim.properties.combatPulse.getVictim() === entity + ) { InteractionType.STILL_INTERACT } else InteractionType.NO_INTERACT } @@ -248,6 +284,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the dragonfire message. + * * @param protection The protection value. * @param fireName The fire breath name. * @return The message to send. @@ -270,6 +307,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Visualizes the audio. + * * @param entity the entity. * @param victim the victim. * @param state the state. @@ -279,7 +317,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { val styleIndex = entity.settings.attackStyleIndex if (state.weapon != null && state.weapon.item != null) { val weapon = state.weapon.item - val audios = weapon.definition.getConfiguration>(ItemConfigParser.ATTACK_AUDIO, null) + val audios = + weapon.definition.getConfiguration>( + ItemConfigParser.ATTACK_AUDIO, + null, + ) if (audios != null) { var audio: Audio? = null if (styleIndex < audios.size) { @@ -291,7 +333,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { playGlobalAudio(entity.location, audio.id) } } else if (type == CombatStyle.MELEE) { - //plays a punching sound when no weapon is equipped + // plays a punching sound when no weapon is equipped playGlobalAudio(entity.location, Sounds.HUMAN_ATTACK_2564) } } else if (entity is NPC) { @@ -303,6 +345,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the combat distance. + * * @param e The entity. * @param v The victim. * @param rawDistance The distance. @@ -313,8 +356,9 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } /** - * Formats the hit for the victim. (called as - * victim.getSwingHandler(false).formatHit(victim, hit)) + * Formats the hit for the victim. (called as victim.getSwingHandler(false).formatHit(victim, + * hit)) + * * @param victim The entity receiving the hit. * @param rawHit The hit to format. * @return The formatted hit. @@ -332,6 +376,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Adjusts the battle state object for this combat swing. + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state. @@ -344,8 +389,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } entity.sendImpact(state) victim.checkImpact(state) - //Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it) - if (victim.id == 4474 && type == CombatStyle.MAGIC || victim.id == 7891 && type == CombatStyle.MELEE) { + // Prevents lumbridge dummies from dying (true to how rs3 / 2009scape in 2009 does it) + if ( + victim.id == 4474 && type == CombatStyle.MAGIC || + victim.id == 7891 && type == CombatStyle.MELEE + ) { EXPERIENCE_MOD = 0.1 victim.fullRestore() if (state.estimatedHit >= 15) { @@ -358,7 +406,8 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { if (victim.id == 757) { EXPERIENCE_MOD = 0.01 } - // Recursively adjustBattleState targets so that multi-target attacks have protection prayers applied. + // Recursively adjustBattleState targets so that multi-target attacks have protection + // prayers applied. if (state.targets != null && state.targets.isNotEmpty()) { if (!(state.targets.size == 1 && state.targets[0] == state)) { for (s in state.targets) { @@ -386,7 +435,10 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } } if (victim is NPC) { - if (victim.properties.protectStyle != null && state.style == victim.properties.protectStyle) { + if ( + victim.properties.protectStyle != null && + state.style == victim.properties.protectStyle + ) { state.neutralizeHits() } } @@ -394,21 +446,31 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Adds the experience for the current combat swing. + * * @param entity The attacking entity. * @param victim The victim. * @param state The battle state. */ open fun addExperience(entity: Entity?, victim: Entity?, state: BattleState?) { - if (entity == null || (victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman)) { + if ( + entity == null || + (victim is Player && entity is Player && entity.asPlayer().ironmanManager.isIronman) + ) { return } var player: Player var attStyle: Int - when(entity) - { - is Familiar -> {player = entity.owner; attStyle = entity.attackStyle} - is Player -> {player = entity; attStyle = entity.properties.attackStyle.style} + when (entity) { + is Familiar -> { + player = entity.owner + attStyle = entity.attackStyle + } + + is Player -> { + player = entity + attStyle = entity.properties.attackStyle.style + } else -> return } if (victim is NPC) EXPERIENCE_MOD *= victim.behavior.getXpMultiplier(victim, player) @@ -457,20 +519,26 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } /** - * Hook for operations that conceptually happen during swing but could mess with experience granting logic if they - * happened earlier. + * Hook for operations that conceptually happen during swing but could mess with experience + * granting logic if they happened earlier. */ open fun postSwing(entity: Entity?, victim: Entity?, state: BattleState?) {} /** * Gets the formated hit. + * * @param attacker The attacking entity. * @param victim The victim. * @param state The battle state. * @param rawHit The hit to format. * @return The formated hit. */ - protected open fun getFormattedHit(attacker: Entity, victim: Entity, state: BattleState, rawHit: Int): Int { + protected open fun getFormattedHit( + attacker: Entity, + victim: Entity, + state: BattleState, + rawHit: Int, + ): Int { var hit = rawHit hit = attacker.getFormattedHit(state, hit).toInt() if (victim is Player) { @@ -501,7 +569,11 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } if (attacker is Player) { val player = attacker.asPlayer() - if (player.equipment[3] != null && player.equipment[3].id == 14726 && state.style == CombatStyle.MAGIC) { + if ( + player.equipment[3] != null && + player.equipment[3].id == 14726 && + state.style == CombatStyle.MAGIC + ) { hit += (hit.toDouble() * 0.15).toInt() } } @@ -515,6 +587,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the default animation of the entity. + * * @param e The entity. * @param style The combat style. * @return The attack animation. @@ -529,6 +602,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Registers a special attack handler. + * * @param itemId The item id. * @param handler The combat swing handler. * @return `True` if succesful. @@ -538,7 +612,17 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { specialHandlers = HashMap() } if (specialHandlers!!.containsKey(itemId)) { - log(this::class.java, Log.ERR, "Already contained special attack handler for item " + itemId + " - [old=" + specialHandlers!![itemId]!!::class.java.simpleName + ", new=" + handler.javaClass.simpleName + "].") + log( + this::class.java, + Log.ERR, + "Already contained special attack handler for item " + + itemId + + " - [old=" + + specialHandlers!![itemId]!!::class.java.simpleName + + ", new=" + + handler.javaClass.simpleName + + "].", + ) return false } return specialHandlers!!.put(itemId, handler) == null @@ -546,9 +630,9 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { /** * Gets the special attack handler for the given item id. + * * @param itemId The item id. - * @return The special attack handler, or `null` if this item has no - * special attack handler. + * @return The special attack handler, or `null` if this item has no special attack handler. */ fun getSpecial(itemId: Int): CombatSwingHandler? { if (specialHandlers == null) { @@ -558,34 +642,30 @@ abstract class CombatSwingHandler(var type: CombatStyle?) { } companion object { - /** - * The amount of experience to get per hit. - */ - @JvmField - var EXPERIENCE_MOD = 4.0 + /** The amount of experience to get per hit. */ + @JvmField + var EXPERIENCE_MOD = 4.0 /** - * Checks if a projectile can be fired from the node location to the victim - * location. + * Checks if a projectile can be fired from the node location to the victim location. + * * @param entity The node. * @param victim The victim. - * @param checkClose If we are checking for a melee attack rather than a - * projectile. + * @param checkClose If we are checking for a melee attack rather than a projectile. * @return `True` if so. */ - @JvmStatic - fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean { + @JvmStatic + fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean { val target = requireNotNull(victim) return RsmodPathfinder.hasLineOfSightBetween( entity.location, entity.size(), target.location, target.size(), - maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE + maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE, ) } } - } enum class SwingHandlerFlag { @@ -593,5 +673,5 @@ enum class SwingHandlerFlag { IGNORE_STAT_BOOSTS_ACCURACY, IGNORE_PRAYER_BOOSTS_DAMAGE, IGNORE_PRAYER_BOOSTS_ACCURACY, - IGNORE_STAT_REDUCTION + IGNORE_STAT_REDUCTION, } diff --git a/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt b/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt index b0455a28b..8bde6e2dc 100644 --- a/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt +++ b/Server/src/main/core/game/node/entity/combat/MeleeSwingHandler.kt @@ -2,11 +2,9 @@ package core.game.node.entity.combat import content.global.skill.skillcapeperks.SkillcapePerks import content.global.skill.slayer.SlayerEquipmentFlags -import content.global.skill.slayer.SlayerManager import content.global.skill.slayer.Tasks import content.global.skill.summoning.SummoningPouch import core.api.* -import core.api.EquipmentSlot import core.game.node.entity.Entity import core.game.node.entity.combat.equipment.ArmourSet import core.game.node.entity.combat.equipment.Weapon @@ -14,87 +12,129 @@ import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.node.entity.skill.Skills -import core.tools.RandomFunction import core.game.world.map.Direction import core.game.world.map.Location import core.game.world.map.RegionManager -import core.game.world.map.path.RsmodPathfinder +import core.tools.RandomFunction import org.rs09.consts.Items import kotlin.math.floor /** * Handles a melee combat swing. + * * @author Emperor * @author Ceikry, Kotlin conversion + cleanup */ -open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) -/** - * Constructs a new `MeleeSwingHandler` {@Code Object}. - */ +open class MeleeSwingHandler(vararg flags: SwingHandlerFlag) +/** Constructs a new `MeleeSwingHandler` {@Code Object}. */ : CombatSwingHandler(CombatStyle.MELEE, *flags) { - override fun canSwing(entity : Entity, victim : Entity) : InteractionType? { - //Credits wolfenzi, https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html - var distance = CombatReach.meleeDistance(entity) - var type = InteractionType.STILL_INTERACT - var goodRange = CombatReach.canMelee(entity, victim, distance) - if (!goodRange && victim.properties.combatPulse.getVictim() !== entity && victim.walkingQueue.isMoving && entity.size() == 1) { - type = InteractionType.MOVE_INTERACT - distance += if (entity.walkingQueue.isRunningBoth) 2 else 1 - goodRange = CombatReach.canMelee(entity, victim, distance) - } - if (!hasMeleeLineOfSight(entity, victim, type)) { - return InteractionType.NO_INTERACT - } - val isRunning = entity.walkingQueue.runDir != -1 - val enemyRunning = victim.walkingQueue.runDir != -1 - // THX 4 fix tom <333. - if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) { - val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2 - if (entity.walkingQueue.isMoving && entity.location.getDistance(victim.location) <= maxDistance && goodRange) { - return type - } else if (goodRange) { - if (canStepTowards(entity, victim) == InteractionType.NO_INTERACT) return InteractionType.NO_INTERACT - if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset() - return type - } - } - return InteractionType.NO_INTERACT - } + override fun canSwing(entity: Entity, victim: Entity): InteractionType? { + // Credits wolfenzi, + // https://www.rune-server.ee/2009scape-development/rs2-server/snippets/608720-arios-hybridding-improve.html + var distance = CombatReach.meleeDistance(entity) + var type = InteractionType.STILL_INTERACT + var goodRange = CombatReach.canMelee(entity, victim, distance) + if ( + !goodRange && + victim.properties.combatPulse.getVictim() !== entity && + victim.walkingQueue.isMoving && + entity.size() == 1 + ) { + type = InteractionType.MOVE_INTERACT + distance += if (entity.walkingQueue.isRunningBoth) 2 else 1 + goodRange = CombatReach.canMelee(entity, victim, distance) + } + if (!hasMeleeLineOfSight(entity, victim, type)) { + return InteractionType.NO_INTERACT + } + val isRunning = entity.walkingQueue.runDir != -1 + val enemyRunning = victim.walkingQueue.runDir != -1 + // THX 4 fix tom <333. + if (super.canSwing(entity, victim) != InteractionType.NO_INTERACT) { + val maxDistance = if (isRunning) if (enemyRunning) 3 else 4 else 2 + if ( + entity.walkingQueue.isMoving && + entity.location.getDistance(victim.location) <= maxDistance && + goodRange + ) { + return type + } else if (goodRange) { + if ( + !CombatReach.isUsingHalberd(entity) && + canStepTowards(entity, victim) == InteractionType.NO_INTERACT + ) + return InteractionType.NO_INTERACT + if (type == InteractionType.STILL_INTERACT) entity.walkingQueue.reset() + return type + } + } + return InteractionType.NO_INTERACT + } - private fun hasMeleeLineOfSight(entity: Entity, victim: Entity, type: InteractionType): Boolean { - val checkClose = !CombatReach.isUsingHalberd(entity) - if (isProjectileClipped(entity, victim, checkClose)) { + private fun hasMeleeLineOfSight( + entity: Entity, + victim: Entity, + type: InteractionType, + ): Boolean { + if (CombatReach.isUsingHalberd(entity)) { + return isProjectileClipped(entity, victim, false) + } + if ( + CombatReach.hasMeleeReach( + entity.location, + entity.size(), + victim.location, + victim.size(), + ) + ) { return true } - if (type != InteractionType.MOVE_INTERACT || !checkClose) { + if (type != InteractionType.MOVE_INTERACT) { return false } - val predictedVictimLocation = CombatMovementPlanner.predictedMovementLocation(victim) ?: return false + val predictedVictimLocation = + CombatMovementPlanner.predictedMovementLocation(victim) ?: return false val projectedEntityLocation = projectedMeleeChaseLocation(entity, victim, predictedVictimLocation) ?: return false if (!isAdjacentTo(entity, projectedEntityLocation, victim, predictedVictimLocation)) { return false } - return RsmodPathfinder.hasLineOfSightBetween( + return CombatReach.hasMeleeReach( projectedEntityLocation, entity.size(), predictedVictimLocation, victim.size(), - maxRaySteps = 1 ) } - private fun projectedMeleeChaseLocation(entity: Entity, victim: Entity, victimLocation: Location): Location? { + private fun projectedMeleeChaseLocation( + entity: Entity, + victim: Entity, + victimLocation: Location, + ): Location? { if (entity.size() != 1 || entity.location.z != victimLocation.z) { return null } val maxSteps = - if (entity is Player && entity.walkingQueue.isRunningBoth && entity.settings.runEnergy >= 1.0) 2 else 1 + if ( + entity is Player && + entity.walkingQueue.isRunningBoth && + entity.settings.runEnergy >= 1.0 + ) + 2 + else 1 var current = entity.location var steps = 0 while (steps < maxSteps && !isAdjacentTo(entity, current, victim, victimLocation)) { val direction = Direction.getDirection(current, victimLocation) ?: return null - if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { + if ( + !direction.canMoveFrom( + current.z, + current.x, + current.y, + RegionManager::getClippingFlag, + ) + ) { return null } val next = current.transform(direction) @@ -111,7 +151,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) entity: Entity, entityLocation: Location, victim: Entity, - victimLocation: Location + victimLocation: Location, ): Boolean { val entityMinX = entityLocation.x val entityMaxX = entityLocation.x + entity.size() @@ -141,18 +181,29 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) if (entity!!.properties.armourSet == ArmourSet.VERAC && RandomFunction.roll(4)) { state.armourEffect = ArmourSet.VERAC } - if (state.armourEffect == ArmourSet.VERAC || isAccurateImpact(entity, victim, CombatStyle.MELEE)) { + if ( + state.armourEffect == ArmourSet.VERAC || + isAccurateImpact(entity, victim, CombatStyle.MELEE) + ) { var max = calculateHit(entity, victim, 1.0) if (victim != null) { - if (entity is NPC && state.armourEffect == ArmourSet.VERAC && victim.hasProtectionPrayer(CombatStyle.MELEE)) max = max * 2 / 3 + if ( + entity is NPC && + state.armourEffect == ArmourSet.VERAC && + victim.hasProtectionPrayer(CombatStyle.MELEE) + ) + max = max * 2 / 3 } state.maximumHit = max hit = RandomFunction.random(max + 1) } state.estimatedHit = hit - if(victim != null) { - if (state.estimatedHit > victim.skills.lifepoints) state.estimatedHit = victim.skills.lifepoints - if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints) state.secondaryHit -= ((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints) + if (victim != null) { + if (state.estimatedHit > victim.skills.lifepoints) + state.estimatedHit = victim.skills.lifepoints + if (state.estimatedHit + state.secondaryHit > victim.skills.lifepoints) + state.secondaryHit -= + ((state.estimatedHit + state.secondaryHit) - victim.skills.lifepoints) } return 1 } @@ -195,7 +246,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) damage = 20 } if (damage > -1 && RandomFunction.random(10) < 4) { - applyPoison (victim, entity, damage) + applyPoison(victim, entity, damage) } } } else if (entity is NPC) { @@ -203,26 +254,36 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) val damage = entity.poisonSeverity() if (poisonous && damage > -1 && RandomFunction.random(10) < 4) { - applyPoison (victim, entity, damage) + applyPoison(victim, entity, damage) } } super.adjustBattleState(entity, victim, state) } override fun calculateAccuracy(entity: Entity?): Int { - //formula taken from wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae. + // formula taken from wiki: + // https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_six:_Calculate_the_hit_chance Yes I know it's old school. It's the best resource we have for potentially authentic formulae. entity ?: return 0 - val styleAttackBonus = entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64 + val styleAttackBonus = + entity.properties.bonuses[entity.properties.attackStyle.bonusType] + 64 when (entity) { is Player -> { var effectiveAttackLevel = entity.skills.getLevel(Skills.ATTACK).toDouble() - if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY)) - effectiveAttackLevel = floor(effectiveAttackLevel + (entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel)) - if(entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE) effectiveAttackLevel += 3 - else if(entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveAttackLevel += 1 + if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_ACCURACY)) + effectiveAttackLevel = + floor( + effectiveAttackLevel + + (entity.prayer.getSkillBonus(Skills.ATTACK) * effectiveAttackLevel) + ) + if (entity.properties.attackStyle.style == WeaponInterface.STYLE_ACCURATE) + effectiveAttackLevel += 3 + else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) + effectiveAttackLevel += 1 effectiveAttackLevel += 8 - if(SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity)){ //Attack skillcape perk + if ( + SkillcapePerks.isActive(SkillcapePerks.PRECISION_STRIKES, entity) + ) { // Attack skillcape perk effectiveAttackLevel += 6 } effectiveAttackLevel *= getSetMultiplier(entity, Skills.ATTACK) @@ -235,14 +296,27 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) // attack bonus for specialized equipments (salve amulets, slayer equips) val amuletId = getItemFromEquipment(entity, EquipmentSlot.NECK)?.id ?: 0 - if ((amuletId == Items.SALVE_AMULET_4081 || amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName)) { + if ( + (amuletId == Items.SALVE_AMULET_4081 || + amuletId == Items.SALVE_AMULETE_10588) && checkUndead(victimName) + ) { effectiveAttackLevel *= if (amuletId == Items.SALVE_AMULET_4081) 1.15 else 1.2 - } else if (getSlayerTask(entity)?.let { task -> + } else if ( + getSlayerTask(entity)?.let { task -> val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0 - task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1 - } == true) { - effectiveAttackLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape - if (getSlayerTask(entity)?.dragon == true && inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862)) + task.ids.contains(victimId) || + (task == Tasks.KALPHITES && + (victimId == 1158)) // Kalphite Queen phase 1 + } == true + ) { + effectiveAttackLevel *= + SlayerEquipmentFlags.getDamAccBonus( + entity + ) // Slayer Helm/ Black Mask/ Slayer cape + if ( + getSlayerTask(entity)?.dragon == true && + inEquipment(entity, Items.DRAGON_SLAYER_GLOVES_12862) + ) effectiveAttackLevel *= 1.1 } @@ -255,8 +329,6 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) } return 0 - - } override fun calculateHit(entity: Entity?, victim: Entity?, modifier: Double): Int { @@ -266,28 +338,43 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) when (entity) { is Player -> { var effectiveStrengthLevel = entity.skills.getLevel(Skills.STRENGTH).toDouble() - if(!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE)) - effectiveStrengthLevel = floor(effectiveStrengthLevel + (entity.prayer.getSkillBonus(Skills.STRENGTH) * effectiveStrengthLevel)) - if(entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE) effectiveStrengthLevel += 3 - else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveStrengthLevel += 1 + if (!flags.contains(SwingHandlerFlag.IGNORE_PRAYER_BOOSTS_DAMAGE)) + effectiveStrengthLevel = + floor( + effectiveStrengthLevel + + (entity.prayer.getSkillBonus(Skills.STRENGTH) * + effectiveStrengthLevel) + ) + if (entity.properties.attackStyle.style == WeaponInterface.STYLE_AGGRESSIVE) + effectiveStrengthLevel += 3 + else if (entity.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) + effectiveStrengthLevel += 1 effectiveStrengthLevel += 8 effectiveStrengthLevel *= getSetMultiplier(entity, Skills.STRENGTH) effectiveStrengthLevel = floor(effectiveStrengthLevel) if (!flags.contains(SwingHandlerFlag.IGNORE_STAT_BOOSTS_DAMAGE)) effectiveStrengthLevel *= styleStrengthBonus else effectiveStrengthLevel *= 64 - if (getSlayerTask(entity)?.let { task -> - val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0 - task.ids.contains(victimId) || (task == Tasks.KALPHITES && (victimId == 1158)) // Kalphite Queen phase 1 - } == true) { - effectiveStrengthLevel *= SlayerEquipmentFlags.getDamAccBonus(entity) //Slayer Helm/ Black Mask/ Slayer cape + if ( + getSlayerTask(entity)?.let { task -> + val victimId = entity.properties.combatPulse?.getVictim()?.id ?: 0 + task.ids.contains(victimId) || + (task == Tasks.KALPHITES && + (victimId == 1158)) // Kalphite Queen phase 1 + } == true + ) { + effectiveStrengthLevel *= + SlayerEquipmentFlags.getDamAccBonus( + entity + ) // Slayer Helm/ Black Mask/ Slayer cape } return (floor((0.5 + (effectiveStrengthLevel / 640.0))) * modifier).toInt() } is NPC -> { val strengthLevel = entity.skills.getLevel(Skills.STRENGTH) + 9 - return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier).toInt() + return (floor((0.5 + (strengthLevel * styleStrengthBonus / 640.0))) * modifier) + .toInt() } } @@ -295,17 +382,28 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) } override fun calculateDefence(victim: Entity?, attacker: Entity?): Int { - //authentic formula, taken from OSRS wiki: https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll + // authentic formula, taken from OSRS wiki: + // https://oldschool.runescape.wiki/w/Damage_per_second/Melee#Step_five:_Calculate_the_Defence_roll victim ?: return 0 attacker ?: return 0 - val styleDefenceBonus = victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64 + val styleDefenceBonus = + victim.properties.bonuses[attacker.properties.attackStyle.bonusType + 5] + 64 when (victim) { is Player -> { var effectiveDefenceLevel = victim.skills.getLevel(Skills.DEFENCE).toDouble() - effectiveDefenceLevel = floor(effectiveDefenceLevel + (victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel)) - if (victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE || victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE) effectiveDefenceLevel += 3 - else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) effectiveDefenceLevel += 1 + effectiveDefenceLevel = + floor( + effectiveDefenceLevel + + (victim.prayer.getSkillBonus(Skills.DEFENCE) * effectiveDefenceLevel) + ) + if ( + victim.properties.attackStyle.style == WeaponInterface.STYLE_DEFENSIVE || + victim.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE + ) + effectiveDefenceLevel += 3 + else if (victim.properties.attackStyle.style == WeaponInterface.STYLE_CONTROLLED) + effectiveDefenceLevel += 1 effectiveDefenceLevel += 8 effectiveDefenceLevel *= getSetMultiplier(victim, Skills.DEFENCE) effectiveDefenceLevel *= familiarDefenceBonus(victim) @@ -355,12 +453,23 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) /** * Check to see whether an NPC is classified as undead. + * * @param name * @return true if so */ private fun checkUndead(name: String): Boolean { - return (name == "Zombie" || name.contains("rmoured") || name == "Ankou" || name == "Crawling Hand" || name == "Banshee" || name == "Ghost" || name == "Ghast" || name == "Mummy" || name.contains("Revenant") - || name == "Skeleton" || name == "Zogre" || name == "Spiritual Mage") + return (name == "Zombie" || + name.contains("rmoured") || + name == "Ankou" || + name == "Crawling Hand" || + name == "Banshee" || + name == "Ghost" || + name == "Ghast" || + name == "Mummy" || + name.contains("Revenant") || + name == "Skeleton" || + name == "Zogre" || + name == "Spiritual Mage") } /** @@ -370,11 +479,12 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) */ private fun familiarDefenceBonus(e: Entity?): Double { if (e !is Player) return 1.0 - val fam = try { - e.familiarManager?.familiar - } catch (ex: Exception) { - null - } ?: return 1.0 + val fam = + try { + e.familiarManager?.familiar + } catch (ex: Exception) { + null + } ?: return 1.0 return when (fam.pouchId) { SummoningPouch.IRON_TITAN_POUCH.pouchId -> 1.10 SummoningPouch.STEEL_TITAN_POUCH.pouchId -> 1.15 @@ -385,6 +495,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag) companion object { /** * Checks if the entity can execute a melee swing from its current location. + * * @param entity The attacking entity. * @param victim The victim. * @return `True` if so. diff --git a/Server/src/test/kotlin/content/CombatMovementTests.kt b/Server/src/test/kotlin/content/CombatMovementTests.kt index f70350e9a..62038f421 100644 --- a/Server/src/test/kotlin/content/CombatMovementTests.kt +++ b/Server/src/test/kotlin/content/CombatMovementTests.kt @@ -362,6 +362,14 @@ class CombatMovementTests { false, Pathfinder.PREVENT_NORTH, ) + // Real map walls flag both edge tiles; melee reach reads the attacker-side tile. + RegionManager.addClippingFlag( + origin.z, + origin.x, + origin.y, + false, + CollisionFlag.WALL_NORTH, + ) RegionManager.addClippingFlag( predictedVictim.z, predictedVictim.x, @@ -410,6 +418,13 @@ class CombatMovementTests { false, Pathfinder.PREVENT_NORTH, ) + RegionManager.removeClippingFlag( + origin.z, + origin.x, + origin.y, + false, + CollisionFlag.WALL_NORTH, + ) RegionManager.removeClippingFlag( predictedVictim.z, predictedVictim.x, @@ -423,6 +438,109 @@ class CombatMovementTests { } } + @Test + fun meleeAttackerShouldPathAroundFenceToReachAdjacentTarget() { + TestUtils.getMockPlayer("combat_fence_walkaround_attacker").use { player -> + val origin = arenaOrigin() + val npcLocation = origin.transform(0, 1, 0) + place(player, origin) + configureMelee(player) + disableRun(player) + + val npc = stationaryNpc(100, npcLocation) + try { + configureMelee(npc) + // A fence blocks movement on the shared edge but not projectiles. + setFence(origin, npcLocation, add = true) + + TestUtils.advanceTicks(1, true) + CombatMovementIntents.clear() + player.playerFlags.lastSceneGraph = origin + player.playerFlags.setUpdateSceneGraph(false) + player.attack(npc) + TestUtils.advanceTicks(5, false) + + assertNotEquals( + origin, + player.location, + "A melee attacker blocked by a fence must walk around it instead of standing still. " + + "player=${player.location}, npc=$npcLocation, " + + CombatMovementIntents.lastResolveSummary(), + ) + assertTrue( + CombatReach.hasMeleeReach( + player.location, + player.size(), + npc.location, + npc.size(), + ), + "The attacker should end on a side tile with melee reach. " + + "player=${player.location}, npc=${npc.location}", + ) + assertTrue(player.properties.combatPulse.isAttacking) + assertFalse(receivedMessage(player, "I can't reach that!")) + } finally { + setFence(origin, npcLocation, add = false) + npc.clear() + CombatMovementIntents.clear() + } + } + } + + @Test + fun meleeAttackerShouldReportUnreachableWhenTargetIsFencedInOnAllSides() { + TestUtils.getMockPlayer("combat_fence_enclosed_attacker").use { player -> + val origin = arenaOrigin() + val npcLocation = origin.transform(0, 1, 0) + val fencedNeighbours = + listOf( + npcLocation.transform(0, 1, 0), + npcLocation.transform(0, -1, 0), + npcLocation.transform(1, 0, 0), + npcLocation.transform(-1, 0, 0), + ) + place(player, origin) + configureMelee(player) + disableRun(player) + + val npc = stationaryNpc(100, npcLocation) + try { + configureMelee(npc) + for (neighbour in fencedNeighbours) { + setFence(npcLocation, neighbour, add = true) + } + + TestUtils.advanceTicks(1, true) + CombatMovementIntents.clear() + player.playerFlags.lastSceneGraph = origin + player.playerFlags.setUpdateSceneGraph(false) + player.attack(npc) + TestUtils.advanceTicks(2, false) + + assertTrue( + receivedMessage(player, "I can't reach that!"), + "A target fenced in on every side must report unreachable. " + + "player=${player.location}, " + CombatMovementIntents.lastResolveSummary(), + ) + assertFalse( + player.properties.combatPulse.isAttacking, + "Combat must stop against a fully fenced-in target.", + ) + assertEquals( + origin, + player.location, + "The attacker must not wander around an unreachable fenced-in target.", + ) + } finally { + for (neighbour in fencedNeighbours) { + setFence(npcLocation, neighbour, add = false) + } + npc.clear() + CombatMovementIntents.clear() + } + } + } + @Test fun overlappingMeleePlayerShouldStepToOpenAttackTileInsteadOfStopping() { TestUtils.getMockPlayer("combat_overlap_player_escape").use { player -> @@ -630,9 +748,12 @@ class CombatMovementTests { val npc = NPC.create(100, npcLocation) npc.init() + val westTile = npcLocation.transform(-1, 0, 0) + val farSideTile = npcLocation.transform(1, 0, 0) try { configureMelee(npc) blockMovementTiles(blockedTiles) + setFence(westTile, npcLocation, add = true) RegionManager.addClippingFlag( npcLocation.z, npcLocation.x, @@ -643,19 +764,23 @@ class CombatMovementTests { player.attack(npc) CombatMovementIntents.clear() - CombatMovementIntents.request(player, npc) - CombatMovementIntents.resolve() - player.walkingQueue.update() + repeat(5) { + CombatMovementIntents.request(player, npc) + CombatMovementIntents.resolve() + player.walkingQueue.update() + assertNotEquals( + npcLocation, + player.location, + "The player must not run onto the NPC footprint. " + + "player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}", + ) + } - assertTrue( - player.location.x < npcLocation.x, - "Melee movement must stop before crossing the NPC footprint when the far-side tile is chosen. " + - "player=${player.location}, npc=$npcLocation, ${CombatMovementIntents.lastResolveSummary()}", - ) - assertNotEquals( - npcLocation, + assertEquals( + farSideTile, player.location, - "The player must not run onto the NPC footprint.", + "A walled-off near side must route the player to the far-side attack tile. " + + "player=${player.location}, npc=$npcLocation, ${CombatMovementIntents.lastResolveSummary()}", ) } finally { RegionManager.removeClippingFlag( @@ -665,6 +790,7 @@ class CombatMovementTests { true, CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER, ) + setFence(westTile, npcLocation, add = false) unblockMovementTiles(blockedTiles) npc.clear() CombatMovementIntents.clear() @@ -1763,6 +1889,31 @@ class CombatMovementTests { } } + /** + * Adds or removes a fence on the shared edge of two cardinally adjacent tiles: movement + * wall flags mirrored onto both tiles (like real map walls), no projectile flags. + */ + private fun setFence(first: Location, second: Location, add: Boolean) { + val flags = + when (second) { + first.transform(0, 1, 0) -> CollisionFlag.WALL_NORTH to CollisionFlag.WALL_SOUTH + first.transform(0, -1, 0) -> CollisionFlag.WALL_SOUTH to CollisionFlag.WALL_NORTH + first.transform(1, 0, 0) -> CollisionFlag.WALL_EAST to CollisionFlag.WALL_WEST + first.transform(-1, 0, 0) -> CollisionFlag.WALL_WEST to CollisionFlag.WALL_EAST + else -> + throw IllegalArgumentException( + "Fence tiles must be cardinally adjacent: $first, $second" + ) + } + if (add) { + RegionManager.addClippingFlag(first.z, first.x, first.y, false, flags.first) + RegionManager.addClippingFlag(second.z, second.x, second.y, false, flags.second) + } else { + RegionManager.removeClippingFlag(first.z, first.x, first.y, false, flags.first) + RegionManager.removeClippingFlag(second.z, second.x, second.y, false, flags.second) + } + } + private fun blockMovementTiles(tiles: List) { for (tile in tiles) { RegionManager.addClippingFlag(tile.z, tile.x, tile.y, false, movementBlockFlag)