package net.alcuria.umbracraft.engine;
import net.alcuria.umbracraft.Config;
import net.alcuria.umbracraft.Game;
import net.alcuria.umbracraft.engine.components.DirectedInputComponent;
import net.alcuria.umbracraft.engine.entities.Entity;
import net.alcuria.umbracraft.engine.map.Map;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Array;
/** Handles pathfinding through a {@link DirectedInputComponent}
* @author Andrew Keturi */
public class Pathfinder {
public static class PathNode implements Comparable<PathNode> {
public PathNode parent;
/* g = dist from start, h=dist from end, f= g+h */
public int x, y, f, g, h;
public PathNode(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int compareTo(PathNode other) {
return f - other.f;
}
public void draw(Color color, int altitude, SpriteBatch batch) {
batch.setColor(color);
final int w = Config.tileWidth;
// Game.debug(color + " " + x + " " + y);
batch.draw(Game.assets().get("debug.png", Texture.class), x * w, y * w + altitude * w, w, w);
batch.setColor(Color.WHITE);
}
/** @param other another {@link PathNode}
* @return <code>true</code> if the two nodes are pointing to the same
* location */
public boolean hasSameLocationAs(final PathNode other) {
return other.x == x && other.y == y;
}
@Override
public String toString() {
return String.format("(%d, %d)", x, y);
}
}
private final Array<PathNode> closed = new Array<PathNode>();
private final DirectedInputComponent component;
private PathNode destination, source;
private final Array<PathNode> open = new Array<PathNode>();
private final Array<PathNode> solution = new Array<PathNode>();
private final boolean threaded = false;
public Pathfinder(DirectedInputComponent component) {
this.component = component;
}
public Array<PathNode> getSolution() {
return solution;
}
private boolean isTraversible(Map map, PathNode cur, int destX, int destY) {
if (map.isStairs(cur.x, cur.y) || map.isStairs(destX, destY)) {
// stair to stair is ok only for 4-way movement
return (cur.x == destX || cur.y == destY);
}
//if diagonal, ensure adjacent tiles are not drops
if (cur.x != destX && cur.y != destY) {
return map.getAltitudeAt(destX, destY) == map.getAltitudeAt(cur.x, cur.y) && map.getAltitudeAt(destX, destY) == map.getAltitudeAt(destX, cur.y) && map.getAltitudeAt(destX, destY) == map.getAltitudeAt(cur.x, destY);
}
return (map.getAltitudeAt(destX, destY) - 1) <= map.getAltitudeAt(cur.x, cur.y);
}
/** Searches a list for a pathNode that matches the given x,y coordinates
* @param list a list of {@link PathNode} objects
* @param x the tile x coords
* @param y the tile y coords
* @return */
private boolean listContains(Array<PathNode> list, int x, int y) {
if (list == null) {
throw new NullPointerException("List cannot be null");
}
for (int i = 0; i < list.size; i++) {
if (list.get(i).x == x && list.get(i).y == y) {
return true;
}
}
return false;
}
public void renderPaths() {
for (PathNode n : open) {
n.draw(Color.GREEN, Game.map().getAltitudeAt(n.x, n.y), Game.batch());
}
for (PathNode n : closed) {
n.draw(Color.RED, Game.map().getAltitudeAt(n.x, n.y), Game.batch());
}
if (source != null) {
source.draw(Color.YELLOW, Game.map().getAltitudeAt(source.x, source.y), Game.batch());
}
if (destination != null) {
destination.draw(Color.MAGENTA, Game.map().getAltitudeAt(destination.x, destination.y), Game.batch());
}
if (solution != null) {
for (PathNode n : solution) {
n.draw(Color.CYAN, Game.map().getAltitudeAt(n.x, n.y), Game.batch());
}
}
}
/** Sets a destination to attempt to find a path to
* @param source the starting {@link PathNode}
* @param destination the ending {@link PathNode} */
public void setTarget(PathNode source, PathNode destination) {
if (source == null) {
throw new NullPointerException("source cannot be null");
}
if (destination == null) {
throw new NullPointerException("destination cannot be null");
}
if (source.hasSameLocationAs(destination)) {
Game.log("Target is already at destination");
return;
}
// clear out the lists
open.clear();
closed.clear();
// set source and dest
this.source = source;
this.destination = destination;
this.source.f = Heuristic.calculateFCost(source, destination, source);
if (threaded) {
new Thread("Pathfinder") {
@Override
public void run() {
solve();
};
}.start();
} else {
solve();
}
}
private void solve() {
final Map map = Game.map();
final int[] dX = { 0, 1, 1, 1, 0, -1, -1, -1 }; // clockwise, from 12 oclock
final int[] dY = { 1, 1, 0, -1, -1, -1, 0, 1 };
open.add(source);
while (true) {
// ensure we still have open nodes
if (open.size <= 0) {
Game.debug("No path found");
break;
}
// get a pointer to the node in the open list with the lowest f cost
open.sort();
PathNode cur = open.removeIndex(0);
closed.add(cur);
if (cur.hasSameLocationAs(destination)) {
Game.debug("Path found!");
solution.clear();
while (cur != null) {
solution.add(cur);
cur = cur.parent;
}
break;
}
// foreach current node neighbor
for (int i = 0; i < dX.length; i++) {
// if neighbor is not traversible or neighbor is in closed, skip it
if (!map.isInBounds(cur.x + dX[i], cur.y + dY[i]) || !isTraversible(map, cur, cur.x + dX[i], cur.y + dY[i]) || listContains(closed, cur.x + dX[i], cur.y + dY[i])) {
continue;
}
PathNode neighbor = new PathNode(cur.x + dX[i], cur.y + dY[i]);
// if new path to neighbor is shorter or not in open
if (neighbor.f < cur.f || !listContains(open, neighbor.x, neighbor.y)) {
// set f cost of nbr
neighbor.f = Heuristic.calculateFCost(source, destination, neighbor);
// set parent of neighbor to current
neighbor.parent = cur;
if (!listContains(open, neighbor.x, neighbor.y)) {
open.add(neighbor);
}
}
}
}
}
public void stop() {
open.clear();
solution.clear();
closed.clear();
}
public void update(Entity entity) {
// if (Gdx.input.isKeyJustPressed(Keys.ENTER)) {
// Entity hero = Game.entities().find(Entity.PLAYER);
// setTarget(new PathNode((int) (entity.position.x / Config.tileWidth), (int) (entity.position.y / Config.tileWidth)), new PathNode((int) (hero.position.x / Config.tileWidth), (int) (hero.position.y / Config.tileWidth)));
// }
}
}