diff --git a/Server/src/main/core/game/interaction/MovementPulse.java b/Server/src/main/core/game/interaction/MovementPulse.java index dba7bc58d..9b402e747 100644 --- a/Server/src/main/core/game/interaction/MovementPulse.java +++ b/Server/src/main/core/game/interaction/MovementPulse.java @@ -1,5 +1,6 @@ package core.game.interaction; +import core.api.utils.Vector; import core.game.node.Node; import core.game.node.entity.Entity; import core.game.node.entity.impl.WalkingQueue; @@ -7,7 +8,6 @@ import core.game.node.entity.npc.NPC; import core.game.node.entity.npc.NPCBehavior; import core.game.node.entity.player.Player; import core.game.system.task.Pulse; -import core.game.world.GameWorld; import core.game.world.map.Direction; import core.game.world.map.Location; import core.game.world.map.Point; @@ -17,669 +17,665 @@ import core.game.world.map.path.Pathfinder; import core.net.packet.PacketRepository; import core.net.packet.context.PlayerContext; import core.net.packet.out.ClearMinimapFlag; -import kotlin.jvm.functions.Function2; import kotlin.Pair; -import core.tools.SystemLogger; -import core.api.utils.Vector; - -import content.region.wilderness.handlers.revenants.RevenantNPC; - -import static core.api.ContentAPIKt.*; +import kotlin.jvm.functions.Function2; import java.util.ArrayList; import java.util.Deque; +import static core.api.ContentAPIKt.*; + /** * Handles a movement task. * * @author Emperor */ public abstract class MovementPulse extends Pulse { - - /** - * The moving entity. - */ - protected Entity mover; - - /** - * The destination node. - */ - protected Node destination; - - /** - * The destination's last location. - */ - private Location last; - - /** - * The pathfinder. - */ - private Pathfinder pathfinder; - - /** - * If running should be forced. - */ - private boolean forceRun; - - /** - * The option handler. - */ - private OptionHandler optionHandler; - - /** - * The use with handler. - */ - private UseWithHandler useHandler; - - /** - * The destination flag. - */ - private DestinationFlag destinationFlag; - - /** - * The location to interact from. - */ - private Location interactLocation; - - /** - * If the path couldn't be fully found. - */ - private boolean near; - - private Function2 overrideMethod; - - private Location previousLoc; - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - */ - public MovementPulse(Entity mover, Node destination) { - this(mover, destination, null, false); - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param forceRun If the entity is forced to run. - */ - public MovementPulse(Entity mover, Node destination, boolean forceRun) { - this(mover, destination, null, forceRun); - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param pathfinder The pathfinder to use. - */ - public MovementPulse(Entity mover, Node destination, Pathfinder pathfinder) { - this(mover, destination, pathfinder, false); - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param optionHandler The option handler used. - */ - public MovementPulse(Entity mover, Node destination, OptionHandler optionHandler) { - this(mover, destination, null, false); - this.optionHandler = optionHandler; - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param useHandler The use with handler used. - */ - public MovementPulse(Entity mover, Node destination, UseWithHandler useHandler) { - this(mover, destination, null, false); - this.useHandler = useHandler; - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param destinationFlag The destination flag. - */ - public MovementPulse(Entity mover, Node destination, DestinationFlag destinationFlag) { - this(mover, destination, null, false); - this.destinationFlag = destinationFlag; - } - - public MovementPulse(Entity mover, Node destination, DestinationFlag destinationFlag, Function2 method){ - this(mover,destination,null,false); - this.destinationFlag = destinationFlag; - this.overrideMethod = method; - } - - /** - * Constructs a new {@code MovementPulse} {@code Object}. - * - * @param mover The moving entity. - * @param destination The destination node. - * @param pathfinder The pathfinder to use. - * @param forceRun If the entity is forced to run. - */ - public MovementPulse(Entity mover, Node destination, Pathfinder pathfinder, boolean forceRun) { - super(1, mover, destination); - this.mover = mover; - this.destination = destination; - if (pathfinder == null) { - if (mover instanceof Player) { - this.pathfinder = Pathfinder.SMART; - } else if (mover instanceof NPC) { - NPC npc = (NPC)mover; - NPCBehavior behavior = npc.behavior; - Pathfinder pf = behavior != null ? behavior.getPathfinderOverride(npc) : null; - this.pathfinder = pf != null ? pf : Pathfinder.DUMB; - } else { - this.pathfinder = Pathfinder.DUMB; - } - } else { - this.pathfinder = pathfinder; - } - this.forceRun = forceRun; - - if (destination instanceof NPC || destination instanceof Player) - destinationFlag = DestinationFlag.ENTITY; - - if (mover.currentMovement != null) { - mover.currentMovement.stop(); - mover.getWalkingQueue().reset(); - } - mover.currentMovement = this; - } - - private void clearInferiorScripts() { - mover.scripts.removeWeakScripts(); - mover.scripts.removeNormalScripts(); - } - - @Override - public boolean update() { - if (!mover.getViewport().getRegion().isActive()) - return false; - - if (!isRunning()) return true; - - if (!validate()) { - stop(); - return true; - } - - clearInferiorScripts(); - - mover.face(null); - if (canInteractWithoutMoving()) { - if (interactImmediately()) { - stop(); - return true; - } + + /** + * The moving entity. + */ + protected Entity mover; + + /** + * The destination node. + */ + protected Node destination; + + /** + * The destination's last location. + */ + private Location last; + + /** + * The pathfinder. + */ + private Pathfinder pathfinder; + + /** + * If running should be forced. + */ + private boolean forceRun; + + /** + * The option handler. + */ + private OptionHandler optionHandler; + + /** + * The use with handler. + */ + private UseWithHandler useHandler; + + /** + * The destination flag. + */ + private DestinationFlag destinationFlag; + + /** + * The location to interact from. + */ + private Location interactLocation; + + /** + * If the path couldn't be fully found. + */ + private boolean near; + + private Function2 overrideMethod; + + private Location previousLoc; + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + */ + public MovementPulse(Entity mover, Node destination) { + this(mover, destination, null, false); } - updatePath(); - - if (tryInteract()) { - stop(); - return true; - } - - return false; - } - - private boolean tryInteract() { - Location ml = mover.getLocation(); - if (interactLocation == null) - return false; - 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. - * - * @return true if the mover can interact without moving - */ - private boolean canInteractWithoutMoving() { - 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; - } - - /** - * Immediately performs the interaction without pathfinding. - * Called when the player is already in a valid interaction position. - * - * @return true if interaction was successful and pulse should stop - */ - private boolean interactImmediately() { - if (destination instanceof Entity) { - mover.face((Entity) destination); + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param forceRun If the entity is forced to run. + */ + public MovementPulse(Entity mover, Node destination, boolean forceRun) { + this(mover, destination, null, forceRun); } - try { - if (pulse()) { - if (mover instanceof Player) { - PacketRepository.send(ClearMinimapFlag.class, new PlayerContext((Player) mover)); + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param pathfinder The pathfinder to use. + */ + public MovementPulse(Entity mover, Node destination, Pathfinder pathfinder) { + this(mover, destination, pathfinder, false); + } + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param optionHandler The option handler used. + */ + public MovementPulse(Entity mover, Node destination, OptionHandler optionHandler) { + this(mover, destination, null, false); + this.optionHandler = optionHandler; + } + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param useHandler The use with handler used. + */ + public MovementPulse(Entity mover, Node destination, UseWithHandler useHandler) { + this(mover, destination, null, false); + this.useHandler = useHandler; + } + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param destinationFlag The destination flag. + */ + public MovementPulse(Entity mover, Node destination, DestinationFlag destinationFlag) { + this(mover, destination, null, false); + this.destinationFlag = destinationFlag; + } + + public MovementPulse(Entity mover, Node destination, DestinationFlag destinationFlag, Function2 method) { + this(mover, destination, null, false); + this.destinationFlag = destinationFlag; + this.overrideMethod = method; + } + + /** + * Constructs a new {@code MovementPulse} {@code Object}. + * + * @param mover The moving entity. + * @param destination The destination node. + * @param pathfinder The pathfinder to use. + * @param forceRun If the entity is forced to run. + */ + public MovementPulse(Entity mover, Node destination, Pathfinder pathfinder, boolean forceRun) { + super(1, mover, destination); + this.mover = mover; + this.destination = destination; + if (pathfinder == null) { + if (mover instanceof Player) { + this.pathfinder = Pathfinder.SMART; + } else if (mover instanceof NPC) { + NPC npc = (NPC) mover; + NPCBehavior behavior = npc.behavior; + Pathfinder pf = behavior != null ? behavior.getPathfinderOverride(npc) : null; + this.pathfinder = pf != null ? pf : Pathfinder.DUMB; + } else { + this.pathfinder = Pathfinder.DUMB; + } + } else { + this.pathfinder = pathfinder; } - return true; - } - } catch (Exception e) { - e.printStackTrace(); + this.forceRun = forceRun; + + if (destination instanceof NPC || destination instanceof Player) destinationFlag = DestinationFlag.ENTITY; + + if (mover.currentMovement != null) { + mover.currentMovement.stop(); + mover.getWalkingQueue().reset(); + } + mover.currentMovement = this; } - return false; - } - - private boolean validate() { - if (mover == null || destination == null || mover.getViewport().getRegion() == null || hasInactiveNode()) { - return false; - } - return isRunning(); - } - - @Override - public void stop() { - super.stop(); - if (destination instanceof Entity) { - mover.face(null); - } - 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) { - return; - } - - Location loc = null; - - if (optionHandler != null) { - loc = optionHandler.getDestination(mover, destination); - } else if (useHandler != null) { - loc = useHandler.getDestination((Player) mover, destination); - } 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 == 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 (!checkAllowMovement()) - return; - if (destination instanceof Entity && previousLoc != null && previousLoc.equals(loc) && hasQueuedMovement(mover)) - return; - - Path path; - Pair truncation = truncateLoc(mover, loc != null ? loc : destination.getLocation()); - if (truncation.getFirst()) { - path = Pathfinder.find(mover, truncation.getSecond(), true, pathfinder); - usingTruncatedPath = true; - } else { - path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder); - interactLocation = null; //reset interactLocation so the below code can set it to the properly-pathfound last bit of path. - usingTruncatedPath = false; - } - near = !path.isSuccessful() || path.isMoveNear(); - - if (!path.getPoints().isEmpty()) { - Point point = path.getPoints().getLast(); - if (forceRun) { - mover.getWalkingQueue().reset(forceRun); - } else { - mover.getWalkingQueue().reset(); - } - 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 = pointLocation; - } - } else if (interactLocation == null && path.isSuccessful() && !path.isMoveNear()) { - interactLocation = mover.getLocation(); - } - previousLoc = loc; - } - last = destination.getLocation(); - if (mover instanceof Player && mover.getAttribute("draw-intersect", false)) { - clearHintIcon((Player) mover); - registerHintIcon((Player) mover, interactLocation, 5); - } - } - - private boolean checkAllowMovement() { - boolean canMove = true; - if (destination instanceof Entity) { - Entity e = (Entity) destination; - Location l = e.getLocation(); - Deque npcPath = e.getWalkingQueue().getQueue(); - 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) { - Point pathElement = npcPath.peek(); - if (pathElement.getX() == l.getX() && pathElement.getY() == l.getY()) - canMove = true; - } - } - } - 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 (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 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)); - } - - - private Location findBorderLocation() { - return findBorderLocation(destination.getLocation()); - } - - /** - * Finds the closest location next to the node. - * - * @return The location to walk to. - */ - private Location findBorderLocation(Location centerDestLoc) { - int size = destination.size(); - Location centerDest = centerDestLoc.transform(size >> 1, size >> 1, 0); - Location center = mover.getLocation().transform(mover.size() >> 1, mover.size() >> 1, 0); - Direction direction = Direction.getLogicalDirection(centerDest, center); - Location delta = Location.getDelta(centerDestLoc, mover.getLocation()); - main: - for (int i = 0; i < 4; i++) { - int amount = 0; - switch (direction) { - case NORTH: - amount = size - delta.getY(); - break; - case EAST: - amount = size - delta.getX(); - break; - case SOUTH: - amount = mover.size() + delta.getY(); - break; - case WEST: - amount = mover.size() + delta.getX(); - break; - default: - return null; - } - for (int j = 0; j < amount; j++) { - for (int s = 0; s < mover.size(); s++) { - switch (direction) { - case NORTH: - if (!direction.canMove(mover.getLocation().transform(s, j + mover.size(), 0))) { - direction = Direction.get((direction.toInteger() + 1) & 3); - continue main; - } - break; - case EAST: - if (!direction.canMove(mover.getLocation().transform(j + mover.size(), s, 0))) { - direction = Direction.get((direction.toInteger() + 1) & 3); - continue main; - } - break; - case SOUTH: - if (!direction.canMove(mover.getLocation().transform(s, -(j + 1), 0))) { - direction = Direction.get((direction.toInteger() + 1) & 3); - continue main; - } - break; - case WEST: - if (!direction.canMove(mover.getLocation().transform(-(j + 1), s, 0))) { - direction = Direction.get((direction.toInteger() + 1) & 3); - continue main; - } - break; - default: - return null; - } - } - } - Location location = mover.getLocation().transform(direction, amount); - return location; - } - return null; - } - - /** - * Checks if the mover is standing on an invalid position. - * - * @param l The location. - * @return {@code True} if so. - */ - private boolean isInsideEntity(Location l) { - if (!(destination instanceof Entity)) { - return false; - } - if (((Entity) destination).getWalkingQueue().isMoving()) { - return false; - } - Location loc = destination.getLocation(); - int size = destination.size(); - return Pathfinder.isStandingIn(l.getX(), l.getY(), mover.size(), mover.size(), loc.getX(), loc.getY(), size, size); - } - - /** - * Gets the forceRun. - * - * @return The forceRun. - */ - public boolean isForceRun() { - return forceRun; - } - - /** - * Sets the forceRun. - * - * @param forceRun The forceRun to set. - */ - public void setForceRun(boolean forceRun) { - this.forceRun = forceRun; - } - - /** - * Sets the current destination. - * - * @param destination The destination. - */ - public void setDestination(Node destination) { - this.destination = destination; - } - - /** - * Sets the last location. - * - * @param last The last location. - */ - public void setLast(Location last) { - this.last = last; - } - + + private void clearInferiorScripts() { + mover.scripts.removeWeakScripts(); + mover.scripts.removeNormalScripts(); + } + + @Override + public boolean update() { + if (!mover.getViewport().getRegion().isActive()) return false; + + if (!isRunning()) return true; + + if (!validate()) { + stop(); + return true; + } + + clearInferiorScripts(); + + mover.face(null); + if (canInteractWithoutMoving()) { + if (interactImmediately()) { + stop(); + return true; + } + } + updatePath(); + + if (tryInteract()) { + stop(); + return true; + } + + return false; + } + + private boolean tryInteract() { + Location ml = mover.getLocation(); + if (interactLocation == null) return false; + 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. + * + * @return true if the mover can interact without moving + */ + private boolean canInteractWithoutMoving() { + 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; + } + + /** + * Immediately performs the interaction without pathfinding. + * Called when the player is already in a valid interaction position. + * + * @return true if interaction was successful and pulse should stop + */ + private boolean interactImmediately() { + if (destination instanceof Entity) { + mover.face((Entity) destination); + } + try { + if (pulse()) { + if (mover instanceof Player) { + PacketRepository.send(ClearMinimapFlag.class, new PlayerContext((Player) mover)); + } + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + private boolean validate() { + if (mover == null || destination == null || mover.getViewport().getRegion() == null || hasInactiveNode()) { + return false; + } + return isRunning(); + } + + @Override + public void stop() { + super.stop(); + if (destination instanceof Entity) { + mover.face(null); + } + 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) { + return; + } + + Location loc = null; + + if (optionHandler != null) { + loc = optionHandler.getDestination(mover, destination); + } else if (useHandler != null) { + loc = useHandler.getDestination((Player) mover, destination); + } 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 == 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 (!checkAllowMovement()) return; + if (destination instanceof Entity && previousLoc != null && previousLoc.equals(loc) && hasQueuedMovement(mover)) return; + + Path path; + Pair truncation = truncateLoc(mover, loc != null ? loc : destination.getLocation()); + if (truncation.getFirst()) { + path = Pathfinder.find(mover, truncation.getSecond(), true, pathfinder); + usingTruncatedPath = true; + } else { + path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder); + interactLocation = null; //reset interactLocation so the below code can set it to the properly-pathfound last bit of path. + usingTruncatedPath = false; + } + near = !path.isSuccessful() || path.isMoveNear(); + + if (!path.getPoints().isEmpty()) { + Point point = path.getPoints().getLast(); + if (forceRun) { + mover.getWalkingQueue().reset(forceRun); + } else { + mover.getWalkingQueue().reset(); + } + 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 = pointLocation; + } + } else if (interactLocation == null && path.isSuccessful() && !path.isMoveNear()) { + interactLocation = mover.getLocation(); + } + previousLoc = loc; + } + last = destination.getLocation(); + if (mover instanceof Player && mover.getAttribute("draw-intersect", false)) { + clearHintIcon((Player) mover); + registerHintIcon((Player) mover, interactLocation, 5); + } + } + + private boolean checkAllowMovement() { + boolean canMove = true; + if (destination instanceof Entity) { + Entity e = (Entity) destination; + Location l = e.getLocation(); + Deque npcPath = e.getWalkingQueue().getQueue(); + 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) { + Point pathElement = npcPath.peek(); + if (pathElement.getX() == l.getX() && pathElement.getY() == l.getY()) canMove = true; + } + } + } + return canMove; + } + + private Location checkForEntityPathInterrupt(Location loc) { + Location ml = mover.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; + int moverSpeed = mover.getWalkingQueue().isRunningBoth() ? 2 : 1; + 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 ticksToReach = moverDist / (float) moverSpeed; + if (predictiveIntersection == null && ticksToReach <= i + 1) { //try to predict an intersection point on the path if possible + predictiveIntersection = points[i]; + break; + } + // Otherwise, target the farthest point along the target's planned movement that's within one mover tick. + if (moverDist <= moverSpeed) { + 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() && !hasQueuedMovement((Entity) destination); + } + + 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 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)); + Vector predictedCenterPos = destination.getMathematicalCenter().plus(pathDiff); + Vector moverCenter = mover.getMathematicalCenter(); + Vector toMover = new Vector(moverCenter.getX() - predictedCenterPos.getX(), moverCenter.getY() - predictedCenterPos.getY()); + if (toMover.magnitude() == 0.0) { + return predictedCenterPos.toLocation(plane); + } + double centerDistance = (destination.size() + mover.size()) / 2.0; + double moverOriginOffset = (mover.size() - 1) / 2.0; + Vector interactCenter = predictedCenterPos.plus(toMover.normalized().times(centerDistance)); + return interactCenter.minus(new Vector(moverOriginOffset, moverOriginOffset)).toLocation(plane); + } + + private Location findBorderLocation() { + return findBorderLocation(destination.getLocation()); + } + + /** + * Finds the closest location next to the node. + * + * @return The location to walk to. + */ + private Location findBorderLocation(Location centerDestLoc) { + int size = destination.size(); + Location centerDest = centerDestLoc.transform(size >> 1, size >> 1, 0); + Location center = mover.getLocation().transform(mover.size() >> 1, mover.size() >> 1, 0); + Direction direction = Direction.getLogicalDirection(centerDest, center); + Location delta = Location.getDelta(centerDestLoc, mover.getLocation()); + main: + for (int i = 0; i < 4; i++) { + int amount = 0; + switch (direction) { + case NORTH: + amount = size - delta.getY(); + break; + case EAST: + amount = size - delta.getX(); + break; + case SOUTH: + amount = mover.size() + delta.getY(); + break; + case WEST: + amount = mover.size() + delta.getX(); + break; + default: + return null; + } + for (int j = 0; j < amount; j++) { + for (int s = 0; s < mover.size(); s++) { + switch (direction) { + case NORTH: + if (!direction.canMove(mover.getLocation().transform(s, j + mover.size(), 0))) { + direction = Direction.get((direction.toInteger() + 1) & 3); + continue main; + } + break; + case EAST: + if (!direction.canMove(mover.getLocation().transform(j + mover.size(), s, 0))) { + direction = Direction.get((direction.toInteger() + 1) & 3); + continue main; + } + break; + case SOUTH: + if (!direction.canMove(mover.getLocation().transform(s, -(j + 1), 0))) { + direction = Direction.get((direction.toInteger() + 1) & 3); + continue main; + } + break; + case WEST: + if (!direction.canMove(mover.getLocation().transform(-(j + 1), s, 0))) { + direction = Direction.get((direction.toInteger() + 1) & 3); + continue main; + } + break; + default: + return null; + } + } + } + Location location = mover.getLocation().transform(direction, amount); + return location; + } + return null; + } + + /** + * Checks if the mover is standing on an invalid position. + * + * @param l The location. + * @return {@code True} if so. + */ + private boolean isInsideEntity(Location l) { + if (!(destination instanceof Entity)) { + return false; + } + if (((Entity) destination).getWalkingQueue().isMoving()) { + return false; + } + Location loc = destination.getLocation(); + int size = destination.size(); + return Pathfinder.isStandingIn(l.getX(), l.getY(), mover.size(), mover.size(), loc.getX(), loc.getY(), size, size); + } + + /** + * Gets the forceRun. + * + * @return The forceRun. + */ + public boolean isForceRun() { + return forceRun; + } + + /** + * Sets the forceRun. + * + * @param forceRun The forceRun to set. + */ + public void setForceRun(boolean forceRun) { + this.forceRun = forceRun; + } + + /** + * Sets the current destination. + * + * @param destination The destination. + */ + public void setDestination(Node destination) { + this.destination = destination; + } + + /** + * Sets the last location. + * + * @param last The last location. + */ + public void setLast(Location last) { + this.last = last; + } + } diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt index 96d4754ac..03dd3352e 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementIntents.kt @@ -29,9 +29,7 @@ object CombatMovementIntents { private data class Intent(val attacker: Entity, val target: Entity) private data class MovementDestination( - val node: Node, - val pathfinder: Pathfinder, - val allowPartialPath: Boolean + val node: Node, val pathfinder: Pathfinder, val allowPartialPath: Boolean ) { val location: Location get() = node.location @@ -48,12 +46,9 @@ object CombatMovementIntents { ) { fun summary(): String { val slowest = slowestIntent?.let { - ", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " + - "candidates=${it.candidateCount} directPathHits=${it.directPathHits} " + - "rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}" + ", slowestIntent=${it.elapsedMicros}us attacker=${it.attacker} target=${it.target} " + "candidates=${it.candidateCount} directPathHits=${it.directPathHits} " + "rsmodRouteCalls=${it.rsmodRouteCalls} losCalls=${it.losCalls}" } ?: "" - return "combatMovementStats intents=$intents candidates=$candidateCount " + - "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest" + return "combatMovementStats intents=$intents candidates=$candidateCount " + "directPathHits=$directPathHits rsmodRouteCalls=$rsmodRouteCalls losCalls=$losCalls$slowest" } companion object { @@ -189,10 +184,7 @@ object CombatMovementIntents { return } - val pending = intents.values.sortedWith( - compareBy { it.attacker.index } - .thenBy { it.target.index } - ) + val pending = intents.values.sortedWith(compareBy { it.attacker.index }.thenBy { it.target.index }) intents.clear() val reservedTiles = LinkedHashSet() @@ -277,11 +269,7 @@ object CombatMovementIntents { val targetLocation = targetLocationFor(attacker, target) val projectedTargetLocation = projectedLocations[target] if (projectedTargetLocation != null && canAttackFrom( - attacker, - target, - attacker.location, - projectedTargetLocation, - trace + attacker, target, attacker.location, projectedTargetLocation, trace ) ) { attacker.walkingQueue.reset() @@ -321,11 +309,7 @@ object CombatMovementIntents { val standingOnCandidate = candidates.any { it.location == attacker.location } val projectedAttackerLocation = CombatMovementPlanner.predictedMovementLocation(attacker) ?: attacker.location if (projectedAttackerLocation != attacker.location && canAttackFrom( - attacker, - target, - projectedAttackerLocation, - targetLocation, - trace + attacker, target, projectedAttackerLocation, targetLocation, trace ) ) { attacker.face(target) @@ -334,13 +318,15 @@ object CombatMovementIntents { return } - val queueStationaryContinuation = attacker.properties.combatPulse.style == CombatStyle.MELEE && - !CombatMovementPlanner.hasMovementStepThisTick(target) + val queueStationaryContinuation = + attacker.properties.combatPulse.style == CombatStyle.MELEE && !CombatMovementPlanner.hasMovementStepThisTick( + target + ) var blockedByReservation = false for (candidate in candidates) { val candidatePath = pathTo(attacker, candidate, trace, queueStationaryContinuation) ?: continue - val attackPath = truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace) - ?: continue + val attackPath = + truncateAtFirstAttackOpportunity(attacker, target, targetLocation, candidatePath, trace) ?: continue if (hasReservedOccupiedTile(reservedTiles, attacker, attackPath.projectedLocation)) { blockedByReservation = true continue @@ -393,11 +379,7 @@ object CombatMovementIntents { } return when (attacker.properties.combatPulse.style) { CombatStyle.RANGE, CombatStyle.MAGIC -> canAttackFromRange( - attacker, - target, - attackerLocation, - targetLocation, - trace + attacker, target, attackerLocation, targetLocation, trace ) else -> canAttackFromMelee(attacker, target, attackerLocation, targetLocation, trace) @@ -405,11 +387,7 @@ object CombatMovementIntents { } private fun movementDestinationsFor( - attacker: Entity, - target: Entity, - targetLocation: Location, - pathfinder: Pathfinder, - trace: IntentTrace + attacker: Entity, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace ): List { if (attacker is NPC && pathfinder === Pathfinder.DUMB) { val destinations = ArrayList(4) @@ -418,7 +396,7 @@ object CombatMovementIntents { destinations.add(MovementDestination(location, pathfinder, allowPartialPath = true)) } } else { - addDumbNpcAttackDestinations(attacker, target, pathfinder, destinations) + destinations.add(MovementDestination(targetLocation, pathfinder, allowPartialPath = true)) } trace.candidateCount += destinations.size return destinations @@ -433,8 +411,9 @@ object CombatMovementIntents { for (location in attackTiles) { destinations.add(MovementDestination(location, pathfinder, allowPartialPath = false)) } - val targetFallback = if (!occupiedTilesOverlap(attacker, target) && - shouldAllowPartialTargetPath(attacker, target, targetLocation) + val targetFallback = if (!occupiedTilesOverlap(attacker, target) && shouldAllowPartialTargetPath( + attacker, target, targetLocation + ) ) { MovementDestination(target, pathfinder, allowPartialPath = true) } else { @@ -456,10 +435,7 @@ object CombatMovementIntents { } private fun attackTilesFor( - attacker: Entity, - target: Entity, - targetLocation: Location, - trace: IntentTrace + attacker: Entity, target: Entity, targetLocation: Location, trace: IntentTrace ): List { if (attacker.properties.combatPulse.style != CombatStyle.MELEE) { return CombatMovementPlanner.candidateAttackTiles(attacker, target, targetLocation) @@ -468,12 +444,11 @@ object CombatMovementIntents { val walkable = borderTiles.filter { RegionManager.isTeleportPermitted(it) } val candidates = walkable.ifEmpty { borderTiles } val attackable = candidates.filter { canAttackFrom(attacker, target, it, targetLocation, trace) } - return attackable.ifEmpty { candidates }.sortedWith( - compareBy { distanceSquared(it, attacker.location) } - .thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) } - .thenBy { it.x } - .thenBy { it.y } - ) + return attackable.ifEmpty { candidates }.sortedWith(compareBy { + distanceSquared( + it, attacker.location + ) + }.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y }) } private fun shouldAllowPartialTargetPath(attacker: Player, target: Entity, targetLocation: Location): Boolean { @@ -488,11 +463,7 @@ object CombatMovementIntents { } private fun playerAttackRangeDestinations( - attacker: Player, - target: Entity, - targetLocation: Location, - pathfinder: Pathfinder, - trace: IntentTrace + attacker: Player, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace ): List { val range = playerAttackRange(attacker) if (range <= CombatReach.meleeDistance(attacker)) { @@ -524,21 +495,14 @@ object CombatMovementIntents { distance += 2 } val rangeWeapon = RangeWeapon.get(attacker.equipment.getNew(EquipmentContainer.SLOT_WEAPON).id) - if (rangeWeapon != null && - (rangeWeapon.weaponType == Weapon.WeaponType.DOUBLE_SHOT || - rangeWeapon.weaponType == Weapon.WeaponType.DEGRADING) - ) { + if (rangeWeapon != null && (rangeWeapon.weaponType == Weapon.WeaponType.DOUBLE_SHOT || rangeWeapon.weaponType == Weapon.WeaponType.DEGRADING)) { distance = 10 } return distance } private fun attackRangeTiles( - attacker: Player, - target: Entity, - targetLocation: Location, - range: Int, - trace: IntentTrace + attacker: Player, target: Entity, targetLocation: Location, range: Int, trace: IntentTrace ): List { val tiles = ArrayList() val minX = targetLocation.x - range @@ -556,22 +520,16 @@ object CombatMovementIntents { } } } - tiles.sortWith( - compareBy { distanceSquared(it, attacker.location) } - .thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) } - .thenBy { it.x } - .thenBy { it.y } - ) + tiles.sortWith(compareBy { + distanceSquared( + it, attacker.location + ) + }.thenBy { distanceSquaredToClosestOccupiedTile(target, targetLocation, it) }.thenBy { it.x }.thenBy { it.y }) val attackTiles = ArrayList(minOf(MAX_RANGED_APPROACH_CANDIDATES, tiles.size)) RsmodPathfinder.loadLineOfSightWindow(targetLocation) for (tile in tiles) { if (!hasProjectileLineOfSight( - tile, - attacker.size(), - target, - targetLocation, - loadWindow = false, - trace = trace + tile, attacker.size(), target, targetLocation, loadWindow = false, trace = trace ) ) { continue @@ -595,8 +553,9 @@ object CombatMovementIntents { if (distance == 1 && !isAdjacentToTarget(attacker, attackerLocation, target, targetLocation)) { return false } - if (distance > 1 && - distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) > distance * distance + if (distance > 1 && distanceSquaredToClosestOccupiedTile( + target, targetLocation, attackerLocation + ) > distance * distance ) { return false } @@ -621,20 +580,18 @@ object CombatMovementIntents { playerAttackRange(attacker) } else { CombatReach.combatDistance( - attacker, - target, - if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7 + attacker, target, if (attacker.properties.combatPulse.style == CombatStyle.MAGIC) 10 else 7 ) } - return distanceSquaredToClosestOccupiedTile(target, targetLocation, attackerLocation) <= range * range && - hasProjectileLineOfSight(attackerLocation, attacker.size(), target, targetLocation, trace = trace) + return distanceSquaredToClosestOccupiedTile( + target, targetLocation, attackerLocation + ) <= range * range && hasProjectileLineOfSight( + attackerLocation, attacker.size(), target, targetLocation, trace = trace + ) } private fun isAdjacentToTarget( - attacker: Entity, - attackerLocation: Location, - target: Entity, - targetLocation: Location + attacker: Entity, attackerLocation: Location, target: Entity, targetLocation: Location ): Boolean { for (i in 0 until attacker.size()) { if (Pathfinder.isStandingIn( @@ -708,19 +665,11 @@ object CombatMovementIntents { val maxRaySteps = if (checkClose) 1 else Int.MAX_VALUE if (!loadWindow) { return RsmodPathfinder.hasLineOfSightBetweenLoaded( - attackerLocation, - attackerSize, - targetLocation, - target.size(), - maxRaySteps = maxRaySteps + attackerLocation, attackerSize, targetLocation, target.size(), maxRaySteps = maxRaySteps ) } return RsmodPathfinder.hasLineOfSightBetween( - attackerLocation, - attackerSize, - targetLocation, - target.size(), - maxRaySteps = maxRaySteps + attackerLocation, attackerSize, targetLocation, target.size(), maxRaySteps = maxRaySteps ) } @@ -746,11 +695,7 @@ object CombatMovementIntents { } private fun pathToTargetFootprint( - attacker: Entity, - target: Entity, - targetLocation: Location, - pathfinder: Pathfinder, - trace: IntentTrace + attacker: Entity, target: Entity, targetLocation: Location, pathfinder: Pathfinder, trace: IntentTrace ): CandidatePath? { val targetMoving = CombatMovementPlanner.hasMovementStepThisTick(target) val movingTargetPath = @@ -798,20 +743,14 @@ object CombatMovementIntents { return null } if (attacker is Player && !path.isMoveNear && isExcessiveCombatDetourToTarget( - attacker, - target, - targetLocation, - path + attacker, target, targetLocation, path ) ) { return null } val steps = immediateMovementSteps(attacker, path) if (attacker is Player && path.isMoveNear && !partialPathMovesCloserToTarget( - attacker, - target, - targetLocation, - steps + attacker, target, targetLocation, steps ) ) { return null @@ -819,9 +758,7 @@ object CombatMovementIntents { if (attacker is Player && steps.any { !RegionManager.isTeleportPermitted( Location.create( - it.x, - it.y, - attacker.location.z + it.x, it.y, attacker.location.z ) ) }) { @@ -832,9 +769,7 @@ object CombatMovementIntents { } private fun directPathTowardMovingTarget( - attacker: Entity, - target: Entity, - targetLocation: Location + attacker: Entity, target: Entity, targetLocation: Location ): CandidatePath? { if (!CombatMovementPlanner.hasMovementStepThisTick(target)) { return null @@ -904,10 +839,7 @@ object CombatMovementIntents { } private fun meleeDestination( - attacker: Entity, - target: Entity, - targetLocation: Location, - direction: Direction + attacker: Entity, target: Entity, targetLocation: Location, direction: Direction ): Location { val minAlignedX = targetLocation.x - attacker.size() + 1 val maxAlignedX = targetLocation.x + target.size() - 1 @@ -941,83 +873,14 @@ object CombatMovementIntents { } } - private fun addDumbNpcAttackDestinations( - attacker: NPC, - target: Entity, - pathfinder: Pathfinder, - destinations: MutableList - ) { - val directions = ArrayList(3) - addDirection(directions, Direction.getLogicalDirection(target.centerLocation, attacker.centerLocation)) - - if (attacker.centerLocation.x < target.centerLocation.x) { - addDirection(directions, Direction.WEST) - } else if (attacker.centerLocation.x > target.centerLocation.x) { - addDirection(directions, Direction.EAST) - } - if (attacker.centerLocation.y < target.centerLocation.y) { - addDirection(directions, Direction.SOUTH) - } else if (attacker.centerLocation.y > target.centerLocation.y) { - addDirection(directions, Direction.NORTH) - } - - for (direction in directions) { - destinations.add( - MovementDestination( - dumbNpcAttackDestination(attacker, target, direction), - pathfinder, - allowPartialPath = true - ) - ) - } - } - private fun addDirection(directions: MutableList, direction: Direction) { if (!directions.contains(direction)) { directions.add(direction) } } - private fun dumbNpcAttackDestination(attacker: NPC, target: Entity, direction: Direction): Location { - val targetLocation = target.location - val minAlignedX = targetLocation.x - attacker.size() + 1 - val maxAlignedX = targetLocation.x + target.size() - 1 - val minAlignedY = targetLocation.y - attacker.size() + 1 - val maxAlignedY = targetLocation.y + target.size() - 1 - - return when (direction) { - Direction.WEST -> Location.create( - targetLocation.x - attacker.size(), - attacker.location.y.coerceIn(minAlignedY, maxAlignedY), - targetLocation.z - ) - - Direction.EAST -> Location.create( - targetLocation.x + target.size(), - attacker.location.y.coerceIn(minAlignedY, maxAlignedY), - targetLocation.z - ) - - Direction.SOUTH -> Location.create( - attacker.location.x.coerceIn(minAlignedX, maxAlignedX), - targetLocation.y - attacker.size(), - targetLocation.z - ) - - else -> Location.create( - attacker.location.x.coerceIn(minAlignedX, maxAlignedX), - targetLocation.y + target.size(), - targetLocation.z - ) - } - } - private fun truncateAtFirstAttackOpportunity( - attacker: Entity, - target: Entity, - targetLocation: Location, - path: CandidatePath, - trace: IntentTrace + attacker: Entity, target: Entity, targetLocation: Location, path: CandidatePath, trace: IntentTrace ): CandidatePath? { val steps = ArrayList(path.steps.size) for (step in path.steps) { @@ -1034,30 +897,20 @@ object CombatMovementIntents { } private fun pathTo( - attacker: Entity, - destination: MovementDestination, - trace: IntentTrace, - queueContinuation: Boolean = false + attacker: Entity, destination: MovementDestination, trace: IntentTrace, queueContinuation: Boolean = false ): CandidatePath? { if (attacker.location == destination.location) { return null } val stepLimit = movementStepLimit(attacker, queueContinuation && !destination.allowPartialPath) - if (attacker is NPC && destination.pathfinder === Pathfinder.DUMB) { - val directPath = directPartialPathTo(attacker, destination, stepLimit) - if (directPath != null) { - trace.directPathHits++ - return directPath - } - return null - } val directPath = directPathTo(attacker, destination, stepLimit) if (directPath != null) { trace.directPathHits++ return directPath } - if (destination.pathfinder === Pathfinder.SMART && - !RsmodPathfinder.canAttempt(attacker.location, destination.location) + if (destination.pathfinder === Pathfinder.SMART && !RsmodPathfinder.canAttempt( + attacker.location, destination.location + ) ) { return null } @@ -1068,18 +921,14 @@ object CombatMovementIntents { return null } if (attacker is Player && !destination.allowPartialPath && isExcessiveCombatDetour( - attacker, - destination, - path + attacker, destination, path ) ) { return null } val steps = immediateMovementSteps(attacker, path, stepLimit) if (attacker is Player && destination.allowPartialPath && !partialPathMovesCloser( - attacker, - destination, - steps + attacker, destination, steps ) ) { return null @@ -1087,9 +936,7 @@ object CombatMovementIntents { if (attacker is Player && steps.any { !RegionManager.isTeleportPermitted( Location.create( - it.x, - it.y, - attacker.location.z + it.x, it.y, attacker.location.z ) ) }) { @@ -1100,9 +947,7 @@ object CombatMovementIntents { } private fun directPathTo( - attacker: Entity, - destination: MovementDestination, - maxSteps: Int = movementStepsFor(attacker) + attacker: Entity, destination: MovementDestination, maxSteps: Int = movementStepsFor(attacker) ): CandidatePath? { if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) { return null @@ -1111,9 +956,7 @@ object CombatMovementIntents { } private fun directPathTo( - attacker: Entity, - destination: Location, - maxSteps: Int = movementStepsFor(attacker) + attacker: Entity, destination: Location, maxSteps: Int = movementStepsFor(attacker) ): CandidatePath? { if (attacker.size() != 1 || attacker.location.z != destination.z) { return null @@ -1145,41 +988,8 @@ object CombatMovementIntents { return CandidatePath(steps, projected) } - private fun directPartialPathTo( - attacker: Entity, - destination: MovementDestination, - maxSteps: Int - ): CandidatePath? { - if (attacker.size() != 1 || destination.node !is Location || attacker.location.z != destination.location.z) { - return null - } - val steps = ArrayList(maxSteps) - var current = attacker.location - var distance = 0 - while (current != destination.location && steps.size < maxSteps) { - if (++distance > MAX_DIRECT_COMBAT_PATH_DISTANCE) { - break - } - val direction = Direction.getDirection(current, destination.location) ?: break - if (!direction.canMoveFrom(current.z, current.x, current.y, RegionManager::getClippingFlag)) { - break - } - val next = current.transform(direction) - if (!RegionManager.isTeleportPermitted(next)) { - break - } - steps.add(Point(next.x, next.y, direction, direction.stepX, direction.stepY)) - current = next - } - val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return null - return CandidatePath(steps, projected) - } - private fun isExcessiveCombatDetourToTarget( - attacker: Player, - target: Entity, - targetLocation: Location, - path: Path + attacker: Player, target: Entity, targetLocation: Location, path: Path ): Boolean { val pathLength = (path.points.size - 1).coerceAtLeast(0) val directDistance = @@ -1188,14 +998,12 @@ object CombatMovementIntents { } private fun partialPathMovesCloserToTarget( - attacker: Player, - target: Entity, - targetLocation: Location, - steps: List + attacker: Player, target: Entity, targetLocation: Location, steps: List ): Boolean { val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false - return distanceSquaredToClosestOccupiedTile(target, targetLocation, projected) < - distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) + return distanceSquaredToClosestOccupiedTile( + target, targetLocation, projected + ) < distanceSquaredToClosestOccupiedTile(target, targetLocation, attacker.location) } private fun isExcessiveCombatDetour(attacker: Player, destination: MovementDestination, path: Path): Boolean { @@ -1205,9 +1013,7 @@ object CombatMovementIntents { } private fun partialPathMovesCloser( - attacker: Player, - destination: MovementDestination, - steps: List + attacker: Player, destination: MovementDestination, steps: List ): Boolean { val projected = steps.lastOrNull()?.let { Location.create(it.x, it.y, attacker.location.z) } ?: return false return projected.getDistance(destination.location) < attacker.location.getDistance(destination.location) @@ -1222,9 +1028,7 @@ object CombatMovementIntents { } private fun immediateMovementSteps( - attacker: Entity, - path: Path, - maxSteps: Int = movementStepsFor(attacker) + attacker: Entity, path: Path, maxSteps: Int = movementStepsFor(attacker) ): List { val steps = ArrayList(maxSteps) for (point in path.points) { @@ -1271,9 +1075,7 @@ object CombatMovementIntents { } private fun shouldStopUnreachableCombat( - attacker: Entity, - target: Entity, - exhaustedLocalApproach: Boolean = false + attacker: Entity, target: Entity, exhaustedLocalApproach: Boolean = false ): Boolean { return attacker is Player && (exhaustedLocalApproach || !CombatMovementPlanner.hasMovementStepThisTick(target)) } @@ -1318,15 +1120,9 @@ object CombatMovementIntents { } private fun occupiedTilesOverlap( - first: Entity, - firstLocation: Location, - second: Entity, - secondLocation: Location + first: Entity, firstLocation: Location, second: Entity, secondLocation: Location ): Boolean { - return firstLocation.x < secondLocation.x + second.size() && - firstLocation.x + first.size() > secondLocation.x && - firstLocation.y < secondLocation.y + second.size() && - firstLocation.y + first.size() > secondLocation.y + return firstLocation.x < secondLocation.x + second.size() && firstLocation.x + first.size() > secondLocation.x && firstLocation.y < secondLocation.y + second.size() && firstLocation.y + first.size() > secondLocation.y } private fun pathfinderFor(attacker: Entity): Pathfinder { diff --git a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt index 301fbdf6d..e031833f4 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatMovementPlanner.kt @@ -14,16 +14,14 @@ import core.game.world.map.path.Pathfinder */ object CombatMovementPlanner { data class Plan( - val targetLocation: Location, - val attackTile: Location? + val targetLocation: Location, val attackTile: Location? ) @JvmStatic fun plan(attacker: Entity, target: Entity): Plan { val targetLocation = predictedTargetLocation(target) return Plan( - targetLocation = targetLocation, - attackTile = chooseTargetBorderTile(attacker, target, targetLocation) + targetLocation = targetLocation, attackTile = chooseTargetBorderTile(attacker, target, targetLocation) ) } @@ -125,14 +123,16 @@ object CombatMovementPlanner { val candidates = borderTiles(target, targetLocation, attacker.size()) val walkable = candidates.filter { RegionManager.isTeleportPermitted(it) } val attackable = walkable.filter { canInteractFrom(attacker, it, target, targetLocation) } - return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith( - compareBy { it.getDistance(attacker.location) } - .thenBy { it.x } - .thenBy { it.y } - ) + return (attackable.ifEmpty { walkable.ifEmpty { candidates } }).sortedWith(compareBy { + it.getDistance( + attacker.location + ) + }.thenBy { it.x }.thenBy { it.y }) } - private fun canInteractFrom(attacker: Entity, location: Location, target: Entity, targetLocation: Location): Boolean { + private fun canInteractFrom( + attacker: Entity, location: Location, target: Entity, targetLocation: Location + ): Boolean { if (attacker.size() == 1 && target.size() == 1) { val direction = Direction.getDirection(location, targetLocation) ?: return false return direction.canMoveFrom(location.z, location.x, location.y, RegionManager::getClippingFlag) diff --git a/Server/src/main/core/game/node/entity/combat/CombatReach.kt b/Server/src/main/core/game/node/entity/combat/CombatReach.kt index cd74c25f8..816fbc2bd 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatReach.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatReach.kt @@ -8,14 +8,7 @@ import core.game.world.map.Direction import core.game.world.map.Location import core.game.world.map.RegionManager.getClippingFlag import core.game.world.map.path.Pathfinder -import core.game.world.map.path.Pathfinder.PREVENT_EAST -import core.game.world.map.path.Pathfinder.PREVENT_NORTH -import core.game.world.map.path.Pathfinder.PREVENT_NORTHEAST -import core.game.world.map.path.Pathfinder.PREVENT_NORTHWEST -import core.game.world.map.path.Pathfinder.PREVENT_SOUTH -import core.game.world.map.path.Pathfinder.PREVENT_SOUTHEAST -import core.game.world.map.path.Pathfinder.PREVENT_SOUTHWEST -import core.game.world.map.path.Pathfinder.PREVENT_WEST +import core.game.world.map.path.Pathfinder.* /** * Shared combat reach calculations. @@ -69,14 +62,13 @@ object CombatReach { return true } } - return victim.getSwingHandler(false).type == CombatStyle.MELEE && - e.withinDistance(victim.location, 1) && - victim.properties.combatPulse.getVictim() === entity && - entity.index < victim.index + return victim.getSwingHandler(false).type == CombatStyle.MELEE && e.withinDistance( + victim.location, + 1 + ) && victim.properties.combatPulse.getVictim() === entity && entity.index < victim.index } return entity.centerLocation.withinDistance( - victim.centerLocation, - distance + (size shr 1) + (victim.size() shr 1) + victim.centerLocation, distance + (size shr 1) + (victim.size() shr 1) ) } @@ -111,8 +103,7 @@ object CombatReach { fun canStepTowards(entity: Entity, victim: Entity): InteractionType { val closestVictimTile = victim.getClosestOccupiedTile(entity.location) val closestEntityTile = entity.getClosestOccupiedTile(closestVictimTile) - val dir = closestEntityTile.deriveDirection(closestVictimTile) - ?: return InteractionType.STILL_INTERACT + val dir = closestEntityTile.deriveDirection(closestVictimTile) ?: return InteractionType.STILL_INTERACT var next = closestEntityTile // A fixed-direction walk can pass beside an oblique target without converging, so @@ -151,30 +142,30 @@ object CombatReach { Direction.WEST -> if (getClippingFlag(next) and PREVENT_WEST != 0) return InteractionType.NO_INTERACT Direction.NORTH_EAST -> { - if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || - getClippingFlag(components[1]) and PREVENT_NORTH != 0 || - getClippingFlag(next) and PREVENT_NORTHEAST != 0 + if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || getClippingFlag(components[1]) and PREVENT_NORTH != 0 || getClippingFlag( + next + ) and PREVENT_NORTHEAST != 0 ) return InteractionType.NO_INTERACT } Direction.NORTH_WEST -> { - if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || - getClippingFlag(components[1]) and PREVENT_NORTH != 0 || - getClippingFlag(next) and PREVENT_NORTHWEST != 0 + if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || getClippingFlag(components[1]) and PREVENT_NORTH != 0 || getClippingFlag( + next + ) and PREVENT_NORTHWEST != 0 ) return InteractionType.NO_INTERACT } Direction.SOUTH_EAST -> { - if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || - getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || - getClippingFlag(next) and PREVENT_SOUTHEAST != 0 + if (getClippingFlag(components[0]) and PREVENT_EAST != 0 || getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || getClippingFlag( + next + ) and PREVENT_SOUTHEAST != 0 ) return InteractionType.NO_INTERACT } Direction.SOUTH_WEST -> { - if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || - getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || - getClippingFlag(next) and PREVENT_SOUTHWEST != 0 + if (getClippingFlag(components[0]) and PREVENT_WEST != 0 || getClippingFlag(components[1]) and PREVENT_SOUTH != 0 || getClippingFlag( + next + ) and PREVENT_SOUTHWEST != 0 ) return InteractionType.NO_INTERACT } } diff --git a/Server/src/main/core/game/node/entity/npc/NPC.java b/Server/src/main/core/game/node/entity/npc/NPC.java index f888c2ac3..426deb8df 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -556,20 +556,25 @@ public class NPC extends Entity { path.setSuccesful(true); Location current = getLocation(); int maxSteps = Math.min(14, Math.max(1, getSpawnReturnDistance(Math.max(1, getWalkRadius())))); + boolean usedSidestep = false; for (int steps = 0; !current.equals(destination) && steps < maxSteps; steps++) { Direction direction = Direction.getDirection(current, destination); - if (direction == null || !canTakeRandomMovementStep(current, direction)) { + Direction stepDirection = chooseRandomMovementStep(current, direction, usedSidestep); + if (stepDirection == null) { path.setSuccesful(false); path.setMoveNear(!path.getPoints().isEmpty()); break; } - Location next = current.transform(direction); + Location next = current.transform(stepDirection); if (!isWithinRandomMovementBounds(next)) { path.setSuccesful(false); path.setMoveNear(!path.getPoints().isEmpty()); break; } - path.getPoints().add(new Point(next.getX(), next.getY(), direction, direction.getStepX(), direction.getStepY())); + if (stepDirection != direction) { + usedSidestep = true; + } + path.getPoints().add(new Point(next.getX(), next.getY(), stepDirection, stepDirection.getStepX(), stepDirection.getStepY())); current = next; } if (!current.equals(destination) && !path.getPoints().isEmpty()) { @@ -578,6 +583,47 @@ public class NPC extends Entity { return path; } + private Direction chooseRandomMovementStep(Location current, Direction direction, boolean usedSidestep) { + if (direction == null) { + return null; + } + Location next = current.transform(direction); + if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, direction)) { + return direction; + } + if (usedSidestep) { + return null; + } + for (Direction sidestep : sidestepDirections(direction)) { + next = current.transform(sidestep); + if (isWithinRandomMovementBounds(next) && canTakeRandomMovementStep(current, sidestep)) { + return sidestep; + } + } + return null; + } + + private Direction[] sidestepDirections(Direction direction) { + switch (direction) { + case NORTH: + case SOUTH: + return new Direction[] { Direction.EAST, Direction.WEST }; + case EAST: + case WEST: + return new Direction[] { Direction.NORTH, Direction.SOUTH }; + case NORTH_EAST: + return new Direction[] { Direction.EAST, Direction.NORTH }; + case SOUTH_EAST: + return new Direction[] { Direction.EAST, Direction.SOUTH }; + case SOUTH_WEST: + return new Direction[] { Direction.WEST, Direction.SOUTH }; + case NORTH_WEST: + return new Direction[] { Direction.WEST, Direction.NORTH }; + default: + return new Direction[0]; + } + } + private boolean canTakeRandomMovementStep(Location current, Direction direction) { ClipMaskSupplier clipMaskSupplier = behavior != null ? behavior.getClippingSupplier(this) : null; if (clipMaskSupplier == null) { diff --git a/Server/src/main/core/game/world/map/path/DumbPathfinder.java b/Server/src/main/core/game/world/map/path/DumbPathfinder.java new file mode 100644 index 000000000..fd5bd4d17 --- /dev/null +++ b/Server/src/main/core/game/world/map/path/DumbPathfinder.java @@ -0,0 +1,478 @@ +package core.game.world.map.path; + +import core.game.world.map.Direction; +import core.game.world.map.Location; +import core.game.world.map.Point; +import core.game.world.map.RegionManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pathfinder for simple local movement. It walks directly toward the + * destination and only tries the horizontal/vertical alternatives of a blocked + * diagonal step; it does not search around obstacles. + */ +public final class DumbPathfinder extends Pathfinder { + + private boolean found; + private int z; + private int x; + private int y; + + @Override + public Path find(Location start, + int size, + Location end, + int sizeX, + int sizeY, + int rotation, + int type, + int walkingFlag, + boolean near, + ClipMaskSupplier clipMaskSupplier) { + ClipMaskSupplier supplier = clipMaskSupplier != null ? clipMaskSupplier : RegionManager::getClippingFlag; + Path path = new Path(); + z = start.getZ(); + x = start.getX(); + y = start.getY(); + List points = new ArrayList<>(20); + path.setSuccesful(true); + while (x != end.getX() || y != end.getY()) { + Direction[] directions = getDirection(x, y, end); + if (type >= 0) { + if ((type < 5 || type == 9) && canDoorInteract(x, y, size, end.getX(), end.getY(), type, rotation, z, supplier)) { + break; + } + if (type < 10 && canDecorationInteract(x, y, size, end.getX(), end.getY(), rotation, type, z, supplier)) { + break; + } + } + if (sizeX != 0 && sizeY != 0) { + if (canInteract(x, y, size, end.getX(), end.getY(), sizeX, sizeY, walkingFlag, z, supplier)) { + break; + } + if (directions.length > 1) { + Direction dir = directions[0]; + if (x + dir.getStepX() == end.getX() && y + dir.getStepY() == end.getY()) { + directions[0] = directions[directions.length - 1]; + directions[directions.length - 1] = dir; + } + } + } + found = true; + if (size < 2) { + checkSingleTraversal(points, supplier, directions); + } else if (size == 2) { + checkDoubleTraversal(points, supplier, directions); + } else { + checkVariableTraversal(points, directions, size, supplier); + } + if (!found) { + path.setMoveNear(x != start.getX() || y != start.getY()); + path.setSuccesful(false); + break; + } + } + path.getPoints().addAll(points); + return path; + } + + private void checkSingleTraversal(List points, ClipMaskSupplier clipMaskSupplier, Direction... directions) { + for (Direction dir : directions) { + found = true; + switch (dir) { + case NORTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y + 1) & PREVENT_NORTH) != 0) { + found = false; + break; + } + points.add(new Point(x, y + 1, dir)); + y++; + break; + case NORTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, + x, + y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + 1, + y + 1) & PREVENT_NORTHEAST) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y + 1, dir)); + x++; + y++; + break; + case EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y, dir)); + x++; + break; + case SOUTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y) & PREVENT_EAST) != 0 || (clipMaskSupplier.getClippingFlag(z, + x, + y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + 1, + y - 1) & PREVENT_SOUTHEAST) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y - 1, dir)); + x++; + y--; + break; + case SOUTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & PREVENT_SOUTH) != 0) { + found = false; + break; + } + points.add(new Point(x, y - 1, dir)); + y--; + break; + case SOUTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, + x, + y - 1) & PREVENT_SOUTH) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x - 1, + y - 1) & PREVENT_SOUTHWEST) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y - 1, dir)); + x--; + y--; + break; + case WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y, dir)); + x--; + break; + case NORTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & PREVENT_WEST) != 0 || (clipMaskSupplier.getClippingFlag(z, + x, + y + 1) & PREVENT_NORTH) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x - 1, + y + 1) & PREVENT_NORTHWEST) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y + 1, dir)); + x--; + y++; + break; + } + if (found) { + break; + } + } + } + + private void checkDoubleTraversal(List points, ClipMaskSupplier clipMaskSupplier, Direction... directions) { + for (Direction dir : directions) { + found = true; + switch (dir) { + case NORTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + 1, + y + 2) & 0x12c01e0) != 0) { + found = false; + break; + } + points.add(new Point(x, y + 1, dir)); + y++; + break; + case NORTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + 2, + y + 2) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + 2, + y + 1) & 0x12c0183) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y + 1, dir)); + x++; + y++; + break; + case EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 2, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + 2, + y + 1) & 0x12c01e0) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y, dir)); + x++; + break; + case SOUTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + 2, + y) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + 2, + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + points.add(new Point(x + 1, y - 1, dir)); + x++; + y--; + break; + case SOUTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + 1, + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + points.add(new Point(x, y - 1, dir)); + y--; + break; + case SOUTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x, + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y - 1, dir)); + x--; + y--; + break; + case WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y + 1) & 0x12c0138) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y, dir)); + x--; + break; + case NORTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y + 2) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x, + y + 2) & 0x12c01e0) != 0) { + found = false; + break; + } + points.add(new Point(x - 1, y + 1, dir)); + x--; + y++; + break; + } + if (found) { + break; + } + } + } + + private void checkVariableTraversal(List points, Direction[] directions, int size, ClipMaskSupplier clipMaskSupplier) { + for (Direction dir : directions) { + found = true; + roar: + switch (dir) { + case NORTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + (size - 1), + y + size) & 0x12c01e0) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x + i, y + size) & 0x12c01f8) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x, y + 1, dir)); + y++; + break; + case NORTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + size, + y + size) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + size, + y + 1) & 0x12c0183) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x + (i + 1), y + size) & 0x12c01f8) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + size, + y + (i + 1)) & 0x12c01e3) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x + 1, y + 1, dir)); + x++; + y++; + break; + case EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + size, y) & 0x12c0183) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + size, + y + (size - 1)) & 0x12c01e0) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x + size, y + i) & 0x12c01e3) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x + 1, y, dir)); + x++; + break; + case SOUTH_EAST: + if ((clipMaskSupplier.getClippingFlag(z, x + 1, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + size, + y + (size - 2)) & 0x12c01e0) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + size, + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x + size, y + (i - 1)) & 0x12c01e3) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + (i + 1), + y - 1) & 0x12c018f) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x + 1, y - 1, dir)); + x++; + y--; + break; + case SOUTH: + if ((clipMaskSupplier.getClippingFlag(z, x, y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + (size - 1), + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x + i, y - 1) & 0x12c018f) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x, y - 1, dir)); + y--; + break; + case SOUTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (size - 2)) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y - 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x + (size - 2), + y - 1) & 0x12c0183) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i - 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + (i - 1), + y - 1) & 0x12c018f) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x - 1, y - 1, dir)); + x--; + y--; + break; + case WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y + (size - 1)) & 0x12c0138) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + i) & 0x12c013e) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x - 1, y, dir)); + x--; + break; + case NORTH_WEST: + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + 1) & 0x12c010e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x - 1, + y + size) & 0x12c0138) != 0 || (clipMaskSupplier.getClippingFlag( + z, + x, + y + size) & 0x12c01e0) != 0) { + found = false; + break; + } + for (int i = 1; i < size - 1; i++) { + if ((clipMaskSupplier.getClippingFlag(z, x - 1, y + (i + 1)) & 0x12c013e) != 0 || (clipMaskSupplier.getClippingFlag(z, + x + (i - 1), + y + size) & 0x12c01f8) != 0) { + found = false; + break roar; + } + } + points.add(new Point(x - 1, y + 1, dir)); + x--; + y++; + break; + } + if (found) { + break; + } + } + } + + private static Direction[] getDirection(int startX, int startY, Location end) { + int endX = end.getX(); + int endY = end.getY(); + if (startX == endX) { + if (startY > endY) { + return new Direction[]{Direction.SOUTH}; + } else if (startY < endY) { + return new Direction[]{Direction.NORTH}; + } + } else if (startY == endY) { + if (startX > endX) { + return new Direction[]{Direction.WEST}; + } + return new Direction[]{Direction.EAST}; + } else { + if (startX < endX && startY < endY) { + return new Direction[]{Direction.NORTH_EAST, Direction.EAST, Direction.NORTH}; + } else if (startX < endX && startY > endY) { + return new Direction[]{Direction.SOUTH_EAST, Direction.EAST, Direction.SOUTH}; + } else if (startX > endX && startY < endY) { + return new Direction[]{Direction.NORTH_WEST, Direction.WEST, Direction.NORTH}; + } else if (startX > endX && startY > endY) { + return new Direction[]{Direction.SOUTH_WEST, Direction.WEST, Direction.SOUTH}; + } + } + return new Direction[0]; + } +} diff --git a/Server/src/main/core/game/world/map/path/Pathfinder.java b/Server/src/main/core/game/world/map/path/Pathfinder.java index 633f47073..fae06fd1f 100644 --- a/Server/src/main/core/game/world/map/path/Pathfinder.java +++ b/Server/src/main/core/game/world/map/path/Pathfinder.java @@ -9,6 +9,7 @@ import core.game.world.map.Location; import core.game.world.map.RegionManager; public abstract class Pathfinder { + public static final int PREVENT_NORTH = 0x12c0120; public static final int PREVENT_EAST = 0x12c0180; public static final int PREVENT_NORTHEAST = 0x12c01e0; @@ -17,56 +18,67 @@ public abstract class Pathfinder { public static final int PREVENT_WEST = 0x12c0108; public static final int PREVENT_SOUTHWEST = 0x12c010e; public static final int PREVENT_NORTHWEST = 0x12c0138; - - + /** * The smart path finder. */ public static final Pathfinder SMART = new RsmodPathfinder(); - + /** * The dumb path finder. */ - public static final Pathfinder DUMB = new RsmodPathfinder(); - + public static final Pathfinder DUMB = new DumbPathfinder(); + /** * The projectile path finder. */ public static final Pathfinder PROJECTILE = new RsmodProjectilePathfinder(); - + /** * Finds a path from the location to the end location. - * @param location The start location. - * @param size The mover size. - * @param end The end location. - * @param sizeX The x-size of the destination node. - * @param sizeY The y-size of the destination node. - * @param rotation The object rotation. - * @param type The object type. + * + * @param location The start location. + * @param size The mover size. + * @param end The end location. + * @param sizeX The x-size of the destination node. + * @param sizeY The y-size of the destination node. + * @param rotation The object rotation. + * @param type The object type. * @param walkingFlag The object walking flag. - * @param near If we should find the nearest location if a path can't be - * found. + * @param near If we should find the nearest location if a path can't be + * found. * @return The path. */ - public abstract Path find(Location location, int size, Location end, int sizeX, int sizeY, int rotation, int type, int walkingFlag, boolean near, ClipMaskSupplier clipMaskSupplier); - + public abstract Path find(Location location, + int size, + Location end, + int sizeX, + int sizeY, + int rotation, + int type, + int walkingFlag, + boolean near, + ClipMaskSupplier clipMaskSupplier); + /** * Finds a path from the start location to the end location. - * @param mover The moving entity. + * + * @param mover The moving entity. * @param destination The destination node. * @return The path. */ public static Path find(Entity mover, Node destination) { return find(mover, destination, true, SMART); } - + /** * Finds a path from the start location to the end location. - * @param mover The moving entity. + * + * @param mover The moving entity. * @param destination The destination node. - * @param near If we should move near the end location, if we can't reach - * it. - * @param finder The pathfinder to use. + * @param near If we should move near the end location, if we can't reach + * it. + * @param finder The pathfinder to use. * @return The path. */ public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder) { @@ -77,46 +89,49 @@ public abstract class Pathfinder { } return find(mover.getLocation(), mover.size(), destination, near, finder, cms); } - - public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder){ - return find(mover.getLocation(),mover.size(),destination,near,finder, RegionManager::getWaterClipFlag); + + public static Path findWater(Entity mover, Node destination, boolean near, Pathfinder finder) { + return find(mover.getLocation(), mover.size(), destination, near, finder, RegionManager::getWaterClipFlag); } - + public static Path find(Entity mover, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) { return find(mover.getLocation(), mover.size(), destination, near, finder, clipMaskSupplier); } - + /** * Finds a path from the start location to the end location. + * * @param destination The destination node. * @return The path. */ public static Path find(Location start, Node destination) { return find(start, destination, true, SMART); } - + public static Path find(Location start, Node destination, int moverSize) { - return find(start, moverSize, destination, true, SMART, null); - } - + return find(start, moverSize, destination, true, SMART, null); + } + /** * Finds a path from the start location to the end location. + * * @param destination The destination node. - * @param near If we should move near the end location, if we can't reach - * it. - * @param finder The pathfinder to use. + * @param near If we should move near the end location, if we can't reach + * it. + * @param finder The pathfinder to use. * @return The path. */ public static Path find(Location start, Node destination, boolean near, Pathfinder finder) { return find(start, 1, destination, near, finder, null); } - + /** * Finds a path from the start location to the end location. + * * @param destination The destination node. - * @param near If we should move near the end location, if we can't reach - * it. - * @param finder The pathfinder to use. + * @param near If we should move near the end location, if we can't reach + * it. + * @param finder The pathfinder to use. * @return The path. */ public static Path find(Location start, int moverSize, Node destination, boolean near, Pathfinder finder, ClipMaskSupplier clipMaskSupplier) { @@ -125,7 +140,16 @@ public abstract class Pathfinder { int type = object.getType(); int rotation = object.getRotation(); if (type == 10 || type == 11 || type == 22) { - return finder.find(start, moverSize, object.getLocation(), object.getDefinition().sizeX, object.getDefinition().sizeY, rotation, type, object.getDefinition().getWalkingFlag(), near, clipMaskSupplier); + return finder.find(start, + moverSize, + object.getLocation(), + object.getDefinition().sizeX, + object.getDefinition().sizeY, + rotation, + type, + object.getDefinition().getWalkingFlag(), + near, + clipMaskSupplier); } return finder.find(start, moverSize, object.getLocation(), 0, 0, rotation, type, 0, near, clipMaskSupplier); } @@ -137,7 +161,7 @@ public abstract class Pathfinder { } return finder.find(start, moverSize, destination.getLocation(), size, size, 0, -1, 0, near, clipMaskSupplier); } - + private static Scenery getRouteScenery(Scenery object) { Scenery wrapper = object.getWrapper(); if (wrapper == object) { @@ -148,51 +172,70 @@ public abstract class Pathfinder { } return wrapper; } - + private static int getFootprintArea(Scenery object) { return object.getDefinition().sizeX * object.getDefinition().sizeY; } - + /** * Checks if interaction with decoration is possible. - * @param curX The current x-coordinate in viewport. - * @param curY The current y-coordinate in viewport. - * @param size The mover size. - * @param destX The destination x-coordinate in viewport. - * @param destY The destination y-coordinate in viewport. - * @param type The object type. + * + * @param curX The current x-coordinate in viewport. + * @param curY The current y-coordinate in viewport. + * @param size The mover size. + * @param destX The destination x-coordinate in viewport. + * @param destY The destination y-coordinate in viewport. + * @param type The object type. * @param rotation The object rotation. * @return {@code True} if so. */ - public static boolean canDecorationInteract(int curX, int curY, int size, int destX, int destY, int rotation, int type, int z, ClipMaskSupplier clipMaskSupplier) { + public static boolean canDecorationInteract(int curX, + int curY, + int size, + int destX, + int destY, + int rotation, + int type, + int z, + ClipMaskSupplier clipMaskSupplier) { return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier); } - + /** * Checks if interaction with a door is possible. - * @param curX The current x-coordinate in viewport. - * @param curY The current y-coordinate in viewport. - * @param size The mover size. - * @param destX The destination x-coordinate in viewport. - * @param destY The destination y-coordinate in viewport. - * @param type The object type. + * + * @param curX The current x-coordinate in viewport. + * @param curY The current y-coordinate in viewport. + * @param size The mover size. + * @param destX The destination x-coordinate in viewport. + * @param destY The destination y-coordinate in viewport. + * @param type The object type. * @param rotation The object rotation. * @return {@code True} if so. */ - public static boolean canDoorInteract(int curX, int curY, int size, int destX, int destY, int type, int rotation, int z, ClipMaskSupplier clipMaskSupplier) { + public static boolean canDoorInteract(int curX, + int curY, + int size, + int destX, + int destY, + int type, + int rotation, + int z, + ClipMaskSupplier clipMaskSupplier) { return RsmodPathfinder.canReach(curX, curY, size, destX, destY, 1, 1, rotation, type, 0, z, clipMaskSupplier); } - + /** * Checks if the mover is standing on the destination. - * @param x The current x-location (in viewport). - * @param y The current y-location (in viewport). + * + * @param x The current x-location (in viewport). + * @param y The current y-location (in viewport). * @param moverSizeX The mover x size. * @param moverSizeY The mover y size. - * @param destX The destination x-location in viewport. - * @param destY The destination y-location in viewport. - * @param sizeX The destination node x-size. - * @param sizeY The destination node y-size. + * @param destX The destination x-location in viewport. + * @param destY The destination y-location in viewport. + * @param sizeX The destination node x-size. + * @param sizeY The destination node y-size. * @return {@code True} if so. */ public static boolean isStandingIn(int x, int y, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY) { @@ -204,32 +247,52 @@ public abstract class Pathfinder { } return true; } - + /** * Checks if interaction is possible from the current location. - * @param x The current x-location (in viewport). - * @param y The current y-location (in viewport). + * + * @param x The current x-location (in viewport). + * @param y The current y-location (in viewport). * @param moverSize The mover size. - * @param destX The destination x-location in viewport. - * @param destY The destination y-location in viewport. - * @param sizeX The destination node x-size. - * @param sizeY The destination node y-size. - * @param walkFlag The walking flag. + * @param destX The destination x-location in viewport. + * @param destY The destination y-location in viewport. + * @param sizeX The destination node x-size. + * @param sizeY The destination node y-size. + * @param walkFlag The walking flag. * @return {@code True} if so. */ - public static boolean canInteract(int x, int y, int moverSize, int destX, int destY, int sizeX, int sizeY, int walkFlag, int z, ClipMaskSupplier clipMaskSupplier) { + public static boolean canInteract(int x, + int y, + int moverSize, + int destX, + int destY, + int sizeX, + int sizeY, + int walkFlag, + int z, + ClipMaskSupplier clipMaskSupplier) { return RsmodPathfinder.canReach(x, y, moverSize, destX, destY, sizeX, sizeY, 0, -1, walkFlag, z, clipMaskSupplier); } - + /** * Checks if interaction is possible from the current location. + * * @param destX The destination x-location in viewport. * @param destY The destination y-location in viewport. * @param sizeX The destination node x-size. * @param sizeY The destination node y-size. * @return {@code True} if so. */ - public static boolean canInteractSized(int curX, int curY, int moverSizeX, int moverSizeY, int destX, int destY, int sizeX, int sizeY, int walkingFlag, int z) { + public static boolean canInteractSized(int curX, + int curY, + int moverSizeX, + int moverSizeY, + int destX, + int destY, + int sizeX, + int sizeY, + int walkingFlag, + int z) { return RsmodPathfinder.canReach(curX, curY, moverSizeX, destX, destY, sizeX, sizeY, 0, -1, walkingFlag, z, null); } } diff --git a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt index a53ffbd17..60506890a 100644 --- a/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodPathfinder.kt @@ -125,9 +125,7 @@ class RsmodPathfinder( } else { CollisionFlagMap().also { loadCollisionWindow( - flags = it, - start = Location.create(srcX, srcY, z), - supplier = clipMaskSupplier + flags = it, start = Location.create(srcX, srcY, z), supplier = clipMaskSupplier ) } } @@ -149,22 +147,14 @@ class RsmodPathfinder( @JvmStatic fun lineOfSight( - start: Location, - dest: Location, - moverSize: Int, - destWidth: Int, - destHeight: Int + start: Location, dest: Location, moverSize: Int, destWidth: Int, destHeight: Int ): RayCast { RegionManager.loadClippingWindow(start, SEARCH_MAP_SIZE) return lineOfSightLoaded(start, dest, moverSize, destWidth, destHeight) } private fun lineOfSightLoaded( - start: Location, - dest: Location, - moverSize: Int, - destWidth: Int, - destHeight: Int + start: Location, dest: Location, moverSize: Int, destWidth: Int, destHeight: Int ): RayCast { return projectileLineFinder.get().lineOfSight( level = start.z, @@ -278,9 +268,7 @@ class RsmodPathfinder( } private fun loadCollisionWindow( - flags: CollisionFlagMap, - start: Location, - supplier: ClipMaskSupplier + flags: CollisionFlagMap, start: Location, supplier: ClipMaskSupplier ) { val baseX = start.x - (SEARCH_MAP_SIZE / 2) val baseY = start.y - (SEARCH_MAP_SIZE / 2) diff --git a/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt b/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt index 1169f875d..6a83ae22c 100644 --- a/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt +++ b/Server/src/main/core/game/world/map/path/RsmodProjectilePathfinder.kt @@ -19,11 +19,7 @@ class RsmodProjectilePathfinder : Pathfinder() { val source = requireNotNull(start) val destination = requireNotNull(end) val rayCast = RsmodPathfinder.lineOfSight( - start = source, - dest = destination, - moverSize = size, - destWidth = sizeX, - destHeight = sizeY + start = source, dest = destination, moverSize = size, destWidth = sizeX, destHeight = sizeY ) val path = Path() if (!rayCast.success) { diff --git a/Server/src/test/kotlin/content/CombatMovementTests.kt b/Server/src/test/kotlin/content/CombatMovementTests.kt index 1ca6d3286..b1f4b8371 100644 --- a/Server/src/test/kotlin/content/CombatMovementTests.kt +++ b/Server/src/test/kotlin/content/CombatMovementTests.kt @@ -6,11 +6,7 @@ import core.api.EquipmentSlot import core.game.global.action.DoorActionHandler import core.game.node.Node import core.game.node.entity.Entity -import core.game.node.entity.combat.BattleState -import core.game.node.entity.combat.CombatReach -import core.game.node.entity.combat.CombatSwingHandler -import core.game.node.entity.combat.CombatMovementIntents -import core.game.node.entity.combat.CombatMovementPlanner +import core.game.node.entity.combat.* import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.combat.spell.CombatSpell import core.game.node.entity.combat.spell.SpellType @@ -24,16 +20,12 @@ import core.game.world.GameWorld import core.game.world.map.Location import core.game.world.map.RegionManager import core.game.world.map.path.Pathfinder -import core.plugin.Plugin import core.net.packet.PacketProcessor import core.net.packet.`in`.Packet -import org.rs09.consts.Items -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue +import core.plugin.Plugin +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import org.rs09.consts.Items import org.rsmod.game.pathfinder.flag.CollisionFlag import kotlin.math.abs @@ -61,8 +53,7 @@ class CombatMovementTests { assertTrue(attacker.properties.combatPulse.isAttacking) assertTrue( - meleeReach(attacker, victim), - "Attacker should stay in melee reach while the victim is running." + meleeReach(attacker, victim), "Attacker should stay in melee reach while the victim is running." ) } } @@ -93,8 +84,7 @@ class CombatMovementTests { assertTrue( meleeReach(attacker, victim), - "Attacker should run with a target leaving melee range. " + - "attacker=${attacker.location}, victim=${victim.location}" + "Attacker should run with a target leaving melee range. " + "attacker=${attacker.location}, victim=${victim.location}" ) } } @@ -119,13 +109,18 @@ class CombatMovementTests { GameWorld.Pulser.updateAll() CombatMovementIntents.resolve() - assertFalse(attacker.walkingQueue.isRunning, "Combat movement must not persist the transient running flag.") + assertFalse( + attacker.walkingQueue.isRunning, "Combat movement must not persist the transient running flag." + ) assertFalse(attacker.walkingQueue.isRunningBoth, "Combat movement must not make the attacker run.") victim.walkingQueue.update() attacker.walkingQueue.update() - assertTrue(attacker.properties.combatPulse.isAttacking, "Combat should remain active while the target is moving.") + assertTrue( + attacker.properties.combatPulse.isAttacking, + "Combat should remain active while the target is moving." + ) assertEquals(-1, attacker.walkingQueue.runDir, "Combat movement must not force run when run is off.") assertFalse(attacker.settings.isRunToggled, "Combat movement must not toggle run on.") assertEquals(100.0, attacker.settings.runEnergy, 0.0, "Walking combat chase must not drain run energy.") @@ -158,7 +153,9 @@ class CombatMovementTests { ) assertFalse(receivedMessage(attacker, "I can't reach that!")) assertFalse(attacker.walkingQueue.isRunningBoth, "Waiting for the next tick must not force running.") - assertEquals(100.0, attacker.settings.runEnergy, 0.0, "Waiting for a moving target must not drain run energy.") + assertEquals( + 100.0, attacker.settings.runEnergy, 0.0, "Waiting for a moving target must not drain run energy." + ) } } } @@ -188,8 +185,7 @@ class CombatMovementTests { assertTrue( meleeReach(attacker, victim), - "Attacker should mirror a live run-click on the tick it is queued. " + - "attacker=${attacker.location}, victim=${victim.location}" + "Attacker should mirror a live run-click on the tick it is queued. " + "attacker=${attacker.location}, victim=${victim.location}" ) } } @@ -326,11 +322,7 @@ class CombatMovementTests { attacker.attack(victim) queueWalk(victim, predictedVictim) RegionManager.addClippingFlag( - predictedVictim.z, - predictedVictim.x, - predictedVictim.y, - false, - Pathfinder.PREVENT_NORTH + predictedVictim.z, predictedVictim.x, predictedVictim.y, false, Pathfinder.PREVENT_NORTH ) RegionManager.addClippingFlag( predictedVictim.z, @@ -359,9 +351,7 @@ class CombatMovementTests { assertNotEquals( origin, attacker.location, - "Melee pressure should not stay on a clipped side of the moving target. " + - CombatMovementIntents.lastResolveSummary() + - " queue=${attacker.walkingQueue.queue.size}" + "Melee pressure should not stay on a clipped side of the moving target. " + CombatMovementIntents.lastResolveSummary() + " queue=${attacker.walkingQueue.queue.size}" ) assertTrue( attacker.properties.combatPulse.isAttacking, @@ -369,16 +359,11 @@ class CombatMovementTests { ) assertTrue( meleeReach(attacker, victim), - "Attacker should end the tick on an attackable side of the moving target. " + - "attacker=${attacker.location}, victim=${victim.location}" + "Attacker should end the tick on an attackable side of the moving target. " + "attacker=${attacker.location}, victim=${victim.location}" ) } finally { RegionManager.removeClippingFlag( - predictedVictim.z, - predictedVictim.x, - predictedVictim.y, - false, - Pathfinder.PREVENT_NORTH + predictedVictim.z, predictedVictim.x, predictedVictim.y, false, Pathfinder.PREVENT_NORTH ) RegionManager.removeClippingFlag( predictedVictim.z, @@ -398,8 +383,7 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_overlap_player_escape").use { player -> val origin = arenaOrigin() val blockedTiles = listOf( - origin.transform(0, 1, 0), - origin.transform(1, 0, 0) + origin.transform(0, 1, 0), origin.transform(1, 0, 0) ) place(player, origin) configureMelee(player) @@ -420,8 +404,7 @@ class CombatMovementTests { assertTrue( player.location == origin.transform(-1, 0, 0) || player.location == origin.transform(0, -1, 0), - "An overlapped melee player should step to the open west/south side instead of stopping. " + - "player=${player.location}, npc=${npc.location}" + "An overlapped melee player should step to the open west/south side instead of stopping. " + "player=${player.location}, npc=${npc.location}" ) assertTrue(CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player))) assertTrue(player.properties.combatPulse.isAttacking) @@ -439,8 +422,7 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_overlap_npc_target").use { player -> val origin = arenaOrigin() val blockedTiles = listOf( - origin.transform(0, 1, 0), - origin.transform(1, 0, 0) + origin.transform(0, 1, 0), origin.transform(1, 0, 0) ) place(player, origin) configureMelee(player) @@ -457,8 +439,7 @@ class CombatMovementTests { assertTrue( npc.location == origin.transform(-1, 0, 0) || npc.location == origin.transform(0, -1, 0), - "An overlapped dumb melee NPC should try the open west/south side, not only north/east. " + - "npc=${npc.location}, player=${player.location}" + "An overlapped dumb melee NPC should try the open west/south side, not only north/east. " + "npc=${npc.location}, player=${player.location}" ) assertTrue(CombatReach.canMelee(npc, player, CombatReach.meleeDistance(npc))) assertTrue(npc.properties.combatPulse.isAttacking) @@ -475,8 +456,7 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_overlap_mutual_player").use { player -> val origin = arenaOrigin() val blockedTiles = listOf( - origin.transform(0, 1, 0), - origin.transform(1, 0, 0) + origin.transform(0, 1, 0), origin.transform(1, 0, 0) ) place(player, origin) configureMelee(player) @@ -499,8 +479,7 @@ class CombatMovementTests { assertTrue(player.location != origin || npc.location != origin) assertTrue( player.location == origin || npc.location == origin, - "When both overlapped actors are attacking, one side stepping out is enough. " + - "player=${player.location}, npc=${npc.location}" + "When both overlapped actors are attacking, one side stepping out is enough. " + "player=${player.location}, npc=${npc.location}" ) assertTrue(CombatReach.canMelee(player, npc, CombatReach.meleeDistance(player))) assertTrue(CombatReach.canMelee(npc, player, CombatReach.meleeDistance(npc))) @@ -563,9 +542,7 @@ class CombatMovementTests { assertTrue(player.properties.combatPulse.isAttacking) assertTrue( player.properties.combatPulse.getNextAttack() > -1, - "Run-enabled melee chase should create an attack opportunity before the walking NPC stops. " + - "player=${player.location}, npc=${npc.location}, " + - CombatMovementIntents.lastResolveSummary() + "Run-enabled melee chase should create an attack opportunity before the walking NPC stops. " + "player=${player.location}, npc=${npc.location}, " + CombatMovementIntents.lastResolveSummary() ) } finally { npc.clear() @@ -580,8 +557,7 @@ class CombatMovementTests { val origin = openHorizontalOrigin() val npcLocation = origin.transform(2, 0, 0) val blockedTiles = listOf( - npcLocation.transform(0, 1, 0), - npcLocation.transform(0, -1, 0) + npcLocation.transform(0, 1, 0), npcLocation.transform(0, -1, 0) ) place(player, origin) configureMelee(player) @@ -594,11 +570,7 @@ class CombatMovementTests { configureMelee(npc) blockMovementTiles(blockedTiles) RegionManager.addClippingFlag( - npcLocation.z, - npcLocation.x, - npcLocation.y, - true, - CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER + npcLocation.z, npcLocation.x, npcLocation.y, true, CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER ) player.attack(npc) @@ -609,17 +581,12 @@ class CombatMovementTests { assertTrue( player.location.x < npcLocation.x, - "Melee movement must stop before crossing the NPC footprint when the far-side tile is chosen. " + - "player=${player.location}, npc=$npcLocation, ${CombatMovementIntents.lastResolveSummary()}" + "Melee movement must stop before crossing the NPC footprint when the far-side tile is chosen. " + "player=${player.location}, npc=$npcLocation, ${CombatMovementIntents.lastResolveSummary()}" ) assertNotEquals(npcLocation, player.location, "The player must not run onto the NPC footprint.") } finally { RegionManager.removeClippingFlag( - npcLocation.z, - npcLocation.x, - npcLocation.y, - true, - CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER + npcLocation.z, npcLocation.x, npcLocation.y, true, CollisionFlag.WALL_WEST_PROJECTILE_BLOCKER ) unblockMovementTiles(blockedTiles) npc.clear() @@ -728,8 +695,7 @@ class CombatMovementTests { assertEquals(seers, player.location) assertFalse( - player.properties.combatPulse.isAttacking, - "Teleporting should discard the previous combat target." + player.properties.combatPulse.isAttacking, "Teleporting should discard the previous combat target." ) } finally { npc.clear() @@ -750,7 +716,9 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") - assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.") + assertFalse( + RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + ) val startDistance = start.getDistance(duck.location) player.attack(duck) @@ -799,7 +767,9 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") - assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.") + assertFalse( + RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + ) player.attack(duck) TestUtils.advanceTicks(12, false) @@ -807,8 +777,11 @@ class CombatMovementTests { assertTrue(player.properties.combatPulse.isAttacking) assertTrue( magicReach(player, duck), - "Autocast should stop on a projectile-clear tile instead of dancing within blocked range. " + - "player=${player.location}, duck=${duck.location}, distance=${player.location.getDistance(duck.location)}" + "Autocast should stop on a projectile-clear tile instead of dancing within blocked range. " + "player=${player.location}, duck=${duck.location}, distance=${ + player.location.getDistance( + duck.location + ) + }" ) assertFalse(receivedMessage(player, "I can't reach that!")) } finally { @@ -830,16 +803,20 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") - assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.") + assertFalse( + RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + ) player.attack(duck) TestUtils.advanceTicks(18, false) assertFalse( player.properties.combatPulse.isAttacking && !magicReach(player, duck), - "Autocast should not stay active while standing in projectile-blocked spell range. " + - "player=${player.location}, duck=${duck.location}, distance=${player.location.getDistance(duck.location)}, " + - "projectile=${CombatSwingHandler.isProjectileClipped(player, duck, false)}" + "Autocast should not stay active while standing in projectile-blocked spell range. " + "player=${player.location}, duck=${duck.location}, distance=${ + player.location.getDistance( + duck.location + ) + }, " + "projectile=${CombatSwingHandler.isProjectileClipped(player, duck, false)}" ) if (!player.properties.combatPulse.isAttacking) { assertTrue(receivedMessage(player, "I can't reach that!")) @@ -871,7 +848,9 @@ class CombatMovementTests { val duck = stationaryNpc(46, duckLocation) try { assertTrue(RegionManager.isTeleportPermitted(start), "Test start tile must be walkable.") - assertFalse(RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water.") + assertFalse( + RegionManager.isTeleportPermitted(duckLocation), "Lumbridge river duck spawn should be on water." + ) val startDistance = start.getDistance(duck.location) player.attack(duck) @@ -894,9 +873,9 @@ class CombatMovementTests { assertFalse(player.properties.combatPulse.isAttacking) assertTrue( receivedMessage(player, "I can't reach that!"), - "Melee combat should report unreachable after pathing. " + - "location=${player.location}, distance=${player.location.getDistance(duck.location)}, " + - "isAttacking=${player.properties.combatPulse.isAttacking}" + "Melee combat should report unreachable after pathing. " + "location=${player.location}, distance=${ + player.location.getDistance(duck.location) + }, " + "isAttacking=${player.properties.combatPulse.isAttacking}" ) assertTrue( player.location.getDistance(duck.location) <= startDistance, @@ -1085,8 +1064,7 @@ class CombatMovementTests { TestUtils.getMockPlayer("combat_safespot_target").use { player -> val origin = arenaOrigin() val blockedTiles = listOf( - origin.transform(1, 0, 0), - origin.transform(0, -1, 0) + origin.transform(1, 0, 0), origin.transform(0, -1, 0) ) place(player, origin.transform(4, 0, 0)) @@ -1151,13 +1129,88 @@ class CombatMovementTests { } } + @Test + fun dumbMeleeNpcShouldNotSidestepAroundImmediateSafespotBlocker() { + TestUtils.getMockPlayer("combat_cardinal_north_safespot_target").use { player -> + val origin = arenaOrigin() + val blocker = origin.transform(0, 1, 0) + val npcStart = origin + place(player, origin.transform(0, 4, 0)) + + val npc = NPC.create(100, npcStart) + npc.init() + try { + configureMelee(npc) + blockMovementTiles(listOf(blocker)) + + npc.attack(player) + CombatMovementIntents.clear() + CombatMovementIntents.request(npc, player) + CombatMovementIntents.resolve() + npc.walkingQueue.update() + + assertEquals( + npcStart, + npc.location, + "A dumb NPC should not route around a blocked next tile on the direct path to the player." + ) + assertTrue(npc.properties.combatPulse.isAttacking) + } finally { + unblockMovementTiles(listOf(blocker)) + npc.clear() + CombatMovementIntents.clear() + } + } + } + + @Test + fun varrockGuardShouldStepAroundBakeryStallAfterRangedTargetRunsNorthEast() { + TestUtils.getMockPlayer("combat_varrock_bakery_target").use { player -> + val spawn = Location.create(2651, 3307, 0) + val originalTargetLocation = Location.create(2657, 3309, 0) + val guardStart = Location.create(2656, 3309, 0) + val targetDestination = Location.create(2658, 3311, 0) + place(player, originalTargetLocation) + configureRanged(player) + enableRun(player) + + val npc = NPC.create(32, guardStart) + npc.init() + npc.properties.spawnLocation = spawn + try { + configureMelee(npc) + + npc.attack(player) + queueRunPath( + player, listOf( + Location.create(2658, 3309, 0), Location.create(2658, 3310, 0), targetDestination + ) + ) + TestUtils.advanceTicks(8, false) + + assertNotEquals( + guardStart, + npc.location, + "The Varrock guard should not stall west of the player when the bakery stall blocks north. " + "guard=${npc.location}, player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}" + ) + assertTrue( + npc.location.getDistance(player.location) < originalTargetLocation.getDistance(targetDestination), + "The guard should keep taking local dumb steps toward the running target without full smart routing. " + "guard=${npc.location}, player=${player.location}, ${CombatMovementIntents.lastResolveSummary()}" + ) + assertTrue(npc.properties.combatPulse.isAttacking) + } finally { + npc.clear() + CombatMovementIntents.clear() + } + } + } + @Test fun dumbMeleeNpcShouldNotRouteAroundWhenBothCornerSidesAreBlocked() { TestUtils.getMockPlayer("combat_blocked_diagonal_corner_target").use { player -> val origin = arenaOrigin() val blockedTiles = listOf( - origin.transform(0, 1, 0), - origin.transform(-1, 0, 0) + origin.transform(0, 1, 0), origin.transform(-1, 0, 0) ) val npcStart = origin.transform(-1, 1, 0) place(player, origin) @@ -1195,10 +1248,7 @@ class CombatMovementTests { val obstacle = origin.transform(1, 0, 0) val playerEnd = origin.transform(2, 0, 0) val playerPath = listOf( - origin.transform(0, 1, 0), - origin.transform(1, 1, 0), - origin.transform(2, 1, 0), - playerEnd + origin.transform(0, 1, 0), origin.transform(1, 1, 0), origin.transform(2, 1, 0), playerEnd ) place(player, origin) enableRun(player) @@ -1242,8 +1292,8 @@ class CombatMovementTests { val door = RegionManager.getObject(doorLocation.z, doorLocation.x, doorLocation.y, 36846) ?: throw AssertionError("Expected Lumbridge house door 36846 at $doorLocation.") assertEquals(1, door.rotation, "Expected the Lumbridge house door to face east.") - val doorConfig = DoorConfigLoader.forId(door.id) - ?: throw AssertionError("Expected door config for ${door.id}.") + val doorConfig = + DoorConfigLoader.forId(door.id) ?: throw AssertionError("Expected door config for ${door.id}.") val openedDoorLocation = door.location.transform(0, 1, 0) DoorActionHandler.open(door, null, doorConfig.replaceId, -1, true, -1, doorConfig.isFence) @@ -1265,10 +1315,7 @@ class CombatMovementTests { } finally { npc.clear() val openedDoor = RegionManager.getObject( - openedDoorLocation.z, - openedDoorLocation.x, - openedDoorLocation.y, - doorConfig.replaceId + openedDoorLocation.z, openedDoorLocation.x, openedDoorLocation.y, doorConfig.replaceId ) if (openedDoor != null) { SceneryBuilder.replace(openedDoor, door) @@ -1284,8 +1331,7 @@ class CombatMovementTests { val start = Location.create(3252, 3227, 0) val npcLocation = Location.create(3253, 3226, 0) val expectedAttackTiles = setOf( - Location.create(3252, 3226, 0), - Location.create(3253, 3227, 0) + Location.create(3252, 3226, 0), Location.create(3253, 3227, 0) ) place(player, start) configureMelee(player) @@ -1307,8 +1353,7 @@ class CombatMovementTests { assertTrue( player.location in expectedAttackTiles, - "Player should step to the nearest cardinal attack tile at the ruin corner, not route away. " + - "player=${player.location}, npc=${npc.location}, ${CombatMovementIntents.lastResolveSummary()}" + "Player should step to the nearest cardinal attack tile at the ruin corner, not route away. " + "player=${player.location}, npc=${npc.location}, ${CombatMovementIntents.lastResolveSummary()}" ) assertTrue(meleeReach(player, npc)) assertTrue(player.properties.combatPulse.isAttacking) @@ -1362,8 +1407,7 @@ class CombatMovementTests { private fun configureMelee(entity: Entity) { entity.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_AGGRESSIVE, - WeaponInterface.BONUS_CRUSH + WeaponInterface.STYLE_AGGRESSIVE, WeaponInterface.BONUS_CRUSH ) entity.properties.combatPulse.updateStyle() } @@ -1372,8 +1416,7 @@ class CombatMovementTests { player.equipment.replace(Item(6724), EquipmentSlot.WEAPON.ordinal) player.equipment.replace(Item(Items.BRONZE_ARROW_882, 100), EquipmentSlot.AMMO.ordinal) player.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_RANGE_ACCURATE, - WeaponInterface.BONUS_RANGE + WeaponInterface.STYLE_RANGE_ACCURATE, WeaponInterface.BONUS_RANGE ) player.properties.combatPulse.updateStyle() } @@ -1384,8 +1427,7 @@ class CombatMovementTests { player.skills.setLevel(Skills.MAGIC, 99) player.properties.autocastSpell = TestAutocastSpell player.properties.attackStyle = WeaponInterface.AttackStyle( - WeaponInterface.STYLE_CAST, - WeaponInterface.BONUS_MAGIC + WeaponInterface.STYLE_CAST, WeaponInterface.BONUS_MAGIC ) player.properties.combatPulse.updateStyle() } @@ -1445,8 +1487,11 @@ class CombatMovementTests { } private fun magicReach(attacker: Entity, victim: Entity): Boolean { - return attacker.location.getDistance(victim.getClosestOccupiedTile(attacker.location)) <= 10.0 && - CombatSwingHandler.isProjectileClipped(attacker, victim, false) + return attacker.location.getDistance(victim.getClosestOccupiedTile(attacker.location)) <= 10.0 && CombatSwingHandler.isProjectileClipped( + attacker, + victim, + false + ) } private object TestAutocastSpell : CombatSpell() { @@ -1488,8 +1533,6 @@ class CombatMovementTests { } } - private val movementBlockFlag = Pathfinder.PREVENT_NORTH or - Pathfinder.PREVENT_EAST or - Pathfinder.PREVENT_SOUTH or - Pathfinder.PREVENT_WEST + private val movementBlockFlag = + Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST } diff --git a/Server/src/test/kotlin/core/PathfinderTests.kt b/Server/src/test/kotlin/core/PathfinderTests.kt index dacda2ac0..721f2abea 100644 --- a/Server/src/test/kotlin/core/PathfinderTests.kt +++ b/Server/src/test/kotlin/core/PathfinderTests.kt @@ -1,8 +1,8 @@ package core import TestUtils -import content.global.handlers.scenery.BankBoothListener import content.global.handlers.npc.NPCTalkListener +import content.global.handlers.scenery.BankBoothListener import content.global.skill.gather.GatheringSkillOptionListeners import content.global.skill.gather.woodcutting.WoodcuttingListener import content.region.misthalin.varrock.dialogue.GrandExchangeClerk @@ -11,25 +11,22 @@ import core.api.log import core.cache.def.impl.NPCDefinition import core.game.dialogue.DialogueInterpreter import core.game.interaction.* -import core.game.node.scenery.Scenery -import core.game.world.map.Direction -import core.game.world.map.Location -import core.game.world.map.RegionManager -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test import core.game.node.Node import core.game.node.entity.impl.PulseType import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player +import core.game.node.scenery.Scenery import core.game.world.GameWorld -import core.game.world.map.Region +import core.game.world.map.Direction +import core.game.world.map.Location +import core.game.world.map.RegionManager import core.game.world.map.path.ClipMaskSupplier import core.game.world.map.path.Pathfinder -import core.net.packet.PacketProcessor -import core.plugin.ClassScanner +import core.game.world.map.path.RsmodPathfinder import core.plugin.Plugin import core.tools.Log -import org.rs09.consts.NPCs +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test import org.rs09.consts.Scenery as SceneryIds class PathfinderTests { @@ -40,31 +37,42 @@ class PathfinderTests { WoodcuttingListener().defineListeners() BankBoothListener().defineListeners() } + val NPC_TEST_LOC = ServerConstants.HOME_LOCATION!!.transform(2, 10, 0) } - @Test fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() { + @Test + fun rsmodPathfinderShouldRejectDestinationsAtTheTruncationLimit() { + val start = Location.create(3165, 3218, 0) + + Assertions.assertTrue(RsmodPathfinder.canAttempt(start, Location.create(3203, 3186, 0))) + Assertions.assertFalse(RsmodPathfinder.canAttempt(start, Location.create(3203, 3185, 0))) + } + + @Test + fun getOccupiedTilesShouldReturnCorrectSetOfTilesThatAnObjectOccupiesAtAllRotations() { //clay fireplace - 13609 - sizex: 1, sizey: 2 val scenery = Scenery(13609, Location.create(50, 50, 0)) scenery.rotation = 0 val occupiedAt0 = scenery.occupiedTiles.toTypedArray() - Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50,51)), occupiedAt0) + Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 51)), occupiedAt0) scenery.rotation = 1 val occupiedAt1 = scenery.occupiedTiles.toTypedArray() - Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(51,50)), occupiedAt1) + Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(51, 50)), occupiedAt1) scenery.rotation = 2 val occupiedAt2 = scenery.occupiedTiles.toTypedArray() - Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(50,49)), occupiedAt2) + Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(50, 49)), occupiedAt2) scenery.rotation = 3 val occupiedAt3 = scenery.occupiedTiles.toTypedArray() - Assertions.assertArrayEquals(arrayOf(Location.create(50,50), Location.create(49,50)), occupiedAt3) + Assertions.assertArrayEquals(arrayOf(Location.create(50, 50), Location.create(49, 50)), occupiedAt3) } - @Test fun smartPathfinderShouldRespectSuppliedClipMask() { + @Test + fun smartPathfinderShouldRespectSuppliedClipMask() { val start = Location.create(3200, 3200, 0) val dest = Location.create(3202, 3200, 0) val blockedDestination = ClipMaskSupplier { _, x, y -> @@ -75,25 +83,42 @@ class PathfinderTests { Assertions.assertTrue(path.isSuccessful) Assertions.assertEquals( - Location.create(3201, 3200, 0), - Location.create(path.points.last.x, path.points.last.y, 0) + Location.create(3201, 3200, 0), Location.create(path.points.last.x, path.points.last.y, 0) ) } - @Test fun dumbPathfinderShouldUseRsmodRouting() { + @Test + fun dumbPathfinderShouldNotRouteAroundBlockedCardinalStep() { val start = Location.create(3200, 3200, 0) val dest = Location.create(3202, 3200, 0) val blockedMiddle = ClipMaskSupplier { _, x, y -> - if (x == 3201 && y == 3200) 0x100 else 0 + if (x == 3201 && y == 3200) movementBlockFlag else 0 } val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedMiddle) - Assertions.assertTrue(path.isSuccessful) - Assertions.assertTrue(path.points.isNotEmpty()) + Assertions.assertFalse(path.isSuccessful) + Assertions.assertTrue(path.points.isEmpty()) } - @Test fun walkingQueueHasPathShouldIgnoreResetAnchor() { + @Test + fun dumbPathfinderShouldUseAxisFallbackForBlockedDiagonalStep() { + val start = Location.create(3200, 3200, 0) + val dest = Location.create(3202, 3202, 0) + val blockedNorth = ClipMaskSupplier { _, x, y -> + if (x == 3200 && y == 3201) movementBlockFlag else 0 + } + + val path = Pathfinder.DUMB.find(start, 1, dest, 0, 0, 0, -1, 0, false, blockedNorth) + + Assertions.assertTrue(path.isSuccessful) + Assertions.assertTrue(path.points.isNotEmpty()) + Assertions.assertEquals(3201, path.points.first.x) + Assertions.assertEquals(3200, path.points.first.y) + } + + @Test + fun walkingQueueHasPathShouldIgnoreResetAnchor() { TestUtils.getMockPlayer("walkingQueueAnchor").use { player -> val start = Location.create(3200, 3200, 0) player.location = start @@ -107,7 +132,8 @@ class PathfinderTests { } } - @Test fun projectilePathfinderShouldUseRsmodLineOfSightFlags() { + @Test + fun projectilePathfinderShouldUseRsmodLineOfSightFlags() { val start = Location.create(3200, 3200, 0) val dest = Location.create(3202, 3200, 0) RegionManager.loadClippingWindow(start, 128) @@ -125,7 +151,8 @@ class PathfinderTests { } } - @Test fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() { + @Test + fun metadataSceneryInteractionShouldTriggerWhenAlreadyAtRsmodApproachTile() { TestUtils.getMockPlayer("bankBoothApproach").use { p -> val (booth, approach) = findReachableBankBoothFixture() p.location = approach @@ -140,16 +167,14 @@ class PathfinderTests { } } - @Test fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() { + @Test + fun metadataSceneryCollectShouldTriggerWhenAlreadyAtRsmodApproachTile() { TestUtils.getMockPlayer("bankBoothCollectApproach").use { p -> val (booth, approach) = findReachableBankBoothFixture() p.location = approach var collected = false InteractionListeners.addMetadata( - booth.id, - IntType.SCENERY, - arrayOf("collect"), - InteractionListener.InteractionMetadata({ _, _, _ -> + booth.id, IntType.SCENERY, arrayOf("collect"), InteractionListener.InteractionMetadata({ _, _, _ -> collected = true true }, 1, false) @@ -166,10 +191,11 @@ class PathfinderTests { } } - @Test fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() { + @Test + fun directObjectMovementPulseShouldTriggerWhenAlreadyAtRsmodApproachTile() { TestUtils.getMockPlayer("objectPulseApproach").use { p -> - val tree = RegionManager.getObject(0, 2720, 3475, 1307) - ?: throw AssertionError("Expected test tree object.") + val tree = + RegionManager.getObject(0, 2720, 3475, 1307) ?: throw AssertionError("Expected test tree object.") val approach = findReachableApproachTile(tree) var pulsed = false p.location = approach @@ -186,7 +212,8 @@ class PathfinderTests { } } - @Test fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() { + @Test + fun pathfinderShouldUseWrapperFootprintWhenSceneryChildIsSmallerThanWrapper() { TestUtils.getMockPlayer("taverleyPatchChildPath").use { p -> val wrapper = RegionManager.getObject(0, 2935, 3437, 8388) ?: throw AssertionError("Expected Taverley tree patch wrapper.") @@ -206,7 +233,8 @@ class PathfinderTests { } } - @Test fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() { + @Test + fun entityMovementPulseShouldTriggerWhenDestinationOverrideIsAlreadyReached() { TestUtils.getMockPlayer("bankerOverrideApproach").use { p -> val npc = NPC.create(0, NPC_TEST_LOC) npc.isNeverWalks = true @@ -226,7 +254,8 @@ class PathfinderTests { } } - @Test fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() { + @Test + fun movingEntityMovementPulseShouldNotInteractFromDiagonalTile() { TestUtils.getMockPlayer("movingNpcDiagonalApproach").use { p -> val origin = Location.create(3200, 3600, 0) val npc = NPC.create(0, origin.transform(1, 1, 0)) @@ -250,8 +279,7 @@ class PathfinderTests { TestUtils.advanceTicks(1, false) Assertions.assertNull( - pulseLocation, - "A moving entity interaction must not trigger from a diagonal non-interaction tile." + pulseLocation, "A moving entity interaction must not trigger from a diagonal non-interaction tile." ) repeat(8) { @@ -276,8 +304,7 @@ class PathfinderTests { 0, actualPulseLocation.z, null - ), - "Entity interaction must fire only from a currently valid interaction tile." + ), "Entity interaction must fire only from a currently valid interaction tile." ) } finally { npc.clear() @@ -285,7 +312,67 @@ class PathfinderTests { } } - @Test fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() { + @Test + fun runEnabledEntityMovementPulseShouldCatchWalkingNpcMovingDirectlyAway() { + TestUtils.getMockPlayer("runNpcInteractionChaser").use { p -> + val origin = openHorizontalInteractionOrigin() + val npc = NPC.create(0, origin.transform(4, 0, 0)) + npc.init() + p.location = origin + p.settings.runEnergy = 100.0 + p.settings.setRunToggled(true) + npc.walkingQueue.reset(false) + npc.walkingQueue.addPath(origin.transform(12, 0, 0).x, origin.transform(12, 0, 0).y) + + var pulseLocation: Location? = null + var pulseTargetLocation: Location? = null + try { + GameWorld.Pulser.submit(object : MovementPulse(p, npc) { + override fun pulse(): Boolean { + pulseLocation = p.location + pulseTargetLocation = npc.location + return true + } + }) + + repeat(6) { + if (pulseLocation == null) { + TestUtils.advanceTicks(1, false) + } + } + + val actualPulseLocation = pulseLocation ?: throw AssertionError( + "Run-enabled player should catch the walking NPC before it stops. " + "player=${p.location}, npc=${npc.location}, queue=${p.walkingQueue.queue}" + ) + val actualTargetLocation = pulseTargetLocation + ?: throw AssertionError("Expected target location to be captured when the pulse fired.") + Assertions.assertNotEquals( + origin.transform(12, 0, 0), + actualTargetLocation, + "The interaction should not wait until the NPC finishes walking away." + ) + Assertions.assertTrue( + Pathfinder.canInteract( + actualPulseLocation.x, + actualPulseLocation.y, + p.size(), + actualTargetLocation.x, + actualTargetLocation.y, + npc.size(), + npc.size(), + 0, + actualPulseLocation.z, + null + ), "Entity interaction must fire from a currently valid interaction tile." + ) + } finally { + npc.clear() + } + } + } + + @Test + fun interactionListenerShouldUseOptionHandlerDestinationWhenNoListenerDestinationOverride() { val npc = NPC.create(0, NPC_TEST_LOC) npc.isNeverWalks = true npc.init() @@ -328,7 +415,8 @@ class PathfinderTests { } } - @Test fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() { + @Test + fun genericTalkToShouldOpenGrandExchangeClerkDialogueFromCounterApproachTile() { GrandExchangePlugin().newInstance(null) if (!DialogueInterpreter.contains(6528)) { GrandExchangeClerk().init() @@ -361,9 +449,7 @@ class PathfinderTests { val base = ServerConstants.HOME_LOCATION!!.transform(8, 8, 0) for (rotation in 0..3) { val booth = Scenery(SceneryIds.BANK_BOOTH_2213, base, 10, rotation) - runCatching { findReachableApproachTile(booth) } - .getOrNull() - ?.let { return booth to it } + runCatching { findReachableApproachTile(booth) }.getOrNull()?.let { return booth to it } } throw AssertionError("Could not find a reachable synthetic bank booth fixture.") } @@ -392,7 +478,8 @@ class PathfinderTests { throw AssertionError("Could not find a reachable approach tile for $scenery.") } - @Test fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() { + @Test + fun movementPulseShouldStopEarlyIfNextToATileOccupiedByTargetObject() { val start = Location.create(2731, 3481) val dest = RegionManager.getObject(0, 2720, 3475, 1307) val p = TestUtils.getMockPlayer("treefindtest") @@ -404,15 +491,17 @@ class PathfinderTests { Assertions.assertEquals(Location.create(2722, 3475, 0), p.location) } - @Test fun movementInteractionShouldTrigger() { + @Test + fun movementInteractionShouldTrigger() { val npc = NPC.create(0, NPC_TEST_LOC) npc.init() var intListenerRan = false - InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = {player: Player, node: Node -> - intListenerRan = true - return@add true - }) + InteractionListeners.add( + 0, IntType.NPC.ordinal, arrayOf("testoptlistener"), method = { player: Player, node: Node -> + intListenerRan = true + return@add true + }) var pluginRan = false val option = Option("testoption", 4) @@ -422,6 +511,7 @@ class PathfinderTests { NPCDefinition.forId(0).handlers["option:testoption"] = this return this } + override fun handle(player: Player?, node: Node?, option: String?): Boolean { pluginRan = true return true @@ -432,7 +522,7 @@ class PathfinderTests { npc.interaction.set(option2) option.handler = testHandler - TestUtils.getMockPlayer("interactionTest").use {p -> + TestUtils.getMockPlayer("interactionTest").use { p -> p.location = ServerConstants.HOME_LOCATION TestUtils.simulateInteraction(p, npc, 0) TestUtils.advanceTicks(10, false) @@ -444,8 +534,9 @@ class PathfinderTests { } } - @Test fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() { - TestUtils.getMockPlayer("idlenpcdest").use {p -> + @Test + fun entityMovingToStationaryNPCShouldNotIdleIndefinitely() { + TestUtils.getMockPlayer("idlenpcdest").use { p -> val startLoc = ServerConstants.HOME_LOCATION p.location = startLoc val npc = NPC.create(0, NPC_TEST_LOC) @@ -461,8 +552,9 @@ class PathfinderTests { } } - @Test fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() { - TestUtils.getMockPlayer("entitystoptest").use {p -> + @Test + fun entityTargetMovementPulseShouldNotStopOnSameTileAsEntity() { + TestUtils.getMockPlayer("entitystoptest").use { p -> p.location = ServerConstants.HOME_LOCATION val npc = NPC.create(0, NPC_TEST_LOC) npc.isNeverWalks = true @@ -478,7 +570,8 @@ class PathfinderTests { } } - @Test fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() { + @Test + fun entityTargetMovementPulseWithExplicitParamsShouldNotStopOnSameTile() { TestUtils.getMockPlayer("entitystoptest2").use { p -> p.location = ServerConstants.HOME_LOCATION val npc = NPC.create(0, NPC_TEST_LOC) @@ -495,7 +588,8 @@ class PathfinderTests { } } - @Test fun doubleMovementPulseToEntityShouldNotStopOnSameTile() { + @Test + fun doubleMovementPulseToEntityShouldNotStopOnSameTile() { TestUtils.getMockPlayer("entitystoptest3").use { p -> p.location = ServerConstants.HOME_LOCATION val npc = NPC.create(0, NPC_TEST_LOC) @@ -518,12 +612,14 @@ class PathfinderTests { } } - @Test fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() { + @Test + fun simulatedInteractionPacketWithMovementFromPluginShouldNotEndOnSameTile() { val testHandler = object : OptionHandler() { override fun newInstance(arg: Any?): Plugin { NPCDefinition.forId(0).handlers["option:testoption"] = this return this } + override fun handle(player: Player?, node: Node?, option: String?): Boolean { log(this::class.java, Log.ERR, "Interaction triggered") return true @@ -547,14 +643,16 @@ class PathfinderTests { } } - @Test fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() { + @Test + fun simulatedInteractionPacketWithMovementFromListenerShouldNotEndOnSameTile() { val npc = NPC.create(0, NPC_TEST_LOC) npc.isNeverWalks = true npc.init() - InteractionListeners.add(0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = {player: Player, node: Node -> - return@add true - }) + InteractionListeners.add( + 0, IntType.NPC.ordinal, arrayOf("testoptlistener2"), method = { player: Player, node: Node -> + return@add true + }) val opt = Option("testoptlistener2", 1) npc.interaction.set(opt) @@ -568,7 +666,8 @@ class PathfinderTests { } } - @Test fun npcShouldReliablyReturnToSpawnLocationIfTooFar() { + @Test + fun npcShouldReliablyReturnToSpawnLocationIfTooFar() { //spawn a player into the area just to make sure it ticks... TestUtils.getMockPlayer("areatest").use { p -> val npc = NPC(1, Location.create(3240, 3226, 0)) @@ -584,7 +683,8 @@ class PathfinderTests { } } - @Test fun npcReturnToSpawnShouldUseOverriddenWalkRadius() { + @Test + fun npcReturnToSpawnShouldUseOverriddenWalkRadius() { TestUtils.getMockPlayer("overriddenRadiusReturn").use { val spawn = ServerConstants.HOME_LOCATION!! val npc = object : NPC(1, spawn.transform(5, 0, 0)) { @@ -606,10 +706,14 @@ class PathfinderTests { } } - @Test fun randomWalkingNpcShouldNotRouteAroundBlockedLocalDestination() { + @Test + fun randomWalkingNpcShouldUseSideStepWhenDirectNorthTileIsBlocked() { val origin = Location.create(3200, 3600, 0) - val blocked = origin.transform(1, 0, 0) - val destination = origin.transform(2, 0, 0) + val blocked = origin.transform(0, 1, 0) + val destination = origin.transform(0, 2, 0) + val sidesteps = setOf( + origin.transform(-1, 0, 0), origin.transform(1, 0, 0) + ) val npc = FixedDestinationNPC(origin, destination, 3) npc.isWalks = true npc.isNeverWalks = false @@ -619,22 +723,66 @@ class PathfinderTests { try { npc.resetWalk() repeat(20) { - npc.handleTickActions() + if (!npc.walkingQueue.hasPath()) { + npc.handleTickActions() + } } - Assertions.assertFalse( + Assertions.assertTrue( npc.walkingQueue.hasPath(), - "Random-walking NPCs should not take an RSMOD detour around a clipped boundary tile." + "Random-walking NPCs should use an open east/west sidestep when the direct north tile is blocked." ) npc.walkingQueue.update() - Assertions.assertEquals(origin, npc.location) + Assertions.assertTrue( + npc.location in sidesteps, + "Random-walking NPC should only take a local sidestep, not route through the blocked north tile. " + "npc=${npc.location}" + ) } finally { RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag) npc.clear() } } - @Test fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() { + @Test + fun randomWalkingNpcShouldNotFullyRouteAroundBlockedLocalDestination() { + val origin = Location.create(3200, 3600, 0) + val blocked = origin.transform(1, 0, 0) + val destination = origin.transform(2, 0, 0) + val sidesteps = setOf( + origin.transform(0, -1, 0), origin.transform(0, 1, 0) + ) + val npc = FixedDestinationNPC(origin, destination, 3) + npc.isWalks = true + npc.isNeverWalks = false + npc.init() + npc.properties.spawnLocation = origin + RegionManager.addClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag) + try { + npc.resetWalk() + repeat(20) { + if (!npc.walkingQueue.hasPath()) { + npc.handleTickActions() + } + } + + Assertions.assertTrue( + npc.walkingQueue.hasPath(), + "Random-walking NPCs should use a local sidestep instead of taking an RSMOD detour." + ) + npc.walkingQueue.update() + Assertions.assertTrue( + npc.location in sidesteps, + "Random-walking NPC should not fully route around a clipped boundary tile. npc=${npc.location}" + ) + Assertions.assertNotEquals(destination, npc.location) + } finally { + RegionManager.removeClippingFlag(blocked.z, blocked.x, blocked.y, false, movementBlockFlag) + npc.clear() + } + } + + @Test + fun npcShouldReliablyReturnToSpawnEvenIfRegionUnloaded() { //spawn a player into the area just to make sure it ticks... TestUtils.getMockPlayer("areaunloadtest").use { p -> val npc = NPC(1, Location.create(3240, 3226, 0)) @@ -654,9 +802,7 @@ class PathfinderTests { } private class FixedDestinationNPC( - location: Location, - private val destination: Location, - private val radius: Int + location: Location, private val destination: Location, private val radius: Int ) : NPC(1, location) { override fun getMovementDestination(): Location { return destination @@ -667,8 +813,32 @@ class PathfinderTests { } } - private val movementBlockFlag = Pathfinder.PREVENT_NORTH or - Pathfinder.PREVENT_EAST or - Pathfinder.PREVENT_SOUTH or - Pathfinder.PREVENT_WEST + private val movementBlockFlag = + Pathfinder.PREVENT_NORTH or Pathfinder.PREVENT_EAST or Pathfinder.PREVENT_SOUTH or Pathfinder.PREVENT_WEST + + private fun openHorizontalInteractionOrigin(): Location { + val start = Location.create(3200, 3600, 0) + for (dy in -16..16) { + for (dx in -16..16) { + val candidate = start.transform(dx, dy, 0) + if ((0..12).all { RegionManager.isTeleportPermitted(candidate.transform(it, 0, 0)) } && (0..11).all { + Pathfinder.canInteract( + candidate.x + it, + candidate.y, + 1, + candidate.x + it + 1, + candidate.y, + 1, + 1, + 0, + candidate.z, + null + ) + }) { + return candidate + } + } + } + throw AssertionError("No open horizontal interaction test line found near $start.") + } }