Melee clipping fix

This commit is contained in:
dam 2026-07-02 05:32:57 +03:00
parent 62f60abc9a
commit 6627efd780
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
5 changed files with 544 additions and 163 deletions

View file

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

View file

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

View file

@ -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<SwingHandlerFlag> = 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<Int, CombatSwingHandler?>? = 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<Any>(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<Array<Audio>>(ItemConfigParser.ATTACK_AUDIO, null)
val audios =
weapon.definition.getConfiguration<Array<Audio>>(
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,
}

View file

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

View file

@ -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<Location>) {
for (tile in tiles) {
RegionManager.addClippingFlag(tile.z, tile.x, tile.y, false, movementBlockFlag)