diff --git a/Server/src/main/content/global/handlers/item/withnpc/MistagEasterEgg.kt b/Server/src/main/content/global/handlers/item/withnpc/MistagEasterEgg.kt index e763a4963..10d7aa15d 100644 --- a/Server/src/main/content/global/handlers/item/withnpc/MistagEasterEgg.kt +++ b/Server/src/main/content/global/handlers/item/withnpc/MistagEasterEgg.kt @@ -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) } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/skill/hunter/tracking/HunterTracking.kt b/Server/src/main/content/global/skill/hunter/tracking/HunterTracking.kt index 429836909..e4ca5c4e1 100644 --- a/Server/src/main/content/global/skill/hunter/tracking/HunterTracking.kt +++ b/Server/src/main/content/global/skill/hunter/tracking/HunterTracking.kt @@ -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){ diff --git a/Server/src/main/content/region/wilderness/handlers/revenants/RevenantController.kt b/Server/src/main/content/region/wilderness/handlers/revenants/RevenantController.kt index dd6184aec..ef861325a 100644 --- a/Server/src/main/content/region/wilderness/handlers/revenants/RevenantController.kt +++ b/Server/src/main/content/region/wilderness/handlers/revenants/RevenantController.kt @@ -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) {} } -} \ No newline at end of file +} diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index d26aca718..a7de58500 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -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 { + 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 diff --git a/Server/src/main/core/api/utils/Vector.kt b/Server/src/main/core/api/utils/Vector.kt index 4c2c3ffed..937861cbc 100644 --- a/Server/src/main/core/api/utils/Vector.kt +++ b/Server/src/main/core/api/utils/Vector.kt @@ -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 diff --git a/Server/src/main/core/game/interaction/MovementPulse.java b/Server/src/main/core/game/interaction/MovementPulse.java index 67caf8676..fb4adbfe2 100644 --- a/Server/src/main/core/game/interaction/MovementPulse.java +++ b/Server/src/main/core/game/interaction/MovementPulse.java @@ -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 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 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 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> 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; diff --git a/Server/src/main/core/game/node/entity/combat/CombatPulse.kt b/Server/src/main/core/game/node/entity/combat/CombatPulse.kt index 009df9029..d294229da 100644 --- a/Server/src/main/core/game/node/entity/combat/CombatPulse.kt +++ b/Server/src/main/core/game/node/entity/combat/CombatPulse.kt @@ -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 } 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 4d63d9396..9c1d0dc6f 100644 --- a/Server/src/main/core/game/node/entity/npc/NPC.java +++ b/Server/src/main/core/game/node/entity/npc/NPC.java @@ -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)); diff --git a/Server/src/main/core/game/node/entity/player/link/appearance/Appearance.java b/Server/src/main/core/game/node/entity/player/link/appearance/Appearance.java index 981d6b024..e4c5ecfb4 100644 --- a/Server/src/main/core/game/node/entity/player/link/appearance/Appearance.java +++ b/Server/src/main/core/game/node/entity/player/link/appearance/Appearance.java @@ -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; } diff --git a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt index 0ec29e6d5..306eac1f9 100644 --- a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt @@ -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)) + } } } diff --git a/Server/src/main/core/game/world/PulseRunner.kt b/Server/src/main/core/game/world/PulseRunner.kt index 923dfd3dc..7ff62a3e3 100644 --- a/Server/src/main/core/game/world/PulseRunner.kt +++ b/Server/src/main/core/game/world/PulseRunner.kt @@ -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 { } } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/path/DumbPathfinder.java b/Server/src/main/core/game/world/map/path/DumbPathfinder.java index 962932ea2..8bcfb32e0 100644 --- a/Server/src/main/core/game/world/map/path/DumbPathfinder.java +++ b/Server/src/main/core/game/world/map/path/DumbPathfinder.java @@ -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]; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt index 90917ece6..a8f6ec219 100644 --- a/Server/src/main/core/game/world/map/path/SmartPathfinder.kt +++ b/Server/src/main/core/game/world/map/path/SmartPathfinder.kt @@ -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) {