mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Fixed combat pathing around unreachable targets
This commit is contained in:
parent
c52de9464d
commit
dd56c211de
2 changed files with 171 additions and 15 deletions
|
|
@ -24,6 +24,7 @@ import java.util.LinkedHashMap
|
|||
*/
|
||||
object CombatMovementIntents {
|
||||
private const val MAX_RANGED_APPROACH_CANDIDATES = 128
|
||||
private const val MAX_PLAYER_COMBAT_PATH_DETOUR = 6.0
|
||||
|
||||
private data class Intent(val attacker: Entity, val target: Entity)
|
||||
private data class MovementDestination(
|
||||
|
|
@ -127,6 +128,7 @@ object CombatMovementIntents {
|
|||
return
|
||||
}
|
||||
|
||||
val targetLocation = targetLocationFor(attacker, target)
|
||||
val projectedTargetLocation = projectedLocations[target]
|
||||
if (projectedTargetLocation != null && canAttackFrom(attacker, target, attacker.location, projectedTargetLocation)) {
|
||||
attacker.walkingQueue.reset()
|
||||
|
|
@ -137,7 +139,8 @@ object CombatMovementIntents {
|
|||
}
|
||||
|
||||
val candidates = movementDestinationsFor(attacker, target)
|
||||
if (candidates.any { it.location == attacker.location }) {
|
||||
val standingOnCandidate = candidates.any { it.location == attacker.location }
|
||||
if (standingOnCandidate && canAttackFrom(attacker, target, attacker.location, targetLocation)) {
|
||||
attacker.walkingQueue.reset()
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = attacker.location
|
||||
|
|
@ -145,7 +148,7 @@ object CombatMovementIntents {
|
|||
return
|
||||
}
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictTargetLocations(attacker).lastOrNull()
|
||||
if (projectedAttackerLocation != null && candidates.any { it.location == projectedAttackerLocation }) {
|
||||
if (projectedAttackerLocation != null && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) {
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = projectedAttackerLocation
|
||||
reservedTiles.addAll(occupiedTiles(attacker, projectedAttackerLocation))
|
||||
|
|
@ -167,7 +170,7 @@ object CombatMovementIntents {
|
|||
reservedTiles.addAll(projectedTiles)
|
||||
return
|
||||
}
|
||||
if (!blockedByReservation && shouldStopUnreachableCombat(attacker, target)) {
|
||||
if (!blockedByReservation && shouldStopUnreachableCombat(attacker, target, standingOnCandidate)) {
|
||||
stopUnreachableCombat(attacker)
|
||||
}
|
||||
}
|
||||
|
|
@ -197,7 +200,13 @@ object CombatMovementIntents {
|
|||
if (attackerLocation.z != targetLocation.z) {
|
||||
return false
|
||||
}
|
||||
return attackerLocation in CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)
|
||||
if (occupiedTilesOverlap(attacker, attackerLocation, target, targetLocation)) {
|
||||
return false
|
||||
}
|
||||
return when (attacker.properties.combatPulse.style) {
|
||||
CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange(attacker, target, attackerLocation, targetLocation)
|
||||
else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun movementDestinationsFor(attacker: Entity, target: Entity): List<MovementDestination> {
|
||||
|
|
@ -225,9 +234,8 @@ object CombatMovementIntents {
|
|||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder) +
|
||||
destinations +
|
||||
targetFallback
|
||||
val rangedDestinations = playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder)
|
||||
return rangedDestinations + destinations + targetFallback
|
||||
}
|
||||
return destinations
|
||||
}
|
||||
|
|
@ -311,11 +319,118 @@ object CombatMovementIntents {
|
|||
).take(MAX_RANGED_APPROACH_CANDIDATES)
|
||||
}
|
||||
|
||||
private fun canAttackFromMelee(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
attackerLocation: Location,
|
||||
targetLocation: Location
|
||||
): Boolean {
|
||||
val distance = CombatReach.meleeDistance(attacker)
|
||||
if (distance == 1 && !isAdjacentToTarget(attacker, attackerLocation, target, targetLocation)) {
|
||||
return false
|
||||
}
|
||||
if (distance > 1 &&
|
||||
attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) > distance
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return hasProjectileLineOfSight(
|
||||
attackerLocation,
|
||||
attacker.size(),
|
||||
target,
|
||||
targetLocation,
|
||||
checkClose = !CombatReach.isUsingHalberd(attacker)
|
||||
)
|
||||
}
|
||||
|
||||
private fun canAttackFromRange(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
attackerLocation: Location,
|
||||
targetLocation: Location
|
||||
): Boolean {
|
||||
val range = if (attacker is Player) {
|
||||
playerAttackRange(attacker)
|
||||
} else {
|
||||
CombatReach.combatDistance(
|
||||
attacker,
|
||||
target,
|
||||
if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7
|
||||
)
|
||||
}
|
||||
return attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) <= range &&
|
||||
hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation)
|
||||
}
|
||||
|
||||
private fun isAdjacentToTarget(
|
||||
attacker: Entity,
|
||||
attackerLocation: Location,
|
||||
target: Entity,
|
||||
targetLocation: Location
|
||||
): Boolean {
|
||||
for (i in 0 until attacker.size()) {
|
||||
if (Pathfinder.isStandingIn(
|
||||
attackerLocation.x - 1,
|
||||
attackerLocation.y + i,
|
||||
1,
|
||||
1,
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
target.size(),
|
||||
target.size()
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(
|
||||
attackerLocation.x + attacker.size(),
|
||||
attackerLocation.y + i,
|
||||
1,
|
||||
1,
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
target.size(),
|
||||
target.size()
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(
|
||||
attackerLocation.x + i,
|
||||
attackerLocation.y - 1,
|
||||
1,
|
||||
1,
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
target.size(),
|
||||
target.size()
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (Pathfinder.isStandingIn(
|
||||
attackerLocation.x + i,
|
||||
attackerLocation.y + attacker.size(),
|
||||
1,
|
||||
1,
|
||||
targetLocation.x,
|
||||
targetLocation.y,
|
||||
target.size(),
|
||||
target.size()
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasProjectileLineOfSight(
|
||||
attackerLocation: Location,
|
||||
attackerSize: Int,
|
||||
target: Entity,
|
||||
targetLocation: Location
|
||||
targetLocation: Location,
|
||||
checkClose: Boolean = false
|
||||
): Boolean {
|
||||
for (sourceX in 0 until attackerSize) {
|
||||
for (sourceY in 0 until attackerSize) {
|
||||
|
|
@ -335,7 +450,7 @@ object CombatMovementIntents {
|
|||
false,
|
||||
RegionManager::getClippingFlag
|
||||
)
|
||||
if (path.isSuccessful) {
|
||||
if (path.isSuccessful && (!checkClose || path.points.size <= 1)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -416,11 +531,31 @@ object CombatMovementIntents {
|
|||
if (!path.reaches(destination.location) && (!destination.allowPartialPath || path.points.isEmpty())) {
|
||||
return null
|
||||
}
|
||||
if (attacker is Player && !destination.allowPartialPath && isExcessiveCombatDetour(attacker, destination, path)) {
|
||||
return null
|
||||
}
|
||||
val steps = immediateMovementSteps(attacker, path)
|
||||
if (attacker is Player && destination.allowPartialPath && !partialPathMovesCloser(attacker, destination, steps)) {
|
||||
return null
|
||||
}
|
||||
if (attacker is Player && steps.any { !RegionManager.isTeleportPermitted(Location.create(it.x, it.y, attacker.location.z)) }) {
|
||||
return null
|
||||
}
|
||||
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null
|
||||
return CandidatePath(steps, projected)
|
||||
}
|
||||
|
||||
private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean {
|
||||
val pathLength = (path.points.size - 1).coerceAtLeast(0)
|
||||
val directDistance = attacker.location.getDistance(destination.location)
|
||||
return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR
|
||||
}
|
||||
|
||||
private fun partialPathMovesCloser(attacker: Player, destination: MovementDestination, steps: List<Point>): Boolean {
|
||||
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false
|
||||
return projected.getDistance(destination.location) < attacker.location.getDistance(destination.location)
|
||||
}
|
||||
|
||||
private fun Path.reaches(destination: Location): Boolean {
|
||||
if (!isSuccessful || isMoveNear) {
|
||||
return false
|
||||
|
|
@ -467,8 +602,12 @@ object CombatMovementIntents {
|
|||
return attacker is Player && attacker.walkingQueue.isRunningBoth && attacker.settings.runEnergy >= 1.0
|
||||
}
|
||||
|
||||
private fun shouldStopUnreachableCombat(attacker: Entity, target: Entity): Boolean {
|
||||
return attacker is Player && CombatMovementPlanner.movementStepsThisTick(target) == 0
|
||||
private fun shouldStopUnreachableCombat(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
exhaustedLocalApproach: Boolean = false
|
||||
): Boolean {
|
||||
return attacker is Player && (exhaustedLocalApproach || CombatMovementPlanner.movementStepsThisTick(target) == 0)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
@ -491,10 +630,19 @@ object CombatMovementIntents {
|
|||
}
|
||||
|
||||
private fun occupiedTilesOverlap(first: Entity, second: Entity): Boolean {
|
||||
return first.location.x < second.location.x + second.size() &&
|
||||
first.location.x + first.size() > second.location.x &&
|
||||
first.location.y < second.location.y + second.size() &&
|
||||
first.location.y + first.size() > second.location.y
|
||||
return occupiedTilesOverlap(first, first.location, second, second.location)
|
||||
}
|
||||
|
||||
private fun occupiedTilesOverlap(
|
||||
first: Entity,
|
||||
firstLocation: Location,
|
||||
second: Entity,
|
||||
secondLocation: Location
|
||||
): Boolean {
|
||||
return firstLocation.x < secondLocation.x + second.size() &&
|
||||
firstLocation.x + first.size() > secondLocation.x &&
|
||||
firstLocation.y < secondLocation.y + second.size() &&
|
||||
firstLocation.y + first.size() > secondLocation.y
|
||||
}
|
||||
|
||||
private fun pathfinderFor(attacker: Entity): Pathfinder {
|
||||
|
|
|
|||
|
|
@ -670,6 +670,10 @@ class CombatMovementTests {
|
|||
"Stopping blocked autocast should not leave a chase path queued."
|
||||
)
|
||||
}
|
||||
assertTrue(
|
||||
player.location.getDistance(duck.location) <= start.getDistance(duck.location),
|
||||
"Blocked autocast should not route away around the river before resolving combat."
|
||||
)
|
||||
} finally {
|
||||
duck.clear()
|
||||
CombatMovementIntents.clear()
|
||||
|
|
@ -716,6 +720,10 @@ class CombatMovementTests {
|
|||
"location=${player.location}, distance=${player.location.getDistance(duck.location)}, " +
|
||||
"isAttacking=${player.properties.combatPulse.isAttacking}"
|
||||
)
|
||||
assertTrue(
|
||||
player.location.getDistance(duck.location) <= startDistance,
|
||||
"Melee combat should not route away around the river before rejecting the duck."
|
||||
)
|
||||
} finally {
|
||||
duck.clear()
|
||||
CombatMovementIntents.clear()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue