diff --git a/data/plugins/entity/mob/following/following.rb b/data/plugins/entity/mob/following/following.rb
new file mode 100755
index 00000000..0ca139c4
--- /dev/null
+++ b/data/plugins/entity/mob/following/following.rb
@@ -0,0 +1,58 @@
+on :message, :walk do |player, msg|
+ player.reset_interacting_mob
+end
+
+on :message, :player_action do |player, msg|
+ ## todo: need a better way of mapping option numbers to their purpose
+ player.follow($world.player_repository.get(msg.index)) if msg.option == 3
+end
+
+##
+# A MobExtension for making a Mob trail behind, or chase a given mob.
+module FollowingMobExtension
+ ##
+ # Follow a mob and trail behind them.
+ def follow(mob)
+ do_follow(self, mob, behind: true)
+ end
+
+ ##
+ # Chase a mob (with the intention of getting in front of them), also optionally
+ # stopping within a projectile distance when at a position where a projectile can
+ # reach the target.
+ def chase(mob, projectile_distance: nil)
+ do_follow(self, mob, front: true, projectile_distance: projectile_distance)
+ end
+end
+
+MobExtension::register(FollowingMobExtension)
+
+private
+
+def do_follow(source, mob, behind: false, front: false, projectile_distance: nil)
+ source.interacting_mob = mob
+
+ schedule 0, true do |task|
+ # stop the task unless we're still interacting with the other mob.
+ unless self.interacting_mob.eql? mob
+ task.stop
+ next
+ end
+
+ next unless source.walking_queue.size <= 1
+
+ distance = mob.position.get_distance(source.position)
+
+ unless projectile_distance.nil?
+ next if distance <= projectile_distance &&
+ $world.collision_manager.raycast(source.position, mob.position)
+ end
+
+ if distance > 15
+ reset_interacting_mob
+ end
+
+ walk_to(mob, behind: behind, front: front)
+ end
+end
+
diff --git a/data/plugins/entity/mob/following/plugin.xml b/data/plugins/entity/mob/following/plugin.xml
new file mode 100755
index 00000000..b4480d88
--- /dev/null
+++ b/data/plugins/entity/mob/following/plugin.xml
@@ -0,0 +1,16 @@
+
+
+ mob-following
+ 1
+ Following
+ Adds following for mobs.
+
+ Steve Soltys
+
+
+
+
+
+ mob-walk-to
+
+
diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java b/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java
index 8b4d8ccc..acb11e1c 100644
--- a/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java
+++ b/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java
@@ -1,5 +1,6 @@
package org.apollo.game.model.area.collision;
+import com.google.common.base.Preconditions;
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
import org.apollo.game.model.area.Region;
@@ -140,6 +141,91 @@ public final class CollisionManager {
}
}
+ /**
+ * Casts a ray into the world to check for impenetrable objects from the given {@code start} position to the
+ * {@code end} position using Bresenham's line algorithm.
+ *
+ * @param start The start position of the ray.
+ * @param end The end position of the ray.
+ * @return {@code true} if an impenetrable object was hit, {@code false} otherwise.
+ */
+ public boolean raycast(Position start, Position end) {
+ Preconditions.checkArgument(start.getHeight() == end.getHeight(), "Positions must be on the same height");
+
+ if (start.equals(end)) {
+ return true;
+ }
+
+ int x0 = start.getX();
+ int x1 = end.getX();
+ int y0 = start.getY();
+ int y1 = start.getY();
+
+ boolean steep = false;
+ if (Math.abs(x0 - x1) < Math.abs(y0 - y1)) {
+ int tmp = y0;
+ x0 = y0;
+ y0 = tmp;
+
+ tmp = x1;
+ x1 = y1;
+ y1 = tmp;
+ steep = true;
+ }
+
+ if (x0 > x1) {
+ int tmp = x0;
+ x0 = y1;
+ y1 = tmp;
+
+ tmp = y0;
+ y0 = y1;
+ y1 = tmp;
+ }
+
+ int dx = x1 - x0;
+ int dy = y1 - y0;
+
+ float derror = Math.abs(dy / (float) dx);
+ float error = 0;
+
+ int y = y0, currX, currY, lastX = 0, lastY = 0;
+ boolean first = true;
+
+ for (int x = x0; x <= x1; x++) {
+ if (steep) {
+ currX = y;
+ currY = x;
+ } else {
+ currX = x;
+ currY = y;
+ }
+
+ error += derror;
+ if (error > 0.5) {
+ y += (y1 > y0 ? 1 : -1);
+ error -= 1.0;
+ }
+
+ if (first) {
+ first = false;
+ continue;
+ }
+
+ Direction direction = Direction.fromDeltas(currX - lastX, currY - lastY);
+ Position lastPosition = new Position(lastX, lastY, start.getHeight());
+
+ if (!traversable(lastPosition, EntityType.PROJECTILE, direction)) {
+ return false;
+ }
+
+ lastX = currX;
+ lastY = currY;
+ }
+
+ return true;
+ }
+
/**
* Apply a {@link CollisionUpdate} flag to a {@link CollisionMatrix}.
*