mirror of
https://gitlab.com/2009scape/2009scape.git
synced 2026-08-28 05:45:10 -06:00
Dumbed down dumb NPCs, fixed a bunch of regressions, improved ::drawroute
And some new tests
This commit is contained in:
parent
894674a12f
commit
fad4726760
8 changed files with 997 additions and 217 deletions
|
|
@ -26,6 +26,7 @@ import content.region.wilderness.handlers.revenants.RevenantNPC;
|
|||
|
||||
import static core.api.ContentAPIKt.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
|
||||
/**
|
||||
|
|
@ -236,50 +237,38 @@ public abstract class MovementPulse extends Pulse {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private boolean tryInteract() {
|
||||
Location ml = mover.getLocation();
|
||||
// Allow being within 1 square of moving entities to interact with them.
|
||||
int radius = destination instanceof Entity && ((Entity)destination).getWalkingQueue().hasPath() ? 1 : 0;
|
||||
if (interactLocation == null)
|
||||
return false;
|
||||
boolean atInteractLocation = Math.max(Math.abs(ml.getX() - interactLocation.getX()), Math.abs(ml.getY() - interactLocation.getY())) <= radius;
|
||||
// Check if already in a valid interaction position for entity destinations
|
||||
boolean canInteractFromCurrentPosition = false;
|
||||
if (!atInteractLocation && destination instanceof Entity) {
|
||||
Entity target = (Entity) destination;
|
||||
Location dl = target.getLocation();
|
||||
boolean onSameTile = ml.getX() == dl.getX() && ml.getY() == dl.getY() && ml.getZ() == dl.getZ();
|
||||
if (!onSameTile) {
|
||||
canInteractFromCurrentPosition = Pathfinder.canInteract(
|
||||
ml.getX(), ml.getY(), mover.size(),
|
||||
dl.getX(), dl.getY(), target.size(), target.size(),
|
||||
0, // walkFlag - "can interact from any unblocked direction"
|
||||
ml.getZ(),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
if (atInteractLocation || canInteractFromCurrentPosition) {
|
||||
try {
|
||||
if (near || pulse()) {
|
||||
if (mover instanceof Player) {
|
||||
if (near) {
|
||||
((Player) mover).getPacketDispatch().sendMessage("I can't reach that.");
|
||||
}
|
||||
PacketRepository.send(ClearMinimapFlag.class, new PlayerContext((Player) mover));
|
||||
}
|
||||
stop();
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e){
|
||||
e.printStackTrace();
|
||||
stop();
|
||||
boolean atInteractLocation = ml.equals(interactLocation);
|
||||
if (destination instanceof Entity) {
|
||||
boolean canInteractFromCurrentPosition = canInteractWithEntityFromCurrentPosition((Entity) destination);
|
||||
if (!canInteractFromCurrentPosition && !(near && atInteractLocation) && !(hasExplicitInteractionLocation() && atInteractLocation)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!atInteractLocation) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (near || pulse()) {
|
||||
if (mover instanceof Player) {
|
||||
if (near) {
|
||||
((Player) mover).getPacketDispatch().sendMessage("I can't reach that.");
|
||||
}
|
||||
PacketRepository.send(ClearMinimapFlag.class, new PlayerContext((Player) mover));
|
||||
}
|
||||
stop();
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
stop();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the mover can interact with an entity destination from their
|
||||
* current position without needing to move.
|
||||
|
|
@ -287,34 +276,38 @@ public abstract class MovementPulse extends Pulse {
|
|||
* @return true if the mover can interact without moving
|
||||
*/
|
||||
private boolean canInteractWithoutMoving() {
|
||||
if (!(destination instanceof Entity)) {
|
||||
return false;
|
||||
}
|
||||
Entity target = (Entity) destination;
|
||||
Location ml = mover.getLocation();
|
||||
Location dl = target.getLocation();
|
||||
if (ml.getX() == dl.getX() && ml.getY() == dl.getY() && ml.getZ() == dl.getZ()) {
|
||||
return false;
|
||||
}
|
||||
if (isInsideEntity(mover.getLocation())) {
|
||||
return false;
|
||||
}
|
||||
if (target.getWalkingQueue().hasPath()) { // For moving entities, allow interaction from 1 tile away
|
||||
int distance = Math.max(
|
||||
Math.abs(ml.getX() - dl.getX()),
|
||||
Math.abs(ml.getY() - dl.getY())
|
||||
);
|
||||
if (distance <= target.size()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Pathfinder.canInteract(
|
||||
ml.getX(), ml.getY(), mover.size(),
|
||||
dl.getX(), dl.getY(), target.size(), target.size(),
|
||||
0, // walkFlag - "can interact from any unblocked direction"
|
||||
ml.getZ(),
|
||||
null
|
||||
);
|
||||
if (!(destination instanceof Entity)) {
|
||||
return false;
|
||||
}
|
||||
return canInteractWithEntityFromCurrentPosition((Entity) destination);
|
||||
}
|
||||
|
||||
private boolean canInteractWithEntityFromCurrentPosition(Entity target) {
|
||||
return canInteractWithEntityFrom(mover.getLocation(), target);
|
||||
}
|
||||
|
||||
private boolean canInteractWithEntityFrom(Location source, Entity target) {
|
||||
Location dl = target.getLocation();
|
||||
if (source.getZ() != dl.getZ()) {
|
||||
return false;
|
||||
}
|
||||
if (Pathfinder.isStandingIn(
|
||||
source.getX(), source.getY(), mover.size(), mover.size(),
|
||||
dl.getX(), dl.getY(), target.size(), target.size()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
return Pathfinder.canInteract(
|
||||
source.getX(), source.getY(), mover.size(),
|
||||
dl.getX(), dl.getY(), target.size(), target.size(),
|
||||
0, // walkFlag - "can interact from any unblocked direction"
|
||||
source.getZ(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hasExplicitInteractionLocation() {
|
||||
return optionHandler != null || useHandler != null || overrideMethod != null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -355,53 +348,53 @@ public abstract class MovementPulse extends Pulse {
|
|||
}
|
||||
last = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds a path to the destination, if necessary.
|
||||
*/
|
||||
private boolean usingTruncatedPath = false;
|
||||
private boolean isMoveNearSet = false;
|
||||
|
||||
public void updatePath() {
|
||||
if (mover instanceof NPC && mover.asNpc().isNeverWalks()) {
|
||||
return;
|
||||
}
|
||||
if(destination == null || destination.getLocation() == null){
|
||||
if (destination == null || destination.getLocation() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location loc = null;
|
||||
|
||||
|
||||
if (optionHandler != null) {
|
||||
loc = optionHandler.getDestination(mover, destination);
|
||||
}
|
||||
else if (useHandler != null) {
|
||||
} else if (useHandler != null) {
|
||||
loc = useHandler.getDestination((Player) mover, destination);
|
||||
}
|
||||
else if (isInsideEntity(mover.getLocation())) {
|
||||
} else if (isInsideEntity(mover.getLocation())) {
|
||||
loc = findBorderLocation();
|
||||
}
|
||||
|
||||
|
||||
if (loc == null && destinationFlag != null && overrideMethod == null) {
|
||||
loc = destinationFlag.getDestination(mover, destination);
|
||||
}
|
||||
else if(loc == null && overrideMethod != null){
|
||||
loc = overrideMethod.invoke(mover,destination);
|
||||
if(loc == destination.getLocation() && destinationFlag != null) loc = destinationFlag.getDestination(mover,destination);
|
||||
} else if (loc == null && overrideMethod != null) {
|
||||
loc = overrideMethod.invoke(mover, destination);
|
||||
if (loc == destination.getLocation() && destinationFlag != null) loc = destinationFlag.getDestination(mover, destination);
|
||||
else if (loc == destination.getLocation()) loc = null;
|
||||
}
|
||||
|
||||
|
||||
if (destination instanceof NPC && mover.getProperties().getCombatPulse().getVictim() != destination)
|
||||
loc = checkForEntityPathInterrupt(loc != null ? loc : destination.getLocation());
|
||||
|
||||
|
||||
if (interactLocation == null)
|
||||
interactLocation = loc;
|
||||
|
||||
if (destination instanceof Entity || interactLocation == null || (mover.getWalkingQueue().getQueue().size() <= 1 && interactLocation.getDistance(mover.getLocation()) > 0) || (usingTruncatedPath && destination.getLocation().getDistance(mover.getLocation()) < 14)) {
|
||||
|
||||
if (destination instanceof Entity || interactLocation == null || (mover.getWalkingQueue().getQueue()
|
||||
.size() <= 1 && interactLocation.getDistance(mover.getLocation()) > 0) || (usingTruncatedPath && destination.getLocation()
|
||||
.getDistance(mover.getLocation()) < 14)) {
|
||||
if (!checkAllowMovement())
|
||||
return;
|
||||
if (destination instanceof Entity && previousLoc != null && previousLoc.equals(loc) && mover.getWalkingQueue().hasPath())
|
||||
if (destination instanceof Entity && previousLoc != null && previousLoc.equals(loc) && hasQueuedMovement(mover))
|
||||
return;
|
||||
|
||||
|
||||
Path path;
|
||||
Pair<Boolean, Location> truncation = truncateLoc(mover, loc != null ? loc : destination.getLocation());
|
||||
if (truncation.getFirst()) {
|
||||
|
|
@ -413,7 +406,7 @@ public abstract class MovementPulse extends Pulse {
|
|||
usingTruncatedPath = false;
|
||||
}
|
||||
near = !path.isSuccessful() || path.isMoveNear();
|
||||
|
||||
|
||||
if (!path.getPoints().isEmpty()) {
|
||||
Point point = path.getPoints().getLast();
|
||||
if (forceRun) {
|
||||
|
|
@ -423,17 +416,31 @@ public abstract class MovementPulse extends Pulse {
|
|||
}
|
||||
int size = path.getPoints().toArray().length;
|
||||
Deque points = path.getPoints();
|
||||
Location lastQueuedEntityLocation = null;
|
||||
for (int i = 0; i < size; i++) {
|
||||
point = path.getPoints().pop();
|
||||
Location pointLocation = Location.create(point.getX(), point.getY(), mover.getLocation().getZ());
|
||||
boolean currentPathPoint = pointLocation.equals(mover.getLocation());
|
||||
if (!currentPathPoint && shouldTruncateEntityPath() && overlapsEntityFootprint(pointLocation, (Entity) destination)) {
|
||||
if (interactLocation == null && lastQueuedEntityLocation != null) {
|
||||
interactLocation = lastQueuedEntityLocation;
|
||||
}
|
||||
break;
|
||||
}
|
||||
mover.getWalkingQueue().addPath(point.getX(), point.getY());
|
||||
if (destination instanceof Entity) {
|
||||
mover.face((Entity) destination);
|
||||
} else {
|
||||
mover.face(null);
|
||||
}
|
||||
|
||||
|
||||
lastQueuedEntityLocation = pointLocation;
|
||||
if (!currentPathPoint && shouldTruncateEntityPath() && canInteractWithEntityFrom(pointLocation, (Entity) destination)) {
|
||||
interactLocation = pointLocation;
|
||||
break;
|
||||
}
|
||||
if (i == size - 1 && interactLocation == null)
|
||||
interactLocation = Location.create(point.getX(), point.getY(), mover.getLocation().getZ());
|
||||
interactLocation = pointLocation;
|
||||
}
|
||||
} else if (interactLocation == null && path.isSuccessful() && !path.isMoveNear()) {
|
||||
interactLocation = mover.getLocation();
|
||||
|
|
@ -446,14 +453,14 @@ public abstract class MovementPulse extends Pulse {
|
|||
registerHintIcon((Player) mover, interactLocation, 5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean checkAllowMovement() {
|
||||
boolean canMove = true;
|
||||
if (destination instanceof Entity) {
|
||||
Entity e = (Entity) destination;
|
||||
Location l = e.getLocation();
|
||||
Deque<Point> npcPath = e.getWalkingQueue().getQueue();
|
||||
if (e.getWalkingQueue().hasPath() && e.getProperties().getCombatPulse().isRunning() && e.getProperties().getCombatPulse().getVictim() == mover)
|
||||
if (hasQueuedMovement(e) && e.getProperties().getCombatPulse().isRunning() && e.getProperties().getCombatPulse().getVictim() == mover)
|
||||
canMove = false;
|
||||
if (!canMove) { //If we normally shouldn't move, but the NPC's pathfinding is not letting them move, then move.
|
||||
if (npcPath.size() == 1) {
|
||||
|
|
@ -465,53 +472,84 @@ public abstract class MovementPulse extends Pulse {
|
|||
}
|
||||
return canMove;
|
||||
}
|
||||
|
||||
private Location checkForEntityPathInterrupt(Location loc) {
|
||||
Location ml = mover.getLocation();
|
||||
Location dl = destination.getLocation();
|
||||
// Lead the target if they're walking/running, unless they're already within interaction range
|
||||
if (loc != null && destination instanceof Entity) {
|
||||
WalkingQueue wq = ((Entity) destination).getWalkingQueue();
|
||||
if (wq.hasPath()) {
|
||||
Point[] points = wq.getQueue().toArray(new Point[0]);
|
||||
if (points.length > 0) {
|
||||
Point p = points[0];
|
||||
Point predictiveIntersection = null;
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
Location closestBorder = getClosestBorderToPoint(points[i], loc.getZ());
|
||||
|
||||
if (!RegionManager.isTeleportPermitted(closestBorder)) { // A nasty hack to discard invalid intersection points
|
||||
continue;
|
||||
}
|
||||
int moverDist = Math.max(Math.abs(ml.getX() - closestBorder.getX()), Math.abs(ml.getY() - closestBorder.getY()));
|
||||
float movementRatio = moverDist / (float) ((i + 1) / (mover.getWalkingQueue().isRunning() ? 2 : 1));
|
||||
if (predictiveIntersection == null && movementRatio <= 1.0) { //try to predict an intersection point on the path if possible
|
||||
predictiveIntersection = points[i];
|
||||
break;
|
||||
}
|
||||
// Otherwise, we target the farthest point along target's planned movement that's within 1 tick's running,
|
||||
// this ensures the player will run to catch up to the target if able.
|
||||
if (moverDist <= 2) {
|
||||
p = points[i];
|
||||
}
|
||||
}
|
||||
if (predictiveIntersection != null)
|
||||
p = predictiveIntersection;
|
||||
|
||||
Location endLoc = getClosestBorderToPoint(p, loc.getZ());
|
||||
|
||||
if (!RegionManager.isTeleportPermitted(endLoc)) { // Basically a prayer
|
||||
return loc;
|
||||
}
|
||||
return endLoc;
|
||||
}
|
||||
}
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
private Location getClosestBorderToPoint (Point p, int plane) {
|
||||
Vector pathDiff = Vector.betweenLocs (destination.getLocation(), Location.create(p.getX(), p.getY(), plane));
|
||||
|
||||
private Location checkForEntityPathInterrupt(Location loc) {
|
||||
Location ml = mover.getLocation();
|
||||
Location dl = destination.getLocation();
|
||||
// Lead the target if they're walking/running, unless they're already within interaction range
|
||||
if (loc != null && destination instanceof Entity) {
|
||||
WalkingQueue wq = ((Entity) destination).getWalkingQueue();
|
||||
if (hasQueuedMovement((Entity) destination)) {
|
||||
Point[] points = queuedMovementPoints(wq);
|
||||
if (points.length > 0) {
|
||||
Point p = points[0];
|
||||
Point predictiveIntersection = null;
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
Location closestBorder = getClosestBorderToPoint(points[i], loc.getZ());
|
||||
|
||||
if (!RegionManager.isTeleportPermitted(closestBorder)) { // A nasty hack to discard invalid intersection points
|
||||
continue;
|
||||
}
|
||||
int moverDist = Math.max(Math.abs(ml.getX() - closestBorder.getX()), Math.abs(ml.getY() - closestBorder.getY()));
|
||||
float movementRatio = moverDist / (float) ((i + 1) / (mover.getWalkingQueue().isRunning() ? 2 : 1));
|
||||
if (predictiveIntersection == null && movementRatio <= 1.0) { //try to predict an intersection point on the path if possible
|
||||
predictiveIntersection = points[i];
|
||||
break;
|
||||
}
|
||||
// Otherwise, we target the farthest point along target's planned movement that's within 1 tick's running,
|
||||
// this ensures the player will run to catch up to the target if able.
|
||||
if (moverDist <= 2) {
|
||||
p = points[i];
|
||||
}
|
||||
}
|
||||
if (predictiveIntersection != null)
|
||||
p = predictiveIntersection;
|
||||
|
||||
Location endLoc = getClosestBorderToPoint(p, loc.getZ());
|
||||
|
||||
if (!RegionManager.isTeleportPermitted(endLoc)) { // Basically a prayer
|
||||
return loc;
|
||||
}
|
||||
return endLoc;
|
||||
}
|
||||
}
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
private boolean shouldTruncateEntityPath() {
|
||||
return destination instanceof Entity && !hasExplicitInteractionLocation();
|
||||
}
|
||||
|
||||
private boolean overlapsEntityFootprint(Location source, Entity target) {
|
||||
Location targetLocation = target.getLocation();
|
||||
return Pathfinder.isStandingIn(
|
||||
source.getX(), source.getY(), mover.size(), mover.size(),
|
||||
targetLocation.getX(), targetLocation.getY(), target.size(), target.size()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hasQueuedMovement(Entity entity) {
|
||||
for (Point point : entity.getWalkingQueue().getQueue()) {
|
||||
if (point.getDirection() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Point[] queuedMovementPoints(WalkingQueue walkingQueue) {
|
||||
ArrayList<Point> points = new ArrayList<>();
|
||||
for (Point point : walkingQueue.getQueue()) {
|
||||
if (point.getDirection() != null) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
return points.toArray(new Point[0]);
|
||||
}
|
||||
|
||||
private Location getClosestBorderToPoint(Point p, int plane) {
|
||||
Vector pathDiff = Vector.betweenLocs(destination.getLocation(), Location.create(p.getX(), p.getY(), plane));
|
||||
Location predictedCenterPos = (destination.getMathematicalCenter().plus(pathDiff)).toLocation(plane);
|
||||
Vector toPlayerNormalized = Vector.betweenLocs(predictedCenterPos, mover.getCenterLocation()).normalized();
|
||||
return predictedCenterPos.transform(toPlayerNormalized.times(destination.size() + 1));
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ import core.game.world.map.RegionManager
|
|||
import core.game.world.map.path.Path
|
||||
import core.game.world.map.path.Pathfinder
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
import java.util.LinkedHashMap
|
||||
import org.rsmod.game.pathfinder.PathFinder as RsmodRouteFinder
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
|
|
@ -37,6 +36,7 @@ object CombatMovementIntents {
|
|||
val location: Location
|
||||
get() = node.location
|
||||
}
|
||||
|
||||
private data class CandidatePath(val steps: List<Point>, val projectedLocation: Location)
|
||||
data class ResolveReport(
|
||||
val intents: Int,
|
||||
|
|
@ -49,11 +49,11 @@ object CombatMovementIntents {
|
|||
fun summary(): String {
|
||||
val slowest = slowestIntent?.let {
|
||||
", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " +
|
||||
"candidates=${it.candidateCount} directPathHits=${it.directPathHits} " +
|
||||
"rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}"
|
||||
"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"
|
||||
"directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
@ -226,13 +226,11 @@ object CombatMovementIntents {
|
|||
return false
|
||||
}
|
||||
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) {
|
||||
if (canAttackFrom(attacker, target, attacker.location, predictedTargetLocation)) {
|
||||
return false
|
||||
}
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location
|
||||
return projectedAttackerLocation !in attackTiles
|
||||
return !canAttackFrom(attacker, target, projectedAttackerLocation, predictedTargetLocation)
|
||||
}
|
||||
|
||||
private fun isActiveMeleeAttacker(attacker: Entity, target: Entity): Boolean {
|
||||
|
|
@ -278,7 +276,14 @@ 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
|
||||
)
|
||||
) {
|
||||
attacker.walkingQueue.reset()
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = attacker.location
|
||||
|
|
@ -297,43 +302,54 @@ object CombatMovementIntents {
|
|||
if (shouldUseTargetFootprintRoute(attacker, target, pathfinder)) {
|
||||
val candidatePath = pathToTargetFootprint(attacker, target, targetLocation, pathfinder, trace)
|
||||
if (candidatePath != null) {
|
||||
if (hasReservedOccupiedTile(reservedTiles, attacker, candidatePath.projectedLocation)) {
|
||||
val attackPath =
|
||||
truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace)
|
||||
if (attackPath != null) {
|
||||
if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) {
|
||||
return
|
||||
}
|
||||
walkPath(attacker, attackPath)
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = attackPath.projectedLocation
|
||||
reserveOccupiedTiles(reservedTiles, attacker, attackPath.projectedLocation)
|
||||
return
|
||||
}
|
||||
walkPath(attacker, candidatePath)
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = candidatePath.projectedLocation
|
||||
reserveOccupiedTiles(reservedTiles, attacker, candidatePath.projectedLocation)
|
||||
return
|
||||
}
|
||||
if (shouldStopUnreachableCombat(attacker, target)) {
|
||||
stopUnreachableCombat(attacker)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val candidates = movementDestinationsFor(attacker, target, targetLocation, pathfinder, trace)
|
||||
val standingOnCandidate = candidates.any { it.location == attacker.location }
|
||||
val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location
|
||||
if (projectedAttackerLocation != attacker.location && canAttackFrom(attacker, target, projectedAttackerLocation, targetLocation, trace)) {
|
||||
if (projectedAttackerLocation != attacker.location && canAttackFrom(
|
||||
attacker,
|
||||
target,
|
||||
projectedAttackerLocation,
|
||||
targetLocation,
|
||||
trace
|
||||
)
|
||||
) {
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = projectedAttackerLocation
|
||||
reserveOccupiedTiles(reservedTiles, attacker, projectedAttackerLocation)
|
||||
return
|
||||
}
|
||||
|
||||
val queueStationaryContinuation = attacker.properties.combatPulse.style == CombatStyle.MELEE &&
|
||||
!CombatMovementPlanner.hasMovementStepThisTick(target)
|
||||
var blockedByReservation = false
|
||||
for (candidate in candidates) {
|
||||
val candidatePath = pathTo(attacker, candidate, trace) ?: continue
|
||||
if (hasReservedOccupiedTile(reservedTiles, attacker, candidatePath.projectedLocation)) {
|
||||
val candidatePath = pathTo(attacker, candidate, trace, queueStationaryContinuation) ?: continue
|
||||
val attackPath = truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace)
|
||||
?: continue
|
||||
if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) {
|
||||
blockedByReservation = true
|
||||
continue
|
||||
}
|
||||
|
||||
walkPath(attacker, candidatePath)
|
||||
walkPath(attacker, attackPath)
|
||||
attacker.face(target)
|
||||
projectedLocations[attacker] = candidatePath.projectedLocation
|
||||
reserveOccupiedTiles(reservedTiles, attacker, candidatePath.projectedLocation)
|
||||
projectedLocations[attacker] = attackPath.projectedLocation
|
||||
reserveOccupiedTiles(reservedTiles, attacker, attackPath.projectedLocation)
|
||||
return
|
||||
}
|
||||
if (!blockedByReservation && shouldStopUnreachableCombat(attacker, target, standingOnCandidate)) {
|
||||
|
|
@ -376,7 +392,14 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -401,7 +424,7 @@ object CombatMovementIntents {
|
|||
return destinations
|
||||
}
|
||||
|
||||
val attackTiles = CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)
|
||||
val attackTiles = attackTilesFor(attacker, target, targetLocation, trace)
|
||||
if (attacker is Player) {
|
||||
val rangedTiles = playerAttackRangeDestinations(attacker, target, targetLocation, pathfinder, trace)
|
||||
val capacity = rangedTiles.size + attackTiles.size + 1
|
||||
|
|
@ -432,6 +455,27 @@ object CombatMovementIntents {
|
|||
return destinations
|
||||
}
|
||||
|
||||
private fun attackTilesFor(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
trace: IntentTrace
|
||||
): List<Location> {
|
||||
if (attacker.properties.combatPulse.style != CombatStyle.MELEE) {
|
||||
return CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation)
|
||||
}
|
||||
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<Location> { distanceSquared(it, attacker.location) }
|
||||
.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }
|
||||
.thenBy { it.x }
|
||||
.thenBy { it.y }
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldAllowPartialTargetPath(attacker: Player, target: Entity, targetLocation: Location): Boolean {
|
||||
val range = playerAttackRange(attacker)
|
||||
if (range <= CombatReach.meleeDistance(attacker)) {
|
||||
|
|
@ -482,7 +526,7 @@ object CombatMovementIntents {
|
|||
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)
|
||||
rangeWeapon.weaponType == Weapon.WeaponType.DEGRADING)
|
||||
) {
|
||||
distance = 10
|
||||
}
|
||||
|
|
@ -521,7 +565,15 @@ object CombatMovementIntents {
|
|||
val attackTiles = ArrayList<Location>(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size))
|
||||
RsmodPathfinder.loadLineOfSightWindow(targetLocation)
|
||||
for (tile in tiles) {
|
||||
if (!hasProjectileLineOfSight(tile, attacker.size(), target, targetLocation, loadWindow = false, trace = trace)) {
|
||||
if (!hasProjectileLineOfSight(
|
||||
tile,
|
||||
attacker.size(),
|
||||
target,
|
||||
targetLocation,
|
||||
loadWindow = false,
|
||||
trace = trace
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
attackTiles.add(tile)
|
||||
|
|
@ -575,7 +627,7 @@ object CombatMovementIntents {
|
|||
)
|
||||
}
|
||||
return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range &&
|
||||
hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation, trace = trace)
|
||||
hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation, trace = trace)
|
||||
}
|
||||
|
||||
private fun isAdjacentToTarget(
|
||||
|
|
@ -700,11 +752,25 @@ object CombatMovementIntents {
|
|||
pathfinder: Pathfinder,
|
||||
trace: IntentTrace
|
||||
): CandidatePath? {
|
||||
val directDestination = naiveMeleeDestination(attacker, target, targetLocation)
|
||||
val directPath = directPathTo(attacker, directDestination)
|
||||
if (directPath != null) {
|
||||
val targetMoving = CombatMovementPlanner.hasMovementStepThisTick(target)
|
||||
val movingTargetPath =
|
||||
if (targetMoving) directPathTowardMovingTarget(attacker, target, targetLocation) else null
|
||||
if (movingTargetPath != null) {
|
||||
trace.directPathHits++
|
||||
return directPath
|
||||
return movingTargetPath
|
||||
}
|
||||
for (directDestination in preferredMeleeDestinations(attacker, target, targetLocation)) {
|
||||
val directPath = directPathTo(attacker, directDestination)
|
||||
if (directPath != null) {
|
||||
trace.directPathHits++
|
||||
return directPath
|
||||
}
|
||||
}
|
||||
if (!targetMoving) {
|
||||
return null
|
||||
}
|
||||
if (attacker is NPC && pathfinder === Pathfinder.DUMB) {
|
||||
return null
|
||||
}
|
||||
if (pathfinder === Pathfinder.SMART && !RsmodPathfinder.canAttempt(attacker.location, targetLocation)) {
|
||||
return null
|
||||
|
|
@ -731,32 +797,148 @@ object CombatMovementIntents {
|
|||
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
|
||||
return CandidatePath(steps, projected)
|
||||
}
|
||||
|
||||
private fun naiveMeleeDestination(attacker: Entity, target: Entity, targetLocation: Location): Location {
|
||||
val destination = RsmodRouteFinder.naiveDestination(
|
||||
sourceX = attacker.location.x,
|
||||
sourceZ = attacker.location.y,
|
||||
sourceWidth = attacker.size(),
|
||||
sourceHeight = attacker.size(),
|
||||
targetX = targetLocation.x,
|
||||
targetZ = targetLocation.y,
|
||||
targetWidth = target.size(),
|
||||
targetHeight = target.size()
|
||||
)
|
||||
return Location.create(destination.x, destination.z, targetLocation.z)
|
||||
private fun directPathTowardMovingTarget(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location
|
||||
): CandidatePath? {
|
||||
if (!CombatMovementPlanner.hasMovementStepThisTick(target)) {
|
||||
return null
|
||||
}
|
||||
if (attacker.size() != 1 || attacker.location.z != targetLocation.z) {
|
||||
return null
|
||||
}
|
||||
val maxSteps = movementStepsFor(attacker)
|
||||
val steps = ArrayList<Point>(maxSteps)
|
||||
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)) {
|
||||
return null
|
||||
}
|
||||
val next = current.transform(direction)
|
||||
if (!RegionManager.isTeleportPermitted(next)) {
|
||||
return null
|
||||
}
|
||||
steps.add(Point(next.x, next.y, direction, direction.stepX, direction.stepY))
|
||||
current = next
|
||||
}
|
||||
while (steps.isNotEmpty()) {
|
||||
val projected = steps.last().let { Location.create(it.x, it.y, attacker.location.z) }
|
||||
if (!occupiedTilesOverlap(attacker, projected, target, targetLocation)) {
|
||||
return CandidatePath(steps, projected)
|
||||
}
|
||||
steps.removeAt(steps.lastIndex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun preferredMeleeDestinations(attacker: Entity, target: Entity, targetLocation: Location): List<Location> {
|
||||
val directions = ArrayList<Direction>(4)
|
||||
val attackerCenter = attacker.centerLocation
|
||||
val targetCenterX = targetLocation.x + (target.size() shr 1)
|
||||
val targetCenterY = targetLocation.y + (target.size() shr 1)
|
||||
val dx = attackerCenter.x - targetCenterX
|
||||
val dy = attackerCenter.y - targetCenterY
|
||||
|
||||
if (abs(dx) >= abs(dy)) {
|
||||
if (dx < 0) {
|
||||
addDirection(directions, Direction.WEST)
|
||||
} else if (dx > 0) {
|
||||
addDirection(directions, Direction.EAST)
|
||||
}
|
||||
}
|
||||
if (abs(dy) >= abs(dx)) {
|
||||
if (dy < 0) {
|
||||
addDirection(directions, Direction.SOUTH)
|
||||
} else if (dy > 0) {
|
||||
addDirection(directions, Direction.NORTH)
|
||||
}
|
||||
}
|
||||
if (attackerCenter.x < targetCenterX) {
|
||||
addDirection(directions, Direction.WEST)
|
||||
} else if (attackerCenter.x > targetCenterX) {
|
||||
addDirection(directions, Direction.EAST)
|
||||
}
|
||||
if (attackerCenter.y < targetCenterY) {
|
||||
addDirection(directions, Direction.SOUTH)
|
||||
} else if (attackerCenter.y > targetCenterY) {
|
||||
addDirection(directions, Direction.NORTH)
|
||||
}
|
||||
|
||||
return directions.map { meleeDestination(attacker, target, targetLocation, it) }
|
||||
}
|
||||
|
||||
private fun meleeDestination(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
direction: Direction
|
||||
): Location {
|
||||
val minAlignedX = targetLocation.x - attacker.size() + 1
|
||||
val maxAlignedX = targetLocation.x + target.size() - 1
|
||||
val minAlignedY = targetLocation.y - attacker.size() + 1
|
||||
val maxAlignedY = targetLocation.y + target.size() - 1
|
||||
|
||||
return when (direction) {
|
||||
Direction.WEST -> Location.create(
|
||||
targetLocation.x - attacker.size(),
|
||||
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
Direction.EAST -> Location.create(
|
||||
targetLocation.x + target.size(),
|
||||
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
Direction.SOUTH -> Location.create(
|
||||
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
|
||||
targetLocation.y - attacker.size(),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
else -> Location.create(
|
||||
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
|
||||
targetLocation.y + target.size(),
|
||||
targetLocation.z
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDumbNpcAttackDestinations(
|
||||
|
|
@ -809,16 +991,19 @@ object CombatMovementIntents {
|
|||
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
Direction.EAST -> Location.create(
|
||||
targetLocation.x + target.size(),
|
||||
attacker.location.y.coerceIn(minAlignedY, maxAlignedY),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
Direction.SOUTH -> Location.create(
|
||||
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
|
||||
targetLocation.y - attacker.size(),
|
||||
targetLocation.z
|
||||
)
|
||||
|
||||
else -> Location.create(
|
||||
attacker.location.x.coerceIn(minAlignedX, maxAlignedX),
|
||||
targetLocation.y + target.size(),
|
||||
|
|
@ -827,11 +1012,46 @@ object CombatMovementIntents {
|
|||
}
|
||||
}
|
||||
|
||||
private fun pathTo(attacker: Entity, destination: MovementDestination, trace: IntentTrace): CandidatePath? {
|
||||
private fun truncateAtFirstAttackOpportunity(
|
||||
attacker: Entity,
|
||||
target: Entity,
|
||||
targetLocation: Location,
|
||||
path: CandidatePath,
|
||||
trace: IntentTrace
|
||||
): CandidatePath? {
|
||||
val steps = ArrayList<Point>(path.steps.size)
|
||||
for (step in path.steps) {
|
||||
val location = Location.create(step.x, step.y, attacker.location.z)
|
||||
if (occupiedTilesOverlap(attacker, location, target, targetLocation)) {
|
||||
return null
|
||||
}
|
||||
steps.add(step)
|
||||
if (canAttackFrom(attacker, target, location, targetLocation, trace)) {
|
||||
return CandidatePath(steps, location)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun pathTo(
|
||||
attacker: Entity,
|
||||
destination: MovementDestination,
|
||||
trace: IntentTrace,
|
||||
queueContinuation: Boolean = false
|
||||
): CandidatePath? {
|
||||
if (attacker.location == destination.location) {
|
||||
return null
|
||||
}
|
||||
val directPath = directPathTo(attacker, destination)
|
||||
val stepLimit = movementStepLimit(attacker, queueContinuation && !destination.allowPartialPath)
|
||||
if (attacker is NPC && destination.pathfinder === Pathfinder.DUMB) {
|
||||
val directPath = directPartialPathTo(attacker, destination, stepLimit)
|
||||
if (directPath != null) {
|
||||
trace.directPathHits++
|
||||
return directPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
val directPath = directPathTo(attacker, destination, stepLimit)
|
||||
if (directPath != null) {
|
||||
trace.directPathHits++
|
||||
return directPath
|
||||
|
|
@ -847,32 +1067,57 @@ object CombatMovementIntents {
|
|||
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)
|
||||
if (attacker is Player && destination.allowPartialPath && !partialPathMovesCloser(attacker, destination, steps)) {
|
||||
val steps = immediateMovementSteps(attacker, path, stepLimit)
|
||||
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
|
||||
return CandidatePath(steps, projected)
|
||||
}
|
||||
|
||||
private fun directPathTo(attacker: Entity, destination: MovementDestination): CandidatePath? {
|
||||
private fun directPathTo(
|
||||
attacker: Entity,
|
||||
destination: MovementDestination,
|
||||
maxSteps: Int = movementStepsFor(attacker)
|
||||
): CandidatePath? {
|
||||
if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) {
|
||||
return null
|
||||
}
|
||||
return directPathTo(attacker, destination.location)
|
||||
return directPathTo(attacker, destination.location, maxSteps)
|
||||
}
|
||||
|
||||
private fun directPathTo(attacker: Entity, destination: Location): CandidatePath? {
|
||||
private fun directPathTo(
|
||||
attacker: Entity,
|
||||
destination: Location,
|
||||
maxSteps: Int = movementStepsFor(attacker)
|
||||
): CandidatePath? {
|
||||
if (attacker.size() != 1 || attacker.location.z != destination.z) {
|
||||
return null
|
||||
}
|
||||
val maxSteps = movementStepsFor(attacker)
|
||||
val steps = ArrayList<Point>(maxSteps)
|
||||
var current = attacker.location
|
||||
var distance = 0
|
||||
|
|
@ -900,6 +1145,36 @@ object CombatMovementIntents {
|
|||
return CandidatePath(steps, projected)
|
||||
}
|
||||
|
||||
private fun directPartialPathTo(
|
||||
attacker: Entity,
|
||||
destination: MovementDestination,
|
||||
maxSteps: Int
|
||||
): CandidatePath? {
|
||||
if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) {
|
||||
return null
|
||||
}
|
||||
val steps = ArrayList<Point>(maxSteps)
|
||||
var current = attacker.location
|
||||
var distance = 0
|
||||
while (current != destination.location && steps.size < maxSteps) {
|
||||
if (++distance > MAX_DIRECT_COMBAT_PATH_DISTANCE) {
|
||||
break
|
||||
}
|
||||
val direction = Direction.getDirection(current, destination.location) ?: break
|
||||
if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) {
|
||||
break
|
||||
}
|
||||
val next = current.transform(direction)
|
||||
if (!RegionManager.isTeleportPermitted(next)) {
|
||||
break
|
||||
}
|
||||
steps.add(Point(next.x, next.y, direction, direction.stepX, direction.stepY))
|
||||
current = next
|
||||
}
|
||||
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null
|
||||
return CandidatePath(steps, projected)
|
||||
}
|
||||
|
||||
private fun isExcessiveCombatDetourToTarget(
|
||||
attacker: Player,
|
||||
target: Entity,
|
||||
|
|
@ -907,7 +1182,8 @@ object CombatMovementIntents {
|
|||
path: Path
|
||||
): Boolean {
|
||||
val pathLength = (path.points.size - 1).coerceAtLeast(0)
|
||||
val directDistance = sqrt(distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location).toDouble())
|
||||
val directDistance =
|
||||
sqrt(distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location).toDouble())
|
||||
return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR
|
||||
}
|
||||
|
||||
|
|
@ -919,7 +1195,7 @@ object CombatMovementIntents {
|
|||
): Boolean {
|
||||
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false
|
||||
return distanceSquaredToClosestOccupiedTile(target, targetLocation, projected) <
|
||||
distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location)
|
||||
distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location)
|
||||
}
|
||||
|
||||
private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean {
|
||||
|
|
@ -928,7 +1204,11 @@ object CombatMovementIntents {
|
|||
return pathLength > directDistance + MAX_PLAYER_COMBAT_PATH_DETOUR
|
||||
}
|
||||
|
||||
private fun partialPathMovesCloser(attacker: Player, destination: MovementDestination, steps: List<Point>): Boolean {
|
||||
private fun partialPathMovesCloser(
|
||||
attacker: Player,
|
||||
destination: MovementDestination,
|
||||
steps: List<Point>
|
||||
): Boolean {
|
||||
val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false
|
||||
return projected.getDistance(destination.location) < attacker.location.getDistance(destination.location)
|
||||
}
|
||||
|
|
@ -941,8 +1221,11 @@ object CombatMovementIntents {
|
|||
return terminal.x == destination.x && terminal.y == destination.y
|
||||
}
|
||||
|
||||
private fun immediateMovementSteps(attacker: Entity, path: Path): List<Point> {
|
||||
val maxSteps = movementStepsFor(attacker)
|
||||
private fun immediateMovementSteps(
|
||||
attacker: Entity,
|
||||
path: Path,
|
||||
maxSteps: Int = movementStepsFor(attacker)
|
||||
): List<Point> {
|
||||
val steps = ArrayList<Point>(maxSteps)
|
||||
for (point in path.points) {
|
||||
if (point.x == attacker.location.x && point.y == attacker.location.y) {
|
||||
|
|
@ -979,6 +1262,10 @@ object CombatMovementIntents {
|
|||
}
|
||||
}
|
||||
|
||||
private fun movementStepLimit(attacker: Entity, queueContinuation: Boolean): Int {
|
||||
return movementStepsFor(attacker) + if (queueContinuation) 1 else 0
|
||||
}
|
||||
|
||||
private fun canRun(attacker: Entity): Boolean {
|
||||
return attacker is Player && attacker.walkingQueue.isRunningBoth && attacker.settings.runEnergy >= 1.0
|
||||
}
|
||||
|
|
@ -1037,9 +1324,9 @@ object CombatMovementIntents {
|
|||
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
|
||||
firstLocation.x + first.size() > secondLocation.x &&
|
||||
firstLocation.y < secondLocation.y + second.size() &&
|
||||
firstLocation.y + first.size() > secondLocation.y
|
||||
}
|
||||
|
||||
private fun pathfinderFor(attacker: Entity): Pathfinder {
|
||||
|
|
|
|||
|
|
@ -321,8 +321,10 @@ class CombatPulse(
|
|||
victim.scripts.removeWeakScripts()
|
||||
}
|
||||
|
||||
if (!isAttacking) {
|
||||
if (!isAttacking)
|
||||
entity.pulseManager.run(this)
|
||||
if (victim is Entity && style == CombatStyle.MELEE) {
|
||||
CombatMovementIntents.trackActiveMelee(entity, victim)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import core.game.node.entity.npc.NPC
|
|||
import core.game.node.entity.player.Player
|
||||
import core.game.node.entity.skill.Skills
|
||||
import core.tools.RandomFunction
|
||||
import core.game.world.map.Direction
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import core.game.world.map.path.RsmodPathfinder
|
||||
import org.rs09.consts.Items
|
||||
import org.rs09.consts.NPCs
|
||||
import kotlin.math.floor
|
||||
|
|
@ -39,7 +43,7 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
distance += if (entity.walkingQueue.isRunningBoth) 2 else 1
|
||||
goodRange = CombatReach.canMelee(entity, victim, distance)
|
||||
}
|
||||
if (!isProjectileClipped(entity, victim, !CombatReach.isUsingHalberd(entity))) {
|
||||
if (!hasMeleeLineOfSight(entity, victim, type)) {
|
||||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
val isRunning = entity.walkingQueue.runDir != -1
|
||||
|
|
@ -58,6 +62,77 @@ open class MeleeSwingHandler (vararg flags: SwingHandlerFlag)
|
|||
return InteractionType.NO_INTERACT
|
||||
}
|
||||
|
||||
private fun hasMeleeLineOfSight(entity: Entity, victim: Entity, type: InteractionType): Boolean {
|
||||
val checkClose = !CombatReach.isUsingHalberd(entity)
|
||||
if (isProjectileClipped(entity, victim, checkClose)) {
|
||||
return true
|
||||
}
|
||||
if (type != InteractionType.MOVE_INTERACT || !checkClose) {
|
||||
return false
|
||||
}
|
||||
val predictedVictimLocation = CombatMovementPlanner.predictedMovementLocation(victim) ?: return false
|
||||
val projectedEntityLocation =
|
||||
projectedMeleeChaseLocation(entity, victim, predictedVictimLocation) ?: return false
|
||||
if (!isAdjacentTo(entity, projectedEntityLocation, victim, predictedVictimLocation)) {
|
||||
return false
|
||||
}
|
||||
return RsmodPathfinder.hasLineOfSightBetween(
|
||||
projectedEntityLocation,
|
||||
entity.size(),
|
||||
predictedVictimLocation,
|
||||
victim.size(),
|
||||
maxRaySteps = 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun projectedMeleeChaseLocation(entity: Entity, victim: Entity, victimLocation: Location): Location? {
|
||||
if (entity.size() != 1 || entity.location.z != victimLocation.z) {
|
||||
return null
|
||||
}
|
||||
val maxSteps =
|
||||
if (entity is Player && entity.walkingQueue.isRunningBoth && entity.settings.runEnergy >= 1.0) 2 else 1
|
||||
var current = entity.location
|
||||
var steps = 0
|
||||
while (steps < maxSteps && !isAdjacentTo(entity, current, victim, victimLocation)) {
|
||||
val direction = Direction.getDirection(current, victimLocation) ?: return null
|
||||
if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) {
|
||||
return null
|
||||
}
|
||||
val next = current.transform(direction)
|
||||
if (!RegionManager.isTeleportPermitted(next)) {
|
||||
return null
|
||||
}
|
||||
current = next
|
||||
steps++
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
private fun isAdjacentTo(
|
||||
entity: Entity,
|
||||
entityLocation: Location,
|
||||
victim: Entity,
|
||||
victimLocation: Location
|
||||
): Boolean {
|
||||
val entityMinX = entityLocation.x
|
||||
val entityMaxX = entityLocation.x + entity.size()
|
||||
val entityMinY = entityLocation.y
|
||||
val entityMaxY = entityLocation.y + entity.size()
|
||||
val victimMinX = victimLocation.x
|
||||
val victimMaxX = victimLocation.x + victim.size()
|
||||
val victimMinY = victimLocation.y
|
||||
val victimMaxY = victimLocation.y + victim.size()
|
||||
|
||||
val xOverlaps = entityMinX < victimMaxX && entityMaxX > victimMinX
|
||||
val yOverlaps = entityMinY < victimMaxY && entityMaxY > victimMinY
|
||||
if (xOverlaps && yOverlaps) {
|
||||
return false
|
||||
}
|
||||
val xTouches = entityMaxX == victimMinX || victimMaxX == entityMinX
|
||||
val yTouches = entityMaxY == victimMinY || victimMaxY == entityMinY
|
||||
return (xTouches && yOverlaps) || (yTouches && xOverlaps)
|
||||
}
|
||||
|
||||
override fun swing(entity: Entity?, victim: Entity?, state: BattleState?): Int {
|
||||
var hit = 0
|
||||
state!!.style = CombatStyle.MELEE
|
||||
|
|
|
|||
|
|
@ -64,6 +64,21 @@ public final class WalkingQueue {
|
|||
|
||||
public ArrayList<GroundItem> routeItems = new ArrayList<GroundItem>();
|
||||
|
||||
/**
|
||||
* Clears the route markers created by ::drawroute.
|
||||
*/
|
||||
private void clearRouteItems() {
|
||||
if (!(entity instanceof Player) || routeItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (GroundItem item : routeItems) {
|
||||
if (item != null) {
|
||||
RegionManager.getRegionPlane(item.getLocation()).remove(item);
|
||||
}
|
||||
}
|
||||
routeItems.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code WalkingQueue} {@code Object}.
|
||||
* @param entity The entity.
|
||||
|
|
@ -92,17 +107,9 @@ public final class WalkingQueue {
|
|||
if (hasTimerActive(entity, "frozen"))
|
||||
return;
|
||||
Point point = walkingQueue.poll();
|
||||
boolean drawPath = entity.getAttribute("routedraw", false);
|
||||
if (point == null) {
|
||||
updateRunEnergy(false);
|
||||
if (isPlayer && drawPath) {
|
||||
for (GroundItem item : routeItems) {
|
||||
if (item != null) {
|
||||
RegionManager.getRegionPlane(item.getLocation()).remove(item);
|
||||
}
|
||||
}
|
||||
routeItems.clear();
|
||||
}
|
||||
clearRouteItems();
|
||||
return;
|
||||
}
|
||||
if (isPlayer && ((Player) entity).getSettings().getRunEnergy() < 1.0) {
|
||||
|
|
@ -375,13 +382,19 @@ public final class WalkingQueue {
|
|||
}
|
||||
return running;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the entity has a path to walk.
|
||||
*
|
||||
* @return {@code True} if so.
|
||||
*/
|
||||
public boolean hasPath() {
|
||||
return !walkingQueue.isEmpty();
|
||||
for (Point point : walkingQueue) {
|
||||
if (point.getDirection() != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -413,6 +426,7 @@ public final class WalkingQueue {
|
|||
);
|
||||
}
|
||||
|
||||
clearRouteItems();
|
||||
walkingQueue.clear();
|
||||
walkingQueue.add(new Point(loc.getX(), loc.getY()));
|
||||
this.running = running;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import org.junit.jupiter.api.Assertions.assertNotEquals
|
|||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.rsmod.game.pathfinder.flag.CollisionFlag
|
||||
import kotlin.math.abs
|
||||
|
||||
class CombatMovementTests {
|
||||
|
|
@ -79,6 +80,8 @@ class CombatMovementTests {
|
|||
enableRun(attacker)
|
||||
enableRun(victim)
|
||||
enablePvp(attacker, victim)
|
||||
attacker.playerFlags.setUpdateSceneGraph(false)
|
||||
victim.playerFlags.setUpdateSceneGraph(false)
|
||||
CombatMovementIntents.clear()
|
||||
|
||||
attacker.attack(victim)
|
||||
|
|
@ -304,6 +307,92 @@ class CombatMovementTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun meleePressureShouldSidestepWhenMovingTargetWillBeAdjacentBehindClipping() {
|
||||
TestUtils.getMockPlayer("combat_clipped_pressure_attacker").use { attacker ->
|
||||
TestUtils.getMockPlayer("combat_clipped_pressure_victim").use { victim ->
|
||||
val origin = arenaOrigin()
|
||||
val currentVictim = origin.transform(1, 0, 0)
|
||||
val predictedVictim = origin.transform(0, 1, 0)
|
||||
place(attacker, origin)
|
||||
place(victim, currentVictim)
|
||||
configureMelee(attacker)
|
||||
configureMelee(victim)
|
||||
enableRun(attacker)
|
||||
enableRun(victim)
|
||||
enablePvp(attacker, victim)
|
||||
CombatMovementIntents.clear()
|
||||
|
||||
attacker.attack(victim)
|
||||
queueWalk(victim, predictedVictim)
|
||||
RegionManager.addClippingFlag(
|
||||
predictedVictim.z,
|
||||
predictedVictim.x,
|
||||
predictedVictim.y,
|
||||
false,
|
||||
Pathfinder.PREVENT_NORTH
|
||||
)
|
||||
RegionManager.addClippingFlag(
|
||||
predictedVictim.z,
|
||||
predictedVictim.x,
|
||||
predictedVictim.y,
|
||||
true,
|
||||
CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER
|
||||
)
|
||||
|
||||
try {
|
||||
assertTrue(
|
||||
CombatMovementIntents.shouldMaintainMeleePressure(attacker, victim),
|
||||
"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."
|
||||
)
|
||||
CombatMovementIntents.resolve()
|
||||
attacker.playerFlags.setUpdateSceneGraph(false)
|
||||
victim.playerFlags.setUpdateSceneGraph(false)
|
||||
victim.walkingQueue.update()
|
||||
attacker.walkingQueue.update()
|
||||
|
||||
assertNotEquals(
|
||||
origin,
|
||||
attacker.location,
|
||||
"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."
|
||||
)
|
||||
assertTrue(
|
||||
meleeReach(attacker, victim),
|
||||
"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
|
||||
)
|
||||
RegionManager.removeClippingFlag(
|
||||
predictedVictim.z,
|
||||
predictedVictim.x,
|
||||
predictedVictim.y,
|
||||
true,
|
||||
CollisionFlag.WALL_SOUTH_PROJECTILE_BLOCKER
|
||||
)
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overlappingMeleePlayerShouldStepToOpenAttackTileInsteadOfStopping() {
|
||||
TestUtils.getMockPlayer("combat_overlap_player_escape").use { player ->
|
||||
|
|
@ -451,6 +540,94 @@ class CombatMovementTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runEnabledMeleePlayerShouldSwingBeforeWalkingNpcStopsMovingAway() {
|
||||
TestUtils.getMockPlayer("combat_walking_npc_chaser").use { player ->
|
||||
val origin = openHorizontalOrigin()
|
||||
val destination = origin.transform(10, 0, 0)
|
||||
place(player, origin)
|
||||
configureMelee(player)
|
||||
enableRun(player)
|
||||
player.playerFlags.setUpdateSceneGraph(false)
|
||||
|
||||
val npc = NPC.create(100, origin.transform(4, 0, 0))
|
||||
npc.init()
|
||||
try {
|
||||
configureMelee(npc)
|
||||
|
||||
player.attack(npc)
|
||||
queueWalk(npc, destination)
|
||||
TestUtils.advanceTicks(5, false)
|
||||
|
||||
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()
|
||||
)
|
||||
} finally {
|
||||
npc.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runningMeleePlayerShouldNotCrossNpcFootprintToReachFarSideAttackTile() {
|
||||
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)
|
||||
)
|
||||
place(player, origin)
|
||||
configureMelee(player)
|
||||
enableRun(player)
|
||||
player.playerFlags.setUpdateSceneGraph(false)
|
||||
|
||||
val npc = NPC.create(100, npcLocation)
|
||||
npc.init()
|
||||
try {
|
||||
configureMelee(npc)
|
||||
blockMovementTiles(blockedTiles)
|
||||
RegionManager.addClippingFlag(
|
||||
npcLocation.z,
|
||||
npcLocation.x,
|
||||
npcLocation.y,
|
||||
true,
|
||||
CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER
|
||||
)
|
||||
|
||||
player.attack(npc)
|
||||
CombatMovementIntents.clear()
|
||||
CombatMovementIntents.request(player, npc)
|
||||
CombatMovementIntents.resolve()
|
||||
player.walkingQueue.update()
|
||||
|
||||
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()}"
|
||||
)
|
||||
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
|
||||
)
|
||||
unblockMovementTiles(blockedTiles)
|
||||
npc.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun movementLockedMeleeAttackerShouldNotMoveButCanAttackIfAlreadyInRange() {
|
||||
TestUtils.getMockPlayer("combat_locked_attacker").use { attacker ->
|
||||
|
|
@ -904,7 +1081,7 @@ class CombatMovementTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun dumbMeleeNpcShouldNotRouteAroundSafespotObstacle() {
|
||||
fun defaultMeleeNpcShouldNotRouteAroundSafespotObstacle() {
|
||||
TestUtils.getMockPlayer("combat_safespot_target").use { player ->
|
||||
val origin = arenaOrigin()
|
||||
val blockedTiles = listOf(
|
||||
|
|
@ -1101,10 +1278,65 @@ class CombatMovementTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun meleePlayerShouldStepToNearestAttackableTileAtLumbridgeRuinCorner() {
|
||||
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)
|
||||
)
|
||||
place(player, start)
|
||||
configureMelee(player)
|
||||
enableRun(player)
|
||||
player.playerFlags.setUpdateSceneGraph(false)
|
||||
|
||||
val npc = NPC.create(1775, npcLocation)
|
||||
npc.init()
|
||||
try {
|
||||
configureMelee(npc)
|
||||
|
||||
assertFalse(
|
||||
CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player)),
|
||||
"The starting diagonal corner should be blocked for melee."
|
||||
)
|
||||
|
||||
player.attack(npc)
|
||||
TestUtils.advanceTicks(2, false)
|
||||
|
||||
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()}"
|
||||
)
|
||||
assertTrue(meleeReach(player, npc))
|
||||
assertTrue(player.properties.combatPulse.isAttacking)
|
||||
assertFalse(receivedMessage(player, "I can't reach that!"))
|
||||
} finally {
|
||||
npc.clear()
|
||||
CombatMovementIntents.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun arenaOrigin(): Location {
|
||||
return Location.create(3200, 3600, 0)
|
||||
}
|
||||
|
||||
private fun openHorizontalOrigin(): Location {
|
||||
val start = arenaOrigin()
|
||||
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)) }) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
throw AssertionError("No open horizontal combat test line found near $start.")
|
||||
}
|
||||
|
||||
private fun place(entity: Entity, location: Location) {
|
||||
entity.location = location
|
||||
RegionManager.move(entity)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,20 @@ class PathfinderTests {
|
|||
Assertions.assertTrue(path.points.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test fun walkingQueueHasPathShouldIgnoreResetAnchor() {
|
||||
TestUtils.getMockPlayer("walkingQueueAnchor").use { player ->
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
player.location = start
|
||||
player.walkingQueue.reset()
|
||||
|
||||
Assertions.assertFalse(player.walkingQueue.hasPath())
|
||||
|
||||
player.walkingQueue.addPath(start.x + 1, start.y)
|
||||
|
||||
Assertions.assertTrue(player.walkingQueue.hasPath())
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun projectilePathfinderShouldUseRsmodLineOfSightFlags() {
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
val dest = Location.create(3202, 3200, 0)
|
||||
|
|
@ -212,6 +226,65 @@ class PathfinderTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() {
|
||||
TestUtils.getMockPlayer("movingNpcDiagonalApproach").use { p ->
|
||||
val origin = Location.create(3200, 3600, 0)
|
||||
val npc = NPC.create(0, origin.transform(1, 1, 0))
|
||||
npc.init()
|
||||
p.location = origin
|
||||
p.settings.runEnergy = 100.0
|
||||
p.settings.setRunToggled(true)
|
||||
npc.walkingQueue.reset(false)
|
||||
npc.walkingQueue.addPath(origin.transform(4, 1, 0).x, origin.transform(4, 1, 0).y)
|
||||
|
||||
var pulseLocation: Location? = null
|
||||
var pulseTargetLocation: Location? = null
|
||||
try {
|
||||
GameWorld.Pulser.submit(object : MovementPulse(p, npc) {
|
||||
override fun pulse(): Boolean {
|
||||
pulseLocation = p.location
|
||||
pulseTargetLocation = npc.location
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
TestUtils.advanceTicks(1, false)
|
||||
Assertions.assertNull(
|
||||
pulseLocation,
|
||||
"A moving entity interaction must not trigger from a diagonal non-interaction tile."
|
||||
)
|
||||
|
||||
repeat(8) {
|
||||
if (pulseLocation == null) {
|
||||
TestUtils.advanceTicks(1, false)
|
||||
}
|
||||
}
|
||||
|
||||
val actualPulseLocation = pulseLocation
|
||||
?: throw AssertionError("Expected the movement pulse to eventually reach the moving NPC.")
|
||||
val actualTargetLocation = pulseTargetLocation
|
||||
?: throw AssertionError("Expected target location to be captured when the pulse fired.")
|
||||
Assertions.assertTrue(
|
||||
Pathfinder.canInteract(
|
||||
actualPulseLocation.x,
|
||||
actualPulseLocation.y,
|
||||
p.size(),
|
||||
actualTargetLocation.x,
|
||||
actualTargetLocation.y,
|
||||
npc.size(),
|
||||
npc.size(),
|
||||
0,
|
||||
actualPulseLocation.z,
|
||||
null
|
||||
),
|
||||
"Entity interaction must fire only from a currently valid interaction tile."
|
||||
)
|
||||
} finally {
|
||||
npc.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() {
|
||||
val npc = NPC.create(0, NPC_TEST_LOC)
|
||||
npc.isNeverWalks = true
|
||||
|
|
|
|||
59
Server/src/test/kotlin/core/WalkingQueueTests.kt
Normal file
59
Server/src/test/kotlin/core/WalkingQueueTests.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package core
|
||||
|
||||
import TestUtils
|
||||
import core.game.world.map.Location
|
||||
import core.game.world.map.RegionManager
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WalkingQueueTests {
|
||||
companion object {
|
||||
init {
|
||||
TestUtils.preTestSetup()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun resetClearsDrawRouteMarkersBeforeQueuingNewDestination() {
|
||||
TestUtils.getMockPlayer("drawRouteReset").use { player ->
|
||||
val start = Location.create(3200, 3200, 0)
|
||||
player.location = start
|
||||
player.setAttribute("routedraw", true)
|
||||
|
||||
val queue = player.walkingQueue
|
||||
queue.reset()
|
||||
queue.addPath(start.x + 3, start.y)
|
||||
|
||||
val firstRouteItemLocation = queue.routeItems.firstOrNull()?.location
|
||||
?: throw AssertionError("Expected the first route to draw route markers.")
|
||||
Assertions.assertNotNull(
|
||||
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
|
||||
DRAW_ROUTE_ITEM_ID,
|
||||
firstRouteItemLocation,
|
||||
player
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
queue.reset()
|
||||
queue.addPath(start.x, start.y + 3)
|
||||
|
||||
Assertions.assertNull(
|
||||
RegionManager.getRegionPlane(firstRouteItemLocation).getItem(
|
||||
DRAW_ROUTE_ITEM_ID,
|
||||
firstRouteItemLocation,
|
||||
player
|
||||
),
|
||||
"The first route marker should be removed when a new movement destination resets the queue."
|
||||
)
|
||||
Assertions.assertTrue(
|
||||
queue.routeItems.any { it.location != firstRouteItemLocation },
|
||||
"Expected the second route to draw its own markers."
|
||||
)
|
||||
} finally {
|
||||
queue.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DRAW_ROUTE_ITEM_ID = 13444
|
||||
Loading…
Add table
Add a link
Reference in a new issue