Skip to content

Commit

Permalink
feat: RPG-120 better position (#111)
Browse files Browse the repository at this point in the history
* feat: something somehow working

* chore: new position iterator

* fix: isEmpty -> !isEmpty

* chore: Position to Direction

* chore: more tests

* feat: new position system

* chore: change bound type to Position

* fix: diagonal teleportation
  • Loading branch information
co012 authored Jun 12, 2022
1 parent 4235311 commit df28510
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 81 deletions.
31 changes: 9 additions & 22 deletions src/main/java/io/rpg/model/data/MapDirection.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,16 @@ public enum MapDirection {
WEST;

/**
* Returns MapDirection corresponding to given vector.
* Direction is calculated using javafx coordinate system.
* Returns vector representation of MapDirection in JavaFx coordinate system.
* @param v Vector with one coordinate equal to 0 and another equal to 1 or -1.
* @return MapDirection.
* @return Point2D
*/
public static MapDirection fromDirectionVector(Point2D v) {
v = v.normalize();

if (v.getX() == 0) {
if (v.getY() == -1) {
return NORTH;
} else if (v.getY() == 1) {
return SOUTH;
} else {
throw new IllegalArgumentException("Vector is equal to [0, 0]");
}
} else {
if (v.getX() == 1) {
return EAST;
} else {
return WEST;
}
}
public Point2D toVector() {
return switch (this) {
case EAST -> new Point2D(1, 0);
case WEST -> new Point2D(-1, 0);
case NORTH -> new Point2D(0, -1);
case SOUTH -> new Point2D(0, 1);
};
}
}
85 changes: 85 additions & 0 deletions src/main/java/io/rpg/model/data/Position.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package io.rpg.model.data;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import javafx.geometry.Point2D;
import org.jetbrains.annotations.NotNull;

/**
* Represents current position by holding row / col values.
* This class can NOT be record due to some issues with
* Gson library.
*/
public final class Position {
public static final Position ZERO = new Position(0, 0);
public final int row;
public final int col;

Expand All @@ -22,10 +28,47 @@ public Position(int row, int col) {
this.col = col;
}

public boolean isInside(Position lowerLeft, Position upperRight) {
return lowerLeft.col <= this.col && lowerLeft.row <= this.row
&& upperRight.col > this.col && upperRight.row > this.row;
}

public boolean isInside(Position upperLeft) {
return isInside(ZERO, upperLeft);
}

public Position(Point2D point2D) {
this((int) Math.round(point2D.getY()), (int) Math.round(point2D.getX()));
}

public Position subtract(@NotNull Position other) {
return new Position(row - other.row, col - other.col);
}

/**
* Calculates most fitting MapDirection with priority on NORTH, SOUTH directions.
* @return MapDirection.
*/
public MapDirection getDirection() {
if (this.equals(ZERO)) {
throw new IllegalArgumentException("Direction of ZERO is undefined");
}

if (Math.abs(col) > Math.abs(row)) {
if (col > 0) {
return MapDirection.EAST;
}
return MapDirection.WEST;
}

if (row > 0) {
return MapDirection.SOUTH;
}

return MapDirection.NORTH;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
Expand Down Expand Up @@ -93,4 +136,46 @@ public void forEachRemaining(Consumer<? super Position> action) {
}
}

public Iterator<Position> getBfsIter(Position upperBound) {
return new FourNeighborBfsIter(this, upperBound);
}

private static class FourNeighborBfsIter implements Iterator<Position> {
private final Set<Position> visited;
private final Queue<Position> queue;
private final Position upperBound;

public FourNeighborBfsIter(Position start, Position upperBound) {
this.upperBound = upperBound;
visited = new HashSet<>();
queue = new LinkedList<>();
queue.add(start);
visited.add(start);
}

@Override
public boolean hasNext() {
return !queue.isEmpty();
}

private void addToQueueIfValid(Position position) {
if (position.isInside(ZERO, upperBound) && !visited.contains(position)) {
queue.add(position);
visited.add(position);
}
}

@Override
public Position next() {
Position position = queue.poll();
assert position != null;
addToQueueIfValid(new Position(position.row + 1, position.col));
addToQueueIfValid(new Position(position.row, position.col + 1));
addToQueueIfValid(new Position(position.row, position.col - 1));
addToQueueIfValid(new Position(position.row - 1, position.col));

return position;
}
}

}
133 changes: 74 additions & 59 deletions src/main/java/io/rpg/model/location/LocationModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import io.rpg.model.data.MapDirection;
import io.rpg.model.data.Position;
import io.rpg.model.object.GameObject;

import java.util.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import org.apache.logging.log4j.LogManager;
Expand All @@ -25,9 +27,9 @@ public class LocationModel extends BaseActionEmitter {
private final HashMap<GameObject, ChangeListener<Point2D>> positionListeners;
private final HashMap<Position, GameObject> positionGameObjectMap;
private HashMap<MapDirection, String> directionToLocationMap;
public Point2D bounds;

public Position bounds;

@SuppressWarnings("unused")
public LocationModel(@NotNull String tag, @NotNull List<GameObject> gameObjects) {
this();
this.tag = tag;
Expand Down Expand Up @@ -57,15 +59,14 @@ public Optional<GameObject> getObject(Position position) {
*/
private void setGameObjects(List<GameObject> gameObjects) {
this.gameObjects = gameObjects;
gameObjects.forEach(this::placeGameObject);
gameObjects.forEach(this::registerGameObject);
gameObjects.forEach(g -> checkAndCorrectBoundsCrossing(g, g.getExactPosition(), false));
gameObjects.forEach(g -> positionGameObjectMap.put(g.getPosition(), g));
}

public void addGameObject(GameObject gameObject) {
gameObjects.add(gameObject);
checkAndCorrectBoundsCrossing(gameObject, gameObject.getExactPosition(), false);
positionGameObjectMap.put(gameObject.getPosition(), gameObject);
placeGameObject(gameObject);
registerGameObject(gameObject);

}
Expand All @@ -78,9 +79,33 @@ private void registerGameObject(GameObject gameObject) {
positionListeners.put(gameObject, positionListener);
}

private void placeGameObject(GameObject gameObject) {
Point2D boundPos = getBoundPosition(gameObject.getExactPosition());
Position position = new Position(boundPos);
if (!positionGameObjectMap.containsKey(position)) {
gameObject.setExactPosition(boundPos);
positionGameObjectMap.put(position, gameObject);
return;
}

Iterator<Position> it = position.getBfsIter(bounds);
while (it.hasNext()) {
Position p = it.next();
if (positionGameObjectMap.containsKey(p)) {
continue;
}

gameObject.setPosition(p);
positionGameObjectMap.put(p, gameObject);
return;
}

throw new RuntimeException("No place to put gameObject" + gameObject);
}

public void removeGameObject(GameObject gameObject) {
gameObjects.remove(gameObject);
positionGameObjectMap.remove(gameObject.getPosition());
positionGameObjectMap.values().remove(gameObject);
unRegisterGameObject(gameObject);
}

Expand All @@ -91,40 +116,66 @@ private void unRegisterGameObject(GameObject gameObject) {
}

private void onGameObjectPositionChange(GameObject gameObject, Point2D oldPosition, Point2D newPosition) {
boolean changeOccurred = checkAndCorrectBoundsCrossing(gameObject, newPosition, true);
Position newPos = new Position(newPosition);
Position oldPos = new Position(oldPosition);

if (changeOccurred) {
if (oldPos.equals(newPos)) {
return;
}

Position newPos = new Position(newPosition);
Position oldPos = new Position(oldPosition);
if (newPos.equals(oldPos)) {
// When previous position was not accepted
if (gameObject.equals(positionGameObjectMap.get(newPos))) {
return;
}

// Collision check
if (positionGameObjectMap.containsKey(newPos) && !positionGameObjectMap.get(newPos)
.equals(gameObject)) {
if (positionGameObjectMap.containsKey(newPos)) {
gameObject.setExactPosition(oldPosition);
return;
}

if (gameObject.equals(positionGameObjectMap.get(oldPos))) {
changeField(gameObject, oldPos, newPos);
if (!newPos.isInside(bounds)) {
Position delta = newPos.subtract(oldPos);
// Don't try to teleport diagonally
if (delta.row * delta.col == 0) {
boolean hasBeenTeleported = tryToTeleport(gameObject, delta);
if (hasBeenTeleported) {
return;
}
}

gameObject.setExactPosition(oldPosition);
return;
}

changeField(gameObject, oldPos, newPos);
notifyApproachOf(gameObject);
}

private boolean tryToTeleport(GameObject gameObject, Position crossingDirection) {
MapDirection teleportDirection = crossingDirection.getDirection();
String nextLocation = directionToLocationMap.get(teleportDirection);
if (nextLocation == null) {
return false;
}

Point2D currentPosition = gameObject.getExactPosition();
Point2D nextPosition = currentPosition.subtract(teleportDirection.toVector().multiply(20));
LocationChangeAction action = new LocationChangeAction(nextLocation, nextPosition, null);
emitAction(action);
return true;
}

private void notifyApproachOf(GameObject gameObject) {
Position position = gameObject.getPosition();
List<GameObject> neighbors = new ArrayList<>(8);

for (Iterator<Position> it = position.getNeighborhoodIter(new Position(bounds)); it.hasNext(); ) {
for (Iterator<Position> it = position.getNeighborhoodIter(bounds); it.hasNext(); ) {
Position p = it.next();
GameObject neighbor = positionGameObjectMap.get(p);
if (neighbor == null) continue;
if (neighbor == null) {
continue;
}

neighbors.add(neighbor);
}
Expand All @@ -137,46 +188,10 @@ private void changeField(GameObject gameObject, Position oldPos, Position newPos
positionGameObjectMap.put(newPos, gameObject);
}


private boolean checkAndCorrectBoundsCrossing(GameObject gameObject, Point2D newPosition, boolean emitAction) {
Point2D boundPosition = getBoundPosition(newPosition);
if (boundPosition.equals(newPosition)) {
return false;
}

gameObject.setExactPosition(boundPosition);
Point2D boundsCrossedDirection = newPosition.subtract(boundPosition)
.normalize();

if (emitAction) {
emitBoundCrossedAction(gameObject, boundsCrossedDirection);
}

return true;
}

private void emitBoundCrossedAction(GameObject gameObject, Point2D boundsCrossedDirection) {
// guard against non cardinal directions vector
if ((boundsCrossedDirection.angle(1, 0) % 90) != 0) {
return;
}
MapDirection direction = MapDirection.fromDirectionVector(boundsCrossedDirection);
Point2D position = gameObject.getExactPosition();
Point2D nextPosition = position.subtract(boundsCrossedDirection.multiply(20));
if (!directionToLocationMap.containsKey(direction)) {
return;
}

String location = directionToLocationMap.get(direction);
LocationChangeAction action = new LocationChangeAction(location, nextPosition, null);
emitAction(action);
}


private Point2D getBoundPosition(Point2D pos) {
double offset = 0.3; // it should be less than 0.5
double x = Math.max(-offset, Math.min(bounds.getX() - 1 + offset, pos.getX()));
double y = Math.max(-offset, Math.min(bounds.getY() - 1 + offset, pos.getY()));
double x = Math.max(-offset, Math.min(bounds.col - 1 + offset, pos.getX()));
double y = Math.max(-offset, Math.min(bounds.row - 1 + offset, pos.getY()));
return new Point2D(x, y);
}

Expand Down Expand Up @@ -209,7 +224,7 @@ public Builder setGameObjects(@NotNull List<GameObject> gameObjects) {
}

public Builder setBounds(Point2D bounds) {
locationModel.bounds = bounds;
locationModel.bounds = new Position(bounds);
return this;
}

Expand Down
Loading

0 comments on commit df28510

Please sign in to comment.