Old dumb pathfinding, some reformatting, various fixes

This commit is contained in:
dam 2026-05-09 19:25:41 +03:00
parent 4328d591f5
commit cd2f57f1b9
No known key found for this signature in database
GPG key ID: 4AF4E722399663FB
11 changed files with 1821 additions and 1254 deletions

File diff suppressed because it is too large Load diff

View file

@ -29,9 +29,7 @@ object CombatMovementIntents {
private data class Intent(val attacker: Entity, val target: Entity)
private data class MovementDestination(
val node: Node,
val pathfinder: Pathfinder,
val allowPartialPath: Boolean
val node: Node, val pathfinder: Pathfinder, val allowPartialPath: Boolean
) {
val location: Location
get() = node.location
@ -48,12 +46,9 @@ object CombatMovementIntents {
) {
fun summary(): String {
val slowest = slowestIntent?.let {
", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " +
"candidates=${it.candidateCount} directPathHits=${it.directPathHits} " +
"rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}"
", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " + "candidates=${it.candidateCount} directPathHits=${it.directPathHits} " + "rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}"
} ?: ""
return "combatMovementStats intents=$intents candidates=$candidateCount " +
"directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest"
return "combatMovementStats intents=$intents candidates=$candidateCount " + "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest"
}
companion object {
@ -189,10 +184,7 @@ object CombatMovementIntents {
return
}
val pending = intents.values.sortedWith(
compareBy<Intent> { it.attacker.index }
.thenBy { it.target.index }
)
val pending = intents.values.sortedWith(compareBy<Intent> { it.attacker.index }.thenBy { it.target.index })
intents.clear()
val reservedTiles = LinkedHashSet<Location>()
@ -277,11 +269,7 @@ object CombatMovementIntents {
val targetLocation = targetLocationFor(attacker, target)
val projectedTargetLocation = projectedLocations[target]
if (projectedTargetLocation != null && canAttackFrom(
attacker,
target,
attacker.location,
projectedTargetLocation,
trace
attacker, target, attacker.location, projectedTargetLocation, trace
)
) {
attacker.walkingQueue.reset()
@ -321,11 +309,7 @@ object CombatMovementIntents {
val standingOnCandidate = candidates.any { it.location == attacker.location }
val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location
if (projectedAttackerLocation != attacker.location && canAttackFrom(
attacker,
target,
projectedAttackerLocation,
targetLocation,
trace
attacker, target, projectedAttackerLocation, targetLocation, trace
)
) {
attacker.face(target)
@ -334,13 +318,15 @@ object CombatMovementIntents {
return
}
val queueStationaryContinuation = attacker.properties.combatPulse.style == CombatStyle.MELEE &&
!CombatMovementPlanner.hasMovementStepThisTick(target)
val queueStationaryContinuation =
attacker.properties.combatPulse.style == CombatStyle.MELEE && !CombatMovementPlanner.hasMovementStepThisTick(
target
)
var blockedByReservation = false
for (candidate in candidates) {
val candidatePath = pathTo(attacker, candidate, trace, queueStationaryContinuation) ?: continue
val attackPath = truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace)
?: continue
val attackPath =
truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace) ?: continue
if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) {
blockedByReservation = true
continue
@ -393,11 +379,7 @@ object CombatMovementIntents {
}
return when (attacker.properties.combatPulse.style) {
CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange(
attacker,
target,
attackerLocation,
targetLocation,
trace
attacker, target, attackerLocation, targetLocation, trace
)
else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation, trace)
@ -405,11 +387,7 @@ object CombatMovementIntents {
}
private fun movementDestinationsFor(
attacker: Entity,
target: Entity,
targetLocation: Location,
pathfinder: Pathfinder,
trace: IntentTrace
attacker: Entity, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace
): List<MovementDestination> {
if (attacker is NPC && pathfinder === Pathfinder.DUMB) {
val destinations = ArrayList<MovementDestination>(4)
@ -418,7 +396,7 @@ object CombatMovementIntents {
destinations.add(MovementDestination(location, pathfinder, allowPartialPath = true))
}
} else {
addDumbNpcAttackDestinations(attacker, target, pathfinder, destinations)
destinations.add(MovementDestination(targetLocation, pathfinder, allowPartialPath = true))
}
trace.candidateCount += destinations.size
return destinations
@ -433,8 +411,9 @@ object CombatMovementIntents {
for (location in attackTiles) {
destinations.add(MovementDestination(location, pathfinder, allowPartialPath = false))
}
val targetFallback = if (!occupiedTilesOverlap(attacker, target) &&
shouldAllowPartialTargetPath(attacker, target, targetLocation)
val targetFallback = if (!occupiedTilesOverlap(attacker, target) && shouldAllowPartialTargetPath(
attacker, target, targetLocation
)
) {
MovementDestination(target, pathfinder, allowPartialPath = true)
} else {
@ -456,10 +435,7 @@ object CombatMovementIntents {
}
private fun attackTilesFor(
attacker: Entity,
target: Entity,
targetLocation: Location,
trace: IntentTrace
attacker: Entity, target: Entity, targetLocation: Location, trace: IntentTrace
): List<Location> {
if (attacker.properties.combatPulse.style != CombatStyle.MELEE) {
return CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)
@ -468,12 +444,11 @@ object CombatMovementIntents {
val walkable = borderTiles.filter { RegionManager.isTeleportPermitted(it) }
val candidates = walkable.ifEmpty { borderTiles }
val attackable = candidates.filter { canAttackFrom(attacker, target, it, targetLocation, trace) }
return attackable.ifEmpty { candidates }.sortedWith(
compareBy<Location> { distanceSquared(it, attacker.location) }
.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }
.thenBy { it.x }
.thenBy { it.y }
)
return attackable.ifEmpty { candidates }.sortedWith(compareBy<Location> {
distanceSquared(
it, attacker.location
)
}.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y })
}
private fun shouldAllowPartialTargetPath(attacker: Player, target: Entity, targetLocation: Location): Boolean {
@ -488,11 +463,7 @@ object CombatMovementIntents {
}
private fun playerAttackRangeDestinations(
attacker: Player,
target: Entity,
targetLocation: Location,
pathfinder: Pathfinder,
trace: IntentTrace
attacker: Player, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace
): List<MovementDestination> {
val range = playerAttackRange(attacker)
if (range <= CombatReach.meleeDistance(attacker)) {
@ -524,21 +495,14 @@ object CombatMovementIntents {
distance += 2
}
val rangeWeapon = RangeWeapon.get(attacker.equipment.getNew(EquipmentContainer.SLOT_WEAPON).id)
if (rangeWeapon != null &&
(rangeWeapon.weaponType == Weapon.WeaponType.DOUBLE_SHOT ||
rangeWeapon.weaponType == Weapon.WeaponType.DEGRADING)
) {
if (rangeWeapon != null && (rangeWeapon.weaponType == Weapon.WeaponType.DOUBLE_SHOT || rangeWeapon.weaponType == Weapon.WeaponType.DEGRADING)) {
distance = 10
}
return distance
}
private fun attackRangeTiles(
attacker: Player,
target: Entity,
targetLocation: Location,
range: Int,
trace: IntentTrace
attacker: Player, target: Entity, targetLocation: Location, range: Int, trace: IntentTrace
): List<Location> {
val tiles = ArrayList<Location>()
val minX = targetLocation.x - range
@ -556,22 +520,16 @@ object CombatMovementIntents {
}
}
}
tiles.sortWith(
compareBy<Location> { distanceSquared(it, attacker.location) }
.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }
.thenBy { it.x }
.thenBy { it.y }
)
tiles.sortWith(compareBy<Location> {
distanceSquared(
it, attacker.location
)
}.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y })
val attackTiles = ArrayList<Location>(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size))
RsmodPathfinder.loadLineOfSightWindow(targetLocation)
for (tile in tiles) {
if (!hasProjectileLineOfSight(
tile,
attacker.size(),
target,
targetLocation,
loadWindow = false,
trace = trace
tile, attacker.size(), target, targetLocation, loadWindow = false, trace = trace
)
) {
continue
@ -595,8 +553,9 @@ object CombatMovementIntents {
if (distance == 1 && !isAdjacentToTarget(attacker, attackerLocation, target, targetLocation)) {
return false
}
if (distance > 1 &&
distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) > distance * distance
if (distance > 1 && distanceSquaredToClosestOccupiedTile(
target, targetLocation, attackerLocation
) > distance * distance
) {
return false
}
@ -621,20 +580,18 @@ object CombatMovementIntents {
playerAttackRange(attacker)
} else {
CombatReach.combatDistance(
attacker,
target,
if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7
attacker, target, if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7
)
}
return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range &&
hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation, trace = trace)
return distanceSquaredToClosestOccupiedTile(
target, targetLocation, attackerLocation
) <= range * range && hasProjectileLineOfSight(
attackerLocation, attacker.size(), target, targetLocation, trace = trace
)
}
private fun isAdjacentToTarget(
attacker: Entity,
attackerLocation: Location,
target: Entity,
targetLocation: Location
attacker: Entity, attackerLocation: Location, target: Entity, targetLocation: Location
): Boolean {
for (i in 0 until attacker.size()) {
if (Pathfinder.isStandingIn(
@ -708,19 +665,11 @@ object CombatMovementIntents {
val maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE
if (!loadWindow) {
return RsmodPathfinder.hasLineOfSightBetweenLoaded(
attackerLocation,
attackerSize,
targetLocation,
target.size(),
maxRaySteps = maxRaySteps
attackerLocation, attackerSize, targetLocation, target.size(), maxRaySteps = maxRaySteps
)
}
return RsmodPathfinder.hasLineOfSightBetween(
attackerLocation,
attackerSize,
targetLocation,
target.size(),
maxRaySteps = maxRaySteps
attackerLocation, attackerSize, targetLocation, target.size(), maxRaySteps = maxRaySteps
)
}
@ -746,11 +695,7 @@ object CombatMovementIntents {
}
private fun pathToTargetFootprint(
attacker: Entity,
target: Entity,
targetLocation: Location,
pathfinder: Pathfinder,
trace: IntentTrace
attacker: Entity, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace
): CandidatePath? {
val targetMoving = CombatMovementPlanner.hasMovementStepThisTick(target)
val movingTargetPath =
@ -798,20 +743,14 @@ object CombatMovementIntents {
return null
}
if (attacker is Player && !path.isMoveNear && isExcessiveCombatDetourToTarget(
attacker,
target,
targetLocation,
path
attacker, target, targetLocation, path
)
) {
return null
}
val steps = immediateMovementSteps(attacker, path)
if (attacker is Player && path.isMoveNear && !partialPathMovesCloserToTarget(
attacker,
target,
targetLocation,
steps
attacker, target, targetLocation, steps
)
) {
return null
@ -819,9 +758,7 @@ object CombatMovementIntents {
if (attacker is Player && steps.any {
!RegionManager.isTeleportPermitted(
Location.create(
it.x,
it.y,
attacker.location.z
it.x, it.y, attacker.location.z
)
)
}) {
@ -832,9 +769,7 @@ object CombatMovementIntents {
}
private fun directPathTowardMovingTarget(
attacker: Entity,
target: Entity,
targetLocation: Location
attacker: Entity, target: Entity, targetLocation: Location
): CandidatePath? {
if (!CombatMovementPlanner.hasMovementStepThisTick(target)) {
return null
@ -904,10 +839,7 @@ object CombatMovementIntents {
}
private fun meleeDestination(
attacker: Entity,
target: Entity,
targetLocation: Location,
direction: Direction
attacker: Entity, target: Entity, targetLocation: Location, direction: Direction
): Location {
val minAlignedX = targetLocation.x - attacker.size() + 1
val maxAlignedX = targetLocation.x + target.size() - 1
@ -941,83 +873,14 @@ object CombatMovementIntents {
}
}
private fun addDumbNpcAttackDestinations(
attacker: NPC,
target: Entity,
pathfinder: Pathfinder,
destinations: MutableList<MovementDestination>
) {
val directions = ArrayList<Direction>(3)
addDirection(directions, Direction.getLogicalDirection(target.centerLocation, attacker.centerLocation))
if (attacker.centerLocation.x < target.centerLocation.x) {
addDirection(directions, Direction.WEST)
} else if (attacker.centerLocation.x > target.centerLocation.x) {
addDirection(directions, Direction.EAST)
}
if (attacker.centerLocation.y < target.centerLocation.y) {
addDirection(directions, Direction.SOUTH)
} else if (attacker.centerLocation.y > target.centerLocation.y) {
addDirection(directions, Direction.NORTH)
}
for (direction in directions) {
destinations.add(
MovementDestination(
dumbNpcAttackDestination(attacker, target, direction),
pathfinder,
allowPartialPath = true
)
)
}
}
private fun addDirection(directions: MutableList<Direction>, direction: Direction) {
if (!directions.contains(direction)) {
directions.add(direction)
}
}
private fun dumbNpcAttackDestination(attacker: NPC, target: Entity, direction: Direction): Location {
val targetLocation = target.location
val minAlignedX = targetLocation.x - attacker.size() + 1
val maxAlignedX = targetLocation.x + target.size() - 1
val minAlignedY = targetLocation.y - attacker.size() + 1
val maxAlignedY = targetLocation.y + target.size() - 1
return when (direction) {
Direction.WEST -> Location.create(
targetLocation.x - attacker.size(),
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
targetLocation.z
)
Direction.EAST -> Location.create(
targetLocation.x + target.size(),
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
targetLocation.z
)
Direction.SOUTH -> Location.create(
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
targetLocation.y - attacker.size(),
targetLocation.z
)
else -> Location.create(
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
targetLocation.y + target.size(),
targetLocation.z
)
}
}
private fun truncateAtFirstAttackOpportunity(
attacker: Entity,
target: Entity,
targetLocation: Location,
path: CandidatePath,
trace: IntentTrace
attacker: Entity, target: Entity, targetLocation: Location, path: CandidatePath, trace: IntentTrace
): CandidatePath? {
val steps = ArrayList<Point>(path.steps.size)
for (step in path.steps) {
@ -1034,30 +897,20 @@ object CombatMovementIntents {
}
private fun pathTo(
attacker: Entity,
destination: MovementDestination,
trace: IntentTrace,
queueContinuation: Boolean = false
attacker: Entity, destination: MovementDestination, trace: IntentTrace, queueContinuation: Boolean = false
): CandidatePath? {
if (attacker.location == destination.location) {
return null
}
val stepLimit = movementStepLimit(attacker, queueContinuation && !destination.allowPartialPath)
if (attacker is NPC && destination.pathfinder === Pathfinder.DUMB) {
val directPath = directPartialPathTo(attacker, destination, stepLimit)
if (directPath != null) {
trace.directPathHits++
return directPath
}
return null
}
val directPath = directPathTo(attacker, destination, stepLimit)
if (directPath != null) {
trace.directPathHits++
return directPath
}
if (destination.pathfinder === Pathfinder.SMART &&
!RsmodPathfinder.canAttempt(attacker.location, destination.location)
if (destination.pathfinder === Pathfinder.SMART && !RsmodPathfinder.canAttempt(
attacker.location, destination.location
)
) {
return null
}
@ -1068,18 +921,14 @@ object CombatMovementIntents {
return null
}
if (attacker is Player && !destination.allowPartialPath && isExcessiveCombatDetour(
attacker,
destination,
path
attacker, destination, path
)
) {
return null
}
val steps = immediateMovementSteps(attacker, path, stepLimit)
if (attacker is Player && destination.allowPartialPath && !partialPathMovesCloser(
attacker,
destination,
steps
attacker, destination, steps
)
) {
return null
@ -1087,9 +936,7 @@ object CombatMovementIntents {
if (attacker is Player && steps.any {
!RegionManager.isTeleportPermitted(
Location.create(
it.x,
it.y,
attacker.location.z
it.x, it.y, attacker.location.z
)
)
}) {
@ -1100,9 +947,7 @@ object CombatMovementIntents {
}
private fun directPathTo(
attacker: Entity,
destination: MovementDestination,
maxSteps: Int = movementStepsFor(attacker)
attacker: Entity, destination: MovementDestination, maxSteps: Int = movementStepsFor(attacker)
): CandidatePath? {
if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) {
return null
@ -1111,9 +956,7 @@ object CombatMovementIntents {
}
private fun directPathTo(
attacker: Entity,
destination: Location,
maxSteps: Int = movementStepsFor(attacker)
attacker: Entity, destination: Location, maxSteps: Int = movementStepsFor(attacker)
): CandidatePath? {
if (attacker.size() != 1 || attacker.location.z != destination.z) {
return null
@ -1145,41 +988,8 @@ object CombatMovementIntents {
return CandidatePath(steps, projected)
}
private fun directPartialPathTo(
attacker: Entity,
destination: MovementDestination,
maxSteps: Int
): CandidatePath? {
if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) {
return null
}
val steps = ArrayList<Point>(maxSteps)
var current = attacker.location
var distance = 0
while (current != destination.location && steps.size < maxSteps) {
if (++distance > MAX_DIRECT_COMBAT_PATH_DISTANCE) {
break
}
val direction = Direction.getDirection(current, destination.location) ?: break
if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) {
break
}
val next = current.transform(direction)
if (!RegionManager.isTeleportPermitted(next)) {
break
}
steps.add(Point(next.x, next.y, direction, direction.stepX, direction.stepY))
current = next
}
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null
return CandidatePath(steps, projected)
}
private fun isExcessiveCombatDetourToTarget(
attacker: Player,
target: Entity,
targetLocation: Location,
path: Path
attacker: Player, target: Entity, targetLocation: Location, path: Path
): Boolean {
val pathLength = (path.points.size - 1).coerceAtLeast(0)
val directDistance =
@ -1188,14 +998,12 @@ object CombatMovementIntents {
}
private fun partialPathMovesCloserToTarget(
attacker: Player,
target: Entity,
targetLocation: Location,
steps: List<Point>
attacker: Player, target: Entity, targetLocation: Location, steps: List<Point>
): Boolean {
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false
return distanceSquaredToClosestOccupiedTile(target, targetLocation, projected) <
distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location)
return distanceSquaredToClosestOccupiedTile(
target, targetLocation, projected
) < distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location)
}
private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean {
@ -1205,9 +1013,7 @@ object CombatMovementIntents {
}
private fun partialPathMovesCloser(
attacker: Player,
destination: MovementDestination,
steps: List<Point>
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)
@ -1222,9 +1028,7 @@ object CombatMovementIntents {
}
private fun immediateMovementSteps(
attacker: Entity,
path: Path,
maxSteps: Int = movementStepsFor(attacker)
attacker: Entity, path: Path, maxSteps: Int = movementStepsFor(attacker)
): List<Point> {
val steps = ArrayList<Point>(maxSteps)
for (point in path.points) {
@ -1271,9 +1075,7 @@ object CombatMovementIntents {
}
private fun shouldStopUnreachableCombat(
attacker: Entity,
target: Entity,
exhaustedLocalApproach: Boolean = false
attacker: Entity, target: Entity, exhaustedLocalApproach: Boolean = false
): Boolean {
return attacker is Player && (exhaustedLocalApproach || !CombatMovementPlanner.hasMovementStepThisTick(target))
}
@ -1318,15 +1120,9 @@ object CombatMovementIntents {
}
private fun occupiedTilesOverlap(
first: Entity,
firstLocation: Location,
second: Entity,
secondLocation: Location
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
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 {

View file

@ -14,16 +14,14 @@ import core.game.world.map.path.Pathfinder
*/
object CombatMovementPlanner {
data class Plan(
val targetLocation: Location,
val attackTile: Location?
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)
targetLocation = targetLocation, attackTile = chooseTargetBorderTile(attacker, target, targetLocation)
)
}
@ -125,14 +123,16 @@ object CombatMovementPlanner {
val candidates = borderTiles(target, targetLocation, attacker.size())
val walkable = candidates.filter { RegionManager.isTeleportPermitted(it) }
val attackable = walkable.filter { canInteractFrom(attacker, it, target, targetLocation) }
return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith(
compareBy<Location> { it.getDistance(attacker.location) }
.thenBy { it.x }
.thenBy { it.y }
)
return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith(compareBy<Location> {
it.getDistance(
attacker.location
)
}.thenBy { it.x }.thenBy { it.y })
}
private fun canInteractFrom(attacker: Entity, location: Location, target: Entity, targetLocation: Location): Boolean {
private fun canInteractFrom(
attacker: Entity, location: Location, target: Entity, targetLocation: Location
): Boolean {
if (attacker.size() == 1 && target.size() == 1) {
val direction = Direction.getDirection(location, targetLocation) ?: return false
return direction.canMoveFrom(location.z, location.x, location.y, RegionManager::getClippingFlag)

View file

@ -8,14 +8,7 @@ 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
import core.game.world.map.path.Pathfinder.*
/**
* Shared combat reach calculations.
@ -69,14 +62,13 @@ object CombatReach {
return true
}
}
return victim.getSwingHandler(false).type == CombatStyle.MELEE &&
e.withinDistance(victim.location, 1) &&
victim.properties.combatPulse.getVictim() === entity &&
entity.index < victim.index
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)
victim.centerLocation, distance + (size shr 1) + (victim.size() shr 1)
)
}
@ -111,8 +103,7 @@ object CombatReach {
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
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
@ -151,30 +142,30 @@ object CombatReach {
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
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
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
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
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
}
}

View file

@ -556,20 +556,25 @@ public class NPC extends Entity {
path.setSuccesful(true);
Location current = getLocation();
int maxSteps = Math.min(14, Math.max(1, getSpawnReturnDistance(Math.max(1, getWalkRadius()))));
boolean usedSidestep = false;
for (int steps = 0; !current.equals(destination) && steps < maxSteps; steps++) {
Direction direction = Direction.getDirection(current, destination);
if (direction == null || !canTakeRandomMovementStep(current, direction)) {
Direction stepDirection = chooseRandomMovementStep(current, direction, usedSidestep);
if (stepDirection == null) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
Location next = current.transform(direction);
Location next = current.transform(stepDirection);
if (!isWithinRandomMovementBounds(next)) {
path.setSuccesful(false);
path.setMoveNear(!path.getPoints().isEmpty());
break;
}
path.getPoints().add(new Point(next.getX(), next.getY(), direction, direction.getStepX(), direction.getStepY()));
if (stepDirection != direction) {
usedSidestep = true;
}
path.getPoints().add(new Point(next.getX(), next.getY(), stepDirection, stepDirection.getStepX(), stepDirection.getStepY()));
current = next;
}
if (!current.equals(destination) && !path.getPoints().isEmpty()) {
@ -578,6 +583,47 @@ public class NPC extends Entity {
return path;
}
private Direction chooseRandomMovementStep(Location current, Direction direction, boolean usedSidestep) {
if (direction == null) {
return null;
}
Location next = current.transform(direction);
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, direction)) {
return direction;
}
if (usedSidestep) {
return null;
}
for (Direction sidestep : sidestepDirections(direction)) {
next = current.transform(sidestep);
if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, sidestep)) {
return sidestep;
}
}
return null;
}
private Direction[] sidestepDirections(Direction direction) {
switch (direction) {
case NORTH:
case SOUTH:
return new Direction[] { Direction.EAST, Direction.WEST };
case EAST:
case WEST:
return new Direction[] { Direction.NORTH, Direction.SOUTH };
case NORTH_EAST:
return new Direction[] { Direction.EAST, Direction.NORTH };
case SOUTH_EAST:
return new Direction[] { Direction.EAST, Direction.SOUTH };
case SOUTH_WEST:
return new Direction[] { Direction.WEST, Direction.SOUTH };
case NORTH_WEST:
return new Direction[] { Direction.WEST, Direction.NORTH };
default:
return new Direction[0];
}
}
private boolean canTakeRandomMovementStep(Location current, Direction direction) {
ClipMaskSupplier clipMaskSupplier = behavior != null ? behavior.getClippingSupplier(this) : null;
if (clipMaskSupplier == null) {

View file

@ -0,0 +1,478 @@
package core.game.world.map.path;
import core.game.world.map.Direction;
import core.game.world.map.Location;
import core.game.world.map.Point;
import core.game.world.map.RegionManager;
import java.util.ArrayList;
import java.util.List;
/**
* Pathfinder for simple local movement. It walks directly toward the
* destination and only tries the horizontal/vertical alternatives of a blocked
* diagonal step; it does not search around obstacles.
*/
public final class DumbPathfinder extends Pathfinder {
private boolean found;
private int z;
private int x;
private int y;
@Override
public Path find(Location start,
int size,
Location end,
int sizeX,
int sizeY,
int rotation,
int type,
int walkingFlag,
boolean near,
ClipMaskSupplier clipMaskSupplier) {
ClipMaskSupplier supplier = clipMaskSupplier != null ? clipMaskSupplier : RegionManager::getClippingFlag;
Path path = new Path();
z = start.getZ();
x = start.getX();
y = start.getY();
List<Point> points = new ArrayList<>(20);
path.setSuccesful(true);
while (x != end.getX() || y != end.getY()) {
Direction[] directions = getDirection(x, y, end);
if (type >= 0) {
if ((type < 5 || type == 9) && canDoorInteract(x, y, size, end.getX(), end.getY(), type, rotation, z, supplier)) {
break;
}
if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), rotation, type, z, supplier)) {
break;
}
}
if (sizeX != 0 && sizeY != 0) {
if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, supplier)) {
break;
}
if (directions.length > 1) {
Direction dir = directions[0];
if (x + dir.getStepX() == end.getX() && y + dir.getStepY() == end.getY()) {
directions[0] = directions[directions.length - 1];
directions[directions.length - 1] = dir;
}
}
}
found = true;
if (size < 2) {
checkSingleTraversal(points, supplier, directions);
} else if (size == 2) {
checkDoubleTraversal(points, supplier, directions);
} else {
checkVariableTraversal(points, directions, size, supplier);
}
if (!found) {
path.setMoveNear(x != start.getX() || y != start.getY());
path.setSuccesful(false);
break;
}
}
path.getPoints().addAll(points);
return path;
}
private void checkSingleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
for (Direction dir : directions) {
found = true;
switch (dir) {
case NORTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0) {
found = false;
break;
}
points.add(new Point(x, y + 1, dir));
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 1,
y + 1) & PREVENT_NORTHEAST) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y + 1, dir));
x++;
y++;
break;
case EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y, dir));
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 1,
y - 1) & PREVENT_SOUTHEAST) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y - 1, dir));
x++;
y--;
break;
case SOUTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0) {
found = false;
break;
}
points.add(new Point(x, y - 1, dir));
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x - 1,
y - 1) & PREVENT_SOUTHWEST) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y - 1, dir));
x--;
y--;
break;
case WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y, dir));
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z,
x,
y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x - 1,
y + 1) & PREVENT_NORTHWEST) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y + 1, dir));
x--;
y++;
break;
}
if (found) {
break;
}
}
}
private void checkDoubleTraversal(List<Point> points, ClipMaskSupplier clipMaskSupplier, Direction... directions) {
for (Direction dir : directions) {
found = true;
switch (dir) {
case NORTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 1,
y + 2) & 0x12c01e0) != 0) {
found = false;
break;
}
points.add(new Point(x, y + 1, dir));
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 2,
y + 1) & 0x12c0183) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y + 1, dir));
x++;
y++;
break;
case EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y + 1) & 0x12c01e0) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y, dir));
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 2,
y) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + 2,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
points.add(new Point(x + 1, y - 1, dir));
x++;
y--;
break;
case SOUTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + 1,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
points.add(new Point(x, y - 1, dir));
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y - 1, dir));
x--;
y--;
break;
case WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + 1) & 0x12c0138) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y, dir));
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y + 2) & 0x12c01e0) != 0) {
found = false;
break;
}
points.add(new Point(x - 1, y + 1, dir));
x--;
y++;
break;
}
if (found) {
break;
}
}
}
private void checkVariableTraversal(List<Point> points, Direction[] directions, int size, ClipMaskSupplier clipMaskSupplier) {
for (Direction dir : directions) {
found = true;
roar:
switch (dir) {
case NORTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (size - 1),
y + size) & 0x12c01e0) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + i, y + size) & 0x12c01f8) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x, y + 1, dir));
y++;
break;
case NORTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + size,
y + 1) & 0x12c0183) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (i + 1)) & 0x12c01e3) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x + 1, y + 1, dir));
x++;
y++;
break;
case EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (size - 1)) & 0x12c01e0) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + i) & 0x12c01e3) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x + 1, y, dir));
x++;
break;
case SOUTH_EAST:
if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + size,
y + (size - 2)) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + size,
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (i + 1),
y - 1) & 0x12c018f) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x + 1, y - 1, dir));
x++;
y--;
break;
case SOUTH:
if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (size - 1),
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x + i, y - 1) & 0x12c018f) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x, y - 1, dir));
y--;
break;
case SOUTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x + (size - 2),
y - 1) & 0x12c0183) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (i - 1),
y - 1) & 0x12c018f) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x - 1, y - 1, dir));
x--;
y--;
break;
case WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + (size - 1)) & 0x12c0138) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + i) & 0x12c013e) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x - 1, y, dir));
x--;
break;
case NORTH_WEST:
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x - 1,
y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(
z,
x,
y + size) & 0x12c01e0) != 0) {
found = false;
break;
}
for (int i = 1; i < size - 1; i++) {
if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z,
x + (i - 1),
y + size) & 0x12c01f8) != 0) {
found = false;
break roar;
}
}
points.add(new Point(x - 1, y + 1, dir));
x--;
y++;
break;
}
if (found) {
break;
}
}
}
private static Direction[] getDirection(int startX, int startY, Location end) {
int endX = end.getX();
int endY = end.getY();
if (startX == endX) {
if (startY > endY) {
return new Direction[]{Direction.SOUTH};
} else if (startY < endY) {
return new Direction[]{Direction.NORTH};
}
} else if (startY == endY) {
if (startX > endX) {
return new Direction[]{Direction.WEST};
}
return new Direction[]{Direction.EAST};
} else {
if (startX < endX && startY < endY) {
return new Direction[]{Direction.NORTH_EAST, Direction.EAST, Direction.NORTH};
} else if (startX < endX && startY > endY) {
return new Direction[]{Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH};
} else if (startX > endX && startY < endY) {
return new Direction[]{Direction.NORTH_WEST, Direction.WEST, Direction.NORTH};
} else if (startX > endX && startY > endY) {
return new Direction[]{Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH};
}
}
return new Direction[0];
}
}

View file

@ -9,6 +9,7 @@ import core.game.world.map.Location;
import core.game.world.map.RegionManager;
public abstract class Pathfinder {
public static final int PREVENT_NORTH = 0x12c0120;
public static final int PREVENT_EAST = 0x12c0180;
public static final int PREVENT_NORTHEAST = 0x12c01e0;
@ -17,56 +18,67 @@ public abstract class Pathfinder {
public static final int PREVENT_WEST = 0x12c0108;
public static final int PREVENT_SOUTHWEST = 0x12c010e;
public static final int PREVENT_NORTHWEST = 0x12c0138;
/**
* The smart path finder.
*/
public static final Pathfinder SMART = new RsmodPathfinder();
/**
* The dumb path finder.
*/
public static final Pathfinder DUMB = new RsmodPathfinder();
public static final Pathfinder DUMB = new DumbPathfinder();
/**
* The projectile path finder.
*/
public static final Pathfinder PROJECTILE = new RsmodProjectilePathfinder();
/**
* Finds a path from the location to the end location.
* @param location The start location.
* @param size The mover size.
* @param end The end location.
* @param sizeX The x-size of the destination node.
* @param sizeY The y-size of the destination node.
* @param rotation The object rotation.
* @param type The object type.
*
* @param location The start location.
* @param size The mover size.
* @param end The end location.
* @param sizeX The x-size of the destination node.
* @param sizeY The y-size of the destination node.
* @param rotation The object rotation.
* @param type The object type.
* @param walkingFlag The object walking flag.
* @param near If we should find the nearest location if a path can't be
* found.
* @param near If we should find the nearest location if a path can't be
* found.
* @return The path.
*/
public abstract Path find(Location location, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier);
public abstract Path find(Location location,
int size,
Location end,
int sizeX,
int sizeY,
int rotation,
int type,
int walkingFlag,
boolean near,
ClipMaskSupplier clipMaskSupplier);
/**
* Finds a path from the start location to the end location.
* @param mover The moving entity.
*
* @param mover The moving entity.
* @param destination The destination node.
* @return The path.
*/
public static Path find(Entity mover, Node destination) {
return find(mover, destination, true, SMART);
}
/**
* Finds a path from the start location to the end location.
* @param mover The moving entity.
*
* @param mover The moving entity.
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) {
@ -77,46 +89,49 @@ public abstract class Pathfinder {
}
return find(mover.getLocation(), mover.size(), destination, near, finder, cms);
}
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){
return find(mover.getLocation(),mover.size(),destination,near,finder, RegionManager::getWaterClipFlag);
public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder) {
return find(mover.getLocation(), mover.size(), destination, near, finder, RegionManager::getWaterClipFlag);
}
public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
return find(mover.getLocation(), mover.size(), destination, near, finder, clipMaskSupplier);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @return The path.
*/
public static Path find(Location start, Node destination) {
return find(start, destination, true, SMART);
}
public static Path find(Location start, Node destination, int moverSize) {
return find(start, moverSize, destination, true, SMART, null);
}
return find(start, moverSize, destination, true, SMART, null);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Location start, Node destination, boolean near, Pathfinder finder) {
return find(start, 1, destination, near, finder, null);
}
/**
* Finds a path from the start location to the end location.
*
* @param destination The destination node.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @param near If we should move near the end location, if we can't reach
* it.
* @param finder The pathfinder to use.
* @return The path.
*/
public static Path find(Location start, int moverSize, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) {
@ -125,7 +140,16 @@ public abstract class Pathfinder {
int type = object.getType();
int rotation = object.getRotation();
if (type == 10 || type == 11 || type == 22) {
return finder.find(start, moverSize, object.getLocation(), object.getDefinition().sizeX, object.getDefinition().sizeY, rotation, type, object.getDefinition().getWalkingFlag(), near, clipMaskSupplier);
return finder.find(start,
moverSize,
object.getLocation(),
object.getDefinition().sizeX,
object.getDefinition().sizeY,
rotation,
type,
object.getDefinition().getWalkingFlag(),
near,
clipMaskSupplier);
}
return finder.find(start, moverSize, object.getLocation(), 0, 0, rotation, type, 0, near, clipMaskSupplier);
}
@ -137,7 +161,7 @@ public abstract class Pathfinder {
}
return finder.find(start, moverSize, destination.getLocation(), size, size, 0, -1, 0, near, clipMaskSupplier);
}
private static Scenery getRouteScenery(Scenery object) {
Scenery wrapper = object.getWrapper();
if (wrapper == object) {
@ -148,51 +172,70 @@ public abstract class Pathfinder {
}
return wrapper;
}
private static int getFootprintArea(Scenery object) {
return object.getDefinition().sizeX * object.getDefinition().sizeY;
}
/**
* Checks if interaction with decoration is possible.
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
*
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
* @param rotation The object rotation.
* @return {@code True} if so.
*/
public static boolean canDecorationInteract(int curX, int curY, int size, int destX, int destY, int rotation, int type, int z, ClipMaskSupplier clipMaskSupplier) {
public static boolean canDecorationInteract(int curX,
int curY,
int size,
int destX,
int destY,
int rotation,
int type,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
}
/**
* Checks if interaction with a door is possible.
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
*
* @param curX The current x-coordinate in viewport.
* @param curY The current y-coordinate in viewport.
* @param size The mover size.
* @param destX The destination x-coordinate in viewport.
* @param destY The destination y-coordinate in viewport.
* @param type The object type.
* @param rotation The object rotation.
* @return {@code True} if so.
*/
public static boolean canDoorInteract(int curX, int curY, int size, int destX, int destY, int type, int rotation, int z, ClipMaskSupplier clipMaskSupplier) {
public static boolean canDoorInteract(int curX,
int curY,
int size,
int destX,
int destY,
int type,
int rotation,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier);
}
/**
* Checks if the mover is standing on the destination.
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
*
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
* @param moverSizeX The mover x size.
* @param moverSizeY The mover y size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @return {@code True} if so.
*/
public static boolean isStandingIn(int x, int y, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY) {
@ -204,32 +247,52 @@ public abstract class Pathfinder {
}
return true;
}
/**
* Checks if interaction is possible from the current location.
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
*
* @param x The current x-location (in viewport).
* @param y The current y-location (in viewport).
* @param moverSize The mover size.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param walkFlag The walking flag.
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @param walkFlag The walking flag.
* @return {@code True} if so.
*/
public static boolean canInteract(int x, int y, int moverSize, int destX, int destY, int sizeX, int sizeY, int walkFlag, int z, ClipMaskSupplier clipMaskSupplier) {
public static boolean canInteract(int x,
int y,
int moverSize,
int destX,
int destY,
int sizeX,
int sizeY,
int walkFlag,
int z,
ClipMaskSupplier clipMaskSupplier) {
return RsmodPathfinder.canReach(x, y, moverSize, destX, destY, sizeX, sizeY, 0, -1, walkFlag, z, clipMaskSupplier);
}
/**
* Checks if interaction is possible from the current location.
*
* @param destX The destination x-location in viewport.
* @param destY The destination y-location in viewport.
* @param sizeX The destination node x-size.
* @param sizeY The destination node y-size.
* @return {@code True} if so.
*/
public static boolean canInteractSized(int curX, int curY, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY, int walkingFlag, int z) {
public static boolean canInteractSized(int curX,
int curY,
int moverSizeX,
int moverSizeY,
int destX,
int destY,
int sizeX,
int sizeY,
int walkingFlag,
int z) {
return RsmodPathfinder.canReach(curX, curY, moverSizeX, destX, destY, sizeX, sizeY, 0, -1, walkingFlag, z, null);
}
}

View file

@ -125,9 +125,7 @@ class RsmodPathfinder(
} else {
CollisionFlagMap().also {
loadCollisionWindow(
flags = it,
start = Location.create(srcX, srcY, z),
supplier = clipMaskSupplier
flags = it, start = Location.create(srcX, srcY, z), supplier = clipMaskSupplier
)
}
}
@ -149,22 +147,14 @@ class RsmodPathfinder(
@JvmStatic
fun lineOfSight(
start: Location,
dest: Location,
moverSize: Int,
destWidth: Int,
destHeight: Int
start: Location, dest: Location, moverSize: Int, destWidth: Int, destHeight: Int
): RayCast {
RegionManager.loadClippingWindow(start, SEARCH_MAP_SIZE)
return lineOfSightLoaded(start, dest, moverSize, destWidth, destHeight)
}
private fun lineOfSightLoaded(
start: Location,
dest: Location,
moverSize: Int,
destWidth: Int,
destHeight: Int
start: Location, dest: Location, moverSize: Int, destWidth: Int, destHeight: Int
): RayCast {
return projectileLineFinder.get().lineOfSight(
level = start.z,
@ -278,9 +268,7 @@ class RsmodPathfinder(
}
private fun loadCollisionWindow(
flags: CollisionFlagMap,
start: Location,
supplier: ClipMaskSupplier
flags: CollisionFlagMap, start: Location, supplier: ClipMaskSupplier
) {
val baseX = start.x - (SEARCH_MAP_SIZE / 2)
val baseY = start.y - (SEARCH_MAP_SIZE / 2)

View file

@ -19,11 +19,7 @@ class RsmodProjectilePathfinder : Pathfinder() {
val source = requireNotNull(start)
val destination = requireNotNull(end)
val rayCast = RsmodPathfinder.lineOfSight(
start = source,
dest = destination,
moverSize = size,
destWidth = sizeX,
destHeight = sizeY
start = source, dest = destination, moverSize = size, destWidth = sizeX, destHeight = sizeY
)
val path = Path()
if (!rayCast.success) {

View file

@ -6,11 +6,7 @@ import core.api.EquipmentSlot
import core.game.global.action.DoorActionHandler
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.combat.BattleState
import core.game.node.entity.combat.CombatReach
import core.game.node.entity.combat.CombatSwingHandler
import core.game.node.entity.combat.CombatMovementIntents
import core.game.node.entity.combat.CombatMovementPlanner
import core.game.node.entity.combat.*
import core.game.node.entity.combat.equipment.WeaponInterface
import core.game.node.entity.combat.spell.CombatSpell
import core.game.node.entity.combat.spell.SpellType
@ -24,16 +20,12 @@ import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.path.Pathfinder
import core.plugin.Plugin
import core.net.packet.PacketProcessor
import core.net.packet.`in`.Packet
import org.rs09.consts.Items
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import core.plugin.Plugin
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.rs09.consts.Items
import org.rsmod.game.pathfinder.flag.CollisionFlag
import kotlin.math.abs
@ -61,8 +53,7 @@ class CombatMovementTests {
assertTrue(attacker.properties.combatPulse.isAttacking)
assertTrue(
meleeReach(attacker, victim),
"Attacker should stay in melee reach while the victim is running."
meleeReach(attacker, victim), "Attacker should stay in melee reach while the victim is running."
)
}
}
@ -93,8 +84,7 @@ class CombatMovementTests {
assertTrue(
meleeReach(attacker, victim),
"Attacker should run with a target leaving melee range. " +
"attacker=${attacker.location}, victim=${victim.location}"
"Attacker should run with a target leaving melee range. " + "attacker=${attacker.location}, victim=${victim.location}"
)
}
}
@ -119,13 +109,18 @@ class CombatMovementTests {
GameWorld.Pulser.updateAll()
CombatMovementIntents.resolve()
assertFalse(attacker.walkingQueue.isRunning, "Combat movement must not persist the transient running flag.")
assertFalse(
attacker.walkingQueue.isRunning, "Combat movement must not persist the transient running flag."
)
assertFalse(attacker.walkingQueue.isRunningBoth, "Combat movement must not make the attacker run.")
victim.walkingQueue.update()
attacker.walkingQueue.update()
assertTrue(attacker.properties.combatPulse.isAttacking, "Combat should remain active while the target is moving.")
assertTrue(
attacker.properties.combatPulse.isAttacking,
"Combat should remain active while the target is moving."
)
assertEquals(-1, attacker.walkingQueue.runDir, "Combat movement must not force run when run is off.")
assertFalse(attacker.settings.isRunToggled, "Combat movement must not toggle run on.")
assertEquals(100.0, attacker.settings.runEnergy, 0.0, "Walking combat chase must not drain run energy.")
@ -158,7 +153,9 @@ class CombatMovementTests {
)
assertFalse(receivedMessage(attacker, "I can't reach that!"))
assertFalse(attacker.walkingQueue.isRunningBoth, "Waiting for the next tick must not force running.")
assertEquals(100.0, attacker.settings.runEnergy, 0.0, "Waiting for a moving target must not drain run energy.")
assertEquals(
100.0, attacker.settings.runEnergy, 0.0, "Waiting for a moving target must not drain run energy."
)
}
}
}
@ -188,8 +185,7 @@ class CombatMovementTests {
assertTrue(
meleeReach(attacker, victim),
"Attacker should mirror a live run-click on the tick it is queued. " +
"attacker=${attacker.location}, victim=${victim.location}"
"Attacker should mirror a live run-click on the tick it is queued. " + "attacker=${attacker.location}, victim=${victim.location}"
)
}
}
@ -326,11 +322,7 @@ class CombatMovementTests {
attacker.attack(victim)
queueWalk(victim, predictedVictim)
RegionManager.addClippingFlag(
predictedVictim.z,
predictedVictim.x,
predictedVictim.y,
false,
Pathfinder.PREVENT_NORTH
predictedVictim.z, predictedVictim.x, predictedVictim.y, false, Pathfinder.PREVENT_NORTH
)
RegionManager.addClippingFlag(
predictedVictim.z,
@ -359,9 +351,7 @@ class CombatMovementTests {
assertNotEquals(
origin,
attacker.location,
"Melee pressure should not stay on a clipped side of the moving target. " +
CombatMovementIntents.lastResolveSummary() +
" queue=${attacker.walkingQueue.queue.size}"
"Melee pressure should not stay on a clipped side of the moving target. " + CombatMovementIntents.lastResolveSummary() + " queue=${attacker.walkingQueue.queue.size}"
)
assertTrue(
attacker.properties.combatPulse.isAttacking,
@ -369,16 +359,11 @@ class CombatMovementTests {
)
assertTrue(
meleeReach(attacker, victim),
"Attacker should end the tick on an attackable side of the moving target. " +
"attacker=${attacker.location}, victim=${victim.location}"
"Attacker should end the tick on an attackable side of the moving target. " + "attacker=${attacker.location}, victim=${victim.location}"
)
} finally {
RegionManager.removeClippingFlag(
predictedVictim.z,
predictedVictim.x,
predictedVictim.y,
false,
Pathfinder.PREVENT_NORTH
predictedVictim.z, predictedVictim.x, predictedVictim.y, false, Pathfinder.PREVENT_NORTH
)
RegionManager.removeClippingFlag(
predictedVictim.z,
@ -398,8 +383,7 @@ class CombatMovementTests {
TestUtils.getMockPlayer("combat_overlap_player_escape").use { player ->
val origin = arenaOrigin()
val blockedTiles = listOf(
origin.transform(0, 1, 0),
origin.transform(1, 0, 0)
origin.transform(0, 1, 0), origin.transform(1, 0, 0)
)
place(player, origin)
configureMelee(player)
@ -420,8 +404,7 @@ class CombatMovementTests {
assertTrue(
player.location == origin.transform(-1, 0, 0) || player.location == origin.transform(0, -1, 0),
"An overlapped melee player should step to the open west/south side instead of stopping. " +
"player=${player.location}, npc=${npc.location}"
"An overlapped melee player should step to the open west/south side instead of stopping. " + "player=${player.location}, npc=${npc.location}"
)
assertTrue(CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player)))
assertTrue(player.properties.combatPulse.isAttacking)
@ -439,8 +422,7 @@ class CombatMovementTests {
TestUtils.getMockPlayer("combat_overlap_npc_target").use { player ->
val origin = arenaOrigin()
val blockedTiles = listOf(
origin.transform(0, 1, 0),
origin.transform(1, 0, 0)
origin.transform(0, 1, 0), origin.transform(1, 0, 0)
)
place(player, origin)
configureMelee(player)
@ -457,8 +439,7 @@ class CombatMovementTests {
assertTrue(
npc.location == origin.transform(-1, 0, 0) || npc.location == origin.transform(0, -1, 0),
"An overlapped dumb melee NPC should try the open west/south side, not only north/east. " +
"npc=${npc.location}, player=${player.location}"
"An overlapped dumb melee NPC should try the open west/south side, not only north/east. " + "npc=${npc.location}, player=${player.location}"
)
assertTrue(CombatReach.canMelee(npc, player, CombatReach.meleeDistance(npc)))
assertTrue(npc.properties.combatPulse.isAttacking)
@ -475,8 +456,7 @@ class CombatMovementTests {
TestUtils.getMockPlayer("combat_overlap_mutual_player").use { player ->
val origin = arenaOrigin()
val blockedTiles = listOf(
origin.transform(0, 1, 0),
origin.transform(1, 0, 0)
origin.transform(0, 1, 0), origin.transform(1, 0, 0)
)
place(player, origin)
configureMelee(player)
@ -499,8 +479,7 @@ class CombatMovementTests {
assertTrue(player.location != origin || npc.location != origin)
assertTrue(
player.location == origin || npc.location == origin,
"When both overlapped actors are attacking, one side stepping out is enough. " +
"player=${player.location}, npc=${npc.location}"
"When both overlapped actors are attacking, one side stepping out is enough. " + "player=${player.location}, npc=${npc.location}"
)
assertTrue(CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player)))
assertTrue(CombatReach.canMelee(npc, player, CombatReach.meleeDistance(npc)))
@ -563,9 +542,7 @@ class CombatMovementTests {
assertTrue(player.properties.combatPulse.isAttacking)
assertTrue(
player.properties.combatPulse.getNextAttack() > -1,
"Run-enabled melee chase should create an attack opportunity before the walking NPC stops. " +
"player=${player.location}, npc=${npc.location}, " +
CombatMovementIntents.lastResolveSummary()
"Run-enabled melee chase should create an attack opportunity before the walking NPC stops. " + "player=${player.location}, npc=${npc.location}, " + CombatMovementIntents.lastResolveSummary()
)
} finally {
npc.clear()
@ -580,8 +557,7 @@ class CombatMovementTests {
val origin = openHorizontalOrigin()
val npcLocation = origin.transform(2, 0, 0)
val blockedTiles = listOf(
npcLocation.transform(0, 1, 0),
npcLocation.transform(0, -1, 0)
npcLocation.transform(0, 1, 0), npcLocation.transform(0, -1, 0)
)
place(player, origin)
configureMelee(player)
@ -594,11 +570,7 @@ class CombatMovementTests {
configureMelee(npc)
blockMovementTiles(blockedTiles)
RegionManager.addClippingFlag(
npcLocation.z,
npcLocation.x,
npcLocation.y,
true,
CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER
npcLocation.z, npcLocation.x, npcLocation.y, true, CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER
)
player.attack(npc)
@ -609,17 +581,12 @@ class CombatMovementTests {
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()}"
"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, player.location, "The player must not run onto the NPC footprint.")
} finally {
RegionManager.removeClippingFlag(
npcLocation.z,
npcLocation.x,
npcLocation.y,
true,
CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER
npcLocation.z, npcLocation.x, npcLocation.y, true, CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER
)
unblockMovementTiles(blockedTiles)
npc.clear()
@ -728,8 +695,7 @@ class CombatMovementTests {
assertEquals(seers, player.location)
assertFalse(
player.properties.combatPulse.isAttacking,
"Teleporting should discard the previous combat target."
player.properties.combatPulse.isAttacking, "Teleporting should discard the previous combat target."
)
} finally {
npc.clear()
@ -750,7 +716,9 @@ class CombatMovementTests {
val duck = stationaryNpc(46, duckLocation)
try {
assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.")
assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.")
assertFalse(
RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water."
)
val startDistance = start.getDistance(duck.location)
player.attack(duck)
@ -799,7 +767,9 @@ class CombatMovementTests {
val duck = stationaryNpc(46, duckLocation)
try {
assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.")
assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.")
assertFalse(
RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water."
)
player.attack(duck)
TestUtils.advanceTicks(12, false)
@ -807,8 +777,11 @@ class CombatMovementTests {
assertTrue(player.properties.combatPulse.isAttacking)
assertTrue(
magicReach(player, duck),
"Autocast should stop on a projectile-clear tile instead of dancing within blocked range. " +
"player=${player.location}, duck=${duck.location}, distance=${player.location.getDistance(duck.location)}"
"Autocast should stop on a projectile-clear tile instead of dancing within blocked range. " + "player=${player.location}, duck=${duck.location}, distance=${
player.location.getDistance(
duck.location
)
}"
)
assertFalse(receivedMessage(player, "I can't reach that!"))
} finally {
@ -830,16 +803,20 @@ class CombatMovementTests {
val duck = stationaryNpc(46, duckLocation)
try {
assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.")
assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.")
assertFalse(
RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water."
)
player.attack(duck)
TestUtils.advanceTicks(18, false)
assertFalse(
player.properties.combatPulse.isAttacking && !magicReach(player, duck),
"Autocast should not stay active while standing in projectile-blocked spell range. " +
"player=${player.location}, duck=${duck.location}, distance=${player.location.getDistance(duck.location)}, " +
"projectile=${CombatSwingHandler.isProjectileClipped(player, duck, false)}"
"Autocast should not stay active while standing in projectile-blocked spell range. " + "player=${player.location}, duck=${duck.location}, distance=${
player.location.getDistance(
duck.location
)
}, " + "projectile=${CombatSwingHandler.isProjectileClipped(player, duck, false)}"
)
if (!player.properties.combatPulse.isAttacking) {
assertTrue(receivedMessage(player, "I can't reach that!"))
@ -871,7 +848,9 @@ class CombatMovementTests {
val duck = stationaryNpc(46, duckLocation)
try {
assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.")
assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.")
assertFalse(
RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water."
)
val startDistance = start.getDistance(duck.location)
player.attack(duck)
@ -894,9 +873,9 @@ class CombatMovementTests {
assertFalse(player.properties.combatPulse.isAttacking)
assertTrue(
receivedMessage(player, "I can't reach that!"),
"Melee combat should report unreachable after pathing. " +
"location=${player.location}, distance=${player.location.getDistance(duck.location)}, " +
"isAttacking=${player.properties.combatPulse.isAttacking}"
"Melee combat should report unreachable after pathing. " + "location=${player.location}, distance=${
player.location.getDistance(duck.location)
}, " + "isAttacking=${player.properties.combatPulse.isAttacking}"
)
assertTrue(
player.location.getDistance(duck.location) <= startDistance,
@ -1085,8 +1064,7 @@ class CombatMovementTests {
TestUtils.getMockPlayer("combat_safespot_target").use { player ->
val origin = arenaOrigin()
val blockedTiles = listOf(
origin.transform(1, 0, 0),
origin.transform(0, -1, 0)
origin.transform(1, 0, 0), origin.transform(0, -1, 0)
)
place(player, origin.transform(4, 0, 0))
@ -1151,13 +1129,88 @@ class CombatMovementTests {
}
}
@Test
fun dumbMeleeNpcShouldNotSidestepAroundImmediateSafespotBlocker() {
TestUtils.getMockPlayer("combat_cardinal_north_safespot_target").use { player ->
val origin = arenaOrigin()
val blocker = origin.transform(0, 1, 0)
val npcStart = origin
place(player, origin.transform(0, 4, 0))
val npc = NPC.create(100, npcStart)
npc.init()
try {
configureMelee(npc)
blockMovementTiles(listOf(blocker))
npc.attack(player)
CombatMovementIntents.clear()
CombatMovementIntents.request(npc, player)
CombatMovementIntents.resolve()
npc.walkingQueue.update()
assertEquals(
npcStart,
npc.location,
"A dumb NPC should not route around a blocked next tile on the direct path to the player."
)
assertTrue(npc.properties.combatPulse.isAttacking)
} finally {
unblockMovementTiles(listOf(blocker))
npc.clear()
CombatMovementIntents.clear()
}
}
}
@Test
fun varrockGuardShouldStepAroundBakeryStallAfterRangedTargetRunsNorthEast() {
TestUtils.getMockPlayer("combat_varrock_bakery_target").use { player ->
val spawn = Location.create(2651, 3307, 0)
val originalTargetLocation = Location.create(2657, 3309, 0)
val guardStart = Location.create(2656, 3309, 0)
val targetDestination = Location.create(2658, 3311, 0)
place(player, originalTargetLocation)
configureRanged(player)
enableRun(player)
val npc = NPC.create(32, guardStart)
npc.init()
npc.properties.spawnLocation = spawn
try {
configureMelee(npc)
npc.attack(player)
queueRunPath(
player, listOf(
Location.create(2658, 3309, 0), Location.create(2658, 3310, 0), targetDestination
)
)
TestUtils.advanceTicks(8, false)
assertNotEquals(
guardStart,
npc.location,
"The Varrock guard should not stall west of the player when the bakery stall blocks north. " + "guard=${npc.location}, player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}"
)
assertTrue(
npc.location.getDistance(player.location) < originalTargetLocation.getDistance(targetDestination),
"The guard should keep taking local dumb steps toward the running target without full smart routing. " + "guard=${npc.location}, player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}"
)
assertTrue(npc.properties.combatPulse.isAttacking)
} finally {
npc.clear()
CombatMovementIntents.clear()
}
}
}
@Test
fun dumbMeleeNpcShouldNotRouteAroundWhenBothCornerSidesAreBlocked() {
TestUtils.getMockPlayer("combat_blocked_diagonal_corner_target").use { player ->
val origin = arenaOrigin()
val blockedTiles = listOf(
origin.transform(0, 1, 0),
origin.transform(-1, 0, 0)
origin.transform(0, 1, 0), origin.transform(-1, 0, 0)
)
val npcStart = origin.transform(-1, 1, 0)
place(player, origin)
@ -1195,10 +1248,7 @@ class CombatMovementTests {
val obstacle = origin.transform(1, 0, 0)
val playerEnd = origin.transform(2, 0, 0)
val playerPath = listOf(
origin.transform(0, 1, 0),
origin.transform(1, 1, 0),
origin.transform(2, 1, 0),
playerEnd
origin.transform(0, 1, 0), origin.transform(1, 1, 0), origin.transform(2, 1, 0), playerEnd
)
place(player, origin)
enableRun(player)
@ -1242,8 +1292,8 @@ class CombatMovementTests {
val door = RegionManager.getObject(doorLocation.z, doorLocation.x, doorLocation.y, 36846)
?: throw AssertionError("Expected Lumbridge house door 36846 at $doorLocation.")
assertEquals(1, door.rotation, "Expected the Lumbridge house door to face east.")
val doorConfig = DoorConfigLoader.forId(door.id)
?: throw AssertionError("Expected door config for ${door.id}.")
val doorConfig =
DoorConfigLoader.forId(door.id) ?: throw AssertionError("Expected door config for ${door.id}.")
val openedDoorLocation = door.location.transform(0, 1, 0)
DoorActionHandler.open(door, null, doorConfig.replaceId, -1, true, -1, doorConfig.isFence)
@ -1265,10 +1315,7 @@ class CombatMovementTests {
} finally {
npc.clear()
val openedDoor = RegionManager.getObject(
openedDoorLocation.z,
openedDoorLocation.x,
openedDoorLocation.y,
doorConfig.replaceId
openedDoorLocation.z, openedDoorLocation.x, openedDoorLocation.y, doorConfig.replaceId
)
if (openedDoor != null) {
SceneryBuilder.replace(openedDoor, door)
@ -1284,8 +1331,7 @@ class CombatMovementTests {
val start = Location.create(3252, 3227, 0)
val npcLocation = Location.create(3253, 3226, 0)
val expectedAttackTiles = setOf(
Location.create(3252, 3226, 0),
Location.create(3253, 3227, 0)
Location.create(3252, 3226, 0), Location.create(3253, 3227, 0)
)
place(player, start)
configureMelee(player)
@ -1307,8 +1353,7 @@ class CombatMovementTests {
assertTrue(
player.location in expectedAttackTiles,
"Player should step to the nearest cardinal attack tile at the ruin corner, not route away. " +
"player=${player.location}, npc=${npc.location}, ${CombatMovementIntents.lastResolveSummary()}"
"Player should step to the nearest cardinal attack tile at the ruin corner, not route away. " + "player=${player.location}, npc=${npc.location}, ${CombatMovementIntents.lastResolveSummary()}"
)
assertTrue(meleeReach(player, npc))
assertTrue(player.properties.combatPulse.isAttacking)
@ -1362,8 +1407,7 @@ class CombatMovementTests {
private fun configureMelee(entity: Entity) {
entity.properties.attackStyle = WeaponInterface.AttackStyle(
WeaponInterface.STYLE_AGGRESSIVE,
WeaponInterface.BONUS_CRUSH
WeaponInterface.STYLE_AGGRESSIVE, WeaponInterface.BONUS_CRUSH
)
entity.properties.combatPulse.updateStyle()
}
@ -1372,8 +1416,7 @@ class CombatMovementTests {
player.equipment.replace(Item(6724), EquipmentSlot.WEAPON.ordinal)
player.equipment.replace(Item(Items.BRONZE_ARROW_882, 100), EquipmentSlot.AMMO.ordinal)
player.properties.attackStyle = WeaponInterface.AttackStyle(
WeaponInterface.STYLE_RANGE_ACCURATE,
WeaponInterface.BONUS_RANGE
WeaponInterface.STYLE_RANGE_ACCURATE, WeaponInterface.BONUS_RANGE
)
player.properties.combatPulse.updateStyle()
}
@ -1384,8 +1427,7 @@ class CombatMovementTests {
player.skills.setLevel(Skills.MAGIC, 99)
player.properties.autocastSpell = TestAutocastSpell
player.properties.attackStyle = WeaponInterface.AttackStyle(
WeaponInterface.STYLE_CAST,
WeaponInterface.BONUS_MAGIC
WeaponInterface.STYLE_CAST, WeaponInterface.BONUS_MAGIC
)
player.properties.combatPulse.updateStyle()
}
@ -1445,8 +1487,11 @@ class CombatMovementTests {
}
private fun magicReach(attacker: Entity, victim: Entity): Boolean {
return attacker.location.getDistance(victim.getClosestOccupiedTile(attacker.location)) <= 10.0 &&
CombatSwingHandler.isProjectileClipped(attacker, victim, false)
return attacker.location.getDistance(victim.getClosestOccupiedTile(attacker.location)) <= 10.0 && CombatSwingHandler.isProjectileClipped(
attacker,
victim,
false
)
}
private object TestAutocastSpell : CombatSpell() {
@ -1488,8 +1533,6 @@ class CombatMovementTests {
}
}
private val movementBlockFlag = Pathfinder.PREVENT_NORTH or
Pathfinder.PREVENT_EAST or
Pathfinder.PREVENT_SOUTH or
Pathfinder.PREVENT_WEST
private val movementBlockFlag =
Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST
}

View file

@ -1,8 +1,8 @@
package core
import TestUtils
import content.global.handlers.scenery.BankBoothListener
import content.global.handlers.npc.NPCTalkListener
import content.global.handlers.scenery.BankBoothListener
import content.global.skill.gather.GatheringSkillOptionListeners
import content.global.skill.gather.woodcutting.WoodcuttingListener
import content.region.misthalin.varrock.dialogue.GrandExchangeClerk
@ -11,25 +11,22 @@ import core.api.log
import core.cache.def.impl.NPCDefinition
import core.game.dialogue.DialogueInterpreter
import core.game.interaction.*
import core.game.node.scenery.Scenery
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import core.game.node.Node
import core.game.node.entity.impl.PulseType
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.scenery.Scenery
import core.game.world.GameWorld
import core.game.world.map.Region
import core.game.world.map.Direction
import core.game.world.map.Location
import core.game.world.map.RegionManager
import core.game.world.map.path.ClipMaskSupplier
import core.game.world.map.path.Pathfinder
import core.net.packet.PacketProcessor
import core.plugin.ClassScanner
import core.game.world.map.path.RsmodPathfinder
import core.plugin.Plugin
import core.tools.Log
import org.rs09.consts.NPCs
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.rs09.consts.Scenery as SceneryIds
class PathfinderTests {
@ -40,31 +37,42 @@ class PathfinderTests {
WoodcuttingListener().defineListeners()
BankBoothListener().defineListeners()
}
val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0)
}
@Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
@Test
fun rsmodPathfinderShouldRejectDestinationsAtTheTruncationLimit() {
val start = Location.create(3165, 3218, 0)
Assertions.assertTrue(RsmodPathfinder.canAttempt(start, Location.create(3203, 3186, 0)))
Assertions.assertFalse(RsmodPathfinder.canAttempt(start, Location.create(3203, 3185, 0)))
}
@Test
fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() {
//clay fireplace - 13609 - sizex: 1, sizey: 2
val scenery = Scenery(13609, Location.create(50, 50, 0))
scenery.rotation = 0
val occupiedAt0 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50,51)), occupiedAt0)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 51)), occupiedAt0)
scenery.rotation = 1
val occupiedAt1 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(51,50)), occupiedAt1)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(51, 50)), occupiedAt1)
scenery.rotation = 2
val occupiedAt2 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(50,49)), occupiedAt2)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 49)), occupiedAt2)
scenery.rotation = 3
val occupiedAt3 = scenery.occupiedTiles.toTypedArray()
Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(49,50)), occupiedAt3)
Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(49, 50)), occupiedAt3)
}
@Test fun smartPathfinderShouldRespectSuppliedClipMask() {
@Test
fun smartPathfinderShouldRespectSuppliedClipMask() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
val blockedDestination = ClipMaskSupplier { _, x, y ->
@ -75,25 +83,42 @@ class PathfinderTests {
Assertions.assertTrue(path.isSuccessful)
Assertions.assertEquals(
Location.create(3201, 3200, 0),
Location.create(path.points.last.x, path.points.last.y, 0)
Location.create(3201, 3200, 0), Location.create(path.points.last.x, path.points.last.y, 0)
)
}
@Test fun dumbPathfinderShouldUseRsmodRouting() {
@Test
fun dumbPathfinderShouldNotRouteAroundBlockedCardinalStep() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
val blockedMiddle = ClipMaskSupplier { _, x, y ->
if (x == 3201 && y == 3200) 0x100 else 0
if (x == 3201 && y == 3200) movementBlockFlag else 0
}
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertTrue(path.points.isNotEmpty())
Assertions.assertFalse(path.isSuccessful)
Assertions.assertTrue(path.points.isEmpty())
}
@Test fun walkingQueueHasPathShouldIgnoreResetAnchor() {
@Test
fun dumbPathfinderShouldUseAxisFallbackForBlockedDiagonalStep() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3202, 0)
val blockedNorth = ClipMaskSupplier { _, x, y ->
if (x == 3200 && y == 3201) movementBlockFlag else 0
}
val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedNorth)
Assertions.assertTrue(path.isSuccessful)
Assertions.assertTrue(path.points.isNotEmpty())
Assertions.assertEquals(3201, path.points.first.x)
Assertions.assertEquals(3200, path.points.first.y)
}
@Test
fun walkingQueueHasPathShouldIgnoreResetAnchor() {
TestUtils.getMockPlayer("walkingQueueAnchor").use { player ->
val start = Location.create(3200, 3200, 0)
player.location = start
@ -107,7 +132,8 @@ class PathfinderTests {
}
}
@Test fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
@Test
fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
val start = Location.create(3200, 3200, 0)
val dest = Location.create(3202, 3200, 0)
RegionManager.loadClippingWindow(start, 128)
@ -125,7 +151,8 @@ class PathfinderTests {
}
}
@Test fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() {
@Test
fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
@ -140,16 +167,14 @@ class PathfinderTests {
}
}
@Test fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() {
@Test
fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("bankBoothCollectApproach").use { p ->
val (booth, approach) = findReachableBankBoothFixture()
p.location = approach
var collected = false
InteractionListeners.addMetadata(
booth.id,
IntType.SCENERY,
arrayOf("collect"),
InteractionListener.InteractionMetadata({ _, _, _ ->
booth.id, IntType.SCENERY, arrayOf("collect"), InteractionListener.InteractionMetadata({ _, _, _ ->
collected = true
true
}, 1, false)
@ -166,10 +191,11 @@ class PathfinderTests {
}
}
@Test fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() {
@Test
fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() {
TestUtils.getMockPlayer("objectPulseApproach").use { p ->
val tree = RegionManager.getObject(0, 2720, 3475, 1307)
?: throw AssertionError("Expected test tree object.")
val tree =
RegionManager.getObject(0, 2720, 3475, 1307) ?: throw AssertionError("Expected test tree object.")
val approach = findReachableApproachTile(tree)
var pulsed = false
p.location = approach
@ -186,7 +212,8 @@ class PathfinderTests {
}
}
@Test fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() {
@Test
fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() {
TestUtils.getMockPlayer("taverleyPatchChildPath").use { p ->
val wrapper = RegionManager.getObject(0, 2935, 3437, 8388)
?: throw AssertionError("Expected Taverley tree patch wrapper.")
@ -206,7 +233,8 @@ class PathfinderTests {
}
}
@Test fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() {
@Test
fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() {
TestUtils.getMockPlayer("bankerOverrideApproach").use { p ->
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
@ -226,7 +254,8 @@ class PathfinderTests {
}
}
@Test fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() {
@Test
fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() {
TestUtils.getMockPlayer("movingNpcDiagonalApproach").use { p ->
val origin = Location.create(3200, 3600, 0)
val npc = NPC.create(0, origin.transform(1, 1, 0))
@ -250,8 +279,7 @@ class PathfinderTests {
TestUtils.advanceTicks(1, false)
Assertions.assertNull(
pulseLocation,
"A moving entity interaction must not trigger from a diagonal non-interaction tile."
pulseLocation, "A moving entity interaction must not trigger from a diagonal non-interaction tile."
)
repeat(8) {
@ -276,8 +304,7 @@ class PathfinderTests {
0,
actualPulseLocation.z,
null
),
"Entity interaction must fire only from a currently valid interaction tile."
), "Entity interaction must fire only from a currently valid interaction tile."
)
} finally {
npc.clear()
@ -285,7 +312,67 @@ class PathfinderTests {
}
}
@Test fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() {
@Test
fun runEnabledEntityMovementPulseShouldCatchWalkingNpcMovingDirectlyAway() {
TestUtils.getMockPlayer("runNpcInteractionChaser").use { p ->
val origin = openHorizontalInteractionOrigin()
val npc = NPC.create(0, origin.transform(4, 0, 0))
npc.init()
p.location = origin
p.settings.runEnergy = 100.0
p.settings.setRunToggled(true)
npc.walkingQueue.reset(false)
npc.walkingQueue.addPath(origin.transform(12, 0, 0).x, origin.transform(12, 0, 0).y)
var pulseLocation: Location? = null
var pulseTargetLocation: Location? = null
try {
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
override fun pulse(): Boolean {
pulseLocation = p.location
pulseTargetLocation = npc.location
return true
}
})
repeat(6) {
if (pulseLocation == null) {
TestUtils.advanceTicks(1, false)
}
}
val actualPulseLocation = pulseLocation ?: throw AssertionError(
"Run-enabled player should catch the walking NPC before it stops. " + "player=${p.location}, npc=${npc.location}, queue=${p.walkingQueue.queue}"
)
val actualTargetLocation = pulseTargetLocation
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
Assertions.assertNotEquals(
origin.transform(12, 0, 0),
actualTargetLocation,
"The interaction should not wait until the NPC finishes walking away."
)
Assertions.assertTrue(
Pathfinder.canInteract(
actualPulseLocation.x,
actualPulseLocation.y,
p.size(),
actualTargetLocation.x,
actualTargetLocation.y,
npc.size(),
npc.size(),
0,
actualPulseLocation.z,
null
), "Entity interaction must fire from a currently valid interaction tile."
)
} finally {
npc.clear()
}
}
}
@Test
fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
@ -328,7 +415,8 @@ class PathfinderTests {
}
}
@Test fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() {
@Test
fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() {
GrandExchangePlugin().newInstance(null)
if (!DialogueInterpreter.contains(6528)) {
GrandExchangeClerk().init()
@ -361,9 +449,7 @@ class PathfinderTests {
val base = ServerConstants.HOME_LOCATION!!.transform(8, 8, 0)
for (rotation in 0..3) {
val booth = Scenery(SceneryIds.BANK_BOOTH_2213, base, 10, rotation)
runCatching { findReachableApproachTile(booth) }
.getOrNull()
?.let { return booth to it }
runCatching { findReachableApproachTile(booth) }.getOrNull()?.let { return booth to it }
}
throw AssertionError("Could not find a reachable synthetic bank booth fixture.")
}
@ -392,7 +478,8 @@ class PathfinderTests {
throw AssertionError("Could not find a reachable approach tile for $scenery.")
}
@Test fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
@Test
fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() {
val start = Location.create(2731, 3481)
val dest = RegionManager.getObject(0, 2720, 3475, 1307)
val p = TestUtils.getMockPlayer("treefindtest")
@ -404,15 +491,17 @@ class PathfinderTests {
Assertions.assertEquals(Location.create(2722, 3475, 0), p.location)
}
@Test fun movementInteractionShouldTrigger() {
@Test
fun movementInteractionShouldTrigger() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.init()
var intListenerRan = false
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = {player: Player, node: Node ->
intListenerRan = true
return@add true
})
InteractionListeners.add(
0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = { player: Player, node: Node ->
intListenerRan = true
return@add true
})
var pluginRan = false
val option = Option("testoption", 4)
@ -422,6 +511,7 @@ class PathfinderTests {
NPCDefinition.forId(0).handlers["option:testoption"] = this
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
pluginRan = true
return true
@ -432,7 +522,7 @@ class PathfinderTests {
npc.interaction.set(option2)
option.handler = testHandler
TestUtils.getMockPlayer("interactionTest").use {p ->
TestUtils.getMockPlayer("interactionTest").use { p ->
p.location = ServerConstants.HOME_LOCATION
TestUtils.simulateInteraction(p, npc, 0)
TestUtils.advanceTicks(10, false)
@ -444,8 +534,9 @@ class PathfinderTests {
}
}
@Test fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
TestUtils.getMockPlayer("idlenpcdest").use {p ->
@Test
fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() {
TestUtils.getMockPlayer("idlenpcdest").use { p ->
val startLoc = ServerConstants.HOME_LOCATION
p.location = startLoc
val npc = NPC.create(0, NPC_TEST_LOC)
@ -461,8 +552,9 @@ class PathfinderTests {
}
}
@Test fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
TestUtils.getMockPlayer("entitystoptest").use {p ->
@Test
fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() {
TestUtils.getMockPlayer("entitystoptest").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
@ -478,7 +570,8 @@ class PathfinderTests {
}
}
@Test fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
@Test
fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() {
TestUtils.getMockPlayer("entitystoptest2").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
@ -495,7 +588,8 @@ class PathfinderTests {
}
}
@Test fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
@Test
fun doubleMovementPulseToEntityShouldNotStopOnSameTile() {
TestUtils.getMockPlayer("entitystoptest3").use { p ->
p.location = ServerConstants.HOME_LOCATION
val npc = NPC.create(0, NPC_TEST_LOC)
@ -518,12 +612,14 @@ class PathfinderTests {
}
}
@Test fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
@Test
fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() {
val testHandler = object : OptionHandler() {
override fun newInstance(arg: Any?): Plugin<Any> {
NPCDefinition.forId(0).handlers["option:testoption"] = this
return this
}
override fun handle(player: Player?, node: Node?, option: String?): Boolean {
log(this::class.java, Log.ERR, "Interaction triggered")
return true
@ -547,14 +643,16 @@ class PathfinderTests {
}
}
@Test fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
@Test
fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() {
val npc = NPC.create(0, NPC_TEST_LOC)
npc.isNeverWalks = true
npc.init()
InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = {player: Player, node: Node ->
return@add true
})
InteractionListeners.add(
0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = { player: Player, node: Node ->
return@add true
})
val opt = Option("testoptlistener2", 1)
npc.interaction.set(opt)
@ -568,7 +666,8 @@ class PathfinderTests {
}
}
@Test fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
@Test
fun npcShouldReliablyReturnToSpawnLocationIfTooFar() {
//spawn a player into the area just to make sure it ticks...
TestUtils.getMockPlayer("areatest").use { p ->
val npc = NPC(1, Location.create(3240, 3226, 0))
@ -584,7 +683,8 @@ class PathfinderTests {
}
}
@Test fun npcReturnToSpawnShouldUseOverriddenWalkRadius() {
@Test
fun npcReturnToSpawnShouldUseOverriddenWalkRadius() {
TestUtils.getMockPlayer("overriddenRadiusReturn").use {
val spawn = ServerConstants.HOME_LOCATION!!
val npc = object : NPC(1, spawn.transform(5, 0, 0)) {
@ -606,10 +706,14 @@ class PathfinderTests {
}
}
@Test fun randomWalkingNpcShouldNotRouteAroundBlockedLocalDestination() {
@Test
fun randomWalkingNpcShouldUseSideStepWhenDirectNorthTileIsBlocked() {
val origin = Location.create(3200, 3600, 0)
val blocked = origin.transform(1, 0, 0)
val destination = origin.transform(2, 0, 0)
val blocked = origin.transform(0, 1, 0)
val destination = origin.transform(0, 2, 0)
val sidesteps = setOf(
origin.transform(-1, 0, 0), origin.transform(1, 0, 0)
)
val npc = FixedDestinationNPC(origin, destination, 3)
npc.isWalks = true
npc.isNeverWalks = false
@ -619,22 +723,66 @@ class PathfinderTests {
try {
npc.resetWalk()
repeat(20) {
npc.handleTickActions()
if (!npc.walkingQueue.hasPath()) {
npc.handleTickActions()
}
}
Assertions.assertFalse(
Assertions.assertTrue(
npc.walkingQueue.hasPath(),
"Random-walking NPCs should not take an RSMOD detour around a clipped boundary tile."
"Random-walking NPCs should use an open east/west sidestep when the direct north tile is blocked."
)
npc.walkingQueue.update()
Assertions.assertEquals(origin, npc.location)
Assertions.assertTrue(
npc.location in sidesteps,
"Random-walking NPC should only take a local sidestep, not route through the blocked north tile. " + "npc=${npc.location}"
)
} finally {
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
npc.clear()
}
}
@Test fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
@Test
fun randomWalkingNpcShouldNotFullyRouteAroundBlockedLocalDestination() {
val origin = Location.create(3200, 3600, 0)
val blocked = origin.transform(1, 0, 0)
val destination = origin.transform(2, 0, 0)
val sidesteps = setOf(
origin.transform(0, -1, 0), origin.transform(0, 1, 0)
)
val npc = FixedDestinationNPC(origin, destination, 3)
npc.isWalks = true
npc.isNeverWalks = false
npc.init()
npc.properties.spawnLocation = origin
RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
try {
npc.resetWalk()
repeat(20) {
if (!npc.walkingQueue.hasPath()) {
npc.handleTickActions()
}
}
Assertions.assertTrue(
npc.walkingQueue.hasPath(),
"Random-walking NPCs should use a local sidestep instead of taking an RSMOD detour."
)
npc.walkingQueue.update()
Assertions.assertTrue(
npc.location in sidesteps,
"Random-walking NPC should not fully route around a clipped boundary tile. npc=${npc.location}"
)
Assertions.assertNotEquals(destination, npc.location)
} finally {
RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag)
npc.clear()
}
}
@Test
fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() {
//spawn a player into the area just to make sure it ticks...
TestUtils.getMockPlayer("areaunloadtest").use { p ->
val npc = NPC(1, Location.create(3240, 3226, 0))
@ -654,9 +802,7 @@ class PathfinderTests {
}
private class FixedDestinationNPC(
location: Location,
private val destination: Location,
private val radius: Int
location: Location, private val destination: Location, private val radius: Int
) : NPC(1, location) {
override fun getMovementDestination(): Location {
return destination
@ -667,8 +813,32 @@ class PathfinderTests {
}
}
private val movementBlockFlag = Pathfinder.PREVENT_NORTH or
Pathfinder.PREVENT_EAST or
Pathfinder.PREVENT_SOUTH or
Pathfinder.PREVENT_WEST
private val movementBlockFlag =
Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST
private fun openHorizontalInteractionOrigin(): Location {
val start = Location.create(3200, 3600, 0)
for (dy in -16..16) {
for (dx in -16..16) {
val candidate = start.transform(dx, dy, 0)
if ((0..12).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) } && (0..11).all {
Pathfinder.canInteract(
candidate.x + it,
candidate.y,
1,
candidate.x + it + 1,
candidate.y,
1,
1,
0,
candidate.z,
null
)
}) {
return candidate
}
}
}
throw AssertionError("No open horizontal interaction test line found near $start.")
}
}