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

View file

@ -7,12 +7,15 @@ import org.rs09.consts.NPCs
import core.game.dialogue.DialogueFile
import core.game.interaction.InteractionListener
import core.game.interaction.IntType
import core.game.world.update.flag.context.Animation
import core.tools.END_DIALOGUE
import core.api.*
class MistagEasterEgg : InteractionListener {
val DIAMOND = Items.DIAMOND_1601
val MISTAG = NPCs.MISTAG_2084
val ZANIK_RING = 14649
val DRUNK_RENDER = 982
override fun defineListeners() {
onUseWith(IntType.NPC,DIAMOND,MISTAG){ player, _, with ->
@ -30,6 +33,18 @@ class MistagEasterEgg : InteractionListener {
player.appearance.transformNPC(-1)
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? {
return super.npc(core.game.dialogue.FacialExpression.OLD_HAPPY,*messages)
}
}
}

View file

@ -217,7 +217,7 @@ abstract class HunterTracking : OptionHandler(){
trail.get(currentIndex)
}
} else {
return false
TrailDefinition(0,TrailType.LINKING,false,Location(0,0,0),Location(0,0,0),Location(0,0,0))
}
when(option){

View file

@ -12,6 +12,7 @@ import core.api.*
import core.tools.SystemLogger
import core.game.system.command.Privilege
import core.game.world.GameWorld
import core.game.world.map.path.Pathfinder
import core.game.world.repository.Repository
import core.tools.Log
import java.lang.Integer.min
@ -108,24 +109,14 @@ class RevenantController : TickListener, Commands {
NONE {
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 {
private val MAX_ROAM_TICKS: Int = 250
override fun execute(revenantNPC: RevenantNPC) {
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 {
if (taskTimeRemaining[revenantNPC]!! <= 0) currentTask[revenantNPC] = NONE
return true
@ -139,6 +130,7 @@ class RevenantController : TickListener, Commands {
fun canMove(revenantNPC: RevenantNPC) : Boolean {
return !revenantNPC.walkingQueue.isMoving
&& !revenantNPC.pulseManager.hasPulseRunning()
&& !revenantNPC.properties.combatPulse.isAttacking
&& !revenantNPC.properties.combatPulse.isInCombat
}
@ -212,7 +204,7 @@ class RevenantController : TickListener, Commands {
RandomFunction.random(-pathVariance, pathVariance),
0
)
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc) {
revenantNPC.pulseManager.run(object : MovementPulse(revenantNPC, nextLoc, Pathfinder.SMART) {
override fun pulse(): Boolean {
return true
}
@ -235,4 +227,4 @@ class RevenantController : TickListener, Commands {
abstract fun execute(revenantNPC: RevenantNPC)
open fun assign(revenantNPC: RevenantNPC) {}
}
}
}

View file

@ -1099,6 +1099,22 @@ fun forceWalk(entity: Entity, dest: Location, type: String) {
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
* @param player the player we are moving

View file

@ -41,6 +41,10 @@ class Vector (val x: Double, val y: Double) {
return -this
}
fun toLocation (plane: Int = 0) : Location {
return Location.create(floor(x).toInt(), floor(y).toInt(), plane)
}
companion object {
@JvmStatic fun betweenLocs (from: Location, to: Location) : Vector {
val xDiff = to.x - from.x

View file

@ -15,11 +15,13 @@ 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 static core.api.ContentAPIKt.getWorldTicks;
import static core.api.ContentAPIKt.log;
import core.tools.Log;
import content.region.wilderness.handlers.revenants.RevenantNPC;
import static core.api.ContentAPIKt.*;
import java.util.Deque;
@ -84,9 +86,6 @@ public abstract class MovementPulse extends Pulse {
private Location previousLoc;
private Location previousMoverLoc;
private int previousMoveTime;
/**
* Constructs a new {@code MovementPulse} {@code Object}.
*
@ -187,38 +186,27 @@ public abstract class MovementPulse extends Pulse {
@Override
public boolean update() {
if (!mover.getViewport().getRegion().isActive())
return false;
if (!validate()) {
stop();
return true;
}
mover.face(null);
if (mover == null || destination == null || mover.getViewport().getRegion() == null) {
updatePath();
if (tryInteract()) {
stop();
return true;
}
if (hasInactiveNode() || !mover.getViewport().getRegion().isActive()) {
stop();
return true;
}
if (!isRunning()) {
return true;
}
findPath();
return false;
}
private boolean tryInteract() {
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.
int radius = destination instanceof Entity && ((Entity)destination).getWalkingQueue().hasPath() ? 1 : 0;
if (interactLocation == null)
@ -243,6 +231,13 @@ public abstract class MovementPulse extends Pulse {
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();
@ -255,20 +250,15 @@ public abstract class MovementPulse extends Pulse {
/**
* 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()) {
return;
}
if(destination.getLocation() == null){
if(destination == null || destination.getLocation() == null){
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;
@ -287,60 +277,35 @@ public abstract class MovementPulse extends Pulse {
else if (useHandler != null) {
loc = useHandler.getDestination((Player) mover, destination);
}
else if (inside) {
else if (isInsideEntity(mover.getLocation())) {
loc = findBorderLocation();
}
} else if (loc == previousLoc && interactLocation != null && mover.getWalkingQueue().hasPath()) return;
if (destination == null) {
return;
}
if (destination instanceof Entity || interactLocation == null) {
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 && 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());
}
}
}
if (destination instanceof NPC)
loc = checkForEntityPathInterrupt(loc != null ? loc : destination.getLocation());
Path path = Pathfinder.find(mover, loc != null ? loc : destination, true, pathfinder);
loc = destination.getLocation();
near = !path.isSuccessful() || path.isMoveNear();
interactLocation = mover.getLocation();
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;
}
}
if (interactLocation == null)
interactLocation = loc;
if (destination instanceof Entity || interactLocation == null || (!mover.getWalkingQueue().hasPath() && interactLocation.getDistance(mover.getLocation()) > 0) || (usingTruncatedPath && destination.getLocation().getDistance(mover.getLocation()) < 14)) {
if (!checkAllowMovement())
return;
Path path;
Pair<Boolean, Location> 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;
}
if (!path.getPoints().isEmpty() && canMove) {
near = !path.isSuccessful() || path.isMoveNear();
if (!path.getPoints().isEmpty()) {
Point point = path.getPoints().getLast();
interactLocation = Location.create(point.getX(), point.getY(), mover.getLocation().getZ());
if (forceRun) {
mover.getWalkingQueue().reset(forceRun);
} else {
@ -356,11 +321,88 @@ public abstract class MovementPulse extends Pulse {
} else {
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();
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.
*/
private Location findBorderLocation() {
private Location findBorderLocation(Location centerDestLoc) {
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);
Direction direction = Direction.getLogicalDirection(centerDest, center);
Location delta = Location.getDelta(destination.getLocation(), mover.getLocation());
Location delta = Location.getDelta(centerDestLoc, mover.getLocation());
main:
for (int i = 0; i < 4; i++) {
int amount = 0;

View file

@ -215,7 +215,7 @@ class CombatPulse(
if (entity == null || victim == null || entity.locks.isMovementLocked) {
return false
}
movement.findPath()
movement.updatePath()
return type == InteractionType.MOVE_INTERACT
}

View file

@ -719,7 +719,7 @@ public class NPC extends Entity {
if (!pathBoundMovement || movementPath == null || movementPath.length < 1) {
Location returnToSpawnLocation = getProperties().getSpawnLocation().transform(-5 + RandomFunction.random(getWalkRadius()), -5 + RandomFunction.random(getWalkRadius()), 0);
int dist = (int) Location.getDistance(location, returnToSpawnLocation);
int pathLimit = 15;
int pathLimit = 14;
if (dist > pathLimit) {
Vector normalizedDir = Vector.betweenLocs(this.location, returnToSpawnLocation).normalized();
returnToSpawnLocation = this.location.transform (normalizedDir.times(pathLimit));

View file

@ -545,6 +545,8 @@ public final class Appearance {
* @return The render animation id.
*/
public int getRenderAnimation() {
if (player.getAttribute("render-anim-override") != null)
return player.getAttribute("render-anim-override", renderAnimationId);
return renderAnimationId;
}

View file

@ -272,5 +272,9 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) {
define("fmanim", Privilege.ADMIN, "", "") {player, args ->
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))
}
}
}

View file

@ -24,7 +24,7 @@ class PulseRunner {
val elapsedTime = measure {
try {
if (!pulse.update()) {
if (!pulse.update() && pulse.isRunning) {
pulses.add(pulse)
}
} catch (e: Exception) {
@ -60,4 +60,4 @@ class PulseRunner {
}
}
}
}
}

View file

@ -83,9 +83,7 @@ public final class DumbPathfinder extends Pathfinder {
Direction last = null;
for (int i = 0; i < points.size() - 1; 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));
}
@ -426,4 +424,4 @@ public final class DumbPathfinder extends Pathfinder {
return new Direction[0];
}
}
}

View file

@ -209,11 +209,9 @@ internal constructor() : Pathfinder() {
if (++attempts > queueX.size) {
return path
}
if (directionFlag != previousDirection) {
previousDirection = directionFlag
queueX[readPosition] = curX
queueY[readPosition++] = curY
}
previousDirection = directionFlag
queueX[readPosition] = curX
queueY[readPosition++] = curY
if (directionFlag and WEST_FLAG != 0) {
curX++
} else if (directionFlag and EAST_FLAG != 0) {