From 5857cba85119361bb9a45c9ff1dc0d8b5c4a61df Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sat, 20 Jun 2026 17:24:28 +0300 Subject: [PATCH] Some code reformatting with ktfmt (Kotlinlang) --- .../entity/combat/CombatMovementIntents.kt | 809 +++++++++++++----- .../entity/combat/CombatMovementPlanner.kt | 71 +- .../game/node/entity/combat/CombatReach.kt | 128 ++- .../game/world/map/path/RsmodPathfinder.kt | 161 ++-- .../map/path/RsmodProjectilePathfinder.kt | 13 +- .../kotlin/content/CombatMovementTests.kt | 468 ++++++---- .../kotlin/content/CombatPerformanceTests.kt | 33 +- 7 files changed, 1160 insertions(+), 523 deletions(-) diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt index c01dda316..4a71b95ea 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -23,8 +23,8 @@ import kotlin.math.abs import kotlin.math.sqrt /** - * Collects combat movement requests during pulse updates and applies them before - * walking queues are ticked. + * Collects combat movement requests during pulse updates and applies them before walking queues are + * ticked. */ object CombatMovementIntents { private const val MAX_RANGED_APPROACH_CANDIDATES = 16 @@ -32,27 +32,35 @@ object CombatMovementIntents { private const val MAX_DIRECT_COMBAT_PATH_DISTANCE = 32 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 } private data class CandidatePath(val steps: List, val projectedLocation: Location) + data class ResolveReport( val intents: Int, val candidateCount: Int, val directPathHits: Int, val rsmodRouteCalls: Int, val losCalls: Int, - val slowestIntent: SlowIntent? + val slowestIntent: SlowIntent?, ) { 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}" - } ?: "" - return "combatMovementStats intents=$intents candidates=$candidateCount " + "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest" + 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}" + } ?: "" + return "combatMovementStats intents=$intents candidates=$candidateCount " + + "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest" } companion object { @@ -67,7 +75,7 @@ object CombatMovementIntents { val candidateCount: Int, val directPathHits: Int, val rsmodRouteCalls: Int, - val losCalls: Int + val losCalls: Int, ) private class IntentTrace { @@ -94,15 +102,16 @@ object CombatMovementIntents { losCalls += trace.losCalls if (elapsedNanos > slowestNanos) { slowestNanos = elapsedNanos - slowestIntent = SlowIntent( - attacker = describe(intent.attacker), - target = describe(intent.target), - elapsedMicros = elapsedNanos / 1_000, - candidateCount = trace.candidateCount, - directPathHits = trace.directPathHits, - rsmodRouteCalls = trace.rsmodRouteCalls, - losCalls = trace.losCalls - ) + slowestIntent = + SlowIntent( + attacker = describe(intent.attacker), + target = describe(intent.target), + elapsedMicros = elapsedNanos / 1_000, + candidateCount = trace.candidateCount, + directPathHits = trace.directPathHits, + rsmodRouteCalls = trace.rsmodRouteCalls, + losCalls = trace.losCalls, + ) } } @@ -113,7 +122,7 @@ object CombatMovementIntents { directPathHits = directPathHits, rsmodRouteCalls = rsmodRouteCalls, losCalls = losCalls, - slowestIntent = slowestIntent + slowestIntent = slowestIntent, ) } @@ -188,7 +197,10 @@ object CombatMovementIntents { return } - val pending = intents.values.sortedWith(compareBy { it.attacker.index }.thenBy { it.target.index }) + val pending = + intents.values.sortedWith( + compareBy { it.attacker.index }.thenBy { it.target.index } + ) intents.clear() val reservedTiles = LinkedHashSet() @@ -221,16 +233,23 @@ object CombatMovementIntents { if (attacker.locks.isMovementLocked) { return false } - val predictedTargetLocation = CombatMovementPlanner.predictedMovementLocation(target) ?: return false + val predictedTargetLocation = + CombatMovementPlanner.predictedMovementLocation(target) ?: return false if (canAttackFrom(attacker, target, attacker.location, predictedTargetLocation)) { return false } - val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location + val projectedAttackerLocation = + CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location return !canAttackFrom(attacker, target, projectedAttackerLocation, predictedTargetLocation) } private fun isActiveMeleeAttacker(attacker: Entity, target: Entity): Boolean { - if (!attacker.isActive || !target.isActive || attacker.location == null || target.location == null) { + if ( + !attacker.isActive || + !target.isActive || + attacker.location == null || + target.location == null + ) { return false } if (attacker.locks.isMovementLocked || attacker.location.z != target.location.z) { @@ -247,7 +266,7 @@ object CombatMovementIntents { intent: Intent, reservedTiles: MutableSet, projectedLocations: MutableMap, - stats: ResolveStats + stats: ResolveStats, ) { val trace = IntentTrace() val start = System.nanoTime() @@ -262,7 +281,7 @@ object CombatMovementIntents { intent: Intent, reservedTiles: MutableSet, projectedLocations: MutableMap, - trace: IntentTrace + trace: IntentTrace, ) { val attacker = intent.attacker val target = intent.target @@ -272,9 +291,15 @@ object CombatMovementIntents { val targetLocation = targetLocationFor(attacker, target) val projectedTargetLocation = projectedLocations[target] - if (projectedTargetLocation != null && canAttackFrom( - attacker, target, attacker.location, projectedTargetLocation, trace - ) + if ( + projectedTargetLocation != null && + canAttackFrom( + attacker, + target, + attacker.location, + projectedTargetLocation, + trace, + ) ) { stopWalk(attacker) face(attacker, target) @@ -282,9 +307,15 @@ object CombatMovementIntents { reserveOccupiedTiles(reservedTiles, attacker, attacker.location) return } - if (projectedTargetLocation != targetLocation && canAttackFrom( - attacker, target, attacker.location, targetLocation, trace - ) + if ( + projectedTargetLocation != targetLocation && + canAttackFrom( + attacker, + target, + attacker.location, + targetLocation, + trace, + ) ) { stopWalk(attacker) face(attacker, target) @@ -295,12 +326,25 @@ object CombatMovementIntents { val pathfinder = pathfinderFor(attacker) if (shouldUseTargetFootprintRoute(attacker, target, pathfinder)) { - val candidatePath = pathToTargetFootprint(attacker, target, targetLocation, pathfinder, trace) + val candidatePath = + pathToTargetFootprint(attacker, target, targetLocation, pathfinder, trace) if (candidatePath != null) { val attackPath = - truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace) + truncateAtFirstAttackOpportunity( + attacker, + target, + targetLocation, + candidatePath, + trace, + ) if (attackPath != null) { - if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) { + if ( + hasReservedOccupiedTile( + reservedTiles, + attacker, + attackPath.projectedLocation, + ) + ) { return } walkPath(attacker, attackPath) @@ -312,10 +356,17 @@ object CombatMovementIntents { } } - val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location - if (projectedAttackerLocation != attacker.location && canAttackFrom( - attacker, target, projectedAttackerLocation, targetLocation, trace - ) + val projectedAttackerLocation = + CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location + if ( + projectedAttackerLocation != attacker.location && + canAttackFrom( + attacker, + target, + projectedAttackerLocation, + targetLocation, + trace, + ) ) { face(attacker, target) projectedLocations[attacker] = projectedAttackerLocation @@ -323,18 +374,25 @@ object CombatMovementIntents { return } - val candidates = movementDestinationsFor(attacker, target, targetLocation, pathfinder, trace) + val candidates = + movementDestinationsFor(attacker, target, targetLocation, pathfinder, trace) val standingOnCandidate = candidates.any { it.location == attacker.location } val queueStationaryContinuation = - attacker.properties.combatPulse.style == CombatStyle.MELEE && !CombatMovementPlanner.hasMovementStepThisTick( - target - ) + 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 candidatePath = + pathTo(attacker, candidate, trace, queueStationaryContinuation) ?: continue val attackPath = - truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace) ?: continue + truncateAtFirstAttackOpportunity( + attacker, + target, + targetLocation, + candidatePath, + trace, + ) ?: continue if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) { blockedByReservation = true continue @@ -346,19 +404,30 @@ object CombatMovementIntents { reserveOccupiedTiles(reservedTiles, attacker, attackPath.projectedLocation) return } - if (!blockedByReservation && shouldStopUnreachableCombat(attacker, target, standingOnCandidate, trace)) { + if ( + !blockedByReservation && + shouldStopUnreachableCombat(attacker, target, standingOnCandidate, trace) + ) { stopUnreachableCombat(attacker) } } private fun canResolve(attacker: Entity, target: Entity): Boolean { - if (!attacker.isActive || !target.isActive || attacker.location == null || target.location == null) { + if ( + !attacker.isActive || + !target.isActive || + attacker.location == null || + target.location == null + ) { return false } if (attacker.location.z != target.location.z || attacker.locks.isMovementLocked) { return false } - if (attacker.properties.combatPulse.getVictim() !== target || !attacker.properties.combatPulse.isAttacking) { + if ( + attacker.properties.combatPulse.getVictim() !== target || + !attacker.properties.combatPulse.isAttacking + ) { return false } if (CombatMovementPlanner.exceedsCombatChaseDistance(attacker, target)) { @@ -377,7 +446,7 @@ object CombatMovementIntents { target: Entity, attackerLocation: Location, targetLocation: Location, - trace: IntentTrace? = null + trace: IntentTrace? = null, ): Boolean { if (attackerLocation.z != targetLocation.z) { return false @@ -386,33 +455,54 @@ object CombatMovementIntents { return false } return when (attacker.properties.combatPulse.style) { - CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange( - attacker, target, attackerLocation, targetLocation, trace - ) + CombatStyle.RANGE, + CombatStyle.MAGIC -> + canAttackFromRange( + attacker, + target, + attackerLocation, + targetLocation, + trace, + ) else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation, trace) } } private fun movementDestinationsFor( - attacker: Entity, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace + attacker: Entity, + target: Entity, + targetLocation: Location, + pathfinder: Pathfinder, + trace: IntentTrace, ): List { if (attacker is NPC && pathfinder === Pathfinder.DUMB) { val destinations = ArrayList(4) if (occupiedTilesOverlap(attacker, target)) { - for (location in CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)) { - destinations.add(MovementDestination(location, pathfinder, allowPartialPath = true)) + for (location in + CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)) { + destinations.add( + MovementDestination(location, pathfinder, allowPartialPath = true) + ) } } else { - destinations.add(MovementDestination(targetLocation, pathfinder, allowPartialPath = true)) + destinations.add( + MovementDestination(targetLocation, pathfinder, allowPartialPath = true) + ) if (chebyshevToFootprint(attacker, target, targetLocation) <= target.size()) { - val border = CombatMovementPlanner.borderTiles(target, targetLocation, attacker.size()) + val border = + CombatMovementPlanner.borderTiles(target, targetLocation, attacker.size()) val walkable = border.filter { RegionManager.isTeleportPermitted(it) } - val sorted = walkable.sortedWith( - compareBy { it.getDistance(attacker.location) }.thenBy { it.x }.thenBy { it.y } - ) + val sorted = + walkable.sortedWith( + compareBy { it.getDistance(attacker.location) } + .thenBy { it.x } + .thenBy { it.y } + ) for (location in sorted) { - destinations.add(MovementDestination(location, pathfinder, allowPartialPath = true)) + destinations.add( + MovementDestination(location, pathfinder, allowPartialPath = true) + ) } } } @@ -422,21 +512,29 @@ object CombatMovementIntents { val attackTiles = attackTilesFor(attacker, target, targetLocation, trace) if (attacker is Player) { - val rangedTiles = playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder, trace) + val rangedTiles = + playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder, trace) val capacity = rangedTiles.size + attackTiles.size + 1 val destinations = ArrayList(capacity) destinations.addAll(rangedTiles) for (location in attackTiles) { - destinations.add(MovementDestination(location, pathfinder, allowPartialPath = false)) - } - val targetFallback = if (!occupiedTilesOverlap(attacker, target) && shouldAllowPartialTargetPath( - attacker, target, targetLocation + destinations.add( + MovementDestination(location, pathfinder, allowPartialPath = false) ) - ) { - MovementDestination(target, pathfinder, allowPartialPath = true) - } else { - null } + val targetFallback = + if ( + !occupiedTilesOverlap(attacker, target) && + shouldAllowPartialTargetPath( + attacker, + target, + targetLocation, + ) + ) { + MovementDestination(target, pathfinder, allowPartialPath = true) + } else { + null + } if (targetFallback != null) { destinations.add(targetFallback) } @@ -453,7 +551,10 @@ object CombatMovementIntents { } private fun attackTilesFor( - attacker: Entity, target: Entity, targetLocation: Location, trace: IntentTrace + attacker: Entity, + target: Entity, + targetLocation: Location, + trace: IntentTrace, ): List { if (attacker.properties.combatPulse.style != CombatStyle.MELEE) { return CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation) @@ -461,15 +562,29 @@ object CombatMovementIntents { val borderTiles = CombatMovementPlanner.borderTiles(target, targetLocation, attacker.size()) 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 { - distanceSquared( - it, attacker.location + val attackable = candidates.filter { + canAttackFrom(attacker, target, it, targetLocation, trace) + } + return attackable + .ifEmpty { candidates } + .sortedWith( + compareBy { + distanceSquared( + it, + attacker.location, + ) + } + .thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) } + .thenBy { it.x } + .thenBy { it.y } ) - }.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y }) } - private fun shouldAllowPartialTargetPath(attacker: Player, target: Entity, targetLocation: Location): Boolean { + private fun shouldAllowPartialTargetPath( + attacker: Player, + target: Entity, + targetLocation: Location, + ): Boolean { val range = playerAttackRange(attacker) if (range <= CombatReach.meleeDistance(attacker)) { return true @@ -477,11 +592,16 @@ object CombatMovementIntents { if (CombatMovementPlanner.hasMovementStepThisTick(target)) { return true } - return distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) > range * range + return distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) > + range * range } private fun playerAttackRangeDestinations( - attacker: Player, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace + attacker: Player, + target: Entity, + targetLocation: Location, + pathfinder: Pathfinder, + trace: IntentTrace, ): List { val range = playerAttackRange(attacker) if (range <= CombatReach.meleeDistance(attacker)) { @@ -505,22 +625,32 @@ object CombatMovementIntents { private fun playerRangedAttackRange(attacker: Player): Int { var distance = 7 - val weaponInterface = attacker.getExtension(WeaponInterface::class.java) as? WeaponInterface + val weaponInterface = + attacker.getExtension(WeaponInterface::class.java) as? WeaponInterface if (weaponInterface?.weaponInterface?.interfaceId == 91) { distance -= 2 } if (attacker.properties.attackStyle.style == WeaponInterface.STYLE_LONG_RANGE) { 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)) { + 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) + ) { 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 { val tiles = ArrayList() val minX = targetLocation.x - range @@ -530,7 +660,10 @@ object CombatMovementIntents { for (x in minX..maxX) { for (y in minY..maxY) { val tile = Location.create(x, y, targetLocation.z) - if (distanceSquaredToClosestOccupiedTile(target, targetLocation, tile) > range * range) { + if ( + distanceSquaredToClosestOccupiedTile(target, targetLocation, tile) > + range * range + ) { continue } if (RegionManager.isTeleportPermitted(tile)) { @@ -538,16 +671,28 @@ object CombatMovementIntents { } } } - tiles.sortWith(compareBy { - distanceSquared( - it, attacker.location - ) - }.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y }) + tiles.sortWith( + compareBy { + distanceSquared( + it, + attacker.location, + ) + } + .thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) } + .thenBy { it.x } + .thenBy { it.y } + ) val attackTiles = ArrayList(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 + if ( + !hasProjectileLineOfSight( + tile, + attacker.size(), + target, + targetLocation, + loadWindow = false, + trace = trace, ) ) { continue @@ -565,15 +710,21 @@ object CombatMovementIntents { target: Entity, attackerLocation: Location, targetLocation: Location, - trace: IntentTrace? = null + trace: IntentTrace? = null, ): Boolean { val distance = CombatReach.meleeDistance(attacker) - if (distance == 1 && !isAdjacentToTarget(attacker, attackerLocation, target, targetLocation)) { + 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 } @@ -583,7 +734,7 @@ object CombatMovementIntents { target, targetLocation, checkClose = !CombatReach.isUsingHalberd(attacker), - trace = trace + trace = trace, ) } @@ -592,27 +743,41 @@ object CombatMovementIntents { target: Entity, attackerLocation: Location, targetLocation: Location, - trace: IntentTrace? = null + trace: IntentTrace? = null, ): Boolean { - val range = if (attacker is Player) { - playerAttackRange(attacker) - } else { - CombatReach.combatDistance( - attacker, target, if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7 - ) - } + val range = + if (attacker is Player) { + playerAttackRange(attacker) + } else { + CombatReach.combatDistance( + 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 - ) + 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( + if ( + Pathfinder.isStandingIn( attackerLocation.x - 1, attackerLocation.y + i, 1, @@ -620,12 +785,13 @@ object CombatMovementIntents { targetLocation.x, targetLocation.y, target.size(), - target.size() + target.size(), ) ) { return true } - if (Pathfinder.isStandingIn( + if ( + Pathfinder.isStandingIn( attackerLocation.x + attacker.size(), attackerLocation.y + i, 1, @@ -633,12 +799,13 @@ object CombatMovementIntents { targetLocation.x, targetLocation.y, target.size(), - target.size() + target.size(), ) ) { return true } - if (Pathfinder.isStandingIn( + if ( + Pathfinder.isStandingIn( attackerLocation.x + i, attackerLocation.y - 1, 1, @@ -646,12 +813,13 @@ object CombatMovementIntents { targetLocation.x, targetLocation.y, target.size(), - target.size() + target.size(), ) ) { return true } - if (Pathfinder.isStandingIn( + if ( + Pathfinder.isStandingIn( attackerLocation.x + i, attackerLocation.y + attacker.size(), 1, @@ -659,7 +827,7 @@ object CombatMovementIntents { targetLocation.x, targetLocation.y, target.size(), - target.size() + target.size(), ) ) { return true @@ -675,7 +843,7 @@ object CombatMovementIntents { targetLocation: Location, checkClose: Boolean = false, loadWindow: Boolean = true, - trace: IntentTrace? = null + trace: IntentTrace? = null, ): Boolean { if (trace != null) { trace.losCalls++ @@ -683,11 +851,19 @@ 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, ) } @@ -697,7 +873,11 @@ object CombatMovementIntents { return dx * dx + dy * dy } - private fun distanceSquaredToClosestOccupiedTile(entity: Entity, location: Location, from: Location): Int { + private fun distanceSquaredToClosestOccupiedTile( + entity: Entity, + location: Location, + from: Location, + ): Int { val closestX = from.x.coerceIn(location.x, location.x + entity.size() - 1) val closestY = from.y.coerceIn(location.y, location.y + entity.size() - 1) val dx = from.x - closestX @@ -705,25 +885,46 @@ object CombatMovementIntents { return dx * dx + dy * dy } - private fun chebyshevToFootprint(attacker: Entity, target: Entity, targetLocation: Location): Int { - val closestX = attacker.location.x.coerceIn(targetLocation.x, targetLocation.x + target.size() - 1) - val closestY = attacker.location.y.coerceIn(targetLocation.y, targetLocation.y + target.size() - 1) - return maxOf(kotlin.math.abs(attacker.location.x - closestX), kotlin.math.abs(attacker.location.y - closestY)) + private fun chebyshevToFootprint( + attacker: Entity, + target: Entity, + targetLocation: Location, + ): Int { + val closestX = + attacker.location.x.coerceIn(targetLocation.x, targetLocation.x + target.size() - 1) + val closestY = + attacker.location.y.coerceIn(targetLocation.y, targetLocation.y + target.size() - 1) + return maxOf( + kotlin.math.abs(attacker.location.x - closestX), + kotlin.math.abs(attacker.location.y - closestY), + ) } - private fun shouldUseTargetFootprintRoute(attacker: Entity, target: Entity, pathfinder: Pathfinder): Boolean { - if (attacker.properties.combatPulse.style != CombatStyle.MELEE || occupiedTilesOverlap(attacker, target)) { + private fun shouldUseTargetFootprintRoute( + attacker: Entity, + target: Entity, + pathfinder: Pathfinder, + ): Boolean { + if ( + attacker.properties.combatPulse.style != CombatStyle.MELEE || + occupiedTilesOverlap(attacker, target) + ) { return false } return attacker !is NPC || pathfinder !== Pathfinder.DUMB } 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 = - if (targetMoving) directPathTowardMovingTarget(attacker, target, targetLocation) else null + if (targetMoving) directPathTowardMovingTarget(attacker, target, targetLocation) + else null if (movingTargetPath != null) { trace.directPathHits++ return movingTargetPath @@ -741,59 +942,85 @@ object CombatMovementIntents { if (attacker is NPC && pathfinder === Pathfinder.DUMB) { return null } - if (pathfinder === Pathfinder.SMART && !RsmodPathfinder.canAttempt(attacker.location, targetLocation)) { + if ( + pathfinder === Pathfinder.SMART && + !RsmodPathfinder.canAttempt(attacker.location, targetLocation) + ) { return null } trace.rsmodRouteCalls++ - val clipMaskSupplier = if (attacker is NPC) { - attacker.behavior?.getClippingSupplier(attacker) - } else { - null - } - val path = pathfinder.find( - attacker.location, - attacker.size(), - targetLocation, - target.size(), - target.size(), - 0, - -1, - 0, - true, - clipMaskSupplier - ) + val clipMaskSupplier = + if (attacker is NPC) { + attacker.behavior?.getClippingSupplier(attacker) + } else { + null + } + val path = + pathfinder.find( + attacker.location, + attacker.size(), + targetLocation, + target.size(), + target.size(), + 0, + -1, + 0, + true, + clipMaskSupplier, + ) if (!path.isSuccessful || path.points.isEmpty()) { return null } - if (attacker is Player && !path.isMoveNear && isExcessiveCombatDetourToTarget( - attacker, target, targetLocation, path - ) + if ( + attacker is Player && + !path.isMoveNear && + isExcessiveCombatDetourToTarget( + attacker, + target, + targetLocation, + path, + ) ) { return null } val steps = immediateMovementSteps(attacker, path) - if (attacker is Player && path.isMoveNear && !partialPathMovesCloserToTarget( - attacker, target, targetLocation, steps - ) + if ( + attacker is Player && + path.isMoveNear && + !partialPathMovesCloserToTarget( + attacker, + target, + targetLocation, + steps, + ) ) { return null } - if (attacker is Player && steps.any { - !RegionManager.isTeleportPermitted( - Location.create( - it.x, it.y, attacker.location.z + if ( + attacker is Player && + steps.any { + !RegionManager.isTeleportPermitted( + Location.create( + it.x, + it.y, + attacker.location.z, + ) ) - ) - }) { + } + ) { return null } - val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null + val projected = + steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } + ?: return null return CandidatePath(steps, projected) } private fun directPathTowardMovingTarget( - attacker: Entity, target: Entity, targetLocation: Location + attacker: Entity, + target: Entity, + targetLocation: Location, ): CandidatePath? { if (!CombatMovementPlanner.hasMovementStepThisTick(target)) { return null @@ -806,7 +1033,14 @@ object CombatMovementIntents { var current = attacker.location while (current != targetLocation && steps.size < maxSteps) { val direction = Direction.getDirection(current, targetLocation) ?: return null - if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { + if ( + !direction.canMoveFrom( + current.z, + current.x, + current.y, + RegionManager::getClippingFlag, + ) + ) { return null } val next = current.transform(direction) @@ -826,7 +1060,11 @@ object CombatMovementIntents { return null } - private fun preferredMeleeDestinations(attacker: Entity, target: Entity, targetLocation: Location): List { + private fun preferredMeleeDestinations( + attacker: Entity, + target: Entity, + targetLocation: Location, + ): List { val directions = ArrayList(4) val attackerCenter = attacker.centerLocation val targetCenterX = targetLocation.x + (target.size() shr 1) @@ -863,7 +1101,10 @@ 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 @@ -871,29 +1112,33 @@ object CombatMovementIntents { 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.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.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 - ) + 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 - ) + else -> + Location.create( + attacker.location.x.coerceIn(minAlignedX, maxAlignedX), + targetLocation.y + target.size(), + targetLocation.z, + ) } } @@ -904,7 +1149,11 @@ object CombatMovementIntents { } 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(path.steps.size) for (step in path.steps) { @@ -921,66 +1170,107 @@ 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) + val stepLimit = + movementStepLimit(attacker, queueContinuation && !destination.allowPartialPath) 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 } trace.rsmodRouteCalls++ - val path = Pathfinder.find(attacker, destination.node, destination.allowPartialPath, destination.pathfinder) - if (!path.reaches(destination.location) && (!destination.allowPartialPath || path.points.isEmpty())) { + val path = + Pathfinder.find( + attacker, + destination.node, + destination.allowPartialPath, + destination.pathfinder, + ) + if ( + !path.reaches(destination.location) && + (!destination.allowPartialPath || path.points.isEmpty()) + ) { return null } - if (attacker is Player && !destination.allowPartialPath && isExcessiveCombatDetour( - attacker, destination, path - ) + if ( + attacker is Player && + !destination.allowPartialPath && + isExcessiveCombatDetour( + attacker, + destination, + path, + ) ) { return null } val steps = immediateMovementSteps(attacker, path, stepLimit) - if (attacker is Player && destination.allowPartialPath && !partialPathMovesCloser( - attacker, destination, steps - ) + if ( + attacker is Player && + destination.allowPartialPath && + !partialPathMovesCloser( + attacker, + destination, + steps, + ) ) { return null } - if (attacker is Player && steps.any { - !RegionManager.isTeleportPermitted( - Location.create( - it.x, it.y, attacker.location.z + if ( + attacker is Player && + steps.any { + !RegionManager.isTeleportPermitted( + Location.create( + it.x, + it.y, + attacker.location.z, + ) ) - ) - }) { + } + ) { return null } - val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null + val projected = + steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } + ?: return null return CandidatePath(steps, projected) } 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) { + if ( + attacker.size() != 1 || + destination.node !is Location || + attacker.location.z != destination.location.z + ) { return null } return directPathTo(attacker, destination.location, maxSteps) } 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 @@ -993,7 +1283,14 @@ object CombatMovementIntents { return null } val direction = Direction.getDirection(current, destination) ?: return null - if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { + if ( + !direction.canMoveFrom( + current.z, + current.x, + current.y, + RegionManager::getClippingFlag, + ) + ) { return null } val next = current.transform(direction) @@ -1013,34 +1310,56 @@ object CombatMovementIntents { } 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 = - sqrt(distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location).toDouble()) + sqrt( + distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) + .toDouble() + ) return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR } private fun partialPathMovesCloserToTarget( - attacker: Player, target: Entity, targetLocation: Location, steps: List + attacker: Player, + target: Entity, + targetLocation: Location, + steps: List, ): Boolean { - val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false + val projected = + steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } + ?: return false return distanceSquaredToClosestOccupiedTile( - target, targetLocation, projected + target, + targetLocation, + projected, ) < distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) } - private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean { + private fun isExcessiveCombatDetour( + attacker: Player, + destination: MovementDestination, + path: Path, + ): Boolean { val pathLength = (path.points.size - 1).coerceAtLeast(0) val directDistance = attacker.location.getDistance(destination.location) return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR } private fun partialPathMovesCloser( - attacker: Player, destination: MovementDestination, steps: List + attacker: Player, + destination: MovementDestination, + steps: List, ): 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) + val projected = + steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } + ?: return false + return projected.getDistance(destination.location) < + attacker.location.getDistance(destination.location) } private fun Path.reaches(destination: Location): Boolean { @@ -1052,7 +1371,9 @@ object CombatMovementIntents { } private fun immediateMovementSteps( - attacker: Entity, path: Path, maxSteps: Int = movementStepsFor(attacker) + attacker: Entity, + path: Path, + maxSteps: Int = movementStepsFor(attacker), ): List { val steps = ArrayList(maxSteps) for (point in path.points) { @@ -1071,7 +1392,10 @@ object CombatMovementIntents { if (attacker.locks.isMovementLocked) { return } - val run = attacker is Player && attacker.walkingQueue.isRunning && attacker.settings.runEnergy >= 1.0 + val run = + attacker is Player && + attacker.walkingQueue.isRunning && + attacker.settings.runEnergy >= 1.0 attacker.walkingQueue.reset(run) for (step in path.steps) { attacker.walkingQueue.addPath(step.x, step.y) @@ -1095,11 +1419,16 @@ object CombatMovementIntents { } private fun canRun(attacker: Entity): Boolean { - return attacker is Player && attacker.walkingQueue.isRunningBoth && attacker.settings.runEnergy >= 1.0 + return attacker is Player && + attacker.walkingQueue.isRunningBoth && + attacker.settings.runEnergy >= 1.0 } private fun shouldStopUnreachableCombat( - attacker: Entity, target: Entity, exhaustedLocalApproach: Boolean = false, trace: IntentTrace? = null + attacker: Entity, + target: Entity, + exhaustedLocalApproach: Boolean = false, + trace: IntentTrace? = null, ): Boolean { if (attacker !is Player) { return false @@ -1114,12 +1443,16 @@ object CombatMovementIntents { } /** - * A moving target should only keep a stalled melee chase alive while the static map - * still allows walking adjacent to it; an alternative (partial) route means the rest - * of the approach is permanently blocked (e.g. an NPC swimming in water), so waiting - * for the target to stand still before rejecting would chase it forever. + * A moving target should only keep a stalled melee chase alive while the static map still + * allows walking adjacent to it; an alternative (partial) route means the rest of the approach + * is permanently blocked (e.g. an NPC swimming in water), so waiting for the target to stand + * still before rejecting would chase it forever. */ - private fun hasMeleeRouteToTarget(attacker: Player, target: Entity, trace: IntentTrace?): Boolean { + private fun hasMeleeRouteToTarget( + attacker: Player, + target: Entity, + trace: IntentTrace?, + ): Boolean { val targetTile = target.getClosestOccupiedTile(attacker.location) if (attacker.location.getDistance(targetTile) > ServerConstants.MAX_PATHFIND_DISTANCE) { return true @@ -1140,7 +1473,11 @@ object CombatMovementIntents { } } - private fun hasReservedOccupiedTile(reservedTiles: Set, entity: Entity, location: Location): Boolean { + private fun hasReservedOccupiedTile( + reservedTiles: Set, + entity: Entity, + location: Location, + ): Boolean { if (entity.size() == 1) { return location in reservedTiles } @@ -1154,7 +1491,11 @@ object CombatMovementIntents { return false } - private fun reserveOccupiedTiles(reservedTiles: MutableSet, entity: Entity, location: Location) { + private fun reserveOccupiedTiles( + reservedTiles: MutableSet, + entity: Entity, + location: Location, + ) { if (entity.size() == 1) { reservedTiles.add(location) return @@ -1171,9 +1512,15 @@ 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 { diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt index e031833f4..29cedfeab 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt @@ -9,19 +9,19 @@ import core.game.world.map.Point import core.game.world.map.RegionManager import core.game.world.map.path.Pathfinder -/** - * Plans combat-specific chase targets without mutating walking queues. - */ +/** Plans combat-specific chase targets without mutating walking queues. */ 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), ) } @@ -31,7 +31,8 @@ object CombatMovementPlanner { return true } val targetTile = target.getClosestOccupiedTile(attacker.location) - return attacker.location.getDistance(targetTile) > ServerConstants.MAX_PATHFIND_DISTANCE * 2.0 + return attacker.location.getDistance(targetTile) > + ServerConstants.MAX_PATHFIND_DISTANCE * 2.0 } @JvmStatic @@ -114,28 +115,46 @@ object CombatMovementPlanner { } @JvmStatic - fun chooseTargetBorderTile(attacker: Entity, target: Entity, targetLocation: Location): Location? { + fun chooseTargetBorderTile( + attacker: Entity, + target: Entity, + targetLocation: Location, + ): Location? { return candidateAttackTiles(attacker, target, targetLocation).firstOrNull() } @JvmStatic - fun candidateAttackTiles(attacker: Entity, target: Entity, targetLocation: Location): List { + fun candidateAttackTiles( + attacker: Entity, + target: Entity, + targetLocation: Location, + ): List { 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 { - it.getDistance( - attacker.location - ) - }.thenBy { it.x }.thenBy { it.y }) + return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith( + compareBy { + it.getDistance(attacker.location) + } + .thenBy { it.x } + .thenBy { it.y } + ) } private fun canInteractFrom( - attacker: Entity, location: Location, target: Entity, targetLocation: Location + 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) + return direction.canMoveFrom( + location.z, + location.x, + location.y, + RegionManager::getClippingFlag, + ) } return Pathfinder.canInteract( location.x, @@ -146,8 +165,10 @@ object CombatMovementPlanner { target.size(), target.size(), 0, - targetLocation.z - ) { z, x, y -> RegionManager.getClippingFlag(z, x, y) } + targetLocation.z, + ) { z, x, y -> + RegionManager.getClippingFlag(z, x, y) + } } @JvmStatic @@ -159,10 +180,18 @@ object CombatMovementPlanner { val maxOffset = targetSize - 1 for (offset in minOffset..maxOffset) { - border.add(Location.create(targetLocation.x - attackerSize, targetLocation.y + offset, plane)) - border.add(Location.create(targetLocation.x + targetSize, targetLocation.y + offset, plane)) - border.add(Location.create(targetLocation.x + offset, targetLocation.y - attackerSize, plane)) - border.add(Location.create(targetLocation.x + offset, targetLocation.y + targetSize, plane)) + border.add( + Location.create(targetLocation.x - attackerSize, targetLocation.y + offset, plane) + ) + border.add( + Location.create(targetLocation.x + targetSize, targetLocation.y + offset, plane) + ) + border.add( + Location.create(targetLocation.x + offset, targetLocation.y - attackerSize, plane) + ) + border.add( + Location.create(targetLocation.x + offset, targetLocation.y + targetSize, plane) + ) } return border.toList() diff --git a/Server/src/main/core/game/node/entity/combat/CombatReach.kt b/Server/src/main/core/game/node/entity/combat/CombatReach.kt index 816fbc2bd..76191b5d5 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatReach.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatReach.kt @@ -10,9 +10,7 @@ import core.game.world.map.RegionManager.getClippingFlag import core.game.world.map.path.Pathfinder import core.game.world.map.path.Pathfinder.* -/** - * Shared combat reach calculations. - */ +/** Shared combat reach calculations. */ object CombatReach { @JvmStatic fun isUsingHalberd(entity: Entity): Boolean { @@ -49,26 +47,74 @@ object CombatReach { val size = entity.size() if (distance == 1) { for (i in 0 until size) { - if (Pathfinder.isStandingIn(e.x - 1, e.y + i, 1, 1, x, y, victim.size(), victim.size())) { + if ( + Pathfinder.isStandingIn( + e.x - 1, + e.y + i, + 1, + 1, + x, + y, + victim.size(), + victim.size(), + ) + ) { return true } - if (Pathfinder.isStandingIn(e.x + size, e.y + i, 1, 1, x, y, victim.size(), victim.size())) { + if ( + Pathfinder.isStandingIn( + e.x + size, + e.y + i, + 1, + 1, + x, + y, + victim.size(), + victim.size(), + ) + ) { return true } - if (Pathfinder.isStandingIn(e.x + i, e.y - 1, 1, 1, x, y, victim.size(), victim.size())) { + if ( + Pathfinder.isStandingIn( + e.x + i, + e.y - 1, + 1, + 1, + x, + y, + victim.size(), + victim.size(), + ) + ) { return true } - if (Pathfinder.isStandingIn(e.x + i, e.y + size, 1, 1, x, y, victim.size(), victim.size())) { + if ( + Pathfinder.isStandingIn( + e.x + i, + e.y + size, + 1, + 1, + x, + y, + victim.size(), + victim.size(), + ) + ) { 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), ) } @@ -81,7 +127,7 @@ object CombatReach { second.location.x, second.location.y, second.size(), - second.size() + second.size(), ) } @@ -103,7 +149,9 @@ 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 @@ -136,37 +184,49 @@ object CombatReach { val components = next.getStepComponents(dir) when (dir) { - Direction.NORTH -> if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT - Direction.EAST -> if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT - Direction.SOUTH -> if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT - Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT + Direction.NORTH -> + if (getClippingFlag(next) and PREVENT_NORTH != 0) return InteractionType.NO_INTERACT + Direction.EAST -> + if (getClippingFlag(next) and PREVENT_EAST != 0) return InteractionType.NO_INTERACT + Direction.SOUTH -> + if (getClippingFlag(next) and PREVENT_SOUTH != 0) return InteractionType.NO_INTERACT + Direction.WEST -> + if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT Direction.NORTH_EAST -> { - if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || getClippingFlag(components[1]) and PREVENT_NORTH != 0 || getClippingFlag( - next - ) and PREVENT_NORTHEAST != 0 - ) return InteractionType.NO_INTERACT + if ( + getClippingFlag(components[0]) and PREVENT_EAST != 0 || + getClippingFlag(components[1]) and PREVENT_NORTH != 0 || + getClippingFlag(next) and PREVENT_NORTHEAST != 0 + ) + return InteractionType.NO_INTERACT } Direction.NORTH_WEST -> { - if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || getClippingFlag(components[1]) and PREVENT_NORTH != 0 || getClippingFlag( - next - ) and PREVENT_NORTHWEST != 0 - ) return InteractionType.NO_INTERACT + if ( + getClippingFlag(components[0]) and PREVENT_WEST != 0 || + getClippingFlag(components[1]) and PREVENT_NORTH != 0 || + getClippingFlag(next) and PREVENT_NORTHWEST != 0 + ) + return InteractionType.NO_INTERACT } Direction.SOUTH_EAST -> { - if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || getClippingFlag( - next - ) and PREVENT_SOUTHEAST != 0 - ) return InteractionType.NO_INTERACT + if ( + getClippingFlag(components[0]) and PREVENT_EAST != 0 || + getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || + getClippingFlag(next) and PREVENT_SOUTHEAST != 0 + ) + return InteractionType.NO_INTERACT } Direction.SOUTH_WEST -> { - if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || getClippingFlag( - next - ) and PREVENT_SOUTHWEST != 0 - ) return InteractionType.NO_INTERACT + 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 } } diff --git a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt index 5df98f1a9..592ff800c 100644 --- a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt @@ -16,9 +16,7 @@ import kotlin.math.floor private const val SEARCH_MAP_SIZE = 128 private const val RING_BUFFER_SIZE = 4096 -class RsmodPathfinder( - private val maxWaypoints: Int = 25 -) : Pathfinder() { +class RsmodPathfinder(private val maxWaypoints: Int = 25) : Pathfinder() { private val defaultFinder = ThreadLocal.withInitial { PathFinder(RegionManager.RSMOD_CLIPPING_FLAGS, SEARCH_MAP_SIZE, RING_BUFFER_SIZE) @@ -35,7 +33,7 @@ class RsmodPathfinder( type: Int, walkingFlag: Int, near: Boolean, - clipMaskSupplier: ClipMaskSupplier? + clipMaskSupplier: ClipMaskSupplier?, ): Path { val source = requireNotNull(start) val destination = requireNotNull(dest) @@ -46,7 +44,10 @@ class RsmodPathfinder( if (magnitude > ServerConstants.MAX_PATHFIND_DISTANCE) { if (canAttempt(source, destination)) { - end = source.transform(vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1)) + end = + source.transform( + vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1) + ) } else { path.isMoveNear = true return path @@ -54,29 +55,31 @@ class RsmodPathfinder( } val shape = routeShape(type, sizeX, sizeY) - val finder = if (clipMaskSupplier == null) { - RegionManager.loadClippingWindow(source, SEARCH_MAP_SIZE) - defaultFinder.get() - } else { - val state = suppliedFinder.get() - state.loadCollisionWindow(source, clipMaskSupplier) - state.finder - } - val route = finder.findPath( - level = source.z, - srcX = source.x, - srcZ = source.y, - destX = end.x, - destZ = end.y, - srcSize = moverSize, - destWidth = if (sizeX == 0) 1 else sizeX, - destHeight = if (sizeY == 0) 1 else sizeY, - objRot = rotation, - objShape = shape, - moveNear = near, - blockAccessFlags = walkingFlag, - maxWaypoints = maxWaypoints - ) + val finder = + if (clipMaskSupplier == null) { + RegionManager.loadClippingWindow(source, SEARCH_MAP_SIZE) + defaultFinder.get() + } else { + val state = suppliedFinder.get() + state.loadCollisionWindow(source, clipMaskSupplier) + state.finder + } + val route = + finder.findPath( + level = source.z, + srcX = source.x, + srcZ = source.y, + destX = end.x, + destZ = end.y, + srcSize = moverSize, + destWidth = if (sizeX == 0) 1 else sizeX, + destHeight = if (sizeY == 0) 1 else sizeY, + objRot = rotation, + objShape = shape, + moveNear = near, + blockAccessFlags = walkingFlag, + maxWaypoints = maxWaypoints, + ) if (route.failed) { return path @@ -117,7 +120,7 @@ class RsmodPathfinder( type: Int, walkingFlag: Int, z: Int, - clipMaskSupplier: ClipMaskSupplier? + clipMaskSupplier: ClipMaskSupplier?, ): Boolean { if (clipMaskSupplier == null) { RegionManager.loadClippingWindow(Location.create(srcX, srcY, z), SEARCH_MAP_SIZE) @@ -133,14 +136,15 @@ class RsmodPathfinder( srcSize = moverSize, objRot = rotation, objShape = routeShape(type, destWidth, destHeight), - blockAccessFlags = walkingFlag + blockAccessFlags = walkingFlag, ) } // Reach checks only read tiles within the source/destination rectangles and // their cross-axis combinations, so loading just their padded bounding box // into a reused map produces the same reads as a freshly allocated full map. val flags = suppliedReachFlags.get() - val destSize = maxOf(if (destWidth == 0) 1 else destWidth, if (destHeight == 0) 1 else destHeight) + val destSize = + maxOf(if (destWidth == 0) 1 else destWidth, if (destHeight == 0) 1 else destHeight) val minX = maxOf(0, minOf(srcX, destX) - 1) val minY = maxOf(0, minOf(srcY, destY) - 1) val maxX = maxOf(srcX + moverSize, destX + destSize) + 1 @@ -163,7 +167,7 @@ class RsmodPathfinder( srcSize = moverSize, objRot = rotation, objShape = routeShape(type, destWidth, destHeight), - blockAccessFlags = walkingFlag + blockAccessFlags = walkingFlag, ) } finally { for (zoneX in (minX shr 3)..(maxX shr 3)) { @@ -176,25 +180,35 @@ 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, - srcX = start.x, - srcZ = start.y, - destX = dest.x, - destZ = dest.y, - srcSize = moverSize, - destWidth = destWidth, - destHeight = destHeight - ) + return projectileLineFinder + .get() + .lineOfSight( + level = start.z, + srcX = start.x, + srcZ = start.y, + destX = dest.x, + destZ = dest.y, + srcSize = moverSize, + destWidth = destWidth, + destHeight = destHeight, + ) } @JvmStatic @@ -204,7 +218,7 @@ class RsmodPathfinder( moverSize: Int, destWidth: Int, destHeight: Int, - maxRaySteps: Int = Int.MAX_VALUE + maxRaySteps: Int = Int.MAX_VALUE, ): Boolean { val rayCast = lineOfSight(start, dest, moverSize, destWidth, destHeight) return rayCast.success && rayCast.coordinates.size <= maxRaySteps @@ -216,10 +230,16 @@ class RsmodPathfinder( sourceSize: Int, targetLocation: Location, targetSize: Int, - maxRaySteps: Int = Int.MAX_VALUE + maxRaySteps: Int = Int.MAX_VALUE, ): Boolean { RegionManager.loadClippingWindow(sourceLocation, SEARCH_MAP_SIZE) - return hasLineOfSightBetweenLoaded(sourceLocation, sourceSize, targetLocation, targetSize, maxRaySteps) + return hasLineOfSightBetweenLoaded( + sourceLocation, + sourceSize, + targetLocation, + targetSize, + maxRaySteps, + ) } @JvmStatic @@ -233,7 +253,7 @@ class RsmodPathfinder( sourceSize: Int, targetLocation: Location, targetSize: Int, - maxRaySteps: Int = Int.MAX_VALUE + maxRaySteps: Int = Int.MAX_VALUE, ): Boolean { if (sourceSize == 1 && targetSize == 1) { if (maxRaySteps == Int.MAX_VALUE) { @@ -256,7 +276,8 @@ class RsmodPathfinder( // step, so tiles further than one orthogonal step apart can // never satisfy a single-step ray. val manhattan = - kotlin.math.abs(source.x - (targetLocation.x + targetX)) + kotlin.math.abs(source.y - (targetLocation.y + targetY)) + kotlin.math.abs(source.x - (targetLocation.x + targetX)) + + kotlin.math.abs(source.y - (targetLocation.y + targetY)) if (manhattan > 1) { continue } @@ -273,17 +294,22 @@ class RsmodPathfinder( return false } - private fun hasSingleTileLineOfSight(sourceLocation: Location, targetLocation: Location): Boolean { - return projectileLineValidator.get().hasLineOfSight( - level = sourceLocation.z, - srcX = sourceLocation.x, - srcZ = sourceLocation.y, - destX = targetLocation.x, - destZ = targetLocation.y, - srcSize = 1, - destWidth = 1, - destHeight = 1 - ) + private fun hasSingleTileLineOfSight( + sourceLocation: Location, + targetLocation: Location, + ): Boolean { + return projectileLineValidator + .get() + .hasLineOfSight( + level = sourceLocation.z, + srcX = sourceLocation.x, + srcZ = sourceLocation.y, + destX = targetLocation.x, + destZ = targetLocation.y, + srcSize = 1, + destWidth = 1, + destHeight = 1, + ) } private fun manhattanDistance(first: Location, second: Location): Int { @@ -292,11 +318,12 @@ class RsmodPathfinder( return dx + dy } - private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = when { - type >= 0 -> type - sizeX != 0 && sizeY != 0 -> 10 - else -> -1 - } + private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = + when { + type >= 0 -> type + sizeX != 0 && sizeY != 0 -> 10 + else -> -1 + } private val suppliedReachFlags = ThreadLocal.withInitial { CollisionFlagMap() } @@ -309,7 +336,9 @@ 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) diff --git a/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt b/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt index 6a83ae22c..160381b12 100644 --- a/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt @@ -14,13 +14,18 @@ class RsmodProjectilePathfinder : Pathfinder() { type: Int, walkingFlag: Int, near: Boolean, - clipMaskSupplier: ClipMaskSupplier? + clipMaskSupplier: ClipMaskSupplier?, ): Path { val source = requireNotNull(start) val destination = requireNotNull(end) - val rayCast = RsmodPathfinder.lineOfSight( - start = source, dest = destination, moverSize = size, destWidth = sizeX, destHeight = sizeY - ) + val rayCast = + RsmodPathfinder.lineOfSight( + start = source, + dest = destination, + moverSize = size, + destWidth = sizeX, + destHeight = sizeY, + ) val path = Path() if (!rayCast.success) { path.isMoveNear = rayCast.alternative diff --git a/Server/src/test/kotlin/content/CombatMovementTests.kt b/Server/src/test/kotlin/content/CombatMovementTests.kt index dca05993c..f70350e9a 100644 --- a/Server/src/test/kotlin/content/CombatMovementTests.kt +++ b/Server/src/test/kotlin/content/CombatMovementTests.kt @@ -53,7 +53,8 @@ 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.", ) } } @@ -84,7 +85,8 @@ 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}", ) } } @@ -110,20 +112,36 @@ class CombatMovementTests { CombatMovementIntents.resolve() assertFalse( - attacker.walkingQueue.isRunning, "Combat movement must not persist the transient running flag." + attacker.walkingQueue.isRunning, + "Combat movement must not persist the transient running flag.", + ) + assertFalse( + attacker.walkingQueue.isRunningBoth, + "Combat movement must not make the attacker run.", ) - 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." + "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.", ) - 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.") } } } @@ -149,12 +167,18 @@ class CombatMovementTests { assertTrue( attacker.properties.combatPulse.isAttacking, - "A moving target temporarily blocking the first walking step should not stop combat." + "A moving target temporarily blocking the first walking step should not stop combat.", ) assertFalse(receivedMessage(attacker, "I can't reach that!")) - assertFalse(attacker.walkingQueue.isRunningBoth, "Waiting for the next tick must not force running.") + 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." + 100.0, + attacker.settings.runEnergy, + 0.0, + "Waiting for a moving target must not drain run energy.", ) } } @@ -185,7 +209,8 @@ 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}", ) } } @@ -210,11 +235,20 @@ class CombatMovementTests { assertTrue(first.properties.combatPulse.isAttacking) assertTrue(second.properties.combatPulse.isAttacking) - assertNotEquals(firstStart, first.location, "First attacker should step toward the target.") - assertNotEquals(secondStart, second.location, "Second attacker should step toward the target.") + assertNotEquals( + firstStart, + first.location, + "First attacker should step toward the target.", + ) + assertNotEquals( + secondStart, + second.location, + "Second attacker should step toward the target.", + ) assertTrue( - first.location.getDistance(second.location) < firstStart.getDistance(secondStart), - "Mutual melee combat should close distance instead of waiting indefinitely." + first.location.getDistance(second.location) < + firstStart.getDistance(secondStart), + "Mutual melee combat should close distance instead of waiting indefinitely.", ) } } @@ -245,7 +279,7 @@ class CombatMovementTests { assertEquals( origin, player.location, - "A stale melee movement intent must not make an already-adjacent attacker sidestep." + "A stale melee movement intent must not make an already-adjacent attacker sidestep.", ) assertTrue(meleeReach(player, npc)) assertTrue(player.properties.combatPulse.isAttacking) @@ -279,11 +313,11 @@ class CombatMovementTests { assertFalse( CombatMovementIntents.shouldMaintainMeleePressure(player, npc), - "The player's existing step should already keep melee range." + "The player's existing step should already keep melee range.", ) assertFalse( CombatMovementIntents.shouldMaintainMeleePressure(npc, player), - "The NPC's existing step should already keep melee range." + "The NPC's existing step should already keep melee range.", ) CombatMovementIntents.clear() @@ -322,25 +356,29 @@ 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, predictedVictim.x, predictedVictim.y, true, - CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER + CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER, ) try { assertTrue( CombatMovementIntents.shouldMaintainMeleePressure(attacker, victim), - "A moving target's clipped predicted side should request melee pressure movement." + "A moving target's clipped predicted side should request melee pressure movement.", ) GameWorld.Pulser.updateAll() assertTrue( CombatMovementIntents.pendingCount() > 0, - "Combat pulse should queue a movement intent for the clipped predicted side." + "Combat pulse should queue a movement intent for the clipped predicted side.", ) CombatMovementIntents.resolve() attacker.playerFlags.setUpdateSceneGraph(false) @@ -351,26 +389,33 @@ 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, - "Combat should remain active while sidestepping a clipped predicted attack side." + "Combat should remain active while sidestepping a clipped predicted attack side.", ) 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, predictedVictim.x, predictedVictim.y, true, - CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER + CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER, ) CombatMovementIntents.clear() } @@ -382,9 +427,11 @@ class CombatMovementTests { fun overlappingMeleePlayerShouldStepToOpenAttackTileInsteadOfStopping() { TestUtils.getMockPlayer("combat_overlap_player_escape").use { player -> val origin = arenaOrigin() - val blockedTiles = listOf( - origin.transform(0, 1, 0), origin.transform(1, 0, 0) - ) + val blockedTiles = + listOf( + origin.transform(0, 1, 0), + origin.transform(1, 0, 0), + ) place(player, origin) configureMelee(player) disableRun(player) @@ -403,8 +450,10 @@ class CombatMovementTests { TestUtils.advanceTicks(1, false) 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}" + 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}", ) assertTrue(CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player))) assertTrue(player.properties.combatPulse.isAttacking) @@ -421,9 +470,11 @@ class CombatMovementTests { fun overlappingMeleeNpcShouldStepToOpenAttackTileInsteadOfStalling() { TestUtils.getMockPlayer("combat_overlap_npc_target").use { player -> val origin = arenaOrigin() - val blockedTiles = listOf( - origin.transform(0, 1, 0), origin.transform(1, 0, 0) - ) + val blockedTiles = + listOf( + origin.transform(0, 1, 0), + origin.transform(1, 0, 0), + ) place(player, origin) configureMelee(player) disableRun(player) @@ -438,8 +489,10 @@ class CombatMovementTests { TestUtils.advanceTicks(1, false) 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}" + 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}", ) assertTrue(CombatReach.canMelee(npc, player, CombatReach.meleeDistance(npc))) assertTrue(npc.properties.combatPulse.isAttacking) @@ -455,9 +508,11 @@ class CombatMovementTests { fun mutualOverlappedMeleeCombatShouldOnlyMoveOneActorIntoAttackRange() { TestUtils.getMockPlayer("combat_overlap_mutual_player").use { player -> val origin = arenaOrigin() - val blockedTiles = listOf( - origin.transform(0, 1, 0), origin.transform(1, 0, 0) - ) + val blockedTiles = + listOf( + origin.transform(0, 1, 0), + origin.transform(1, 0, 0), + ) place(player, origin) configureMelee(player) disableRun(player) @@ -479,7 +534,8 @@ 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))) @@ -511,7 +567,7 @@ class CombatMovementTests { assertTrue(player.properties.combatPulse.isAttacking) assertTrue( player.location.getDistance(npc.location) <= 2.0, - "Player should continue closing on a moving melee NPC target." + "Player should continue closing on a moving melee NPC target.", ) } finally { npc.clear() @@ -538,11 +594,17 @@ class CombatMovementTests { queueWalk(npc, destination) TestUtils.advanceTicks(5, false) - assertNotEquals(destination, npc.location, "The NPC should still be walking during this assertion.") + assertNotEquals( + destination, + npc.location, + "The NPC should still be walking during this assertion.", + ) 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() @@ -556,9 +618,11 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_no_cross_footprint_attacker").use { player -> val origin = openHorizontalOrigin() val npcLocation = origin.transform(2, 0, 0) - val blockedTiles = listOf( - npcLocation.transform(0, 1, 0), npcLocation.transform(0, -1, 0) - ) + val blockedTiles = + listOf( + npcLocation.transform(0, 1, 0), + npcLocation.transform(0, -1, 0), + ) place(player, origin) configureMelee(player) enableRun(player) @@ -570,7 +634,11 @@ 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) @@ -581,12 +649,21 @@ 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.", ) - 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() @@ -610,10 +687,14 @@ class CombatMovementTests { attacker.attack(victim) TestUtils.advanceTicks(2, false) - assertEquals(origin, attacker.location, "Movement lock should prevent combat chase movement.") + assertEquals( + origin, + attacker.location, + "Movement lock should prevent combat chase movement.", + ) assertTrue( attacker.properties.combatPulse.isAttacking, - "Movement lock should not prevent an in-range melee swing." + "Movement lock should not prevent an in-range melee swing.", ) } } @@ -638,7 +719,7 @@ class CombatMovementTests { assertTrue(player.properties.combatPulse.isAttacking) assertTrue( player.location.getDistance(npc.getClosestOccupiedTile(player.location)) <= 1.0, - "Melee reach should be measured against the large target's occupied border." + "Melee reach should be measured against the large target's occupied border.", ) } finally { npc.clear() @@ -662,12 +743,12 @@ class CombatMovementTests { assertFalse( player.properties.combatPulse.isAttacking, - "Cross-map combat targets should be discarded before combat movement pathfinding." + "Cross-map combat targets should be discarded before combat movement pathfinding.", ) assertEquals(seers, player.location) assertTrue( player.walkingQueue.queue.size <= 1, - "Stopping stale combat should not queue a path toward the old target." + "Stopping stale combat should not queue a path toward the old target.", ) } finally { npc.clear() @@ -695,7 +776,8 @@ 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() @@ -715,9 +797,13 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { - assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") + assertTrue( + RegionManager.isTeleportPermitted(start), + "Test start tile must be walkable.", + ) assertFalse( - RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + RegionManager.isTeleportPermitted(duckLocation), + "Lumbridge river duck spawn should be on water.", ) val startDistance = start.getDistance(duck.location) @@ -726,7 +812,7 @@ class CombatMovementTests { assertTrue( player.properties.combatPulse.isAttacking, - "Ranged combat should keep chasing a water NPC instead of rejecting before movement." + "Ranged combat should keep chasing a water NPC instead of rejecting before movement.", ) assertFalse(receivedMessage(player, "I can't reach that!")) @@ -734,18 +820,18 @@ class CombatMovementTests { assertTrue( player.location.getDistance(duck.location) < startDistance, - "Player should path toward ranged attack range." + "Player should path toward ranged attack range.", ) assertFalse(receivedMessage(player, "I can't reach that!")) TestUtils.advanceTicks(8, false) assertTrue( player.properties.combatPulse.isAttacking, - "Ranged combat should remain active after moving into range." + "Ranged combat should remain active after moving into range.", ) assertTrue( player.location.getDistance(duck.location) <= 10.0, - "Player should stop once close enough to attack the duck from land." + "Player should stop once close enough to attack the duck from land.", ) assertFalse(receivedMessage(player, "I can't reach that!")) } finally { @@ -766,9 +852,13 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { - assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") + assertTrue( + RegionManager.isTeleportPermitted(start), + "Test start tile must be walkable.", + ) assertFalse( - RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + RegionManager.isTeleportPermitted(duckLocation), + "Lumbridge river duck spawn should be on water.", ) player.attack(duck) @@ -777,11 +867,12 @@ 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=${ + "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 { @@ -802,9 +893,13 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { - assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") + assertTrue( + RegionManager.isTeleportPermitted(start), + "Test start tile must be walkable.", + ) assertFalse( - RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + RegionManager.isTeleportPermitted(duckLocation), + "Lumbridge river duck spawn should be on water.", ) player.attack(duck) @@ -812,22 +907,24 @@ class CombatMovementTests { 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=${ + "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)}" + }, " + + "projectile=${CombatSwingHandler.isProjectileClipped(player, duck, false)}", ) if (!player.properties.combatPulse.isAttacking) { assertTrue(receivedMessage(player, "I can't reach that!")) assertTrue( player.walkingQueue.queue.size <= 1, - "Stopping blocked autocast should not leave a chase path queued." + "Stopping blocked autocast should not leave a chase path queued.", ) } assertTrue( player.location.getDistance(duck.location) <= start.getDistance(duck.location), - "Blocked autocast should not route away around the river before resolving combat." + "Blocked autocast should not route away around the river before resolving combat.", ) } finally { duck.clear() @@ -847,9 +944,13 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { - assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") + assertTrue( + RegionManager.isTeleportPermitted(start), + "Test start tile must be walkable.", + ) assertFalse( - RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + RegionManager.isTeleportPermitted(duckLocation), + "Lumbridge river duck spawn should be on water.", ) val startDistance = start.getDistance(duck.location) @@ -858,7 +959,7 @@ class CombatMovementTests { assertTrue( player.properties.combatPulse.isAttacking, - "Melee combat should attempt to path before reporting that a water NPC cannot be reached." + "Melee combat should attempt to path before reporting that a water NPC cannot be reached.", ) assertFalse(receivedMessage(player, "I can't reach that!")) @@ -866,20 +967,22 @@ class CombatMovementTests { assertTrue( player.location.getDistance(duck.location) < startDistance, - "Player should path toward the duck before failing melee reach." + "Player should path toward the duck before failing melee reach.", ) TestUtils.advanceTicks(20, false) assertFalse(player.properties.combatPulse.isAttacking) assertTrue( receivedMessage(player, "I can't reach that!"), - "Melee combat should report unreachable after pathing. " + "location=${player.location}, distance=${ + "Melee combat should report unreachable after pathing. " + + "location=${player.location}, distance=${ player.location.getDistance(duck.location) - }, " + "isAttacking=${player.properties.combatPulse.isAttacking}" + }, " + + "isAttacking=${player.properties.combatPulse.isAttacking}", ) assertTrue( player.location.getDistance(duck.location) <= startDistance, - "Melee combat should not route away around the river before rejecting the duck." + "Melee combat should not route away around the river before rejecting the duck.", ) } finally { duck.clear() @@ -900,10 +1003,14 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { - assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") + assertTrue( + RegionManager.isTeleportPermitted(start), + "Test start tile must be walkable.", + ) for (tile in swimTiles) { assertFalse( - RegionManager.isTeleportPermitted(tile), "Lumbridge river duck swim tile should be on water." + RegionManager.isTeleportPermitted(tile), + "Lumbridge river duck swim tile should be on water.", ) } @@ -914,7 +1021,7 @@ class CombatMovementTests { queueWalk(duck, swimTiles[(tick + 1) % swimTiles.size]) assertTrue( CombatMovementPlanner.hasMovementStepThisTick(duck), - "The duck must be moving every tick for this scenario." + "The duck must be moving every tick for this scenario.", ) TestUtils.advanceTicks(1, false) if (receivedMessage(player, "I can't reach that!")) { @@ -925,19 +1032,22 @@ class CombatMovementTests { assertTrue( rejectedAfter >= 0, - "Melee combat against a constantly swimming water NPC should be rejected once the " + "walkable approach is exhausted instead of chasing forever. " + "player=${player.location}, duck=${duck.location}" + "Melee combat against a constantly swimming water NPC should be rejected once the " + + "walkable approach is exhausted instead of chasing forever. " + + "player=${player.location}, duck=${duck.location}", ) assertTrue( rejectedAfter <= 12, - "Rejection should happen shortly after the player reaches the river bank, " + "not after a long futile chase (rejected after $rejectedAfter ticks)." + "Rejection should happen shortly after the player reaches the river bank, " + + "not after a long futile chase (rejected after $rejectedAfter ticks).", ) assertFalse( player.properties.combatPulse.isAttacking, - "Combat should stop when the moving water NPC is rejected." + "Combat should stop when the moving water NPC is rejected.", ) assertTrue( player.walkingQueue.queue.size <= 1, - "Stopping unreachable combat should not keep a movement path queued." + "Stopping unreachable combat should not keep a movement path queued.", ) } finally { duck.clear() @@ -965,7 +1075,7 @@ class CombatMovementTests { assertTrue( player.properties.combatPulse.isAttacking, - "Unreachable local combat targets should first path as close as possible." + "Unreachable local combat targets should first path as close as possible.", ) assertFalse(receivedMessage(player, "I can't reach that!")) @@ -973,17 +1083,17 @@ class CombatMovementTests { assertTrue( player.location.getDistance(npc.location) < startDistance, - "Player should move toward the nearest reachable tile first." + "Player should move toward the nearest reachable tile first.", ) TestUtils.advanceTicks(6, false) assertFalse( player.properties.combatPulse.isAttacking, - "Unreachable combat targets should stop once the closest reachable tile is reached." + "Unreachable combat targets should stop once the closest reachable tile is reached.", ) assertTrue( player.walkingQueue.queue.size <= 1, - "Stopping unreachable combat should not keep a movement path queued." + "Stopping unreachable combat should not keep a movement path queued.", ) assertTrue(receivedMessage(player, "I can't reach that!")) } finally { @@ -1014,7 +1124,7 @@ class CombatMovementTests { assertFalse((player.session as MockSession).disconnected) assertTrue( npc.location.getDistance(player.location) < 8.0, - "Retaliating NPC should step toward a nearby ranged attacker." + "Retaliating NPC should step toward a nearby ranged attacker.", ) } finally { npc.clear() @@ -1080,7 +1190,7 @@ class CombatMovementTests { assertTrue( player.walkingQueue.queue.size in 2..3, - "Combat movement should only queue the immediate movement step(s), not the full chase path." + "Combat movement should only queue the immediate movement step(s), not the full chase path.", ) } finally { npc.clear() @@ -1109,7 +1219,11 @@ class CombatMovementTests { npc.walkingQueue.update() assertNotEquals(-1, npc.walkingQueue.walkDir) - assertEquals(-1, npc.walkingQueue.runDir, "NPC combat retaliation should walk, not force-run.") + assertEquals( + -1, + npc.walkingQueue.runDir, + "NPC combat retaliation should walk, not force-run.", + ) } finally { npc.clear() CombatMovementIntents.clear() @@ -1121,9 +1235,11 @@ class CombatMovementTests { fun defaultMeleeNpcShouldNotRouteAroundSafespotObstacle() { TestUtils.getMockPlayer("combat_safespot_target").use { player -> val origin = arenaOrigin() - val blockedTiles = listOf( - origin.transform(1, 0, 0), origin.transform(0, -1, 0) - ) + val blockedTiles = + listOf( + origin.transform(1, 0, 0), + origin.transform(0, -1, 0), + ) place(player, origin.transform(4, 0, 0)) val npc = NPC.create(100, origin) @@ -1141,7 +1257,7 @@ class CombatMovementTests { assertEquals( origin, npc.location, - "Default dumb NPC combat pathing should not choose alternate border tiles to route around safespots." + "Default dumb NPC combat pathing should not choose alternate border tiles to route around safespots.", ) assertTrue(npc.properties.combatPulse.isAttacking) } finally { @@ -1175,7 +1291,7 @@ class CombatMovementTests { assertEquals( westSide, npc.location, - "A diagonal dumb NPC should try the other corner-adjacent side when its preferred side is blocked." + "A diagonal dumb NPC should try the other corner-adjacent side when its preferred side is blocked.", ) assertTrue(meleeReach(npc, player)) assertTrue(npc.properties.combatPulse.isAttacking) @@ -1197,7 +1313,10 @@ class CombatMovementTests { npc.init() try { configureMelee(npc) - assertFalse(meleeReach(npc, player), "Precondition: NPC should be diagonally adjacent, not in melee range.") + assertFalse( + meleeReach(npc, player), + "Precondition: NPC should be diagonally adjacent, not in melee range.", + ) npc.attack(player) CombatMovementIntents.clear() @@ -1207,7 +1326,7 @@ class CombatMovementTests { assertTrue( meleeReach(npc, player), - "A diagonally-adjacent dumb NPC on open terrain should step to an orthogonally-adjacent attack tile." + "A diagonally-adjacent dumb NPC on open terrain should step to an orthogonally-adjacent attack tile.", ) assertTrue(npc.properties.combatPulse.isAttacking) } finally { @@ -1240,7 +1359,7 @@ class CombatMovementTests { assertEquals( npcStart, npc.location, - "A dumb NPC should not route around a blocked next tile on the direct path to the player." + "A dumb NPC should not route around a blocked next tile on the direct path to the player.", ) assertTrue(npc.properties.combatPulse.isAttacking) } finally { @@ -1270,20 +1389,26 @@ class CombatMovementTests { npc.attack(player) queueRunPath( - player, listOf( - Location.create(2658, 3309, 0), Location.create(2658, 3310, 0), targetDestination - ) + 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()}" + "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()}" + 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 { @@ -1297,9 +1422,11 @@ class CombatMovementTests { 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) - ) + val blockedTiles = + listOf( + origin.transform(0, 1, 0), + origin.transform(-1, 0, 0), + ) val npcStart = origin.transform(-1, 1, 0) place(player, origin) @@ -1318,7 +1445,7 @@ class CombatMovementTests { assertEquals( npcStart, npc.location, - "A diagonal dumb NPC should not rotate to the far side when both corner-adjacent attack tiles are blocked." + "A diagonal dumb NPC should not rotate to the far side when both corner-adjacent attack tiles are blocked.", ) assertTrue(npc.properties.combatPulse.isAttacking) } finally { @@ -1335,9 +1462,13 @@ class CombatMovementTests { val origin = arenaOrigin() 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 - ) + val playerPath = + listOf( + origin.transform(0, 1, 0), + origin.transform(1, 1, 0), + origin.transform(2, 1, 0), + playerEnd, + ) place(player, origin) enableRun(player) @@ -1355,7 +1486,7 @@ class CombatMovementTests { assertEquals( origin, npc.location, - "Default dumb NPCs should pursue the target-facing side, then stall when that side is blocked." + "Default dumb NPCs should pursue the target-facing side, then stall when that side is blocked.", ) assertFalse(meleeReach(npc, player)) assertTrue(npc.properties.combatPulse.isAttacking) @@ -1377,13 +1508,23 @@ class CombatMovementTests { configureMelee(player) disableRun(player) - val door = RegionManager.getObject(doorLocation.z, doorLocation.x, doorLocation.y, 36846) - ?: throw AssertionError("Expected Lumbridge house door 36846 at $doorLocation.") + 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}.") + 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) + DoorActionHandler.open( + door, + null, + doorConfig.replaceId, + -1, + true, + -1, + doorConfig.isFence, + ) val npc = NPC.create(100, Location.create(3231, 3236, 0)) npc.init() @@ -1395,16 +1536,20 @@ class CombatMovementTests { assertEquals( expectedAttackTile, player.location, - "Player should route north around the opened southern door instead of bouncing south." + "Player should route north around the opened southern door instead of bouncing south.", ) assertTrue(player.properties.combatPulse.isAttacking) assertTrue(meleeReach(player, npc)) assertFalse(receivedMessage(player, "I can't reach that!")) } finally { npc.clear() - val openedDoor = RegionManager.getObject( - openedDoorLocation.z, openedDoorLocation.x, openedDoorLocation.y, doorConfig.replaceId - ) + val openedDoor = + RegionManager.getObject( + openedDoorLocation.z, + openedDoorLocation.x, + openedDoorLocation.y, + doorConfig.replaceId, + ) if (openedDoor != null) { SceneryBuilder.replace(openedDoor, door) } @@ -1418,9 +1563,11 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_lumbridge_ruin_corner_attacker").use { player -> 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) - ) + val expectedAttackTiles = + setOf( + Location.create(3252, 3226, 0), + Location.create(3253, 3227, 0), + ) place(player, start) configureMelee(player) enableRun(player) @@ -1433,7 +1580,7 @@ class CombatMovementTests { assertFalse( CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player)), - "The starting diagonal corner should be blocked for melee." + "The starting diagonal corner should be blocked for melee.", ) player.attack(npc) @@ -1441,7 +1588,8 @@ 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) @@ -1462,7 +1610,9 @@ class CombatMovementTests { for (dy in -16..16) { for (dx in -16..16) { val candidate = start.transform(dx, dy, 0) - if ((0..10).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) }) { + if ( + (0..10).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) } + ) { return candidate } } @@ -1494,18 +1644,22 @@ class CombatMovementTests { } private fun configureMelee(entity: Entity) { - entity.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_AGGRESSIVE, WeaponInterface.BONUS_CRUSH - ) + entity.properties.attackStyle = + WeaponInterface.AttackStyle( + WeaponInterface.STYLE_AGGRESSIVE, + WeaponInterface.BONUS_CRUSH, + ) entity.properties.combatPulse.updateStyle() } private fun configureRanged(player: Player) { 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 - ) + player.properties.attackStyle = + WeaponInterface.AttackStyle( + WeaponInterface.STYLE_RANGE_ACCURATE, + WeaponInterface.BONUS_RANGE, + ) player.properties.combatPulse.updateStyle() } @@ -1514,9 +1668,11 @@ class CombatMovementTests { player.skills.setStaticLevel(Skills.MAGIC, 99) player.skills.setLevel(Skills.MAGIC, 99) player.properties.autocastSpell = TestAutocastSpell - player.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_CAST, WeaponInterface.BONUS_MAGIC - ) + player.properties.attackStyle = + WeaponInterface.AttackStyle( + WeaponInterface.STYLE_CAST, + WeaponInterface.BONUS_MAGIC, + ) player.properties.combatPulse.updateStyle() } @@ -1553,13 +1709,18 @@ class CombatMovementTests { continue } val candidate = target.transform(dx, dy, 0) - if (candidate.getDistance(target) >= minDistance && RegionManager.isTeleportPermitted(candidate)) { + if ( + candidate.getDistance(target) >= minDistance && + RegionManager.isTeleportPermitted(candidate) + ) { return candidate } } } } - throw AssertionError("No walkable tile found near $target between $minDistance and $maxRadius tiles.") + throw AssertionError( + "No walkable tile found near $target between $minDistance and $maxRadius tiles." + ) } private fun enablePvp(first: Entity, second: Entity) { @@ -1575,11 +1736,13 @@ 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() { @@ -1591,11 +1754,9 @@ class CombatMovementTests { return 1 } - override fun visualize(entity: Entity, target: Node?) { - } + override fun visualize(entity: Entity, target: Node?) {} - override fun visualizeImpact(entity: Entity?, target: Entity?, state: BattleState?) { - } + override fun visualizeImpact(entity: Entity?, target: Entity?, state: BattleState?) {} override fun newInstance(arg: SpellType?): Plugin { return this @@ -1622,5 +1783,8 @@ class CombatMovementTests { } private val movementBlockFlag = - Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST + Pathfinder.PREVENT_NORTH or + Pathfinder.PREVENT_EAST or + Pathfinder.PREVENT_SOUTH or + Pathfinder.PREVENT_WEST } diff --git a/Server/src/test/kotlin/content/CombatPerformanceTests.kt b/Server/src/test/kotlin/content/CombatPerformanceTests.kt index ae65c3c78..2a337fa93 100644 --- a/Server/src/test/kotlin/content/CombatPerformanceTests.kt +++ b/Server/src/test/kotlin/content/CombatPerformanceTests.kt @@ -37,9 +37,10 @@ class CombatPerformanceTests { measureCombatTick(load, it) } - val durations = LongArray(MEASURED_TICKS) { tick -> - measureCombatTick(load, tick + WARMUP_TICKS) - } + val durations = + LongArray(MEASURED_TICKS) { tick -> + measureCombatTick(load, tick + WARMUP_TICKS) + } val sorted = durations.sorted() val p90Index = ((sorted.size * 9 + 9) / 10 - 1).coerceIn(0, sorted.lastIndex) val p90 = sorted[p90Index] @@ -50,12 +51,12 @@ class CombatPerformanceTests { p90 <= HEADROOM_TICK_BUDGET_MILLIS, "650-player combat p90 tick time should leave room for slower live hardware. " + "durations=${durationText}ms, p90=${p90}ms, " + - "budget=${HEADROOM_TICK_BUDGET_MILLIS}ms" + "budget=${HEADROOM_TICK_BUDGET_MILLIS}ms", ) assertTrue( max <= LIVE_TICK_BUDGET_MILLIS, "650-player combat tick should remain under the live 600ms server tick budget. " + - "durations=${durationText}ms, max=${max}ms" + "durations=${durationText}ms, max=${max}ms", ) } finally { load?.close() @@ -83,9 +84,10 @@ class CombatPerformanceTests { configureMeleePlayer(player) } - val pairs = players.chunked(2).mapIndexed { index, pair -> - CombatPair(pair[0], pair[1], pairOrigin(index)) - } + val pairs = + players.chunked(2).mapIndexed { index, pair -> + CombatPair(pair[0], pair[1], pairOrigin(index)) + } val load = CombatLoad(players, pairs, previousWildPvp) GameWorld.settings!!.wild_pvp_enabled = true @@ -105,7 +107,7 @@ class CombatPerformanceTests { assertEquals( TOTAL_PLAYER_COUNT, CombatMovementIntents.pendingCount(), - "The performance fixture should exercise one combat movement intent per loaded player." + "The performance fixture should exercise one combat movement intent per loaded player.", ) val start = System.nanoTime() @@ -114,10 +116,11 @@ class CombatPerformanceTests { } private fun configureMeleePlayer(player: Player) { - player.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_AGGRESSIVE, - WeaponInterface.BONUS_CRUSH - ) + player.properties.attackStyle = + WeaponInterface.AttackStyle( + WeaponInterface.STYLE_AGGRESSIVE, + WeaponInterface.BONUS_CRUSH, + ) player.properties.combatPulse.updateStyle() player.properties.combatLevel = 126 player.skills.setStaticLevel(Skills.HITPOINTS, 10_000) @@ -131,13 +134,13 @@ class CombatPerformanceTests { private data class CombatPair( val first: Player, val second: Player, - val origin: Location + val origin: Location, ) private class CombatLoad( val players: List, private val pairs: List, - private val previousWildPvp: Boolean + private val previousWildPvp: Boolean, ) : AutoCloseable { fun resetPairPositionsAndMovement(tick: Int) {