mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Melee clipping fix
This commit is contained in:
parent
62f60abc9a
commit
6627efd780
5 changed files with 544 additions and 163 deletions
|
|
@ -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++
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue