diff --git a/api/src/main/java/com/bartlomiejpluta/base/lib/ai/FollowObjectAI.java b/api/src/main/java/com/bartlomiejpluta/base/lib/ai/FollowObjectAI.java index 44a0e92a..e7424be6 100644 --- a/api/src/main/java/com/bartlomiejpluta/base/lib/ai/FollowObjectAI.java +++ b/api/src/main/java/com/bartlomiejpluta/base/lib/ai/FollowObjectAI.java @@ -2,7 +2,7 @@ package com.bartlomiejpluta.base.lib.ai; import com.bartlomiejpluta.base.api.ai.AI; import com.bartlomiejpluta.base.api.ai.NPC; -import com.bartlomiejpluta.base.api.location.Locationable; +import com.bartlomiejpluta.base.api.entity.Entity; import com.bartlomiejpluta.base.api.map.layer.object.ObjectLayer; import com.bartlomiejpluta.base.api.move.MoveEvent; import com.bartlomiejpluta.base.util.path.MovementPath; @@ -10,8 +10,10 @@ import com.bartlomiejpluta.base.util.path.PathExecutor; import com.bartlomiejpluta.base.util.pathfinder.PathFinder; import lombok.NonNull; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; -public abstract class FollowObjectAI implements AI { +@Slf4j +public abstract class FollowObjectAI implements AI { private final PathFinder finder; private final PathExecutor executor; @@ -22,55 +24,88 @@ public abstract class FollowObjectAI impl private MovementPath path = null; + private boolean sees = false; + protected FollowObjectAI(@NonNull PathFinder finder, @NonNull N npc, @NonNull T target) { this.finder = finder; this.npc = npc; - this.target = target; this.executor = new PathExecutor<>(npc); + + setTarget(target); + + npc.addEventListener(MoveEvent.TYPE, this::onMove); } - public void recomputePath() { - path = null; - } + private void onMove(MoveEvent event) { + if (npc.getStrategy() != this) { + return; + } - public void recomputePath(@NonNull MoveEvent event) { var movable = event.getMovable(); - // Refresh only when target has been displaced + if (movable == npc) { + this.sees = sees(npc, target); + return; + } + + // Recalculate path only when target has been displaced // or another object is blocking current path - if (movable == target || (path != null && path.contains(movable))) { - path = null; + if (movable == target) { + this.path = null; + this.sees = sees(npc, target); + return; + } + + // Recalculate path when another object is blocking current path + if (path != null && path.contains(movable)) { + this.path = null; } } @Override public void nextActivity(ObjectLayer layer, float dt) { - if (!npc.isMoving()) { - var distance = npc.manhattanDistance(target); - - if (distance == 1) { - npc.setFaceDirection(npc.getDirectionTowards(target)); - interact(npc, target, layer, dt); - } else if (sees(npc, target, layer, distance)) { - follow(npc, target, layer, dt); - - if (path == null || path.isEmpty()) { - path = finder.findPath(layer, npc, target.getCoordinates()); - executor.setPath(path); - } - - executor.execute(layer, dt); - } else { - idle(npc, target, layer, dt); - } + if (npc.isMoving()) { + return; } + + var distance = npc.manhattanDistance(target); + + if (distance == 1) { + npc.setFaceDirection(npc.getDirectionTowards(target)); + interact(npc, target, layer, dt); + return; + } + + if (sees) { + follow(npc, target, layer, dt); + + // Calculate path + if (path == null || path.isEmpty()) { + path = finder.findPath(layer, npc, target.getCoordinates()); + executor.setPath(path); + } + + executor.execute(layer, dt); + return; + } + + + idle(npc, target, layer, dt); } - protected abstract boolean sees(N npc, T target, ObjectLayer layer, int distance); - protected abstract void interact(N npc, T target, ObjectLayer layer, float dt); + protected abstract boolean sees(N npc, T target); - protected abstract void follow(N npc, T target, ObjectLayer layer, float dt); + protected void interact(N npc, T target, ObjectLayer layer, float dt) { - protected abstract void idle(N npc, T target, ObjectLayer layer, float dt); + } + + protected void follow(N npc, T target, ObjectLayer layer, float dt) { + + } + + protected void idle(N npc, T target, ObjectLayer layer, float dt) { + + } } + diff --git a/api/src/main/java/com/bartlomiejpluta/base/util/math/BresenhamLine.java b/api/src/main/java/com/bartlomiejpluta/base/util/math/BresenhamLine.java new file mode 100644 index 00000000..c45a4bd1 --- /dev/null +++ b/api/src/main/java/com/bartlomiejpluta/base/util/math/BresenhamLine.java @@ -0,0 +1,67 @@ +package com.bartlomiejpluta.base.util.math; + +import org.joml.Vector2i; +import org.joml.Vector2ic; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; + +public class BresenhamLine { + public static List bresenhamLine(Vector2ic start, Vector2ic end) { + return bresenhamLine(start.x(), start.y(), end.x(), end.y()); + } + + public static List bresenhamLine(int startX, int startY, int endX, int endY) { + var list = new LinkedList(); + + bresenhamLine(startX, startY, endX, endY, v -> { + list.add(v); + return null; + }); + + return list; + } + + public static T bresenhamLine(Vector2ic start, Vector2ic end, Function consumer) { + return bresenhamLine(start.x(), start.y(), end.x(), end.y(), consumer, null); + } + + public static T bresenhamLine(int startX, int startY, int endX, int endY, Function consumer) { + return bresenhamLine(startX, startY, endX, endY, consumer, null); + } + + public static T bresenhamLine(Vector2ic start, Vector2ic end, Function consumer, T defaultValue) { + return bresenhamLine(start.x(), start.y(), end.x(), end.y(), consumer, defaultValue); + } + + public static T bresenhamLine(int startX, int startY, int endX, int endY, Function consumer, T defaultValue) { + int dx = Math.abs(endX - startX), dy = Math.abs(endY - startY); + int sx = startX < endX ? 1 : -1, sy = startY < endY ? 1 : -1; + int err = dx - dy; + + while (true) { + var result = consumer.apply(new Vector2i(startX, startY)); + + if (result != null) { + return result; + } + + if (startX == endX && startY == endY) break; + + int e2 = err << 1; + + if (e2 > -dy) { + err -= dy; + startX += sx; + } + + if (e2 < dx) { + err += dx; + startY += sy; + } + } + + return defaultValue; + } +} diff --git a/api/src/main/java/com/bartlomiejpluta/base/util/visibility/VisibilityChecker.java b/api/src/main/java/com/bartlomiejpluta/base/util/visibility/VisibilityChecker.java new file mode 100644 index 00000000..cc80961e --- /dev/null +++ b/api/src/main/java/com/bartlomiejpluta/base/util/visibility/VisibilityChecker.java @@ -0,0 +1,96 @@ +package com.bartlomiejpluta.base.util.visibility; + + +import com.bartlomiejpluta.base.api.character.Character; +import org.joml.Vector2ic; + +import static com.bartlomiejpluta.base.util.math.BresenhamLine.bresenhamLine; + +public class VisibilityChecker { + final int[][] DIRS = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}}; // UP, LEFT, DOWN, RIGHT + + + public static boolean isInCone(Character observer, Vector2ic target, float angle, int maxDistance) { + var origin = observer.getCoordinates(); + var direction = observer.getFaceDirection().vector; + return isInCone(origin.x(), origin.y(), direction.x(), direction.y(), target.x(), target.y(), angle, maxDistance); + } + + /** + * Checks if a point (x, y) lies within a vision cone defined by an origin (x0, y0), + * a direction vector (dx, dy), a cone angle, and a maximum distance. + * + * @param x0 The x-coordinate of the origin point. + * @param y0 The y-coordinate of the origin point. + * @param dx The x-component of the direction vector (should be normalized or non-zero). + * @param dy The y-component of the direction vector (should be normalized or non-zero). + * @param x The x-coordinate of the point to check. + * @param y The y-coordinate of the point to check. + * @param angle The full angle of the cone in radians. + * @param maxDistance The maximum distance from the origin to consider (points beyond are outside the cone). + * @return true if the point is within the cone and range, false otherwise. + */ + public static boolean isInCone(int x0, int y0, int dx, int dy, int x, int y, float angle, int maxDistance) { + int vx = x - x0; + int vy = y - y0; + int lenSq = vx * vx + vy * vy; + if (lenSq == 0) return true; // point is exactly at origin + if (maxDistance > 0 && lenSq > maxDistance * maxDistance) return false; // outside max distance + + // Compute dot product between direction and vector to point + int dot = dx * vx + dy * vy; + + // Precompute cosine of half the cone angle + float halfAngle = angle * 0.5f; + float cosHalfAngle = (float) Math.cos(halfAngle); + + // Normalize dot by length of vector to point (len) + float len = (float) Math.sqrt(lenSq); + float cosTheta = dot / len; + + // Check if the point is within the cone + return cosTheta >= cosHalfAngle; + } + + public static boolean canSee(Character observer, Character target, float angle, int range, boolean direction) { + var layer = observer.getLayer(); + + // Observer and target are on different layers + if (layer != target.getLayer()) { + return false; + } + + // Target is out of observer's visibility area + if (observer.chebyshevDistance(target) > range) { + return false; + } + + // Observer does not look at the target's direction + var targetCoords = target.getCoordinates(); + if (direction && !isInCone(observer, targetCoords, angle, range)) { + return false; + } + + var observerCoords = observer.getCoordinates(); + + // Checking line of sight + return bresenhamLine(observer.getCoordinates(), target.getCoordinates(), v -> { + // Don't check observer coords + if (observerCoords.equals(v)) { + return null; + } + + // Target is visible + if (targetCoords.equals(v)) { + return true; + } + + // Obstacle on the sight line + if (!layer.isTileReachable(v)) { + return false; + } + + return null; + }, false); + } +}