mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Use RSMOD LOS in combat movement
- Add reusable RSMOD line-of-sight helpers for projectile checks - Avoid hot movement prediction list allocations during combat ticks - Use direct projectile LOS checks for combat movement, swings, and peltables - Reuse loaded LOS windows during ranged and magic approach filtering - Extend pathfinder tests for the direct RSMOD LOS helpers
This commit is contained in:
parent
bee7395f3e
commit
9c5a4c88d5
7 changed files with 328 additions and 118 deletions
|
|
@ -7,7 +7,6 @@ import core.game.node.entity.impl.Projectile
|
|||
import core.game.node.entity.player.Player
|
||||
import core.game.node.item.Item
|
||||
import core.game.system.task.Pulse
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.update.flag.context.Graphics
|
||||
import org.rs09.consts.Items
|
||||
|
||||
|
|
@ -47,7 +46,7 @@ class PlayerPeltables : InteractionListener {
|
|||
|
||||
val other = node.asPlayer()
|
||||
|
||||
if (!Pathfinder.find(player, other, false, Pathfinder.PROJECTILE).isSuccessful) {
|
||||
if (!hasLineOfSight(player, other)) {
|
||||
sendDialogue(player, "You can't reach them!")
|
||||
return true
|
||||
}
|
||||
|
|
@ -112,4 +111,4 @@ class PlayerPeltables : InteractionListener {
|
|||
|
||||
return equipped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,18 +126,13 @@ object CombatMovementIntents {
|
|||
if (attacker.locks.isMovementLocked) {
|
||||
return false
|
||||
}
|
||||
val targetSteps = CombatMovementPlanner.movementStepsThisTick(target)
|
||||
if (targetSteps == 0) {
|
||||
return false
|
||||
}
|
||||
val predictedTargetLocation = CombatMovementPlanner.predictTargetLocations(target, targetSteps).lastOrNull()
|
||||
?: target.location
|
||||
val predictedTargetLocation = CombatMovementPlanner.predictedMovementLocation(target) ?: return false
|
||||
val borderTiles = CombatMovementPlanner.borderTiles(target, predictedTargetLocation, attacker.size())
|
||||
val attackTiles = borderTiles.filter { RegionManager.isTeleportPermitted(it) }.ifEmpty { borderTiles }
|
||||
if (attacker.location in attackTiles) {
|
||||
return false
|
||||
}
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictTargetLocations(attacker).lastOrNull()
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location
|
||||
return projectedAttackerLocation !in attackTiles
|
||||
}
|
||||
|
||||
|
|
@ -185,8 +180,8 @@ object CombatMovementIntents {
|
|||
|
||||
val candidates = movementDestinationsFor(attacker, target, targetLocation)
|
||||
val standingOnCandidate = candidates.any { it.location == attacker.location }
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictTargetLocations(attacker).lastOrNull()
|
||||
if (projectedAttackerLocation != null && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) {
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location
|
||||
if (projectedAttackerLocation != attacker.location && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation)) {
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = projectedAttackerLocation
|
||||
reservedTiles.addAll(occupiedTiles(attacker, projectedAttackerLocation))
|
||||
|
|
@ -282,10 +277,10 @@ object CombatMovementIntents {
|
|||
if (range <= CombatReach.meleeDistance(attacker)) {
|
||||
return true
|
||||
}
|
||||
if (CombatMovementPlanner.movementStepsThisTick(target) > 0) {
|
||||
if (CombatMovementPlanner.hasMovementStepThisTick(target)) {
|
||||
return true
|
||||
}
|
||||
return attacker.location.getDistance(closestOccupiedTile(target, targetLocation, attacker.location)) > range
|
||||
return distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) > range * range
|
||||
}
|
||||
|
||||
private fun playerAttackRangeDestinations(
|
||||
|
|
@ -342,21 +337,21 @@ object CombatMovementIntents {
|
|||
if (!RegionManager.isTeleportPermitted(tile)) {
|
||||
continue
|
||||
}
|
||||
val closestTargetTile = closestOccupiedTile(target, targetLocation, tile)
|
||||
if (tile.getDistance(closestTargetTile) <= range) {
|
||||
if (distanceSquaredToClosestOccupiedTile(target, targetLocation, tile) <= range * range) {
|
||||
tiles.add(tile)
|
||||
}
|
||||
}
|
||||
}
|
||||
tiles.sortWith(
|
||||
compareBy<Location> { it.getDistance(attacker.location) }
|
||||
.thenBy { it.getDistance(closestOccupiedTile(target, targetLocation, it)) }
|
||||
compareBy<Location> { distanceSquared(it, attacker.location) }
|
||||
.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }
|
||||
.thenBy { it.x }
|
||||
.thenBy { it.y }
|
||||
)
|
||||
val attackTiles = ArrayList<Location>(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size))
|
||||
RsmodPathfinder.loadLineOfSightWindow(targetLocation)
|
||||
for (tile in tiles) {
|
||||
if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation)) {
|
||||
if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation, loadWindow = false)) {
|
||||
continue
|
||||
}
|
||||
attackTiles.add(tile)
|
||||
|
|
@ -378,7 +373,7 @@ object CombatMovementIntents {
|
|||
return false
|
||||
}
|
||||
if (distance > 1 &&
|
||||
attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) > distance
|
||||
distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) > distance * distance
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -406,7 +401,7 @@ object CombatMovementIntents {
|
|||
if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7
|
||||
)
|
||||
}
|
||||
return attackerLocation.getDistance(closestOccupiedTile(target, targetLocation, attackerLocation)) <= range &&
|
||||
return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range &&
|
||||
hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation)
|
||||
}
|
||||
|
||||
|
|
@ -478,42 +473,40 @@ object CombatMovementIntents {
|
|||
attackerSize: Int,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
checkClose: Boolean = false
|
||||
checkClose: Boolean = false,
|
||||
loadWindow: Boolean = true
|
||||
): Boolean {
|
||||
for (sourceX in 0 until attackerSize) {
|
||||
for (sourceY in 0 until attackerSize) {
|
||||
val source = attackerLocation.transform(sourceX, sourceY, 0)
|
||||
for (targetX in 0 until target.size()) {
|
||||
for (targetY in 0 until target.size()) {
|
||||
val destination = targetLocation.transform(targetX, targetY, 0)
|
||||
val path = Pathfinder.PROJECTILE.find(
|
||||
source,
|
||||
1,
|
||||
destination,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
RegionManager::getClippingFlag
|
||||
)
|
||||
if (path.isSuccessful && (!checkClose || path.points.size <= 1)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE
|
||||
if (!loadWindow) {
|
||||
return RsmodPathfinder.hasLineOfSightBetweenLoaded(
|
||||
attackerLocation,
|
||||
attackerSize,
|
||||
targetLocation,
|
||||
target.size(),
|
||||
maxRaySteps = maxRaySteps
|
||||
)
|
||||
}
|
||||
return false
|
||||
return RsmodPathfinder.hasLineOfSightBetween(
|
||||
attackerLocation,
|
||||
attackerSize,
|
||||
targetLocation,
|
||||
target.size(),
|
||||
maxRaySteps = maxRaySteps
|
||||
)
|
||||
}
|
||||
|
||||
private fun closestOccupiedTile(entity: Entity, location: Location, from: Location): Location {
|
||||
return Location.create(
|
||||
from.x.coerceIn(location.x, location.x + entity.size() - 1),
|
||||
from.y.coerceIn(location.y, location.y + entity.size() - 1),
|
||||
location.z
|
||||
)
|
||||
private fun distanceSquared(first: Location, second: Location): Int {
|
||||
val dx = first.x - second.x
|
||||
val dy = first.y - second.y
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
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
|
||||
val dy = from.y - closestY
|
||||
return dx * dx + dy * dy
|
||||
}
|
||||
|
||||
private fun dumbNpcAttackDestinations(attacker: NPC, target: Entity): List<Location> {
|
||||
|
|
@ -675,9 +668,7 @@ object CombatMovementIntents {
|
|||
}
|
||||
|
||||
private fun targetLocationFor(attacker: Entity, target: Entity): Location {
|
||||
val targetSteps = CombatMovementPlanner.movementStepsThisTick(target)
|
||||
val predictionSteps = targetSteps.coerceAtMost(movementStepsFor(attacker))
|
||||
return CombatMovementPlanner.predictTargetLocations(target, predictionSteps).lastOrNull() ?: target.location
|
||||
return CombatMovementPlanner.predictedTargetLocation(target, movementStepsFor(attacker))
|
||||
}
|
||||
|
||||
private fun movementStepsFor(attacker: Entity): Int {
|
||||
|
|
@ -697,7 +688,7 @@ object CombatMovementIntents {
|
|||
target: Entity,
|
||||
exhaustedLocalApproach: Boolean = false
|
||||
): Boolean {
|
||||
return attacker is Player && (exhaustedLocalApproach || CombatMovementPlanner.movementStepsThisTick(target) == 0)
|
||||
return attacker is Player && (exhaustedLocalApproach || !CombatMovementPlanner.hasMovementStepThisTick(target))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
|||
|
|
@ -38,7 +38,12 @@ object CombatMovementPlanner {
|
|||
|
||||
@JvmStatic
|
||||
fun predictedTargetLocation(target: Entity): Location {
|
||||
return predictTargetLocations(target).lastOrNull() ?: target.location
|
||||
return predictedTargetLocation(target, 2)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedTargetLocation(target: Entity, maxSteps: Int): Location {
|
||||
return predictedMovementLocation(target, maxSteps) ?: target.location
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
@ -51,30 +56,58 @@ object CombatMovementPlanner {
|
|||
if (maxSteps <= 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val queued = movementPoints(target, 2)
|
||||
if (queued.isEmpty()) {
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
val firstPoint = first ?: return emptyList()
|
||||
val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps)
|
||||
if (steps <= 0) {
|
||||
return emptyList()
|
||||
}
|
||||
val steps = if (canMoveTwoStepsThisTick(target, queued)) maxSteps.coerceAtMost(2) else 1
|
||||
val predicted = ArrayList<Location>(steps)
|
||||
for (i in 0 until steps) {
|
||||
val point = queued[i]
|
||||
predicted.add(Location.create(point.x, point.y, target.location.z))
|
||||
predicted.add(Location.create(firstPoint.x, firstPoint.y, target.location.z))
|
||||
if (steps > 1 && second != null) {
|
||||
predicted.add(Location.create(second.x, second.y, target.location.z))
|
||||
}
|
||||
return predicted
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun movementStepsThisTick(target: Entity): Int {
|
||||
val queued = movementPoints(target, 2)
|
||||
if (queued.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
return if (canMoveTwoStepsThisTick(target, queued)) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
return movementStepsThisTick(target, first ?: return 0, second)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasMovementStepThisTick(target: Entity): Boolean {
|
||||
return firstMovementPoint(target) != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun predictedMovementLocation(target: Entity): Location? {
|
||||
return predictedMovementLocation(target, 2)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
@ -135,22 +168,45 @@ object CombatMovementPlanner {
|
|||
return border.toList()
|
||||
}
|
||||
|
||||
private fun movementPoints(target: Entity, limit: Int): List<Point> {
|
||||
val points = ArrayList<Point>(limit)
|
||||
@JvmStatic
|
||||
fun predictedMovementLocation(target: Entity, maxSteps: Int): Location? {
|
||||
if (maxSteps <= 0) {
|
||||
return null
|
||||
}
|
||||
var first: Point? = null
|
||||
var second: Point? = null
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction == null) {
|
||||
continue
|
||||
}
|
||||
points.add(point)
|
||||
if (points.size >= limit) {
|
||||
if (first == null) {
|
||||
first = point
|
||||
} else {
|
||||
second = point
|
||||
break
|
||||
}
|
||||
}
|
||||
return points
|
||||
val firstPoint = first ?: return null
|
||||
val steps = movementStepsThisTick(target, firstPoint, second).coerceAtMost(maxSteps)
|
||||
val point = if (steps > 1 && second != null) second else firstPoint
|
||||
return Location.create(point.x, point.y, target.location.z)
|
||||
}
|
||||
|
||||
private fun canMoveTwoStepsThisTick(target: Entity, queued: List<Point>): Boolean {
|
||||
if (queued.size <= 1 || queued.first().isRunDisabled || !target.walkingQueue.isRunningBoth) {
|
||||
private fun firstMovementPoint(target: Entity): Point? {
|
||||
for (point in target.walkingQueue.queue) {
|
||||
if (point.direction != null) {
|
||||
return point
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun movementStepsThisTick(target: Entity, first: Point, second: Point?): Int {
|
||||
return if (canMoveTwoStepsThisTick(target, first, second)) 2 else 1
|
||||
}
|
||||
|
||||
private fun canMoveTwoStepsThisTick(target: Entity, first: Point, second: Point?): Boolean {
|
||||
if (second == null || first.isRunDisabled || !target.walkingQueue.isRunningBoth) {
|
||||
return false
|
||||
}
|
||||
return target !is Player || target.settings.runEnergy >= 1.0
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class CombatPulse(
|
|||
false
|
||||
} else {
|
||||
val timedOut = combatTimeOut++ > entity.properties.combatTimeOut
|
||||
if (timedOut && entity is Player && CombatMovementPlanner.movementStepsThisTick(victim!!) == 0) {
|
||||
if (timedOut && entity is Player && !CombatMovementPlanner.hasMovementStepThisTick(victim!!)) {
|
||||
CombatMovementIntents.stopUnreachableCombat(entity)
|
||||
}
|
||||
timedOut
|
||||
|
|
@ -209,7 +209,7 @@ class CombatPulse(
|
|||
return false
|
||||
}
|
||||
if (CombatMovementPlanner.exceedsCombatChaseDistance(attacker, target)) {
|
||||
if (attacker is Player && CombatMovementPlanner.movementStepsThisTick(target) == 0) {
|
||||
if (attacker is Player && !CombatMovementPlanner.hasMovementStepThisTick(target)) {
|
||||
CombatMovementIntents.stopUnreachableCombat(attacker)
|
||||
} else {
|
||||
stop()
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ import core.game.node.entity.skill.Skills
|
|||
import content.global.skill.summoning.familiar.Familiar
|
||||
import core.api.log
|
||||
import core.api.playGlobalAudio
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.path.Pathfinder.*
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
import core.game.world.update.flag.context.Animation
|
||||
import core.tools.RandomFunction
|
||||
import core.game.system.config.ItemConfigParser
|
||||
|
|
@ -576,21 +575,14 @@ abstract class CombatSwingHandler(var type: CombatStyle?) {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun isProjectileClipped(entity: Node, victim: Node?, checkClose: Boolean): Boolean {
|
||||
for(x1 in 0 until entity.size()) {
|
||||
for(y1 in 0 until entity.size()) {
|
||||
val src = entity.location.transform(x1, y1, 0)
|
||||
for(x2 in 0 until victim!!.size()) {
|
||||
for(y2 in 0 until victim!!.size()) {
|
||||
val dst = victim!!.location.transform(x2, y2, 0)
|
||||
val path = PROJECTILE.find(src, 1, dst, 1, 1, 0, 0, 0, false, RegionManager::getClippingFlag)
|
||||
if(path.isSuccessful && (!checkClose || path.points.size <= 1)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
val target = requireNotNull(victim)
|
||||
return RsmodPathfinder.hasLineOfSightBetween(
|
||||
entity.location,
|
||||
entity.size(),
|
||||
target.location,
|
||||
target.size(),
|
||||
maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,24 @@ import core.api.utils.Vector
|
|||
import core.game.world.map.Location
|
||||
import core.game.world.map.Point
|
||||
import core.game.world.map.RegionManager
|
||||
import org.rsmod.game.pathfinder.LinePathFinder
|
||||
import org.rsmod.game.pathfinder.PathFinder
|
||||
import org.rsmod.game.pathfinder.RayCast
|
||||
import org.rsmod.game.pathfinder.collision.CollisionFlagMap
|
||||
import org.rsmod.game.pathfinder.reach.ReachStrategy
|
||||
import kotlin.math.floor
|
||||
|
||||
private const val SEARCH_MAP_SIZE = 128
|
||||
private const val RING_BUFFER_SIZE = 4096
|
||||
|
||||
class RsmodPathfinder : Pathfinder() {
|
||||
class RsmodPathfinder(
|
||||
private val maxWaypoints: Int = 25
|
||||
) : Pathfinder() {
|
||||
|
||||
private val routeFinder = ThreadLocal.withInitial { RouteFinderState() }
|
||||
private val defaultFinder = ThreadLocal.withInitial {
|
||||
PathFinder(RegionManager.RSMOD_CLIPPING_FLAGS, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
|
||||
}
|
||||
private val suppliedFinder = ThreadLocal.withInitial { RouteFinderState() }
|
||||
|
||||
override fun find(
|
||||
start: Location?,
|
||||
|
|
@ -30,16 +38,13 @@ class RsmodPathfinder : Pathfinder() {
|
|||
): Path {
|
||||
val source = requireNotNull(start)
|
||||
val destination = requireNotNull(dest)
|
||||
val supplier = clipMaskSupplier ?: ClipMaskSupplier { z, x, y ->
|
||||
RegionManager.getClippingFlag(z, x, y)
|
||||
}
|
||||
val path = Path()
|
||||
var end = destination
|
||||
val vector = Vector.betweenLocs(source, destination)
|
||||
val magnitude = floor(vector.magnitude())
|
||||
|
||||
if (magnitude > ServerConstants.MAX_PATHFIND_DISTANCE) {
|
||||
if (magnitude < 50.0) {
|
||||
if (canAttempt(source, destination)) {
|
||||
end = source.transform(vector.normalized() * (ServerConstants.MAX_PATHFIND_DISTANCE - 1))
|
||||
} else {
|
||||
path.isMoveNear = true
|
||||
|
|
@ -47,15 +52,16 @@ class RsmodPathfinder : Pathfinder() {
|
|||
}
|
||||
}
|
||||
|
||||
val state = routeFinder.get()
|
||||
state.loadCollisionWindow(source, supplier)
|
||||
|
||||
val shape = when {
|
||||
type != 0 -> type - 1
|
||||
sizeX != 0 && sizeY != 0 -> 10
|
||||
else -> -1
|
||||
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 = state.finder.findPath(
|
||||
val route = finder.findPath(
|
||||
level = source.z,
|
||||
srcX = source.x,
|
||||
srcZ = source.y,
|
||||
|
|
@ -67,26 +73,180 @@ class RsmodPathfinder : Pathfinder() {
|
|||
objRot = rotation,
|
||||
objShape = shape,
|
||||
moveNear = near,
|
||||
blockAccessFlags = walkingFlag
|
||||
blockAccessFlags = walkingFlag,
|
||||
maxWaypoints = maxWaypoints
|
||||
)
|
||||
|
||||
if (route.failed) {
|
||||
return path
|
||||
}
|
||||
|
||||
var currentX = source.x
|
||||
var currentY = source.y
|
||||
path.points.add(Point(currentX, currentY))
|
||||
for (waypoint in route.waypoints) {
|
||||
path.points.add(Point(waypoint.x, waypoint.z))
|
||||
while (currentX != waypoint.x || currentY != waypoint.z) {
|
||||
currentX += waypoint.x.compareTo(currentX)
|
||||
currentY += waypoint.z.compareTo(currentY)
|
||||
path.points.add(Point(currentX, currentY))
|
||||
}
|
||||
}
|
||||
path.setSuccesful(true)
|
||||
path.isMoveNear = route.alternative || end != destination
|
||||
return path
|
||||
}
|
||||
|
||||
private class RouteFinderState {
|
||||
val flags = CollisionFlagMap()
|
||||
val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun canAttempt(start: Location, dest: Location): Boolean {
|
||||
val distance = floor(Vector.betweenLocs(start, dest).magnitude())
|
||||
return distance < ServerConstants.MAX_PATHFIND_DISTANCE * 2.0
|
||||
}
|
||||
|
||||
fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) {
|
||||
@JvmStatic
|
||||
fun canReach(
|
||||
srcX: Int,
|
||||
srcY: Int,
|
||||
moverSize: Int,
|
||||
destX: Int,
|
||||
destY: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
rotation: Int,
|
||||
type: Int,
|
||||
walkingFlag: Int,
|
||||
z: Int,
|
||||
clipMaskSupplier: ClipMaskSupplier?
|
||||
): Boolean {
|
||||
val flags = if (clipMaskSupplier == null) {
|
||||
RegionManager.loadClippingWindow(Location.create(srcX, srcY, z), SEARCH_MAP_SIZE)
|
||||
RegionManager.RSMOD_CLIPPING_FLAGS
|
||||
} else {
|
||||
CollisionFlagMap().also {
|
||||
loadCollisionWindow(
|
||||
flags = it,
|
||||
start = Location.create(srcX, srcY, z),
|
||||
supplier = clipMaskSupplier
|
||||
)
|
||||
}
|
||||
}
|
||||
return ReachStrategy.reached(
|
||||
flags = flags,
|
||||
level = z,
|
||||
srcX = srcX,
|
||||
srcZ = srcY,
|
||||
destX = destX,
|
||||
destZ = destY,
|
||||
destWidth = if (destWidth == 0) 1 else destWidth,
|
||||
destHeight = if (destHeight == 0) 1 else destHeight,
|
||||
srcSize = moverSize,
|
||||
objRot = rotation,
|
||||
objShape = routeShape(type, destWidth, destHeight),
|
||||
blockAccessFlags = walkingFlag
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun lineOfSight(
|
||||
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
|
||||
): 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
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSight(
|
||||
start: Location,
|
||||
dest: Location,
|
||||
moverSize: Int,
|
||||
destWidth: Int,
|
||||
destHeight: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE
|
||||
): Boolean {
|
||||
val rayCast = lineOfSight(start, dest, moverSize, destWidth, destHeight)
|
||||
return rayCast.success && rayCast.coordinates.size <= maxRaySteps
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSightBetween(
|
||||
sourceLocation: Location,
|
||||
sourceSize: Int,
|
||||
targetLocation: Location,
|
||||
targetSize: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE
|
||||
): Boolean {
|
||||
RegionManager.loadClippingWindow(sourceLocation, SEARCH_MAP_SIZE)
|
||||
return hasLineOfSightBetweenLoaded(sourceLocation, sourceSize, targetLocation, targetSize, maxRaySteps)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadLineOfSightWindow(center: Location) {
|
||||
RegionManager.loadClippingWindow(center, SEARCH_MAP_SIZE)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasLineOfSightBetweenLoaded(
|
||||
sourceLocation: Location,
|
||||
sourceSize: Int,
|
||||
targetLocation: Location,
|
||||
targetSize: Int,
|
||||
maxRaySteps: Int = Int.MAX_VALUE
|
||||
): Boolean {
|
||||
for (sourceX in 0 until sourceSize) {
|
||||
for (sourceY in 0 until sourceSize) {
|
||||
val source = sourceLocation.transform(sourceX, sourceY, 0)
|
||||
for (targetX in 0 until targetSize) {
|
||||
for (targetY in 0 until targetSize) {
|
||||
val destination = targetLocation.transform(targetX, targetY, 0)
|
||||
val rayCast = lineOfSightLoaded(source, destination, 1, 1, 1)
|
||||
if (rayCast.success && rayCast.coordinates.size <= maxRaySteps) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun routeShape(type: Int, sizeX: Int, sizeY: Int): Int = when {
|
||||
type >= 0 -> type
|
||||
sizeX != 0 && sizeY != 0 -> 10
|
||||
else -> -1
|
||||
}
|
||||
|
||||
private val projectileLineFinder = ThreadLocal.withInitial {
|
||||
LinePathFinder(RegionManager.RSMOD_PROJECTILE_FLAGS)
|
||||
}
|
||||
|
||||
private fun loadCollisionWindow(
|
||||
flags: CollisionFlagMap,
|
||||
start: Location,
|
||||
supplier: ClipMaskSupplier
|
||||
) {
|
||||
val baseX = start.x - (SEARCH_MAP_SIZE / 2)
|
||||
val baseY = start.y - (SEARCH_MAP_SIZE / 2)
|
||||
for (x in baseX until baseX + SEARCH_MAP_SIZE) {
|
||||
|
|
@ -96,4 +256,13 @@ class RsmodPathfinder : Pathfinder() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RouteFinderState {
|
||||
val flags = CollisionFlagMap()
|
||||
val finder = PathFinder(flags, SEARCH_MAP_SIZE, RING_BUFFER_SIZE)
|
||||
|
||||
fun loadCollisionWindow(start: Location, supplier: ClipMaskSupplier) {
|
||||
loadCollisionWindow(flags, start, supplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ class PathfinderTests {
|
|||
val blocked = Pathfinder.PROJECTILE.find(start, 1, dest, 0, 0, 0, -1, 0, false, null)
|
||||
|
||||
Assertions.assertFalse(blocked.isSuccessful)
|
||||
Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetween(start, 1, dest, 1))
|
||||
RsmodPathfinder.loadLineOfSightWindow(start)
|
||||
Assertions.assertFalse(RsmodPathfinder.hasLineOfSightBetweenLoaded(start, 1, dest, 1))
|
||||
} finally {
|
||||
RegionManager.setRsmodFlag(0, 3201, 3200, true, 0)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue