diff --git a/src/main/java/io/luna/game/event/impl/WalkingEvent.java b/src/main/java/io/luna/game/event/impl/WalkingEvent.java index 170683a9..8e8dda0b 100644 --- a/src/main/java/io/luna/game/event/impl/WalkingEvent.java +++ b/src/main/java/io/luna/game/event/impl/WalkingEvent.java @@ -1,7 +1,7 @@ package io.luna.game.event.impl; import io.luna.game.model.mob.Player; -import io.luna.game.model.mob.WalkingQueue.Step; +import io.luna.math.Vector2; import java.util.Deque; @@ -15,7 +15,7 @@ public final class WalkingEvent extends PlayerEvent implements ControllableEvent /** * The walking path. */ - private final Deque path; + private final Deque path; /** * If the player is running. @@ -41,7 +41,7 @@ public final class WalkingEvent extends PlayerEvent implements ControllableEvent * @param pathSize The path size. * @param opcode The opcode. */ - public WalkingEvent(Player player, Deque path, boolean running, int pathSize, int opcode) { + public WalkingEvent(Player player, Deque path, boolean running, int pathSize, int opcode) { super(player); this.path = path; this.running = running; @@ -52,7 +52,7 @@ public WalkingEvent(Player player, Deque path, boolean running, int pathSi /** * @return The walking path. */ - public Deque getPath() { + public Deque getPath() { return path; } diff --git a/src/main/java/io/luna/game/model/Direction.java b/src/main/java/io/luna/game/model/Direction.java index 405765e9..0f24c468 100644 --- a/src/main/java/io/luna/game/model/Direction.java +++ b/src/main/java/io/luna/game/model/Direction.java @@ -1,7 +1,7 @@ package io.luna.game.model; import com.google.common.collect.ImmutableList; -import io.luna.game.model.mob.WalkingQueue.Step; +import io.luna.math.Vector2; import java.util.Set; @@ -14,15 +14,15 @@ * @author Graham */ public enum Direction { - NONE(-1, new Step(0, 0)), - NORTH_WEST(0, new Step(-1, 1)), - NORTH(1, new Step(0, 1)), - NORTH_EAST(2, new Step(1, 1)), - WEST(3, new Step(-1, 0)), - EAST(4, new Step(1, 0)), - SOUTH_WEST(5, new Step(-1, -1)), - SOUTH(6, new Step(0, -1)), - SOUTH_EAST(7, new Step(1, -1)); + NONE(-1, new Vector2(0, 0)), + NORTH_WEST(0, new Vector2(-1, 1)), + NORTH(1, new Vector2(0, 1)), + NORTH_EAST(2, new Vector2(1, 1)), + WEST(3, new Vector2(-1, 0)), + EAST(4, new Vector2(1, 0)), + SOUTH_WEST(5, new Vector2(-1, -1)), + SOUTH(6, new Vector2(0, -1)), + SOUTH_EAST(7, new Vector2(1, -1)); // todo cleanup, documentation /** * A list of directions representing all possible directions of the NPC view cone, in order. @@ -58,19 +58,19 @@ public enum Direction { * The direction identifier. */ private final int id; - private final Step translate; + private final Vector2 translate; /** * Creates a new {@link Direction}. * * @param id The direction identifier. */ - Direction(int id, Step translate) { + Direction(int id, Vector2 translate) { this.id = id; this.translate = translate; } - public Step getTranslation() { + public Vector2 getTranslation() { return translate; } @@ -141,11 +141,13 @@ public Direction opposite() { * * @param currentX The current x coordinate. * @param currentY The current y coordinate. - * @param nextX The next x coordinate. - * @param nextY The next y coordinate. + * @param nextX The next x coordinate. + * @param nextY The next y coordinate. * @return The direction between the current and next coordinates. + * @deprecated Use {@link Vector2#directionTowards(Vector2)}. */ @SuppressWarnings("Duplicates") + @Deprecated public static Direction between(int currentX, int currentY, int nextX, int nextY) { int deltaX = Integer.signum(nextX - currentX); int deltaY = Integer.signum(nextY - currentY); @@ -184,8 +186,11 @@ public static Direction between(int currentX, int currentY, int nextX, int nextY * @param current The current step. * @param next The next step. * @return The direction between the current and next steps. + * + * @deprecated Use {@link Vector2#directionTowards(Vector2)}. */ - public static Direction between(Step current, Step next) { + @Deprecated + public static Direction between(Vector2 current, Vector2 next) { return between(current.getX(), current.getY(), next.getX(), next.getY()); } diff --git a/src/main/java/io/luna/game/model/Position.java b/src/main/java/io/luna/game/model/Position.java index 3c84d697..96419c72 100644 --- a/src/main/java/io/luna/game/model/Position.java +++ b/src/main/java/io/luna/game/model/Position.java @@ -3,7 +3,7 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.Range; import io.luna.game.model.chunk.Chunk; -import io.luna.game.model.mob.WalkingQueue.Step; +import io.luna.math.Vector2; import java.util.Comparator; import java.util.Objects; @@ -222,7 +222,7 @@ public Position translate(int amountX, int amountY) { * @return The translated position. */ public Position translate(int amount, Direction direction) { - Step translation = direction.getTranslation(); + Vector2 translation = direction.getTranslation(); return translate(amount * translation.getX(), amount * translation.getY(), 0); } diff --git a/src/main/java/io/luna/game/model/mob/WalkingQueue.java b/src/main/java/io/luna/game/model/mob/WalkingQueue.java index 2b853dac..47d7cae5 100644 --- a/src/main/java/io/luna/game/model/mob/WalkingQueue.java +++ b/src/main/java/io/luna/game/model/mob/WalkingQueue.java @@ -1,6 +1,5 @@ package io.luna.game.model.mob; -import com.google.common.base.MoreObjects; import com.google.common.util.concurrent.ListenableFuture; import io.luna.game.model.Direction; import io.luna.game.model.Entity; @@ -12,6 +11,7 @@ import io.luna.game.model.path.EuclideanHeuristic; import io.luna.game.model.path.PathfindingAlgorithm; import io.luna.game.model.path.SimplePathfindingAlgorithm; +import io.luna.math.Vector2; import io.luna.util.RandomUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -31,90 +31,15 @@ public final class WalkingQueue { */ private static final Logger logger = LogManager.getLogger(); - /** - * A model representing a step in the walking queue. - */ - public static final class Step { - - /** - * The x coordinate. - */ - private final int x; - - /** - * The y coordinate. - */ - private final int y; - - /** - * Creates a new {@link Step}. - * - * @param x The x coordinate. - * @param y The y coordinate. - */ - public Step(int x, int y) { - this.x = x; - this.y = y; - } - - /** - * Creates a new {@link Step}. - * - * @param position The position. - */ - public Step(Position position) { - this(position.getX(), position.getY()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof Step) { - Step other = (Step) obj; - return x == other.x && y == other.y; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(x, y); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("x", x) - .add("y", y) - .toString(); - } - - /** - * @return The x coordinate. - */ - public int getX() { - return x; - } - - /** - * @return The y coordinate. - */ - public int getY() { - return y; - } - } - /** * A deque of current steps. */ - private final Deque current = new ArrayDeque<>(); + private final Deque current = new ArrayDeque<>(); /** * A deque of previous steps. */ - private final Deque previous = new ArrayDeque<>(); + private final Deque previous = new ArrayDeque<>(); /** * The collision manager. @@ -182,13 +107,13 @@ public void process() { if(mob instanceof Npc && mob.asNpc().isStationary()) { return; } - Step currentStep = new Step(mob.getPosition()); + Vector2 currentStep = new Vector2(mob.getPosition()); Direction walkingDirection = Direction.NONE; Direction runningDirection = Direction.NONE; boolean restoreEnergy = true; - Step nextStep = current.poll(); + Vector2 nextStep = current.poll(); if (nextStep != null) { walkingDirection = Direction.between(currentStep, nextStep); boolean blocked = false;// !collisionManager.traversable(mob.getPosition(), EntityType.NPC, walkingDirection); @@ -254,7 +179,7 @@ public void walk(Position destination) { * @param target The destination target. */ public void walkUntilReached(Entity target) { - Deque newPath = new ArrayDeque<>(); + Deque newPath = new ArrayDeque<>(); Deque path = pathfindingAlgorithm.find(mob.getPosition(), target.getPosition()); Position lastPosition = mob.getPosition(); for (; ; ) { @@ -264,7 +189,7 @@ public void walkUntilReached(Entity target) { if (nextPosition == null || reached) { break; } - newPath.add(new Step(nextPosition)); + newPath.add(new Vector2(nextPosition)); lastPosition = nextPosition; } addPath(newPath); @@ -280,7 +205,7 @@ public void lazyWalk(Position destination) { logger.warn("Existing lazy walk request in progress."); return; } - ListenableFuture> pathResult = mob.getService().submit(() -> findPath(destination)); + ListenableFuture> pathResult = mob.getService().submit(() -> findPath(destination)); pathResult.addListener(() -> { try { addPath(pathResult.get()); @@ -360,14 +285,14 @@ public void walkBehind(Mob target) { * * @param path The path to walk. */ - public void addPath(Deque path) { + public void addPath(Deque path) { int size = path.size(); if (size == 1) { addFirst(path.poll()); } else if (size > 1) { addFirst(path.poll()); for (; ; ) { - Step nextStep = path.poll(); + Vector2 nextStep = path.poll(); if (nextStep == null) { break; } @@ -389,11 +314,11 @@ public void clear() { * * @param step The step to add. */ - private void addFirst(Step step) { + private void addFirst(Vector2 step) { current.clear(); - Queue backtrack = new ArrayDeque<>(); + Deque backtrack = new ArrayDeque<>(); for (; ; ) { - Step prev = previous.pollLast(); + Vector2 prev = previous.pollLast(); if (prev == null) { break; } @@ -413,10 +338,10 @@ private void addFirst(Step step) { * * @param next The step to add. */ - private void add(Step next) { - Step last = current.peekLast(); + void add(Vector2 next) { + Vector2 last = current.peekLast(); if (last == null) { - last = new Step(mob.getPosition()); + last = new Vector2(mob.getPosition()); } int nextX = next.getX(); @@ -438,7 +363,7 @@ private void add(Step next) { } else if (deltaY > 0) { deltaY--; } - current.add(new Step(nextX - deltaX, nextY - deltaY)); + current.add(new Vector2(nextX - deltaX, nextY - deltaY)); } } @@ -496,16 +421,16 @@ private void incrementRunEnergy() { * @param target The target. * @return The path. */ - private Deque findPath(Position target) { + private Deque findPath(Position target) { Deque positionPath = pathfindingAlgorithm.find(mob.getPosition(), target); - Deque stepPath = new ArrayDeque<>(positionPath.size()); + Deque stepPath = new ArrayDeque<>(positionPath.size()); for (; ; ) { // TODO remove step class? Position next = positionPath.poll(); if (next == null) { break; } - stepPath.add(new Step(next)); + stepPath.add(new Vector2(next)); } return stepPath; } diff --git a/src/main/java/io/luna/math/Vector2.java b/src/main/java/io/luna/math/Vector2.java new file mode 100644 index 00000000..f7fd3599 --- /dev/null +++ b/src/main/java/io/luna/math/Vector2.java @@ -0,0 +1,139 @@ +package io.luna.math; + +import com.google.common.base.MoreObjects; +import io.luna.game.model.Position; + +import java.util.Objects; + +/** + * A vector with 2 axis (x,y). + * + * @author lare96 + * @author notjuanortiz + */ +public final class Vector2 { + + /** + * The x coordinate. + */ + private int x; + + /** + * The y coordinate. + */ + private int y; + + /** + * Creates a new {@link Vector2}. + * + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Vector2(int x, int y) { + this.x = x; + this.y = y; + } + + /** + * Creates a new {@link Vector2}. + * + * @param position The position. + */ + public Vector2(Position position) { + this(position.getX(), position.getY()); + } + + public Vector2 add(Vector2 other) { + x += other.x; + y += other.y; + return this; + } + + public Vector2 subtract(Vector2 other) { + x -= other.x; + y -= other.y; + return this; + } + + /** + * Returns a new vector containing the maximum components of this vector and another vector. + * + * @param other The other vector to compare against. + * @return A new vector where each component is the maximum of the corresponding components. + */ + public Vector2 max(Vector2 other) { + x = Math.max(x, other.x); + y = Math.max(y, other.y); + return this; + } + + /** + * Returns a new vector containing the minimum components of this vector and another vector. + * + * @param other The other vector to compare against. + * @return A new vector where each component is the minimum of the corresponding components. + */ + public Vector2 min(Vector2 other) { + x = Math.min(x, other.x); + y = Math.min(y, other.y); + return this; + } + + /** + * Converts this vector into a unit vector with component values of -1, 0, or 1. + *

+ * This normalization function is optimized for a grid-based system. + *

+ */ + public Vector2 normalize() { + x = Integer.signum(x); + y = Integer.signum(y); + return this; + } + + /** + * Returns a unit vector representing the direction from this vector to another vector. + */ + public Vector2 directionTowards(Vector2 other) { + return other.subtract(this).normalize(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Vector2) { + Vector2 other = (Vector2) obj; + return x == other.x && y == other.y; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("x", x) + .add("y", y) + .toString(); + } + + /** + * @return The x coordinate. + */ + public int getX() { + return x; + } + + /** + * @return The y coordinate. + */ + public int getY() { + return y; + } +} diff --git a/src/main/java/io/luna/net/msg/in/WalkingMessageReader.java b/src/main/java/io/luna/net/msg/in/WalkingMessageReader.java index 6ca8db35..f325b75d 100644 --- a/src/main/java/io/luna/net/msg/in/WalkingMessageReader.java +++ b/src/main/java/io/luna/net/msg/in/WalkingMessageReader.java @@ -3,7 +3,7 @@ import io.luna.game.event.impl.WalkingEvent; import io.luna.game.model.mob.Player; import io.luna.game.model.mob.WalkingQueue; -import io.luna.game.model.mob.WalkingQueue.Step; +import io.luna.math.Vector2; import io.luna.net.codec.ByteMessage; import io.luna.net.codec.ByteOrder; import io.luna.net.codec.ValueType; @@ -44,10 +44,10 @@ public WalkingEvent decode(Player player, GameMessage msg) { path[i][1] = payload.get(); } - Deque steps = new ArrayDeque<>(pathSize + 1); - steps.add(new Step(firstStepX, firstStepY)); + Deque steps = new ArrayDeque<>(pathSize + 1); + steps.add(new Vector2(firstStepX, firstStepY)); for (int i = 0; i < pathSize; i++) { - steps.add(new Step(path[i][0] + firstStepX, path[i][1] + firstStepY)); + steps.add(new Vector2(path[i][0] + firstStepX, path[i][1] + firstStepY)); } return new WalkingEvent(player, steps, running, pathSize, opcode); } diff --git a/src/test/java/io/luna/math/Vector2Test.java b/src/test/java/io/luna/math/Vector2Test.java new file mode 100644 index 00000000..f43d61f8 --- /dev/null +++ b/src/test/java/io/luna/math/Vector2Test.java @@ -0,0 +1,120 @@ +package io.luna.math; + + +import io.luna.game.model.Position; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class Vector2Test { + + @Test + void testConstruction() { + // Test basic construction + Vector2 vec = new Vector2(3, 4); + assertEquals(3, vec.getX()); + assertEquals(4, vec.getY()); + + // Test construction from Position + Position pos = new Position(5, 6); + Vector2 fromPos = new Vector2(pos); + assertEquals(5, fromPos.getX()); + assertEquals(6, fromPos.getY()); + } + + @Test + void testAdd() { + Vector2 vec1 = new Vector2(2, 3); + Vector2 vec2 = new Vector2(4, 5); + Vector2 result = vec1.add(vec2); + + assertEquals(6, result.getX()); // 2 + 4 + assertEquals(8, result.getY()); // 3 + 5 + } + + @Test + void testSubtract() { + Vector2 vec1 = new Vector2(10, 8); + Vector2 vec2 = new Vector2(3, 5); + Vector2 result = vec1.subtract(vec2); + + assertEquals(7, result.getX()); // 10 - 3 + assertEquals(3, result.getY()); // 8 - 5 + } + + + @Test + void testMax() { + Vector2 vec1 = new Vector2(2, 5); + Vector2 vec2 = new Vector2(4, 3); + Vector2 result = vec2.max(vec1); + + assertEquals(4, result.getX()); // 4 > 2 + assertEquals(5, result.getY()); // 5 > 3 + } + + @Test + void testMin() { + Vector2 vec1 = new Vector2(8, 5); + Vector2 vec2 = new Vector2(3, 7); + Vector2 result = vec1.min(vec2); + + assertEquals(3, result.getX()); // 3 < 8 + assertEquals(5, result.getY()); // 5 < 7 + } + + @Test + void testNormalize() { + // Test with non-zero vector + Vector2 vec = new Vector2(5, -200); + Vector2 normalized = vec.normalize(); + + // Both values are positive, therefore the normalized vector should be (1, -1), or facing south-east + assertEquals(1, normalized.getX()); + assertEquals(-1, normalized.getY()); + + // Test with zero vector + Vector2 zero = new Vector2(0, 0); + Vector2 zeroNormalized = zero.normalize(); + assertEquals(0, zeroNormalized.getX()); + assertEquals(0, zeroNormalized.getY()); + } + + @Test + void testDirectionTowards() { + Vector2 vec1 = new Vector2(4, 2); + Vector2 vec2 = new Vector2(1, 6); + Vector2 distance = vec1.directionTowards(vec2); + + // The distance vector is subtracted, then normalized. + // The subtraction vector should be is (-3, 4) + // Normalized to (-1, 1) which is pointing north-west. + assertEquals(-1, distance.getX()); // 1 - 4 = -3, norm(-3) = -1 + assertEquals(1, distance.getY()); // 6 - 2 = 4, norm(4) = 1 + } + + @Test + void testEqualsAndHashCode() { + Vector2 vec1 = new Vector2(1, 2); + Vector2 vec2 = new Vector2(1, 2); + Vector2 vec3 = new Vector2(3, 4); + + // Test equality + assertEquals(vec1, vec2); + assertNotEquals(vec1, vec3); + + // Test hash code consistency + assertEquals(vec1.hashCode(), vec2.hashCode()); + assertNotEquals(vec1.hashCode(), vec3.hashCode()); + } + + @Test + void testToString() { + Vector2 vec = new Vector2(5, 10); + String str = vec.toString(); + + assertTrue(str.contains("x=5")); + assertTrue(str.contains("y=10")); + } +} +