From e5a6638e2fc0bc4f524906639c42893998c4e594 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Mon, 2 Jan 2017 03:58:58 +0000 Subject: [PATCH] Add support for player / npc following Implements a new MobExtension plugin which adds 'follow', and 'chase' mixins that allow the mob to follow behind another mob, and chase them while keeping at a safe distance to fire projectiles. Also adds a new public method 'raycast' to CollisionManager, for drawing a line through the world using bresenham's line algorithm whille checking for any impenetrable objects. --- .../plugins/entity/mob/following/following.rb | 58 +++++++++++++ data/plugins/entity/mob/following/plugin.xml | 16 ++++ .../area/collision/CollisionManager.java | 86 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100755 data/plugins/entity/mob/following/following.rb create mode 100755 data/plugins/entity/mob/following/plugin.xml 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}. *