diff --git a/forge-gui-android/libs/arm64-v8a/libgdx-box2d.so b/forge-gui-android/libs/arm64-v8a/libgdx-box2d.so
new file mode 100644
index 00000000000..e562acc0b86
Binary files /dev/null and b/forge-gui-android/libs/arm64-v8a/libgdx-box2d.so differ
diff --git a/forge-gui-android/libs/armeabi-v7a/libgdx-box2d.so b/forge-gui-android/libs/armeabi-v7a/libgdx-box2d.so
new file mode 100644
index 00000000000..eef089879ed
Binary files /dev/null and b/forge-gui-android/libs/armeabi-v7a/libgdx-box2d.so differ
diff --git a/forge-gui-android/libs/x86/libgdx-box2d.so b/forge-gui-android/libs/x86/libgdx-box2d.so
new file mode 100644
index 00000000000..df1fef60837
Binary files /dev/null and b/forge-gui-android/libs/x86/libgdx-box2d.so differ
diff --git a/forge-gui-android/libs/x86_64/libgdx-box2d.so b/forge-gui-android/libs/x86_64/libgdx-box2d.so
new file mode 100644
index 00000000000..852d26455f4
Binary files /dev/null and b/forge-gui-android/libs/x86_64/libgdx-box2d.so differ
diff --git a/forge-gui-mobile-dev/libs/gdx-box2d-platform-natives.jar b/forge-gui-mobile-dev/libs/gdx-box2d-platform-natives.jar
new file mode 100644
index 00000000000..d2f8f105679
Binary files /dev/null and b/forge-gui-mobile-dev/libs/gdx-box2d-platform-natives.jar differ
diff --git a/forge-gui-mobile-dev/pom.xml b/forge-gui-mobile-dev/pom.xml
index 1f3ccfe4ad5..14aeb894349 100644
--- a/forge-gui-mobile-dev/pom.xml
+++ b/forge-gui-mobile-dev/pom.xml
@@ -256,5 +256,11 @@
gdx-controllers-desktop
2.2.3-SNAPSHOT
+
+ com.badlogicgames.gdx
+ gdx-box2d-platform
+ 1.11.0
+ natives-desktop
+
diff --git a/forge-gui-mobile/pom.xml b/forge-gui-mobile/pom.xml
index 46790b274d2..6fd4c1c5cec 100644
--- a/forge-gui-mobile/pom.xml
+++ b/forge-gui-mobile/pom.xml
@@ -80,6 +80,16 @@
5.2.3
compile
+
+ com.badlogicgames.gdx
+ gdx-box2d
+ 1.11.0
+
+
+ com.badlogicgames.gdx
+ gdx-ai
+ 1.8.2
+
diff --git a/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java b/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java
index 30d1ffe0699..42fdc921be0 100644
--- a/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java
+++ b/forge-gui-mobile/src/forge/adventure/character/EnemySprite.java
@@ -1,5 +1,11 @@
package forge.adventure.character;
+import com.badlogic.gdx.ai.steer.Steerable;
+import com.badlogic.gdx.ai.steer.SteeringAcceleration;
+import com.badlogic.gdx.ai.steer.SteeringBehavior;
+import com.badlogic.gdx.ai.steer.behaviors.*;
+import com.badlogic.gdx.ai.steer.utils.paths.LinePath;
+import com.badlogic.gdx.ai.utils.Location;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Batch;
@@ -16,12 +22,16 @@ import forge.adventure.player.AdventurePlayer;
import forge.adventure.util.Current;
import forge.adventure.util.MapDialog;
import forge.adventure.util.Reward;
+import forge.adventure.util.pathfinding.MovementBehavior;
+import forge.adventure.util.pathfinding.NavigationVertex;
+import forge.adventure.util.pathfinding.ProgressableGraphPath;
import forge.card.CardRarity;
import forge.deck.Deck;
import forge.item.PaperCard;
import forge.util.Aggregates;
import forge.util.MyRandom;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@@ -30,7 +40,20 @@ import java.util.stream.Collectors;
* EnemySprite
* Character sprite that represents an Enemy
*/
-public class EnemySprite extends CharacterSprite {
+public class EnemySprite extends CharacterSprite implements Steerable {
+
+ private static final SteeringAcceleration steerOutput =
+ new SteeringAcceleration(new Vector2());
+
+ Vector2 position;
+ float orientation;
+ Vector2 linearVelocity = new Vector2(1, 0);
+ float angularVelocity;
+ float maxSpeed;
+ boolean independentFacing;
+ SteeringBehavior behavior;
+ boolean tagged;
+
EnemyData data;
public MapDialog dialog; //Dialog to show on contact. Overrides standard battle (can be started as an action)
public MapDialog defeatDialog; //Dialog to show on defeat. Overrides standard death (can be removed as an action)
@@ -53,9 +76,12 @@ public class EnemySprite extends CharacterSprite {
public float threatRange = 0.0f; //If range < threatRange, begin pursuit
public float pursueRange = 0.0f; //If range > pursueRange, abandon pursuit
public float fleeRange = 0.0f; //If range < fleeRange, attempt to move away to fleeRange
- private boolean aggro = false;
+ public float speedModifier = 0.0f; // Increase or decrease default speed
+ public boolean aggro = false;
public boolean ignoreDungeonEffect = false;
public String questStageID;
+ private ProgressableGraphPath navPath;
+ public Vector2 fleeTarget;
public EnemySprite(EnemyData enemyData) {
this(0,enemyData);
@@ -64,16 +90,19 @@ public class EnemySprite extends CharacterSprite {
public EnemySprite(int id, EnemyData enemyData) {
super(id,enemyData.sprite);
data = enemyData;
+ initializeBaseMovementBehavior();
}
public void parseWaypoints(String waypoints){
String[] wp = waypoints.replaceAll("\\s", "").split(",");
for (String s : wp) {
movementBehaviors.addLast(new MovementBehavior());
- if (s.startsWith("wait")) {
- movementBehaviors.peekLast().duration = Float.parseFloat(s.substring(4));
- } else {
- movementBehaviors.peekLast().destination = Integer.parseInt(s);
+ if (!movementBehaviors.isEmpty()) {
+ if (s.startsWith("wait")) {
+ movementBehaviors.peekLast().duration = Float.parseFloat(s.substring(4));
+ } else {
+ movementBehaviors.peekLast().destination = s;
+ }
}
}
}
@@ -83,7 +112,13 @@ public class EnemySprite extends CharacterSprite {
float scale = data == null ? 1f : data.scale;
if (scale < 0)
scale = 1f;
- boundingRect.set(getX(), getY(), getWidth()*scale, getHeight()*scale);
+
+ float width = getWidth()*scale* collisionHeight;
+ float height = getHeight()*scale* collisionHeight;
+ float x = getX() + (getWidth() - width)/2;
+ float y = getY() + (getHeight() - height)/2;
+
+ boundingRect.set(x, y, width, height);
unfreezeRange = 30f * scale;
}
@@ -94,6 +129,183 @@ public class EnemySprite extends CharacterSprite {
moveBy(diff.x, diff.y,delta);
}
+ public void initializeBaseMovementBehavior() {
+ Location seekTarget = new Location() {
+ @Override
+ public Vector2 getPosition() {
+ return navPath.nodes.get(0).pos;
+ }
+
+ @Override
+ public float getOrientation() {
+ return 0;
+ }
+
+ @Override
+ public void setOrientation(float orientation) {
+
+ }
+
+ @Override
+ public float vectorToAngle(Vector2 vector) {
+ return 0;
+ }
+
+ @Override
+ public Vector2 angleToVector(Vector2 outVector, float angle) {
+ return null;
+ }
+
+ @Override
+ public Location newLocation() {
+ return null;
+ }
+ };
+ Seek seek = new Seek<>(this);
+ seek.setTarget(seekTarget);
+
+ Array wp = new Array<>();
+ if (navPath != null && navPath.nodes != null) {
+ for (NavigationVertex v : navPath.nodes)
+ wp.add(v.pos);
+ }
+ LinePath linePath = null;
+ FollowPath followWaypoints = null;
+ if (wp.size == 1) {
+ wp.insert(0, pos());
+ }
+ if (wp.size >= 2) {
+ linePath = new LinePath(wp, false);
+ followWaypoints = new FollowPath<>(this, linePath);
+ followWaypoints.setPathOffset(0.5f);
+ }
+
+ Arrive moveDirectlyToDestination = new Arrive<>(this, new Location() {
+ @Override
+ public Vector2 getPosition() {
+ if (navPath == null || navPath.nodes.size == 0)
+ return pos();
+ return navPath.get(0).pos;
+ }
+
+ @Override
+ public float getOrientation() {
+ return 0;
+ }
+
+ @Override
+ public void setOrientation(float orientation) {
+
+ }
+
+ @Override
+ public float vectorToAngle(Vector2 vector) {
+ return 0;
+ }
+
+ @Override
+ public Vector2 angleToVector(Vector2 outVector, float angle) {
+ return null;
+ }
+
+ @Override
+ public Location newLocation() {
+ return null;
+ }
+ })
+ .setTimeToTarget(0.01f)
+ .setArrivalTolerance(0f)
+ .setDecelerationRadius(10);
+
+ if (followWaypoints != null)
+ setBehavior(followWaypoints);
+ else
+ setBehavior(moveDirectlyToDestination);
+ }
+
+ public void setBehavior(SteeringBehavior behavior) {
+ this.behavior = behavior;
+ }
+
+ public SteeringBehavior getBehavior() {
+ return behavior;
+ }
+
+ public void update(float delta) {
+ if(behavior != null) {
+ behavior.calculateSteering(steerOutput);
+ while (steerOutput.isZero() && navPath != null && navPath.getCount() > 1) {
+ navPath.remove(0);
+ behavior.calculateSteering(steerOutput);
+ }
+ applySteering(delta);
+ }
+ }
+
+ private void applySteering(float delta) {
+ if(!steerOutput.linear.isZero()) {
+ Vector2 force = steerOutput.linear.scl(delta);
+ force.setLength(Math.min(speed() * delta, force.len()));
+ moveBy(force.x, force.y);
+ }
+ }
+
+ @Override
+ public float vectorToAngle (Vector2 vector) {
+ return (float)Math.atan2(-vector.x, vector.y);
+ }
+
+ @Override
+ public Vector2 angleToVector (Vector2 outVector, float angle) {
+ outVector.x = -(float)Math.sin(angle);
+ outVector.y = (float)Math.cos(angle);
+ return outVector;
+ }
+
+ @Override
+ public Vector2 getLinearVelocity() {
+ return linearVelocity;
+ }
+
+ @Override
+ public float getAngularVelocity() {
+ return angularVelocity;
+ }
+ @Override
+ public float getBoundingRadius() {
+ return getWidth()/2;
+ }
+
+ @Override
+ public boolean isTagged() {
+ return tagged;
+ }
+
+ @Override
+ public Vector2 getPosition() {
+ return pos();
+ }
+
+ @Override
+ public float getOrientation() {
+ return orientation;
+ }
+
+ @Override
+ public void setOrientation(float value) {
+ orientation = value;
+ }
+
+ @Override
+ public Location newLocation() {
+ return null;
+ }
+
+ @Override
+ public void setTagged(boolean value) {
+ tagged = value;
+ }
+
public void freezeMovement(){
_freeze = true;
setPosition(_previousPosition6.x, _previousPosition6.y);
@@ -102,15 +314,15 @@ public class EnemySprite extends CharacterSprite {
// Combined with player doing the same, should no longer be colliding to immediately re-enter battle if mob still present
}
- public Vector2 getTargetVector(PlayerSprite player, float delta) {
+ public Vector2 getTargetVector(PlayerSprite player, ArrayList sortedGraphNodes, float delta) {
//todo - this can be integrated into overworld movement as well, giving flee behaviors or moving to generated waypoints
Vector2 target = pos();
- Vector2 routeToPlayer = new Vector2(player.pos()).sub(target);
+ Vector2 spriteToPlayer = new Vector2(player.pos()).sub(target);
if (_freeze){
//Mob has defeated player in battle, hold still until player has a chance to move away.
//Without this moving enemies can immediately restart battle.
- if (routeToPlayer.len() < unfreezeRange) {
+ if (spriteToPlayer.len() < unfreezeRange) {
timer += delta;
return Vector2.Zero;
}
@@ -119,53 +331,81 @@ public class EnemySprite extends CharacterSprite {
}
}
+ NavigationVertex targetPoint = null;
if (threatRange > 0 || fleeRange > 0){
- if (routeToPlayer.len() <= threatRange || (aggro && routeToPlayer.len() <= pursueRange))
+ if (spriteToPlayer.len() <= threatRange || (aggro && spriteToPlayer.len() <= pursueRange))
{
+ if (sortedGraphNodes != null) {
+ for (NavigationVertex candidate : sortedGraphNodes) {
+ Vector2 candidateToPlayer = new Vector2(candidate.pos).sub(player.pos());
+ if ((candidateToPlayer.x * candidateToPlayer.x) + (candidateToPlayer.y * candidateToPlayer.y) <
+ (spriteToPlayer.x * spriteToPlayer.x) + (spriteToPlayer.y * spriteToPlayer.y)) {
+ targetPoint = candidate;
+ break;
+ }
+ }
+ }
aggro = true;
- return routeToPlayer;
+ if (targetPoint != null) {
+ return targetPoint.pos;
+ }
+ return new Vector2(player.pos());
}
- if (routeToPlayer.len() <= fleeRange)
+ if (spriteToPlayer.len() <= fleeRange)
{
- Float fleeDistance = fleeRange - routeToPlayer.len();
- return new Vector2(target).sub(player.pos()).setLength(fleeDistance);
+ //todo: replace with inverse A* variant, seeking max total distance from player in X generations
+ // of movement, valuing each node by distance from player divided by closest distance(s) in path
+ // in order to make close passes to escape less appealing than maintaining moderate distance
+ float fleeDistance = fleeRange - spriteToPlayer.len();
+ return new Vector2(pos()).sub(player.pos()).setLength(fleeDistance).add(pos());
+ }
+ if (aggro && spriteToPlayer.len() > pursueRange) {
+ aggro = false;
+ initializeBaseMovementBehavior();
}
}
- if (movementBehaviors.size() > 0){
+ if (movementBehaviors.peek() != null){
+ MovementBehavior peek = movementBehaviors.peek();
+ //TODO - This first block needs to be redone, doesn't work as intended and can also possibly skip behaviors in rare situations
+// if (peek.getDuration() == 0 && target.equals(_previousPosition6) && timer >= _movementTimeout)
+// {
+// //stationary in an untimed behavior, move on to next behavior attempt to get unstuck
+// if (movementBehaviors.size() > 1) {
+// MovementBehavior current = movementBehaviors.pop();
+// current.currentTargetVector = null;
+// movementBehaviors.addLast(current);
+// }
+// }
+ //else
+ if (peek.getDuration() == 0 && peek.getNextTargetVector(pos()).dst(pos()) < 2){
+ //this is a location based behavior that has been completed. Move on to the next behavior
+
+ MovementBehavior current = movementBehaviors.pop();
+ current.currentTargetVector = null;
+ movementBehaviors.addLast(current);
- if (movementBehaviors.peek().getDuration() == 0 && target.equals(_previousPosition6) && timer >= _movementTimeout)
- {
- //stationary in an untimed behavior, move on to next behavior attempt to get unstuck
- if (movementBehaviors.size() > 1) {
- movementBehaviors.addLast(movementBehaviors.pop());
- timer = 0.0f;
- }
}
- else if (movementBehaviors.peek().pos().sub(pos()).len() < 0.3){
- //this is a location based behavior that has been completed. Move on if there are more behaviors
- if (movementBehaviors.size() > 1) {
- movementBehaviors.addLast(movementBehaviors.pop());
- timer = 0.0f;
- }
- }
- else if ( movementBehaviors.peek().getDuration() > 0)
+ else if ( peek.getDuration() > 0)
{
- if (timer >= movementBehaviors.peek().getDuration() + delta)
+ if (timer >= peek.getDuration() + delta)
{
//this is a timed behavior that has been completed. Move to the next behavior and restart the timer
- movementBehaviors.addLast(movementBehaviors.pop());
- timer = 0.0f;
+ MovementBehavior current = movementBehaviors.pop();
+ current.currentTargetVector = null;
+ movementBehaviors.addLast(current);
}
else{
timer += delta;//this is a timed behavior that has not been completed, continue this behavior
+ return new Vector2(pos());
}
}
- if (movementBehaviors.peek().pos().len() > 0.3)
- target = new Vector2(movementBehaviors.peek().pos()).sub(pos());
- else target = Vector2.Zero;
+ if (peek.getNextTargetVector(pos()).dst(pos()) > 0.3) {
+ target = new Vector2(peek.getNextTargetVector(pos()));
+ }
+ else target = new Vector2(pos());
}
- else target = Vector2.Zero;
+ else target = new Vector2(pos());
return target;
}
public void updatePositon()
@@ -313,7 +553,7 @@ public class EnemySprite extends CharacterSprite {
}
public float speed() {
- return data.speed;
+ return Float.max(data.speed + speedModifier, 0);
}
public float getLifetime() {
@@ -322,44 +562,71 @@ public class EnemySprite extends CharacterSprite {
return Math.max(data.lifetime, lifetime);
}
+ //Pathfinding integration below this line
- public class MovementBehavior {
-
- //temporary placeholders for overworld behavior integration
- public boolean wander = false;
- public boolean flee = false;
- public boolean stop = false;
- //end temporary
-
- float duration = 0.0f;
- float x = 0.0f;
- float y = 0.0f;
-
- int destination = 0;
-
- public float getX(){
- return x;
- }
- public float getY(){
- return y;
- }
- public float getDuration(){
- return duration;
- }
- public int getDestination(){
- return destination;
- }
-
- public void setX(float newVal){
- x = newVal;
- }
- public void setY(float newVal){
- y = newVal;
- }
-
- public Vector2 pos() {
- return new Vector2(getX(), getY());
- }
+ public void setNavPath(ProgressableGraphPath navPath) {
+ this.navPath = navPath;
}
+
+ public ProgressableGraphPath getNavPath() {
+ return navPath;
+ }
+
+ @Override
+ public float getZeroLinearSpeedThreshold() {
+ return 0;
+ }
+
+ @Override
+ public void setZeroLinearSpeedThreshold(float value) {
+
+ }
+
+ @Override
+ public float getMaxLinearSpeed() {
+ return 500;
+ }
+
+ @Override
+ public void setMaxLinearSpeed(float maxLinearSpeed) {
+
+ }
+
+ @Override
+ public float getMaxLinearAcceleration() {
+ return 5000;
+ }
+
+ @Override
+ public void setMaxLinearAcceleration(float maxLinearAcceleration) {
+
+ }
+
+ @Override
+ public float getMaxAngularSpeed() {
+ return 0;
+ }
+
+ @Override
+ public void setMaxAngularSpeed(float maxAngularSpeed) {
+
+ }
+
+ @Override
+ public float getMaxAngularAcceleration() {
+ return 0;
+ }
+
+ @Override
+ public void setMaxAngularAcceleration(float maxAngularAcceleration) {
+
+ }
+
+ public void steer(Vector2 currentVector) {
+
+ }
+
+
+
}
diff --git a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java
index 8d9a047321e..481308f593f 100644
--- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java
+++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java
@@ -316,6 +316,7 @@ public class RewardScene extends UIScene {
if (type == Type.Shop) {
this.shopActor = shopActor;
this.changes = shopActor.getMapStage().getChanges();
+ addToSelectable(restockButton);
}
for (Actor actor : new Array.ArrayIterator<>(generated)) {
actor.remove();
@@ -323,6 +324,7 @@ public class RewardScene extends UIScene {
((RewardActor) actor).dispose();
}
}
+ addToSelectable(doneButton);
generated.clear();
Actor card = ui.findActor("cards");
diff --git a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java
index 86e8018d38d..4de3e7c087a 100644
--- a/forge-gui-mobile/src/forge/adventure/stage/MapStage.java
+++ b/forge-gui-mobile/src/forge/adventure/stage/MapStage.java
@@ -1,6 +1,5 @@
package forge.adventure.stage;
-
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.controllers.Controllers;
import com.badlogic.gdx.graphics.g2d.Batch;
@@ -14,6 +13,7 @@ import com.badlogic.gdx.maps.tiled.TiledMapTileLayer;
import com.badlogic.gdx.maps.tiled.objects.TiledMapTileMapObject;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.physics.box2d.*;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
@@ -23,9 +23,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
-import com.badlogic.gdx.utils.Align;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.Scaling;
+import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.utils.Timer;
import com.github.tommyettinger.textra.TextraButton;
import com.github.tommyettinger.textra.TextraLabel;
@@ -37,6 +35,9 @@ import forge.adventure.data.*;
import forge.adventure.pointofintrest.PointOfInterestChanges;
import forge.adventure.scene.*;
import forge.adventure.util.*;
+import forge.adventure.util.pathfinding.NavigationMap;
+import forge.adventure.util.pathfinding.NavigationVertex;
+import forge.adventure.util.pathfinding.ProgressableGraphPath;
import forge.adventure.world.WorldSave;
import forge.assets.FBufferedImage;
import forge.assets.FImageComplex;
@@ -53,6 +54,7 @@ import forge.sound.SoundSystem;
import java.time.LocalDate;
import java.util.*;
+import java.util.Queue;
/**
@@ -61,9 +63,10 @@ import java.util.*;
public class MapStage extends GameStage {
public static MapStage instance;
final Array actors = new Array<>();
-
- TiledMap map;
- Array collisionRect = new Array<>();
+ public com.badlogic.gdx.physics.box2d.World gdxWorld;
+ public TiledMap tiledMap;
+ public Array collisionRect = new Array<>();
+ public Map navMaps = new HashMap<>();
private boolean isInMap = false;
MapLayer spriteLayer;
private PointOfInterestChanges changes;
@@ -87,7 +90,13 @@ public class MapStage extends GameStage {
private boolean respawnEnemies;
private boolean canFailDungeon = false;
protected ArrayList enemies = new ArrayList<>();
- protected Map waypoints = new HashMap<>();
+ public Map waypoints = new HashMap<>();
+
+ //todo: add additional graphs for other sprite sizes if desired. Current implementation
+ // allows for mobs of any size to fit into 16x16 tiles for navigation purposes
+ float collisionWidthMod = 0.4f;
+ float defaultSpriteSize = 16f;
+ float navMapSize = defaultSpriteSize * collisionWidthMod;
public boolean getDialogOnlyInput() {
return dialogOnlyInput;
@@ -126,6 +135,7 @@ public class MapStage extends GameStage {
private boolean freezeAllEnemyBehaviors = false;
protected MapStage() {
+ gdxWorld = new World(new Vector2(0, 0),false);
dialog = Controls.newDialog("");
eventTouchDown = new InputEvent();
eventTouchDown.setPointer(-1);
@@ -309,10 +319,11 @@ public class MapStage extends GameStage {
}
public void loadMap(TiledMap map, String sourceMap, String targetMap, int spawnTargetId) {
+ gdxWorld = new World(new Vector2(0, 0),false);
isLoadingMatch = false;
isInMap = true;
GameHUD.getInstance().showHideMap(false);
- this.map = map;
+ this.tiledMap = map;
for (MapActor actor : new Array.ArrayIterator<>(actors)) {
actor.remove();
foregroundSprites.removeActor(actor);
@@ -320,6 +331,7 @@ public class MapStage extends GameStage {
positions.clear();
actors.clear();
collisionRect.clear();
+ waypoints.clear();
if (collisionGroup != null)
collisionGroup.remove();
@@ -401,8 +413,9 @@ public class MapStage extends GameStage {
} while (oldSize != collisionRect.size);
if (spriteLayer == null) System.err.print("Warning: No spriteLayer present in map.\n");
- replaceWaypoints();
-
+ navMaps.clear();
+ navMaps.put(navMapSize, new NavigationMap(navMapSize));
+ navMaps.get(navMapSize).initializeGeometryGraph();
getPlayerSprite().stop();
}
@@ -428,17 +441,6 @@ public class MapStage extends GameStage {
}
}
- void replaceWaypoints() {
- for (EnemySprite enemy : enemies) {
- for (EnemySprite.MovementBehavior behavior : enemy.movementBehaviors) {
- if (behavior.getDestination() > 0 && waypoints.containsKey(behavior.getDestination())) {
- behavior.setX(waypoints.get(behavior.getDestination()).x);
- behavior.setY(waypoints.get(behavior.getDestination()).y);
- }
- }
- }
- }
-
static public boolean containsOrEquals(Rectangle r1, Rectangle r2) {
float xmi = r2.x;
float xma = xmi + r2.width;
@@ -456,7 +458,7 @@ public class MapStage extends GameStage {
for (MapObject collision : cell.getTile().getObjects()) {
if (collision instanceof RectangleMapObject) {
Rectangle r = ((RectangleMapObject) collision).getRectangle();
- collisionRect.add(new Rectangle((Math.round(layer.getTileWidth() * x) + r.x), (Math.round(layer.getTileHeight() * y) + r.y), Math.round(r.width), Math.round(r.height)));
+ collisionRect.add(new Rectangle(((layer.getTileWidth() * x) + r.x), ((layer.getTileHeight() * y) + r.y), Math.round(r.width), Math.round(r.height)));
}
}
}
@@ -657,6 +659,10 @@ public class MapStage extends GameStage {
if (dialogObject != null && !dialogObject.toString().isEmpty()) {
mob.parseWaypoints(dialogObject.toString());
}
+ if (prop.containsKey("speedModifier")) //Increase or decrease default speed for this mob
+ {
+ mob.speedModifier = Float.parseFloat(prop.get("speedModifier").toString());
+ }
enemies.add(mob);
addMapActor(obj, mob);
@@ -1026,8 +1032,6 @@ public class MapStage extends GameStage {
for (Integer i : idsToRemove) deleteObject(i);
}
- final Rectangle tempBoundingRect = new Rectangle();
-
@Override
protected void onActing(float delta) {
if (isPaused() || isDialogOnlyInput())
@@ -1040,6 +1044,9 @@ public class MapStage extends GameStage {
}
else return;
}
+ float mobSize = navMapSize; //todo: replace with actual size if multiple nav maps implemented
+ ArrayList verticesNearPlayer = new ArrayList<>(navMaps.get(mobSize).navGraph.getNodes());
+ verticesNearPlayer.sort(Comparator.comparingInt(o -> Math.round((o.pos.x - player.pos().x) * (o.pos.x - player.pos().x) + (o.pos.y - player.pos().y) * (o.pos.y - player.pos().y))));
if (!freezeAllEnemyBehaviors) {
while (it.hasNext()) {
@@ -1048,35 +1055,52 @@ public class MapStage extends GameStage {
continue;
}
mob.updatePositon();
- mob.targetVector = mob.getTargetVector(player, delta);
- Vector2 currentVector = new Vector2(mob.targetVector);
+
+ ProgressableGraphPath navPath = new ProgressableGraphPath<>(0);
+ if (mob.getData().flying) {
+ navPath.add(new NavigationVertex(mob.getTargetVector(player, null,delta)));
+ } else {
+ Vector2 destination = mob.getTargetVector(player, verticesNearPlayer, delta);
+
+ if (destination.epsilonEquals(mob.pos()) && !mob.aggro) {
+ mob.setAnimation(CharacterSprite.AnimationTypes.Idle);
+ continue;
+ }
+ if (destination.equals(mob.targetVector) && mob.getNavPath() != null)
+ navPath = mob.getNavPath();
+
+ if (navPath.nodes.size == 0 || !destination.equals(mob.targetVector)) {
+ mob.targetVector = destination;
+ navPath = navMaps.get(mobSize).findShortestPath(mobSize, mob.pos(), mob.targetVector);
+ }
+
+ if (mob.aggro) {
+ navPath.add(new NavigationVertex(player.pos()));
+ }
+ }
+
+ if (navPath == null || navPath.getCount() == 0 || navPath.get(0) == null) {
+ mob.setAnimation(CharacterSprite.AnimationTypes.Idle);
+ continue;
+ }
+ Vector2 currentVector = null;
+
+ while (navPath.getCount() > 0 && navPath.get(0) != null && (navPath.get(0).pos == null || navPath.get(0).pos.dst(mob.pos()) < 0.5f)) {
+
+ navPath.remove(0);
+
+ }
+ if (navPath.getCount() != 0) {
+ currentVector = new Vector2(navPath.get(0).pos).sub(mob.pos());
+ }
+ mob.setNavPath(navPath);
mob.clearActions();
- if (mob.targetVector.len() == 0.0f) {
+ if (currentVector == null || (currentVector.x == 0.0f && currentVector.y == 0.0f)) {
mob.setAnimation(CharacterSprite.AnimationTypes.Idle);
continue;
}
-
- currentVector.setLength(Math.min(mob.speed() * delta, mob.targetVector.len()));
-
- tempBoundingRect.set(mob.getX() + currentVector.x, mob.getY() + currentVector.y, mob.getWidth() * 0.4f, mob.getHeight() * 0.4f);
-
- if (!mob.getData().flying && isColliding(tempBoundingRect))//if direct path is not possible
- {
- currentVector = adjustMovement(currentVector,tempBoundingRect);
- tempBoundingRect.set(mob.getX() + currentVector.x, mob.getY(), mob.getWidth() * 0.4f, mob.getHeight() * 0.4f);
- if (isColliding(tempBoundingRect))//if only x path is not possible
- {
- tempBoundingRect.set(mob.getX(), mob.getY() + currentVector.y, mob.getWidth() * 0.4f, mob.getHeight() * 0.4f);
- if (!isColliding(tempBoundingRect))//if y path is possible
- {
- mob.moveBy(0, currentVector.y, delta);
- }
- } else {
- mob.moveBy(currentVector.x, 0, delta);
- }
- } else {
- mob.moveBy(currentVector.x, currentVector.y, delta);
- }
+ mob.steer(currentVector);
+ mob.update(delta);
}
}
@@ -1087,9 +1111,6 @@ public class MapStage extends GameStage {
if (positions.size() > 4)
positions.remove();
-
-
-
for (MapActor actor : new Array.ArrayIterator<>(actors)) {
if (actor.collideWithPlayer(player)) {
if (actor instanceof EnemySprite) {
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/Box2dRaycastCollisionDetector.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/Box2dRaycastCollisionDetector.java
new file mode 100644
index 00000000000..66371d5aa93
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/Box2dRaycastCollisionDetector.java
@@ -0,0 +1,56 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.utils.Collision;
+import com.badlogic.gdx.ai.utils.Ray;
+import com.badlogic.gdx.ai.utils.RaycastCollisionDetector;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.physics.box2d.Fixture;
+import com.badlogic.gdx.physics.box2d.RayCastCallback;
+import com.badlogic.gdx.physics.box2d.World;
+
+public class Box2dRaycastCollisionDetector implements RaycastCollisionDetector {
+
+ World world;
+ Box2dRaycastCollisionDetector.Box2dRaycastCallback callback;
+
+ public Box2dRaycastCollisionDetector (World world) {
+ this(world, new Box2dRaycastCollisionDetector.Box2dRaycastCallback());
+ }
+
+ public Box2dRaycastCollisionDetector (World world, Box2dRaycastCollisionDetector.Box2dRaycastCallback callback) {
+ this.world = world;
+ this.callback = callback;
+ }
+
+ @Override
+ public boolean collides (Ray ray) {
+ return findCollision(null, ray);
+ }
+
+ @Override
+ public boolean findCollision (Collision outputCollision, Ray inputRay) {
+ callback.collided = false;
+ if (!inputRay.start.epsilonEquals(inputRay.end, MathUtils.FLOAT_ROUNDING_ERROR)) {
+ callback.outputCollision = outputCollision;
+ world.rayCast(callback, inputRay.start, inputRay.end);
+ }
+ return callback.collided;
+ }
+
+ public static class Box2dRaycastCallback implements RayCastCallback {
+ public Collision outputCollision;
+ public boolean collided;
+
+ public Box2dRaycastCallback () {
+ }
+
+ @Override
+ public float reportRayFixture (Fixture fixture, Vector2 point, Vector2 normal, float fraction) {
+ if (outputCollision != null) outputCollision.set(point, normal);
+ collided = true;
+ return fraction;
+ }
+ }
+}
+
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/EuclidianHeuristic.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/EuclidianHeuristic.java
new file mode 100644
index 00000000000..eaf8b1af48a
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/EuclidianHeuristic.java
@@ -0,0 +1,12 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.pfa.Heuristic;
+
+public class EuclidianHeuristic implements Heuristic {
+
+ @Override
+ public float estimate(NavigationVertex start, NavigationVertex end) {
+ return start.pos.dst(end.pos);
+ }
+}
+
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/MovementBehavior.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/MovementBehavior.java
new file mode 100644
index 00000000000..a784b827c60
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/MovementBehavior.java
@@ -0,0 +1,59 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.math.Vector2;
+import forge.adventure.stage.MapStage;
+import forge.util.Aggregates;
+
+public class MovementBehavior {
+ public float duration = 0.0f;
+ float x = 0.0f;
+ float y = 0.0f;
+
+ public String destination = "";
+
+ public float getX(){
+ return x;
+ }
+ public float getY(){
+ return y;
+ }
+ public float getDuration(){
+ return duration;
+ }
+ public Vector2 currentTargetVector;
+ public Vector2 getNextTargetVector(Vector2 currentPosition){
+ if (currentTargetVector != null) {
+ return currentTargetVector;
+ }
+ if (destination.isEmpty()) {
+ currentTargetVector = new Vector2(currentPosition);
+ } else {
+ if (destination.startsWith("r")) {
+ String[] randomWaypoints = destination.replaceAll("r", "").split("-");
+ if (randomWaypoints.length > 0) {
+ int selectedWaypoint = Integer.parseInt(Aggregates.random(randomWaypoints));
+ if (MapStage.getInstance().waypoints.containsKey(selectedWaypoint)) {
+ currentTargetVector = new Vector2(MapStage.getInstance().waypoints.get(selectedWaypoint));
+ }
+ }
+ else {
+ currentTargetVector = new Vector2(currentPosition);
+ }
+ } else if (destination.startsWith("w")) {
+ currentTargetVector = new Vector2(currentPosition);
+ duration = Float.parseFloat(destination.replaceAll("w", ""));
+ } else if (MapStage.getInstance().waypoints.containsKey(Integer.parseInt(destination))) {
+ currentTargetVector = new Vector2(MapStage.getInstance().waypoints.get(Integer.parseInt(destination)));
+ }
+ }
+
+ return currentTargetVector;
+ }
+
+ public void setX(float newVal){
+ x = newVal;
+ }
+ public void setY(float newVal){
+ y = newVal;
+ }
+}
\ No newline at end of file
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationEdge.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationEdge.java
new file mode 100644
index 00000000000..6c6fbbf1903
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationEdge.java
@@ -0,0 +1,31 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.pfa.Connection;
+import com.badlogic.gdx.math.Vector2;
+
+public class NavigationEdge implements Connection {
+ NavigationVertex fromVertex;
+ NavigationVertex toVertex;
+ float cost;
+
+ public NavigationEdge(NavigationVertex from, NavigationVertex to) {
+ this.fromVertex = from;
+ this.toVertex = to;
+ cost = Vector2.dst(fromVertex.pos.x, fromVertex.pos.y, toVertex.pos.x, toVertex.pos.y);
+ }
+
+ @Override
+ public float getCost() {
+ return cost;
+ }
+
+ @Override
+ public NavigationVertex getFromNode() {
+ return fromVertex;
+ }
+
+ @Override
+ public NavigationVertex getToNode() {
+ return toVertex;
+ }
+}
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationGraph.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationGraph.java
new file mode 100644
index 00000000000..88ebfdf2699
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationGraph.java
@@ -0,0 +1,163 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.pfa.Connection;
+import com.badlogic.gdx.ai.pfa.indexed.IndexedAStarPathFinder;
+import com.badlogic.gdx.ai.pfa.indexed.IndexedGraph;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public class NavigationGraph implements IndexedGraph {
+ private int lastNodeIndex = 0;
+ Map nodes = new HashMap<>();
+
+ EuclidianHeuristic navigationHeuristic = new EuclidianHeuristic();
+
+ public NavigationVertex addVertex(NavigationVertex node){
+ node.index = lastNodeIndex;
+ lastNodeIndex++;
+ nodes.put(node.index,node);
+ return node;
+ }
+
+ public NavigationVertex addVertex(Vector2 position) {
+ return addVertex(new NavigationVertex(position));
+ }
+
+ public NavigationVertex addVertex(float x, float y) {
+ return addVertex(new NavigationVertex(x,y));
+ }
+
+ public void removeVertex(NavigationVertex node) {
+ for (NavigationVertex v : node.incomingEdges.keys()) {
+ v.removeEdges(node);
+ }
+ nodes.remove(node.index >=0? node.index: lookupIndex(node));
+ }
+
+ public void removeVertex(Vector2 position) {
+ removeVertex(getVertexByPosition(position));
+ }
+
+ public void removeVertex(float x, float y) {
+ removeVertex(new Vector2(x,y));
+ }
+
+ public void removeVertices(Collection vertices) {
+ for (NavigationVertex v : vertices) {
+ removeVertex(v);
+ }
+ }
+
+ public void removeVertexIf(Predicate predicate) {
+ removeVertices(nodes.values().stream().filter(predicate).collect(Collectors.toList()));
+ }
+
+ public int lookupIndex(NavigationVertex item) {
+ return lookupIndex(item.pos);
+ }
+
+ public int lookupIndex(Vector2 pos) {
+ for (int i : nodes.keySet())
+ if (nodes.get(i).pos.equals(pos)) return i;
+
+ return -1;
+ }
+
+ public void addEdge(NavigationVertex fromNode, NavigationVertex toNode) {
+ if (fromNode.index < 0) {
+ fromNode = getVertexByPosition(fromNode.pos);
+ }
+ if (toNode.index < 0) {
+ toNode = getVertexByPosition(toNode.pos);
+ }
+
+ if (edgeExists(fromNode, toNode)) {
+ System.out.println(fromNode.pos + " is already connected to " + toNode.pos);
+ return;
+ }
+
+ if (!(fromNode.index < 0) || toNode.index < 0) {
+ NavigationEdge fromAToB = new NavigationEdge(fromNode, toNode);
+ NavigationEdge fromBToA = new NavigationEdge(toNode, fromNode);
+ fromNode.outgoingEdges.put(toNode, fromAToB);
+ fromNode.incomingEdges.put(toNode, fromBToA);
+ toNode.outgoingEdges.put(fromNode, fromBToA);
+ toNode.incomingEdges.put(fromNode, fromAToB);
+ }
+ }
+
+ public void addEdge(Vector2 fromNode, NavigationVertex toNode) {
+ addEdge(new NavigationVertex(fromNode), toNode);
+ }
+
+ public void addEdge(NavigationVertex fromNode, Vector2 toNode) {
+ addEdge(fromNode, new NavigationVertex(toNode));
+ }
+
+ public void addEdgeUnchecked(NavigationVertex fromNode, NavigationVertex toNode) {
+ //Assumes that nodes are in graph, are not connected already, and have correct index
+
+ NavigationEdge fromAToB = new NavigationEdge(fromNode, toNode);
+ NavigationEdge fromBToA = new NavigationEdge(toNode, fromNode);
+
+ fromNode.outgoingEdges.put(toNode, fromAToB);
+ fromNode.incomingEdges.put(toNode, fromBToA);
+ toNode.outgoingEdges.put(fromNode, fromBToA);
+ toNode.incomingEdges.put(fromNode, fromAToB);
+ }
+
+ public int getIndex(NavigationVertex node) {
+ return node.index;
+ }
+
+ public int getNodeCount() {
+ return lastNodeIndex;
+ }
+
+ @Override
+ public Array> getConnections(NavigationVertex fromNode) {
+ return fromNode.getAllConnections();
+ }
+
+ public boolean edgeExists(NavigationVertex fromNode, NavigationVertex toNode) {
+ if (fromNode.index < 0) {
+ fromNode = getVertexByPosition(fromNode.pos);
+ }
+ if (toNode.index < 0) {
+ toNode = getVertexByPosition(toNode.pos);
+ }
+ return fromNode.outgoingEdges.containsKey(toNode);
+ }
+
+ public Collection getNodes() {
+ return nodes.values();
+ }
+
+ public ProgressableGraphPath findPath(Vector2 origin, Vector2 destination) {
+ ProgressableGraphPath navPath = new ProgressableGraphPath<>();
+
+ NavigationVertex originVertex = getVertexByPosition(origin);
+ NavigationVertex destinationVertex = getVertexByPosition(destination);
+
+ if (originVertex.index > -1 && destinationVertex.index > -1) {
+
+ new IndexedAStarPathFinder<>(this).searchNodePath(originVertex, destinationVertex, navigationHeuristic, navPath);
+ }
+ return navPath;
+ }
+
+ public NavigationVertex getVertexByPosition(Vector2 position) {
+ return nodes.get(lookupIndex(position));
+ }
+
+ public boolean containsNode(Vector2 nodePosition) {
+ return nodes.containsKey(lookupIndex(nodePosition));
+ }
+}
+
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationMap.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationMap.java
new file mode 100644
index 00000000000..7984d6a43bb
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationMap.java
@@ -0,0 +1,201 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.math.Rectangle;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.physics.box2d.*;
+import com.badlogic.gdx.utils.Array;
+import forge.adventure.stage.MapStage;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+public class NavigationMap {
+ float spriteSize = 16f;
+ boolean rayCollided = false;
+
+ public NavigationGraph navGraph = new NavigationGraph();
+
+ Array navBounds = new Array<>();
+ float half = (spriteSize / 2);
+
+ public NavigationMap(float spriteSize) {
+ this.spriteSize = spriteSize;
+ this.half = spriteSize / 2;
+ }
+
+ RayCastCallback callback = new RayCastCallback() {
+ @Override
+ public float reportRayFixture(Fixture fixture, Vector2 vector2, Vector2 vector21, float v) {
+ if (v < 1.0)
+ rayCollided = true;
+ return 0;
+ }
+ };
+
+ public void initializeGeometryGraph() {
+ navGraph = new NavigationGraph();
+
+ for (int i = 0; i < MapStage.getInstance().collisionRect.size; i++) {
+ Rectangle r1 = MapStage.getInstance().collisionRect.get(i);
+
+ if (r1.width < 3 && r1.height < 3)
+ continue;
+ int offsetX = -8;
+ int offsetY = 0;
+
+ BodyDef bodyDef = new BodyDef();
+ bodyDef.type = BodyDef.BodyType.StaticBody;
+ bodyDef.position.set(r1.x + r1.getWidth() / 2 + offsetX, r1.y + r1.getHeight() / 2 + offsetY);
+ Body body = MapStage.getInstance().gdxWorld.createBody(bodyDef);
+
+ PolygonShape polygonShape = new PolygonShape();
+ polygonShape.setAsBox(((r1.getWidth() + spriteSize) / 2), ((r1.getHeight() + spriteSize) / 2));
+ FixtureDef fixture = new FixtureDef();
+ fixture.shape = polygonShape;
+ fixture.density = 1;
+
+ body.createFixture(fixture);
+ polygonShape.dispose();
+ }
+
+ float width = Float.parseFloat(MapStage.getInstance().tiledMap.getProperties().get("width").toString());
+ float height = Float.parseFloat(MapStage.getInstance().tiledMap.getProperties().get("height").toString());
+ float tileHeight = Float.parseFloat(MapStage.getInstance().tiledMap.getProperties().get("tileheight").toString());
+ float tileWidth = Float.parseFloat(MapStage.getInstance().tiledMap.getProperties().get("tilewidth").toString());
+
+ NavigationVertex[][] points = new NavigationVertex[(int)width][(int)height];
+
+ for (int i = 0; i < width; i++) {
+ for (int j = 0; j < height; j++) {
+ points[i][j] = navGraph.addVertex(i* tileWidth + (tileWidth/ 2), j*tileHeight + (tileHeight/ 2));
+ if (i > 0) {
+ navGraph.addEdgeUnchecked(points[i][j],points[i-1][j]);
+ }
+
+ if (j > 0) {
+ navGraph.addEdgeUnchecked(points[i][j],points[i][j-1]);
+ }
+
+ if (i > 0 && j > 0) {
+ navGraph.addEdgeUnchecked(points[i][j],points[i-1][j-1]);
+ }
+
+ if (i > 0 && j + 1 < height) {
+ navGraph.addEdgeUnchecked(points[i][j],points[i-1][j+1]);
+ }
+ //remaining connections will be added by subsequent nodes
+ }
+ }
+
+ Array fixtures = new Array<>();
+ if (MapStage.getInstance().gdxWorld != null) {
+ MapStage.getInstance().gdxWorld.getFixtures(fixtures);
+ for (Fixture fix : fixtures) {
+ navGraph.removeVertexIf(vertex -> fix.testPoint(vertex.pos));
+ }
+ }
+
+ navGraph.removeVertexIf(v -> navGraph.getConnections(v).isEmpty());
+
+ //Add additional vertices for map waypoints
+ for (Vector2 waypointVector : MapStage.getInstance().waypoints.values()) {
+ NavigationVertex waypointVertex = navGraph.addVertex(waypointVector);
+
+ ArrayList vertices = new ArrayList<>(navGraph.nodes.values());
+ vertices.sort(Comparator.comparingInt(o -> Math.round((o.pos.x - waypointVector.x) * (o.pos.x - waypointVector.x) + (o.pos.y - waypointVector.y) * (o.pos.y - waypointVector.y))));
+
+ for (int i = 0, j=0; i < vertices.size() && j < 4; i++) {
+ if (waypointVector.epsilonEquals(vertices.get(i).pos))
+ continue; //rayCast() crashes if params are equal
+ rayCollided = false;
+ MapStage.getInstance().gdxWorld.rayCast(callback, waypointVector, vertices.get(i).pos);
+ if (!rayCollided) {
+ navGraph.addEdgeUnchecked(waypointVertex, vertices.get(i));
+ j++;
+ }
+ }
+ }
+ }
+
+
+ public ProgressableGraphPath findShortestPath(Float spriteSize, Vector2 origin, Vector2 destination) {
+ Array fixtures = new Array<>();
+ MapStage.getInstance().gdxWorld.getFixtures(fixtures);
+
+ boolean originPrecalculated = navGraph.containsNode(origin);
+ boolean destinationPrecalculated = navGraph.containsNode(destination);
+
+ try {
+ if (!originPrecalculated)
+ navGraph.addVertex(origin);
+
+ if (!destinationPrecalculated)
+ navGraph.addVertex(destination);
+
+ ArrayList vertices = new ArrayList<>();
+
+ if (!(originPrecalculated && destinationPrecalculated)) {
+ vertices.addAll(navGraph.nodes.values());
+ vertices.sort(Comparator.comparingInt(o -> Math.round((o.pos.x - origin.x) * (o.pos.x - origin.x) + (o.pos.y - origin.y) * (o.pos.y - origin.y))));
+ }
+
+ if (!originPrecalculated) {
+ for (int i = 0, j=0; i < vertices.size() && j < 10; i++) {
+ if (origin.epsilonEquals(vertices.get(i).pos))
+ continue; //rayCast() crashes if params are equal
+ rayCollided = false;
+ MapStage.getInstance().gdxWorld.rayCast(callback, origin, vertices.get(i).pos);
+ if (!rayCollided) {
+ navGraph.addEdge(origin, vertices.get(i));
+ j++;
+ }
+ }
+ }
+
+ if (!destinationPrecalculated) {
+ for (int i = 0, j=0; i < vertices.size() && j < 10; i++) {
+ if (destination.epsilonEquals(vertices.get(i).pos))
+ continue; //shouldn't happen, but would crash during rayCast if it did
+ rayCollided = false;
+ MapStage.getInstance().gdxWorld.rayCast(callback, vertices.get(i).pos, destination);
+ if (!rayCollided) {
+ navGraph.addEdge(destination, vertices.get(i));
+ j++;
+ }
+ }
+ }
+
+
+ ProgressableGraphPath shortestPath = navGraph.findPath(origin, destination);
+
+ if (false) { //todo - re-evaluate. 8-way node links may be smooth enough to skip the extra raycast overhead
+ //Trim path by cutting any unnecessary nodes
+ for (int i = 0; i < shortestPath.getCount(); i++) {
+ for (int j = shortestPath.getCount() - 1; j > i + 1; j--) {
+ rayCollided = false;
+ MapStage.getInstance().gdxWorld.rayCast(callback, shortestPath.get(i).pos, shortestPath.get(j).pos);
+ if (!rayCollided) {
+ shortestPath.remove(j - 1);
+ i = 0;
+ j = shortestPath.getCount();
+ }
+ }
+ }
+ }
+
+ if (!originPrecalculated)
+ navGraph.removeVertex(origin);
+ if (!destinationPrecalculated)
+ navGraph.removeVertex(destination);
+ return shortestPath;
+ }
+ catch(Exception e){
+ if (!originPrecalculated && navGraph.lookupIndex(origin) > -1)
+ navGraph.removeVertex(origin);
+ if (!destinationPrecalculated && navGraph.lookupIndex(destination) > -1)
+ navGraph.removeVertex(destination);
+ throw(e);
+ }
+ }
+}
+
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationVertex.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationVertex.java
new file mode 100644
index 00000000000..4a98adf04dd
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/NavigationVertex.java
@@ -0,0 +1,43 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.pfa.Connection;
+import com.badlogic.gdx.math.Vector2;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.ObjectMap;
+
+public class NavigationVertex {
+ public Vector2 pos = Vector2.Zero;
+ public ObjectMap incomingEdges = new ObjectMap<>();
+ public ObjectMap outgoingEdges = new ObjectMap<>();
+ int index = -1;
+
+ public NavigationVertex(Vector2 position) {
+ pos = position;
+ }
+
+ public NavigationVertex(float x, float y) {
+ pos = new Vector2(x, y);
+ }
+
+ public boolean hasEdgeTo(NavigationVertex otherNode) {
+ return incomingEdges.containsKey(otherNode);
+ }
+
+ public Array> getAllConnections() {
+
+ Array> ret = new Array<>();
+
+ for (NavigationEdge e : incomingEdges.values()) {
+ ret.add(e);
+ }
+ for (NavigationEdge e : outgoingEdges.values()) {
+ ret.add(e);
+ }
+ return ret;
+ }
+
+ public void removeEdges(NavigationVertex node) {
+ outgoingEdges.remove(node);
+ incomingEdges.remove(node);
+ }
+}
\ No newline at end of file
diff --git a/forge-gui-mobile/src/forge/adventure/util/pathfinding/ProgressableGraphPath.java b/forge-gui-mobile/src/forge/adventure/util/pathfinding/ProgressableGraphPath.java
new file mode 100644
index 00000000000..dfca02fdfd6
--- /dev/null
+++ b/forge-gui-mobile/src/forge/adventure/util/pathfinding/ProgressableGraphPath.java
@@ -0,0 +1,57 @@
+package forge.adventure.util.pathfinding;
+
+import com.badlogic.gdx.ai.pfa.GraphPath;
+import com.badlogic.gdx.utils.Array;
+
+import java.util.Iterator;
+
+public class ProgressableGraphPath implements GraphPath {
+ public final Array nodes;
+
+ /** Creates a {@code DefaultGraphPath} with no nodes. */
+ public ProgressableGraphPath () {
+ this(new Array());
+ }
+
+ /** Creates a {@code DefaultGraphPath} with the given capacity and no nodes. */
+ public ProgressableGraphPath (int capacity) {
+ this(new Array(capacity));
+ }
+
+ /** Creates a {@code DefaultGraphPath} with the given nodes. */
+ public ProgressableGraphPath (Array nodes) {
+ this.nodes = nodes;
+ }
+
+ @Override
+ public void clear () {
+ nodes.clear();
+ }
+
+ @Override
+ public int getCount () {
+ return nodes.size;
+ }
+
+ @Override
+ public void add (N node) {
+ nodes.add(node);
+ }
+
+ @Override
+ public N get (int index) {
+ return nodes.get(index);
+ }
+
+ @Override
+ public void reverse () {
+ nodes.reverse();
+ }
+
+ @Override
+ public Iterator iterator () {
+ return nodes.iterator();
+ }
+
+ public void remove (int index) { nodes.removeIndex(index);}
+}
diff --git a/forge-gui/res/adventure/common/decks/standard/archivist.dck b/forge-gui/res/adventure/common/decks/standard/archivist.dck
new file mode 100644
index 00000000000..99a13771b8a
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/archivist.dck
@@ -0,0 +1,36 @@
+[metadata]
+Name=Archivist
+[Main]
+3 Ancient Den|BRC|1
+4 Archive Trap|PLIST|1
+3 Archivist|ULG|1
+2 Archivist of Oghma|CLB|2
+2 Automatic Librarian|DMU|1
+4 Bury in Books|J22|1
+4 Compulsive Research|CLB|1
+4 Field Research|ZNR|1
+3 Geist of the Archives|EMN|1
+2 Ghost Quarter|SLD|1
+2 Island|JMP|1
+1 Island|JMP|3
+1 Island|JMP|4
+1 Island|JMP|5
+2 Island|JMP|7
+4 Jace's Archivist|M12|1
+2 Key to the Archive|YMID|1
+3 Magus of the Moat|FUT|1
+2 Oath of Lieges|EXO|1
+2 Ormos, Archive Keeper|JMP|1
+4 Overwhelmed Archivist|MID|1
+1 Plains|JMP|1
+1 Plains|JMP|7
+1 Preston, the Vanisher|J22|1
+4 Razortide Bridge|BRC|1
+4 Seat of the Synod|J22|1
+2 Temple of Enlightenment|SCD|1
+1 Tolarian Academy|VMA|1
+1 Tome of the Infinite|J21|1
+4 Walking Archive|PLIST|1
+2 Winds of Abandon|PLIST|1
+[Sideboard]
+1 Wizard's Spellbook|AFR|1
diff --git a/forge-gui/res/adventure/common/decks/standard/golem_sentinel.dck b/forge-gui/res/adventure/common/decks/standard/golem_sentinel.dck
new file mode 100644
index 00000000000..590395187f1
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/golem_sentinel.dck
@@ -0,0 +1,24 @@
+[metadata]
+Name=Golem Sentinel
+[Main]
+4 Amaranthine Wall|DOM|1
+4 Consulate Skygate|BBD|1
+4 Living Wall|30A|1
+3 Plains|DMU|1
+7 Plains|DMU|2
+6 Plains|DMU|3
+5 Plains|DMU|4
+4 Rolling Stones|STH|1
+4 Secluded Steppe|J21|1
+4 Shield-Wall Sentinel|DMU|1
+4 Stalwart Shield-Bearers|ROE|1
+4 Steel Wall|TD2|1
+1 Sunweb|MIR|1
+4 Walking Bulwark|DMU|1
+4 Wall of Junk|DMR|1
+4 Wall of Spears|DPA|1
+1 Wall of Swords|30A|1
+4 Weathered Sentinels|NCC|1
+4 Wingmantle Chaplain|DMU|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/decks/standard/golem_sentinel_2.dck b/forge-gui/res/adventure/common/decks/standard/golem_sentinel_2.dck
new file mode 100644
index 00000000000..5eb7c1f4285
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/golem_sentinel_2.dck
@@ -0,0 +1,20 @@
+[metadata]
+Name=Golem Sentinel 2
+[Main]
+4 Buried Ruin|ONC|1
+4 Chief of the Foundry|BRC|1
+4 Chronomaton|DDM|1
+4 Cryptic Caves|M20|1
+4 Darksteel Citadel|BRC|1
+4 Desert|AFC|1
+4 Haunted Guardian|AVR|1
+4 Lightning-Core Excavator|J21|1
+4 Locthwain Gargoyle|J22|1
+4 Patchwork Automaton|NEO|1
+4 Steel Overseer|J22|1
+4 Urza's Saga|MH2|1
+4 Walking Ballista|J22|1
+4 Wall of Forgotten Pharaohs|AKR|1
+4 Wall of Junk|DMR|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/decks/standard/golem_sentinel_3.dck b/forge-gui/res/adventure/common/decks/standard/golem_sentinel_3.dck
new file mode 100644
index 00000000000..a105a1f01d5
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/golem_sentinel_3.dck
@@ -0,0 +1,20 @@
+[metadata]
+Name=Golem Sentinel 3
+[Main]
+4 Buried Ruin|ONC|1
+4 Chief of the Foundry|BRC|1
+4 Chronomaton|DDM|1
+4 Cryptic Caves|M20|1
+4 Custodian of the Trove|DTK|1
+4 Darksteel Citadel|BRC|1
+4 Desert|AFC|1
+4 Haunted Guardian|AVR|1
+4 Lightning-Core Excavator|J21|1
+4 Locthwain Gargoyle|J22|1
+4 Patchwork Automaton|NEO|1
+4 Steel Overseer|J22|1
+4 Urza's Saga|MH2|1
+4 Walking Ballista|J22|1
+4 Wall of Forgotten Pharaohs|AKR|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/decks/standard/pirate2.dck b/forge-gui/res/adventure/common/decks/standard/pirate2.dck
new file mode 100644
index 00000000000..245287e0533
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/pirate2.dck
@@ -0,0 +1,34 @@
+[metadata]
+Name=Pirate 2
+[Main]
+2 Abrade|SCD|1
+2 Admiral Beckett Brass|PLIST|1
+4 Aether Hub|KLR|1
+1 Aethersphere Harvester|KLR|1
+1 Canyon Slough|AKR|1
+2 Captain Lannery Storm|J22|1
+1 Censor|J21|1
+3 Chandra, Torch of Defiance|Q06|1
+4 Deadeye Tracker|XLN|1
+4 Dire Fleet Captain|XLN|1
+2 Dire Fleet Ravager|XLN|1
+3 Dragonskull Summit|DMC|1
+2 Dreamcaller Siren|XLN|1
+3 Drowned Catacomb|SLD|1
+4 Fatal Push|F17|1
+3 Fathom Fleet Captain|XLN|1
+2 Fell Flagship|XLN|1
+1 Fetid Pools|AKR|1
+2 Fiery Cannonade|CMR|1
+3 Island|LTR|1
+3 Jace, Cunning Castaway|PS18|1
+3 Kari Zev, Skyship Raider|J22|1
+2 Lightning-Rig Crew|CMR|1
+3 Lookout's Dispersal|J22|1
+2 March of the Drowned|XLN|1
+2 Mountain|WHO|1
+4 Spirebluff Canal|KLR|1
+3 Swamp|LTR|1
+4 Unlicensed Disintegration|KLD|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/decks/standard/pirate3.dck b/forge-gui/res/adventure/common/decks/standard/pirate3.dck
new file mode 100644
index 00000000000..a75fe03feef
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/pirate3.dck
@@ -0,0 +1,29 @@
+[metadata]
+Name=Pirate 3
+[Main]
+4 Captain Lannery Storm|J22|1
+2 Captivating Crew|XLN|1
+4 Chart a Course|JMP|1
+2 Daring Saboteur|NCC|1
+3 Dire Fleet Hoarder|2XM|1
+4 Dragonskull Summit|DMC|1
+2 Dreamcaller Siren|XLN|1
+4 Drowned Catacomb|SLD|1
+2 Evolving Wilds|SIS|1
+4 Fatal Push|F17|1
+4 Fathom Fleet Captain|XLN|1
+2 Fell Flagship|XLN|1
+2 Island|LTR|1
+4 Kitesail Freebooter|XLN|1
+4 Lookout's Dispersal|J22|1
+2 Mountain|WHO|1
+4 Negate|MOM|1
+4 Ruin Raider|XLN|1
+4 Siren Stormtamer|CMR|1
+4 Spirebluff Canal|KLR|1
+2 Swamp|LTR|1
+4 Tezzeret the Schemer|KLR|1
+2 Walk the Plank|XLN|1
+2 Wanted Scoundrels|XLN|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/decks/standard/pirate_captain_2.dck b/forge-gui/res/adventure/common/decks/standard/pirate_captain_2.dck
new file mode 100644
index 00000000000..059aa3ae0bb
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/pirate_captain_2.dck
@@ -0,0 +1,29 @@
+[metadata]
+Name=Pirate Captain 2
+[Main]
+1 Blood Money|CLB|2
+2 Deadeye Tracker|XLN|1
+4 Deadly Derision|MOM|1
+4 Desperate Castaways|MB1|1
+2 Dire Fleet Hoarder|2XM|1
+2 Dire Fleet Interloper|XLN|1
+2 Dire Fleet Poisoner|J21|1
+1 Dire Fleet Ravager|XLN|1
+4 Drain Life|5ED|1
+4 Fathom Fleet Captain|XLN|1
+2 Sleek Schooner|XLN|1
+2 Kitesail Freebooter|XLN|1
+2 March of the Drowned|XLN|1
+1 Marut|CLB|1
+4 Pirate's Cutlass|CMR|1
+1 Pitiless Plunderer|RIX|1
+2 Reckoner Bankbuster|NEO|1
+1 Revel in Riches|XLN|1
+6 Swamp|XLN|1
+8 Swamp|XLN|2
+2 Swamp|XLN|3
+4 Swamp|XLN|4
+1 Treasure Chest|AFR|3
+1 Treasure Vault|AFR|1
+2 Undercity Scrounger|NEO|1
+[Sideboard]
diff --git a/forge-gui/res/adventure/common/decks/standard/spirit.dck b/forge-gui/res/adventure/common/decks/standard/spirit.dck
new file mode 100644
index 00000000000..0d401b34e38
--- /dev/null
+++ b/forge-gui/res/adventure/common/decks/standard/spirit.dck
@@ -0,0 +1,25 @@
+[metadata]
+Name=Spirit
+[Main]
+2 Dreamshackle Geist|DBL|1
+4 Dungeon Geists|DKA|1
+4 Ephara's Dispersal|MOM|1
+2 Geist of the Archives|EMN|1
+10 Island|MOM|1
+4 Island|MOM|2
+4 Island|MOM|3
+1 Kira, Great Glass-Spinner|JMP|1
+4 Lantern Bearer|DBL|1
+1 Latch Seeker|AVR|1
+2 Mirrorhall Mimic|DBL|1
+1 Murmuring Phantasm|JMP|1
+4 Patrician Geist|DBL|1
+4 Shriekgeist|IMA|1
+4 Sinister Sabotage|SCD|1
+1 Stormbound Geist|DKA|1
+4 Supreme Phantom|M19|1
+2 Think Tank|ODY|1
+4 Thoughtbound Phantasm|GRN|1
+4 Tocasia's Dig Site|BRO|1
+[Sideboard]
+
diff --git a/forge-gui/res/adventure/common/maps/map/_template.tmx b/forge-gui/res/adventure/common/maps/map/_template.tmx
new file mode 100644
index 00000000000..36c1b541012
--- /dev/null
+++ b/forge-gui/res/adventure/common/maps/map/_template.tmx
@@ -0,0 +1,38 @@
+
+
diff --git a/forge-gui/res/adventure/common/maps/map/main_story/templeofchandra.tmx b/forge-gui/res/adventure/common/maps/map/main_story/templeofchandra.tmx
index 57fe3a1f26a..ef636fe4d17 100644
--- a/forge-gui/res/adventure/common/maps/map/main_story/templeofchandra.tmx
+++ b/forge-gui/res/adventure/common/maps/map/main_story/templeofchandra.tmx
@@ -1,5 +1,5 @@
-