Refactored movement pulse

Fixed bug with Vinesweeper gnomes not checking flags
Fixed hunter tracking
This commit is contained in:
Ceikry
2023-06-29 01:46:00 +00:00
committed by Ryan
parent da33c89a70
commit 06e8279f2a
13 changed files with 192 additions and 121 deletions
@@ -7,12 +7,15 @@ import org.rs09.consts.NPCs
import core.game.dialogue.DialogueFile import core.game.dialogue.DialogueFile
import core.game.interaction.InteractionListener import core.game.interaction.InteractionListener
import core.game.interaction.IntType import core.game.interaction.IntType
import core.game.world.update.flag.context.Animation
import core.tools.END_DIALOGUE import core.tools.END_DIALOGUE
import core.api.*
class MistagEasterEgg : InteractionListener { class MistagEasterEgg : InteractionListener {
val DIAMOND = Items.DIAMOND_1601 val DIAMOND = Items.DIAMOND_1601
val MISTAG = NPCs.MISTAG_2084 val MISTAG = NPCs.MISTAG_2084
val ZANIK_RING = 14649 val ZANIK_RING = 14649
val DRUNK_RENDER = 982
override fun defineListeners() { override fun defineListeners() {
onUseWith(IntType.NPC,DIAMOND,MISTAG){ player, _, with -> onUseWith(IntType.NPC,DIAMOND,MISTAG){ player, _, with ->
@@ -30,6 +33,18 @@ class MistagEasterEgg : InteractionListener {
player.appearance.transformNPC(-1) player.appearance.transformNPC(-1)
return@onUnequip true return@onUnequip true
} }
onEquip(Items.BEER_1917){player, _ ->
setAttribute(player, "render-anim-override", DRUNK_RENDER)
return@onEquip true
}
onUnequip(Items.BEER_1917){player, _ ->
removeAttribute(player, "render-anim-override")
player.appearance.setDefaultAnimations()
player.appearance.sync()
return@onUnequip true
}
} }
} }
@@ -55,4 +70,4 @@ class MistagEasterEggDialogue(val hasRing: Boolean): DialogueFile(){
override fun npc(vararg messages: String?): Component? { override fun npc(vararg messages: String?): Component? {
return super.npc(core.game.dialogue.FacialExpression.OLD_HAPPY,*messages) return super.npc(core.game.dialogue.FacialExpression.OLD_HAPPY,*messages)
} }
} }
@@ -217,7 +217,7 @@ abstract class HunterTracking : OptionHandler(){
trail.get(currentIndex) trail.get(currentIndex)
} }
} else { } else {
return false TrailDefinition(0,TrailType.LINKING,false,Location(0,0,0),Location(0,0,0),Location(0,0,0))
} }
when(option){ when(option){
@@ -12,6 +12,7 @@ import core.api.*
import core.tools.SystemLogger import core.tools.SystemLogger
import core.game.system.command.Privilege import core.game.system.command.Privilege
import core.game.world.GameWorld import core.game.world.GameWorld
import core.game.world.map.path.Pathfinder
import core.game.world.repository.Repository import core.game.world.repository.Repository
import core.tools.Log import core.tools.Log
import java.lang.Integer.min import java.lang.Integer.min
@@ -108,24 +109,14 @@ class RevenantController : TickListener, Commands {
NONE { NONE {
override fun execute(revenantNPC: RevenantNPC) {} override fun execute(revenantNPC: RevenantNPC) {}
}, },
INTENTIONAL_IDLE {
private val MAX_IDLE_TIME: Int = 50
override fun execute(revenantNPC: RevenantNPC) {
if (taskTimeRemaining[revenantNPC] == 0) currentTask[revenantNPC] = NONE
}
override fun assign(revenantNPC: RevenantNPC) {
taskTimeRemaining[revenantNPC] = RandomFunction.random(MAX_IDLE_TIME)
}
},
RANDOM_ROAM { RANDOM_ROAM {
private val MAX_ROAM_TICKS: Int = 250 private val MAX_ROAM_TICKS: Int = 250
override fun execute(revenantNPC: RevenantNPC) { override fun execute(revenantNPC: RevenantNPC) {
if (!canMove(revenantNPC)) return if (!canMove(revenantNPC)) return
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, getNextLocation(revenantNPC)) { val nextLoc = getNextLocation(revenantNPC)
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc, Pathfinder.SMART) {
override fun pulse(): Boolean { override fun pulse(): Boolean {
if (taskTimeRemaining[revenantNPC]!! <= 0) currentTask[revenantNPC] = NONE if (taskTimeRemaining[revenantNPC]!! <= 0) currentTask[revenantNPC] = NONE
return true return true
@@ -139,6 +130,7 @@ class RevenantController : TickListener, Commands {
fun canMove(revenantNPC: RevenantNPC) : Boolean { fun canMove(revenantNPC: RevenantNPC) : Boolean {
return !revenantNPC.walkingQueue.isMoving return !revenantNPC.walkingQueue.isMoving
&& !revenantNPC.pulseManager.hasPulseRunning()
&& !revenantNPC.properties.combatPulse.isAttacking && !revenantNPC.properties.combatPulse.isAttacking
&& !revenantNPC.properties.combatPulse.isInCombat && !revenantNPC.properties.combatPulse.isInCombat
} }
@@ -212,7 +204,7 @@ class RevenantController : TickListener, Commands {
RandomFunction.random(-pathVariance, pathVariance), RandomFunction.random(-pathVariance, pathVariance),
0 0
) )
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc) { revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc, Pathfinder.SMART) {
override fun pulse(): Boolean { override fun pulse(): Boolean {
return true return true
} }
@@ -235,4 +227,4 @@ class RevenantController : TickListener, Commands {
abstract fun execute(revenantNPC: RevenantNPC) abstract fun execute(revenantNPC: RevenantNPC)
open fun assign(revenantNPC: RevenantNPC) {} open fun assign(revenantNPC: RevenantNPC) {}
} }
} }
+16
View File
@@ -1099,6 +1099,22 @@ fun forceWalk(entity: Entity, dest: Location, type: String) {
path.walk(entity) path.walk(entity)
} }
/**
* Returns a location truncated to the appropriate pathfinding limit
**/
fun truncateLoc (mover: Entity, destination: Location) : Pair<Boolean,Location> {
val vector = Vector.betweenLocs(mover.location, destination)
val normVec = vector.normalized()
val mag = vector.magnitude()
var multiplier = if (mover is NPC) 14.0 else ServerConstants.MAX_PATHFIND_DISTANCE.toDouble()
var clampedMultiplier = min(multiplier, mag)
var truncated = multiplier == clampedMultiplier
return Pair(truncated, mover.location.transform(normVec * clampedMultiplier))
}
/** /**
* Force a player to move from the start location to the dest location * Force a player to move from the start location to the dest location
* @param player the player we are moving * @param player the player we are moving
+4
View File
@@ -41,6 +41,10 @@ class Vector (val x: Double, val y: Double) {
return -this return -this
} }
fun toLocation (plane: Int = 0) : Location {
return Location.create(floor(x).toInt(), floor(y).toInt(), plane)
}
companion object { companion object {
@JvmStatic fun betweenLocs (from: Location, to: Location) : Vector { @JvmStatic fun betweenLocs (from: Location, to: Location) : Vector {
val xDiff = to.x - from.x val xDiff = to.x - from.x
@@ -15,11 +15,13 @@ import core.net.packet.PacketRepository;
import core.net.packet.context.PlayerContext; import core.net.packet.context.PlayerContext;
import core.net.packet.out.ClearMinimapFlag; import core.net.packet.out.ClearMinimapFlag;
import kotlin.jvm.functions.Function2; import kotlin.jvm.functions.Function2;
import kotlin.Pair;
import core.tools.SystemLogger; import core.tools.SystemLogger;
import core.api.utils.Vector;
import static core.api.ContentAPIKt.getWorldTicks; import content.region.wilderness.handlers.revenants.RevenantNPC;
import static core.api.ContentAPIKt.log;
import core.tools.Log; import static core.api.ContentAPIKt.*;
import java.util.Deque; import java.util.Deque;
@@ -84,9 +86,6 @@ public abstract class MovementPulse extends Pulse {
private Location previousLoc; private Location previousLoc;
private Location previousMoverLoc;
private int previousMoveTime;
/** /**
* Constructs a new {@code MovementPulse} {@code Object}. * Constructs a new {@code MovementPulse} {@code Object}.
* *
@@ -187,38 +186,27 @@ public abstract class MovementPulse extends Pulse {
@Override @Override
public boolean update() { public boolean update() {
if (!mover.getViewport().getRegion().isActive())
return false;
if (!validate()) {
stop();
return true;
}
mover.face(null); mover.face(null);
if (mover == null || destination == null || mover.getViewport().getRegion() == null) { updatePath();
if (tryInteract()) {
stop(); stop();
return true; return true;
} }
if (hasInactiveNode() || !mover.getViewport().getRegion().isActive()) { return false;
stop(); }
return true;
} private boolean tryInteract() {
if (!isRunning()) {
return true;
}
findPath();
Location ml = mover.getLocation(); Location ml = mover.getLocation();
if (previousMoverLoc == null || !previousMoverLoc.equals(ml)) {
previousMoverLoc = Location.create(ml);
previousMoveTime = getWorldTicks();
}
else if (getWorldTicks() - previousMoveTime >= 25) {
if (mover instanceof Player) {
((Player) mover).getPacketDispatch().sendMessage("I can't reach that.");
PacketRepository.send(ClearMinimapFlag.class, new PlayerContext((Player) mover));
}
log(this.getClass(), Log.FINE, mover.getName() + " was trying to move to " + interactLocation + " from " + ml + " but hasn't changed location in 25 ticks. More info follows:");
log(this.getClass(), Log.FINE, " -> Locked? " + mover.getLocks().isMovementLocked());
log(this.getClass(), Log.FINE, " -> Has path? " + mover.getWalkingQueue().hasPath());
stop();
return true;
}
// Allow being within 1 square of moving entities to interact with them. // Allow being within 1 square of moving entities to interact with them.
int radius = destination instanceof Entity && ((Entity)destination).getWalkingQueue().hasPath() ? 1 : 0; int radius = destination instanceof Entity && ((Entity)destination).getWalkingQueue().hasPath() ? 1 : 0;
if (interactLocation == null) if (interactLocation == null)
@@ -243,6 +231,13 @@ public abstract class MovementPulse extends Pulse {
return false; return false;
} }
private boolean validate() {
if (mover == null || destination == null || mover.getViewport().getRegion() == null || hasInactiveNode()) {
return false;
}
return isRunning();
}
@Override @Override
public void stop() { public void stop() {
super.stop(); super.stop();
@@ -255,20 +250,15 @@ public abstract class MovementPulse extends Pulse {
/** /**
* Finds a path to the destination, if necessary. * Finds a path to the destination, if necessary.
*/ */
public void findPath() { private boolean usingTruncatedPath = false;
private boolean isMoveNearSet = false;
public void updatePath() {
if (mover instanceof NPC && mover.asNpc().isNeverWalks()) { if (mover instanceof NPC && mover.asNpc().isNeverWalks()) {
return; return;
} }
if(destination.getLocation() == null){ if(destination == null || destination.getLocation() == null){
return; return;
} }
boolean inside = isInsideEntity(mover.getLocation());
/* This appears to have been a premature optimization that lead to a bug that would cause both entities
to completely stop moving mid-combat/mid-follow-dance/etc
if (last != null && last.equals(destination.getLocation()) && !inside) {
return;
}
*/
Location loc = null; Location loc = null;
@@ -287,60 +277,35 @@ public abstract class MovementPulse extends Pulse {
else if (useHandler != null) { else if (useHandler != null) {
loc = useHandler.getDestination((Player) mover, destination); loc = useHandler.getDestination((Player) mover, destination);
} }
else if (inside) { else if (isInsideEntity(mover.getLocation())) {
loc = findBorderLocation(); loc = findBorderLocation();
} }
} else if (loc == previousLoc && interactLocation != null && mover.getWalkingQueue().hasPath()) return;
if (destination == null) {
return;
} }
if (destination instanceof Entity || interactLocation == null) { if (destination instanceof NPC)
Location ml = mover.getLocation(); loc = checkForEntityPathInterrupt(loc != null ? loc : destination.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 && Math.max(Math.abs(ml.getX() - dl.getX()), Math.abs(ml.getY() - dl.getY())) > 1) {
WalkingQueue wq = ((Entity)destination).getWalkingQueue();
if(wq.hasPath()) {
Point[] points = wq.getQueue().toArray(new Point[0]);
if(points.length > 0) {
Point p = points[0];
for(int i=0; i<points.length; i++) {
// 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(Math.max(Math.abs(ml.getX() - points[i].getX()), Math.abs(ml.getY() - points[i].getY())) <= 2) {
p = points[i];
}
}
loc.setX(p.getX());
loc.setY(p.getY());
}
}
}
Path path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder); if (interactLocation == null)
loc = destination.getLocation(); interactLocation = loc;
near = !path.isSuccessful() || path.isMoveNear();
interactLocation = mover.getLocation(); if (destination instanceof Entity || interactLocation == null || (!mover.getWalkingQueue().hasPath() && interactLocation.getDistance(mover.getLocation()) > 0) || (usingTruncatedPath && destination.getLocation().getDistance(mover.getLocation()) < 14)) {
boolean canMove = true; if (!checkAllowMovement())
if (destination instanceof Entity) { return;
Entity e = (Entity) destination;
Location l = e.getLocation(); Path path;
Deque<Point> npcPath = e.getWalkingQueue().getQueue(); Pair<Boolean, Location> truncation = truncateLoc(mover, loc != null ? loc : destination.getLocation());
if (e.getWalkingQueue().hasPath() && e.getProperties().getCombatPulse().isRunning() && e.getProperties().getCombatPulse().getVictim() == mover) if (truncation.getFirst()) {
canMove = false; path = Pathfinder.find(mover, truncation.getSecond(), true, pathfinder);
if (!canMove) { //If we normally shouldn't move, but the NPC's pathfinding is not letting them move, then move. usingTruncatedPath = true;
if (npcPath.size() == 1) { } else {
Point pathElement = npcPath.peek(); path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder);
if (pathElement.getX() == l.getX() && pathElement.getY() == l.getY()) interactLocation = null; //reset interactLocation so the below code can set it to the properly-pathfound last bit of path.
canMove = true; usingTruncatedPath = false;
}
}
} }
if (!path.getPoints().isEmpty() && canMove) { near = !path.isSuccessful() || path.isMoveNear();
if (!path.getPoints().isEmpty()) {
Point point = path.getPoints().getLast(); Point point = path.getPoints().getLast();
interactLocation = Location.create(point.getX(), point.getY(), mover.getLocation().getZ());
if (forceRun) { if (forceRun) {
mover.getWalkingQueue().reset(forceRun); mover.getWalkingQueue().reset(forceRun);
} else { } else {
@@ -356,11 +321,88 @@ public abstract class MovementPulse extends Pulse {
} else { } else {
mover.face(null); mover.face(null);
} }
if (i == size - 1 && interactLocation == null)
interactLocation = Location.create(point.getX(), point.getY(), mover.getLocation().getZ());
} }
previousLoc = loc;
} }
previousLoc = loc;
} }
last = destination.getLocation(); 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<Point> npcPath = e.getWalkingQueue().getQueue();
if (e.getWalkingQueue().hasPath() && 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(wq.hasPath()) {
Point[] points = wq.getQueue().toArray(new Point[0]);
if(points.length > 0) {
Point p = points[0];
Point predictiveIntersection = null;
for(int i=0; i<points.length; i++) {
Location closestBorder = getClosestBorderToPoint (points[i], loc.getZ());
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());
return endLoc;
}
}
}
return loc;
}
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()));
}
private Location findBorderLocation() {
return findBorderLocation(destination.getLocation());
} }
/** /**
@@ -368,12 +410,12 @@ public abstract class MovementPulse extends Pulse {
* *
* @return The location to walk to. * @return The location to walk to.
*/ */
private Location findBorderLocation() { private Location findBorderLocation(Location centerDestLoc) {
int size = destination.size(); int size = destination.size();
Location centerDest = destination.getLocation().transform(size >> 1, size >> 1, 0); Location centerDest = centerDestLoc.transform(size >> 1, size >> 1, 0);
Location center = mover.getLocation().transform(mover.size() >> 1, mover.size() >> 1, 0); Location center = mover.getLocation().transform(mover.size() >> 1, mover.size() >> 1, 0);
Direction direction = Direction.getLogicalDirection(centerDest, center); Direction direction = Direction.getLogicalDirection(centerDest, center);
Location delta = Location.getDelta(destination.getLocation(), mover.getLocation()); Location delta = Location.getDelta(centerDestLoc, mover.getLocation());
main: main:
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int amount = 0; int amount = 0;
@@ -215,7 +215,7 @@ class CombatPulse(
if (entity == null || victim == null || entity.locks.isMovementLocked) { if (entity == null || victim == null || entity.locks.isMovementLocked) {
return false return false
} }
movement.findPath() movement.updatePath()
return type == InteractionType.MOVE_INTERACT return type == InteractionType.MOVE_INTERACT
} }
@@ -719,7 +719,7 @@ public class NPC extends Entity {
if (!pathBoundMovement || movementPath == null || movementPath.length < 1) { if (!pathBoundMovement || movementPath == null || movementPath.length < 1) {
Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0); Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0);
int dist = (int) Location.getDistance(location, returnToSpawnLocation); int dist = (int) Location.getDistance(location, returnToSpawnLocation);
int pathLimit = 15; int pathLimit = 14;
if (dist > pathLimit) { if (dist > pathLimit) {
Vector normalizedDir = Vector.betweenLocs(this.location, returnToSpawnLocation).normalized(); Vector normalizedDir = Vector.betweenLocs(this.location, returnToSpawnLocation).normalized();
returnToSpawnLocation = this.location.transform (normalizedDir.times(pathLimit)); returnToSpawnLocation = this.location.transform (normalizedDir.times(pathLimit));
@@ -545,6 +545,8 @@ public final class Appearance {
* @return The render animation id. * @return The render animation id.
*/ */
public int getRenderAnimation() { public int getRenderAnimation() {
if (player.getAttribute("render-anim-override") != null)
return player.getAttribute("render-anim-override", renderAnimationId);
return renderAnimationId; return renderAnimationId;
} }
@@ -272,5 +272,9 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) {
define("fmanim", Privilege.ADMIN, "", "") {player, args -> define("fmanim", Privilege.ADMIN, "", "") {player, args ->
setAttribute(player, "fmanim", args[1].toIntOrNull() ?: -1) setAttribute(player, "fmanim", args[1].toIntOrNull() ?: -1)
} }
define("drawintersect", Privilege.ADMIN, "", "Visualizes the predicted intersection point with an NPC") {player, _ ->
setAttribute(player, "draw-intersect", !getAttribute(player, "draw-intersect", false))
}
} }
} }
@@ -24,7 +24,7 @@ class PulseRunner {
val elapsedTime = measure { val elapsedTime = measure {
try { try {
if (!pulse.update()) { if (!pulse.update() && pulse.isRunning) {
pulses.add(pulse) pulses.add(pulse)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -60,4 +60,4 @@ class PulseRunner {
} }
} }
} }
} }
@@ -83,9 +83,7 @@ public final class DumbPathfinder extends Pathfinder {
Direction last = null; Direction last = null;
for (int i = 0; i < points.size() - 1; i++) { for (int i = 0; i < points.size() - 1; i++) {
Point p = points.get(i); Point p = points.get(i);
if (p.getDirection() != last) { path.getPoints().add(p);
path.getPoints().add(p);
}
} }
path.getPoints().add(points.get(points.size() - 1)); path.getPoints().add(points.get(points.size() - 1));
} }
@@ -426,4 +424,4 @@ public final class DumbPathfinder extends Pathfinder {
return new Direction[0]; return new Direction[0];
} }
} }
@@ -209,11 +209,9 @@ internal constructor() : Pathfinder() {
if (++attempts > queueX.size) { if (++attempts > queueX.size) {
return path return path
} }
if (directionFlag != previousDirection) { previousDirection = directionFlag
previousDirection = directionFlag queueX[readPosition] = curX
queueX[readPosition] = curX queueY[readPosition++] = curY
queueY[readPosition++] = curY
}
if (directionFlag and WEST_FLAG != 0) { if (directionFlag and WEST_FLAG != 0) {
curX++ curX++
} else if (directionFlag and EAST_FLAG != 0) { } else if (directionFlag and EAST_FLAG != 0) {