mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Extract combat reach helpers and document movement rewrite progress
Move shared combat reach checks out of swing handlers and update melee, range, and magic handlers to delegate to the new reach boundary. Add focused combat tests for large-target melee reach and planner behavior, and update the living combat movement rewrite notes with completed progress and next steps.
This commit is contained in:
parent
a816e3378e
commit
71e8812124
8 changed files with 351 additions and 147 deletions
|
|
@ -0,0 +1,99 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
import core.game.world.map.RegionManager
|
||||
|
||||
/**
|
||||
* Plans combat-specific chase targets without mutating walking queues.
|
||||
*/
|
||||
object CombatMovementPlanner {
|
||||
data class Plan(
|
||||
val targetLocation: Location,
|
||||
val attackTile: Location?
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun plan(attacker: Entity, target: Entity): Plan {
|
||||
val targetLocation = predictedTargetLocation(target)
|
||||
return Plan(
|
||||
targetLocation = targetLocation,
|
||||
attackTile = chooseTargetBorderTile(attacker, target, targetLocation)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedTargetLocation(target: Entity): Location {
|
||||
return predictTargetLocations(target).lastOrNull() ?: target.location
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictTargetLocations(target: Entity): List<Location> {
|
||||
return predictTargetLocations(target, movementStepsThisTick(target))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictTargetLocations(target: Entity, maxSteps: Int): List<Location> {
|
||||
if (maxSteps <= 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val queued = movementPoints(target)
|
||||
if (queued.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val steps = if (queued.first().isRunDisabled) 1 else maxSteps.coerceAtMost(2)
|
||||
return queued.take(steps).map { Location.create(it.x, it.y, target.location.z) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun movementStepsThisTick(target: Entity): Int {
|
||||
val queued = movementPoints(target)
|
||||
if (queued.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
return if (target.walkingQueue.isRunningBoth && !queued.first().isRunDisabled && queued.size > 1) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun chooseTargetBorderTile(attacker: Entity, target: Entity): Location? {
|
||||
return chooseTargetBorderTile(attacker, target, predictedTargetLocation(target))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun chooseTargetBorderTile(attacker: Entity, target: Entity, targetLocation: Location): Location? {
|
||||
val candidates = borderTiles(target, targetLocation, attacker.size())
|
||||
val reachable = candidates.filter { RegionManager.isTeleportPermitted(it) }
|
||||
return (reachable.ifEmpty { candidates }).minWithOrNull(
|
||||
compareBy<Location> { it.getDistance(attacker.location) }
|
||||
.thenBy { it.x }
|
||||
.thenBy { it.y }
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun borderTiles(target: Entity, targetLocation: Location, attackerSize: Int): List<Location> {
|
||||
val targetSize = target.size()
|
||||
val border = LinkedHashSet<Location>()
|
||||
val plane = targetLocation.z
|
||||
val minOffset = -attackerSize + 1
|
||||
val maxOffset = targetSize - 1
|
||||
|
||||
for (offset in minOffset..maxOffset) {
|
||||
border.add(Location.create(targetLocation.x - attackerSize, targetLocation.y + offset, plane))
|
||||
border.add(Location.create(targetLocation.x + targetSize, targetLocation.y + offset, plane))
|
||||
border.add(Location.create(targetLocation.x + offset, targetLocation.y - attackerSize, plane))
|
||||
border.add(Location.create(targetLocation.x + offset, targetLocation.y + targetSize, plane))
|
||||
}
|
||||
|
||||
return border.toList()
|
||||
}
|
||||
|
||||
private fun movementPoints(target: Entity): List<Point> {
|
||||
return target.walkingQueue.queue.filter { it.direction != null }
|
||||
}
|
||||
}
|
||||
171
Server/src/main/core/game/node/entity/combat/CombatReach.kt
Normal file
171
Server/src/main/core/game/node/entity/combat/CombatReach.kt
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package core.game.node.entity.combat
|
||||
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.Player
|
||||
import core.game.world.map.Direction
|
||||
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.PREVENT_EAST
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_NORTH
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_NORTHEAST
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_NORTHWEST
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_SOUTH
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_SOUTHEAST
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_SOUTHWEST
|
||||
import core.game.world.map.path.Pathfinder.PREVENT_WEST
|
||||
|
||||
/**
|
||||
* Shared combat reach calculations.
|
||||
*/
|
||||
object CombatReach {
|
||||
@JvmStatic
|
||||
fun isUsingHalberd(entity: Entity): Boolean {
|
||||
if (entity is Player) {
|
||||
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
|
||||
if (weapon != null) {
|
||||
return weapon.id in 3190..3204 || weapon.id == 6599
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
return entity.id == 8612
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun meleeDistance(entity: Entity): Int {
|
||||
return if (isUsingHalberd(entity)) 2 else 1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
|
||||
val e = entity.location
|
||||
if (victim == null) {
|
||||
return false
|
||||
}
|
||||
if (entity.id == 7135 && entity.location.withinDistance(victim.location, 2)) {
|
||||
return true
|
||||
}
|
||||
val x = victim.location.x
|
||||
val y = victim.location.y
|
||||
val size = entity.size()
|
||||
if (distance == 1) {
|
||||
for (i in 0 until size) {
|
||||
if (Pathfinder.isStandingIn(e.x - 1, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + size, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y - 1, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y + size, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (e == victim.location) {
|
||||
return true
|
||||
}
|
||||
return victim.getSwingHandler(false).type == CombatStyle.MELEE &&
|
||||
e.withinDistance(victim.location, 1) &&
|
||||
victim.properties.combatPulse.getVictim() === entity &&
|
||||
entity.index < victim.index
|
||||
}
|
||||
return entity.centerLocation.withinDistance(
|
||||
victim.centerLocation,
|
||||
distance + (size shr 1) + (victim.size() shr 1)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun combatDistance(entity: Entity, victim: Entity, rawDistance: Int): Int {
|
||||
var distance = rawDistance
|
||||
if (entity is NPC && entity.definition.combatDistance > 0) {
|
||||
distance = entity.definition.combatDistance
|
||||
}
|
||||
return (entity.size() shr 1) + (victim.size() shr 1) + distance
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canReach(entity: Entity, victim: Entity, distance: Int): Boolean {
|
||||
return victim.centerLocation.withinDistance(entity.centerLocation, distance)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
|
||||
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
|
||||
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
|
||||
val dir = closestEntityTile.deriveDirection(closestVictimTile)
|
||||
?: return InteractionType.STILL_INTERACT
|
||||
var next = closestEntityTile
|
||||
|
||||
// A fixed-direction walk can pass beside an oblique target without converging, so
|
||||
// limit the skipped gap to the number of steps available from the starting distance.
|
||||
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
|
||||
for (i in 0 until maxSkipSteps) {
|
||||
if (next.getDistance(closestVictimTile) <= 3) {
|
||||
break
|
||||
}
|
||||
next = next.transform(dir)
|
||||
}
|
||||
if (next.getDistance(closestVictimTile) > 3) {
|
||||
return InteractionType.STILL_INTERACT
|
||||
}
|
||||
|
||||
var result = InteractionType.STILL_INTERACT
|
||||
val maxIterations = next.getDistance(closestVictimTile).toInt()
|
||||
for (i in 0 until maxIterations) {
|
||||
next = next.transform(dir)
|
||||
result = checkStepInterval(dir, next)
|
||||
if (result == InteractionType.NO_INTERACT) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkStepInterval(dir: Direction, next: Location): InteractionType {
|
||||
val components = next.getStepComponents(dir)
|
||||
|
||||
when (dir) {
|
||||
Direction.NORTH -> if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.EAST -> if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
|
||||
Direction.SOUTH -> if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
|
||||
|
||||
Direction.NORTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_NORTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.NORTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_NORTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_NORTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_SOUTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0 ||
|
||||
getClippingFlag(components[1]) and PREVENT_SOUTH != 0 ||
|
||||
getClippingFlag(next) and PREVENT_SOUTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
}
|
||||
|
||||
return InteractionType.STILL_INTERACT
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,7 @@ 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.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
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.update.flag.context.Animation
|
||||
import core.tools.RandomFunction
|
||||
|
|
@ -248,77 +244,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
}
|
||||
|
||||
protected fun canStepTowards(entity: Entity, victim: Entity): InteractionType {
|
||||
val closestVictimTile = victim.getClosestOccupiedTile(entity.location)
|
||||
val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile)
|
||||
val dir = closestEntityTile.deriveDirection(closestVictimTile)
|
||||
?: return InteractionType.STILL_INTERACT //if we cannot derive a direction, it's because both tiles are the same, so hand off control to the main logic which already handles this case
|
||||
var next = closestEntityTile
|
||||
|
||||
//Skip the initial gap in distance if it exists, because standard pathfinding would already stop us before this point if something was between us and the NPC or vice versa.
|
||||
//A fixed-direction walk can only arrive within its starting distance in steps; on oblique approaches it
|
||||
//passes beside the target and never converges, so the walk is hard-capped at that many steps.
|
||||
val maxSkipSteps = next.getDistance(closestVictimTile).toInt() + 1
|
||||
for (i in 0 until maxSkipSteps) {
|
||||
if (next.getDistance(closestVictimTile) <= 3) break
|
||||
next = next.transform(dir)
|
||||
}
|
||||
if (next.getDistance(closestVictimTile) > 3) return InteractionType.STILL_INTERACT //never converged (oblique approach), so defer to the range checks instead
|
||||
|
||||
var result: InteractionType = InteractionType.STILL_INTERACT
|
||||
val maxIterations = next.getDistance(closestVictimTile).toInt()
|
||||
for (i in 0 until maxIterations) { //step towards the target tile, checking if anything would obstruct us on the way, and immediately breaking + returning if it does.
|
||||
next = next.transform(dir)
|
||||
result = checkStepInterval(dir, next)
|
||||
if (result == InteractionType.NO_INTERACT) break
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkStepInterval(
|
||||
dir: Direction,
|
||||
next: Location
|
||||
): InteractionType {
|
||||
val components = next.getStepComponents(dir)
|
||||
|
||||
when (dir) {
|
||||
Direction.NORTH -> if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.EAST -> if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT
|
||||
Direction.SOUTH -> if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT
|
||||
Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT
|
||||
|
||||
Direction.NORTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_NORTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.NORTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_NORTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_NORTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_EAST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_EAST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_SOUTHEAST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
Direction.SOUTH_WEST -> {
|
||||
if (getClippingFlag(components[0]) and PREVENT_WEST != 0
|
||||
|| getClippingFlag(components[1]) and PREVENT_SOUTH != 0
|
||||
|| getClippingFlag(next) and PREVENT_SOUTHWEST != 0
|
||||
) return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return InteractionType.STILL_INTERACT
|
||||
return CombatReach.canStepTowards(entity, victim)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -384,13 +310,7 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
* @return The actual distance used for combat.
|
||||
*/
|
||||
open fun getCombatDistance(e: Entity, v: Entity, rawDistance: Int): Int {
|
||||
var distance = rawDistance
|
||||
if (e is NPC) {
|
||||
if (e.definition.combatDistance > 0) {
|
||||
distance = e.definition.combatDistance
|
||||
}
|
||||
}
|
||||
return (e.size() shr 1) + (v.size() shr 1) + distance
|
||||
return CombatReach.combatDistance(e, v, rawDistance)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ open class MagicSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
var distance = 10
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
|
||||
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
|
||||
if (victim.walkingQueue.isMoving && !goodRange) {
|
||||
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
|
||||
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
}
|
||||
if (goodRange && isAttackable(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import content.global.skill.slayer.Tasks
|
|||
import content.global.skill.summoning.SummoningPouch
|
||||
import core.api.*
|
||||
import core.api.EquipmentSlot
|
||||
import core.game.container.impl.EquipmentContainer
|
||||
import core.game.node.entity.Entity
|
||||
import core.game.node.entity.combat.equipment.ArmourSet
|
||||
import core.game.node.entity.combat.equipment.Weapon
|
||||
|
|
@ -15,10 +14,8 @@ 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.game.world.map.path.Pathfinder
|
||||
import core.tools.RandomFunction
|
||||
import org.rs09.consts.Items
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
|
|
@ -33,15 +30,15 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
: 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 = if (usingHalberd(entity)) 2 else 1
|
||||
var distance = CombatReach.meleeDistance(entity)
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
var goodRange = canMelee(entity, victim, distance)
|
||||
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 = canMelee(entity, victim, distance)
|
||||
goodRange = CombatReach.canMelee(entity, victim, distance)
|
||||
}
|
||||
if (!isProjectileClipped(entity, victim, !usingHalberd(entity))) {
|
||||
if (!isProjectileClipped(entity, victim, !CombatReach.isUsingHalberd(entity))) {
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
val isRunning = entity.walkingQueue.runDir != -1
|
||||
|
|
@ -311,23 +308,6 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks if the entity is using a halberd.
|
||||
* @param entity The entity.
|
||||
* @return `True` if so.
|
||||
*/
|
||||
private fun usingHalberd(entity: Entity): Boolean {
|
||||
if (entity is Player) {
|
||||
val weapon = entity.equipment[EquipmentContainer.SLOT_WEAPON]
|
||||
if (weapon != null) {
|
||||
return weapon.id in 3190..3204 || weapon.id == 6599
|
||||
}
|
||||
} else if (entity is NPC) {
|
||||
return entity.id == 8612
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entity can execute a melee swing from its current location.
|
||||
* @param entity The attacking entity.
|
||||
|
|
@ -335,37 +315,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
* @return `True` if so.
|
||||
*/
|
||||
fun canMelee(entity: Entity, victim: Entity?, distance: Int): Boolean {
|
||||
val e = entity.location
|
||||
if (victim == null) {
|
||||
return false
|
||||
}
|
||||
if (entity.id == 7135 && entity.location.withinDistance(victim.location, 2)) {
|
||||
return true
|
||||
}
|
||||
val x = victim.location.x
|
||||
val y = victim.location.y
|
||||
val size = entity.size()
|
||||
if (distance == 1) {
|
||||
for (i in 0 until size) {
|
||||
if (Pathfinder.isStandingIn(e.x - 1, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + size, e.y + i, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y - 1, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(e.x + i, e.y + size, 1, 1, x, y, victim.size(), victim.size())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (e == victim.location) {
|
||||
return true
|
||||
}
|
||||
return victim.getSwingHandler(false).type == CombatStyle.MELEE && e.withinDistance(victim.location, 1) && victim.properties.combatPulse.getVictim() === entity && entity.index < victim.index
|
||||
}
|
||||
return entity.centerLocation.withinDistance(victim.centerLocation, distance + (size shr 1) + (victim.size() shr 1))
|
||||
return CombatReach.canMelee(entity, victim, distance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,10 +53,10 @@ open class RangeSwingHandler (vararg flags: SwingHandlerFlag) : CombatSwingHandl
|
|||
distance = 10
|
||||
}
|
||||
}
|
||||
var goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, distance))
|
||||
var goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, distance))
|
||||
var type = InteractionType.STILL_INTERACT
|
||||
if (victim.walkingQueue.isMoving && !goodRange) {
|
||||
goodRange = victim.centerLocation.withinDistance(entity.centerLocation, getCombatDistance(entity, victim, ++distance))
|
||||
goodRange = CombatReach.canReach(entity, victim, getCombatDistance(entity, victim, ++distance))
|
||||
type = InteractionType.MOVE_INTERACT
|
||||
}
|
||||
if (goodRange && super.canSwing(entity, victim) != InteractionType.NO_INTERACT) {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ package content
|
|||
|
||||
import TestUtils
|
||||
import content.global.handlers.item.equipment.special.ChinchompaSwingHandler
|
||||
import core.ServerConstants
|
||||
import core.api.EquipmentSlot
|
||||
import core.game.container.impl.EquipmentContainer.updateBonuses
|
||||
import core.game.interaction.IntType
|
||||
import core.game.interaction.InteractionListeners
|
||||
import core.game.node.entity.combat.CombatMovementPlanner
|
||||
import core.game.node.entity.combat.CombatReach
|
||||
import core.game.node.entity.combat.MagicSwingHandler
|
||||
import core.game.node.entity.combat.MeleeSwingHandler
|
||||
import core.game.node.entity.combat.RangeSwingHandler
|
||||
import core.game.node.entity.combat.SwingHandlerFlag
|
||||
import core.game.node.entity.combat.equipment.WeaponInterface
|
||||
import core.game.node.entity.npc.NPC
|
||||
import core.game.node.entity.player.link.prayer.PrayerType
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.game.node.item.Item
|
||||
|
|
@ -135,4 +139,48 @@ class CombatTests {
|
|||
Assertions.assertEquals(damageBaseline, handler.calculateHit(p, p, 1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatReachUsesOccupiedTilesForLargeMeleeTargets() {
|
||||
TestUtils.getMockPlayer("combatReachLargeTarget").use { attacker ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
val victim = NPC.create(100, origin.transform(1, 0, 0))
|
||||
victim.setSize(2)
|
||||
attacker.location = origin
|
||||
|
||||
Assertions.assertTrue(CombatReach.canMelee(attacker, victim, 1))
|
||||
Assertions.assertTrue(MeleeSwingHandler.canMelee(attacker, victim, 1))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementPlannerPredictsRunningTargetSteps() {
|
||||
TestUtils.getMockPlayer("combatPlannerRunner").use { target ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
target.location = origin
|
||||
target.walkingQueue.reset(true)
|
||||
target.walkingQueue.addPath(origin.x + 3, origin.y)
|
||||
|
||||
Assertions.assertEquals(
|
||||
listOf(origin.transform(1, 0, 0), origin.transform(2, 0, 0)),
|
||||
CombatMovementPlanner.predictTargetLocations(target)
|
||||
)
|
||||
Assertions.assertEquals(origin.transform(2, 0, 0), CombatMovementPlanner.predictedTargetLocation(target))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combatMovementPlannerChoosesClosestTargetBorderTile() {
|
||||
TestUtils.getMockPlayer("combatPlannerAttacker").use { attacker ->
|
||||
val origin = ServerConstants.HOME_LOCATION!!.transform(32, 32, 0)
|
||||
val victim = NPC.create(100, origin.transform(4, 0, 0))
|
||||
victim.setSize(2)
|
||||
attacker.location = origin
|
||||
|
||||
Assertions.assertEquals(
|
||||
origin.transform(3, 0, 0),
|
||||
CombatMovementPlanner.chooseTargetBorderTile(attacker, victim, victim.location)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,10 +91,26 @@ Initial targets:
|
|||
|
||||
## Suggested Implementation Sequence
|
||||
|
||||
1. Extract a `CombatReach` utility from the swing handlers.
|
||||
2. Add a `CombatMovementPlanner` that can choose target border tiles and predict
|
||||
1. [x] Extract a `CombatReach` utility from the swing handlers.
|
||||
2. [x] Add a `CombatMovementPlanner` that can choose target border tiles and predict
|
||||
one or two target movement steps from `WalkingQueue`.
|
||||
3. Add a combat movement intent phase that resolves step conflicts
|
||||
3. [ ] Add a combat movement intent phase that resolves step conflicts
|
||||
deterministically.
|
||||
4. Remove the private `MovementPulse` from `CombatPulse`.
|
||||
5. Enable the pending tests one scenario at a time.
|
||||
4. [ ] Remove the private `MovementPulse` from `CombatPulse`.
|
||||
5. [ ] Enable the pending tests one scenario at a time.
|
||||
|
||||
## Progress Log
|
||||
|
||||
- 2026-04-28: Extracted `CombatReach` in `core.game.node.entity.combat`.
|
||||
Melee occupied-tile reach, halberd reach, generic center-distance reach,
|
||||
NPC combat-distance overrides, and straight-line step validation now live
|
||||
behind that utility. Existing swing handlers delegate to it, while
|
||||
`MeleeSwingHandler.canMelee(...)` remains as a compatibility wrapper for
|
||||
current callers. Added a focused regression in `CombatTests` for large-target
|
||||
melee occupied-tile reach.
|
||||
- 2026-04-28: Added a standalone `CombatMovementPlanner` that reads
|
||||
`WalkingQueue` without mutating it, predicts the target's next one or two
|
||||
movement locations using run state and run-disabled path points, and chooses
|
||||
the closest valid border tile around the predicted occupied area. The planner
|
||||
is not yet wired into `CombatPulse`; the next step is the deterministic combat
|
||||
movement intent phase.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue