Files
Mindustry/core/src/mindustry/ai/HierarchyPathFinder.java

766 lines
32 KiB
Java
Raw Normal View History

2023-11-04 14:00:17 -04:00
package mindustry.ai;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
2023-11-04 17:18:53 -04:00
import arc.util.*;
2023-11-04 17:37:29 -04:00
import mindustry.annotations.Annotations.*;
2023-11-04 17:18:53 -04:00
import mindustry.content.*;
2023-11-06 22:51:47 -05:00
import mindustry.core.*;
2023-11-04 14:00:17 -04:00
import mindustry.game.EventType.*;
2023-11-04 17:18:53 -04:00
import mindustry.game.*;
2023-11-04 17:37:29 -04:00
import mindustry.gen.*;
2023-11-04 14:00:17 -04:00
import mindustry.graphics.*;
2023-11-06 22:51:47 -05:00
import mindustry.ui.*;
2023-11-04 14:00:17 -04:00
import static mindustry.Vars.*;
import static mindustry.ai.Pathfinder.*;
2023-11-04 17:18:53 -04:00
//https://webdocs.cs.ualberta.ca/~mmueller/ps/hpastar.pdf
//https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf
2023-11-04 14:00:17 -04:00
public class HierarchyPathFinder{
static final boolean debug = true;
static final int[] offsets = {
1, 0, //right: bottom to top
0, 1, //top: left to right
0, 0, //left: bottom to top
0, 0 //bottom: left to right
};
static final int[] moveDirs = {
0, 1,
1, 0,
0, 1,
1, 0
};
static final int[] nextOffsets = {
1, 0,
0, 1,
-1, 0,
0, -1
};
//maps pathCost -> flattened array of clusters in 2D
Cluster[][] clusters;
int clusterSize = 12;
int cwidth, cheight;
2023-11-06 22:51:47 -05:00
//TODO: make thread-local (they are dereferenced rarely anyway)
2023-11-06 19:21:29 -05:00
static PathfindQueue frontier = new PathfindQueue();
//node index -> total cost
static IntFloatMap costs = new IntFloatMap();
//
static IntSet usedEdges = new IntSet();
2023-11-06 22:51:47 -05:00
static IntSeq bfsQueue = new IntSeq();
2023-11-06 19:21:29 -05:00
static LongSeq tmpEdges = new LongSeq();
//node index (NodeIndex struct) -> node it came from
static IntIntMap cameFrom = new IntIntMap();
2023-11-04 14:00:17 -04:00
public HierarchyPathFinder(){
Events.on(WorldLoadEvent.class, event -> {
//TODO 5 path costs, arbitrary number
clusters = new Cluster[5][];
clusterSize = 12; //TODO arbitrary
cwidth = Mathf.ceil((float)world.width() / clusterSize);
cheight = Mathf.ceil((float)world.height() / clusterSize);
2023-11-04 17:37:29 -04:00
for(int cy = 0; cy < cwidth; cy++){
for(int cx = 0; cx < cheight; cx++){
2023-11-04 14:00:17 -04:00
createCluster(Team.sharded.id, costGround, cx, cy);
}
}
});
//TODO very inefficient, this is only for debugging
Events.on(TileChangeEvent.class, e -> {
createCluster(Team.sharded.id, costGround, e.tile.x / clusterSize, e.tile.y / clusterSize);
});
if(debug){
Events.run(Trigger.draw, () -> {
int team = Team.sharded.id;
int cost = costGround;
if(clusters == null || clusters[cost] == null) return;
Draw.draw(Layer.overlayUI, () -> {
Lines.stroke(1f);
for(int cx = 0; cx < cwidth; cx++){
for(int cy = 0; cy < cheight; cy++){
var cluster = clusters[cost][cy * cwidth + cx];
if(cluster != null){
2023-11-06 22:51:47 -05:00
Lines.stroke(0.5f);
Draw.color(Color.gray);
Lines.stroke(1f);
2023-11-04 14:00:17 -04:00
Lines.rect(cx * clusterSize * tilesize - tilesize/2f, cy * clusterSize * tilesize - tilesize/2f, clusterSize * tilesize, clusterSize * tilesize);
2023-11-04 17:37:29 -04:00
2023-11-04 14:00:17 -04:00
for(int d = 0; d < 4; d++){
IntSeq portals = cluster.portals[d];
if(portals != null){
for(int i = 0; i < portals.size; i++){
int pos = portals.items[i];
int from = Point2.x(pos), to = Point2.y(pos);
float width = tilesize * (Math.abs(from - to) + 1), height = tilesize;
2023-11-04 17:37:29 -04:00
portalToVec(cluster, cx, cy, d, i, Tmp.v1);
2023-11-06 22:51:47 -05:00
Draw.color(Color.brown);
2023-11-04 17:37:29 -04:00
Lines.ellipse(30, Tmp.v1.x, Tmp.v1.y, width / 2f, height / 2f, d * 90f - 90f);
LongSeq connections = cluster.portalConnections[d] == null ? null : cluster.portalConnections[d][i];
if(connections != null){
2023-11-06 22:51:47 -05:00
Draw.color(Color.forest);
2023-11-04 17:37:29 -04:00
for(int coni = 0; coni < connections.size; coni ++){
long con = connections.items[coni];
portalToVec(cluster, cx, cy, IntraEdge.dir(con), IntraEdge.portal(con), Tmp.v2);
2023-11-04 14:00:17 -04:00
2023-11-04 17:37:29 -04:00
float
x1 = Tmp.v1.x, y1 = Tmp.v1.y,
x2 = Tmp.v2.x, y2 = Tmp.v2.y,
mx = (cx * clusterSize + clusterSize / 2f) * tilesize, my = (cy * clusterSize + clusterSize / 2f) * tilesize;
//Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20);
Lines.line(x1, y1, x2, y2);
2023-11-04 14:00:17 -04:00
2023-11-04 17:37:29 -04:00
}
}
2023-11-04 14:00:17 -04:00
}
}
}
2023-11-04 17:18:53 -04:00
2023-11-04 17:37:29 -04:00
//TODO draw connections.
/*
2023-11-04 17:18:53 -04:00
Draw.color(Color.magenta);
for(var con : cluster.cons){
float
x1 = Point2.x(con.posFrom) * tilesize, y1 = Point2.y(con.posFrom) * tilesize,
x2 = Point2.x(con.posTo) * tilesize, y2 = Point2.y(con.posTo) * tilesize,
mx = (cx * clusterSize + clusterSize/2f) * tilesize, my = (cy * clusterSize + clusterSize/2f) * tilesize;
//Lines.curve(x1, y1, mx, my, mx, my, x2, y2, 20);
Lines.line(x1, y1, x2, y2);
2023-11-04 17:37:29 -04:00
}*/
2023-11-04 14:00:17 -04:00
}
}
}
2023-11-06 22:51:47 -05:00
Lines.stroke(3f);
Draw.color(Color.orange);
int node = findClosestNode(Team.sharded.id, 0, player.tileX(), player.tileY());
int dest = findClosestNode(Team.sharded.id, 0, World.toTile(Core.input.mouseWorldX()), World.toTile(Core.input.mouseWorldY()));
if(node != Integer.MAX_VALUE && dest != Integer.MAX_VALUE){
var result = clusterAstar(0, node, dest);
if(result != null){
2023-11-07 00:11:44 -05:00
for(int i = -1; i < result.size - 1; i++){
int endCluster = NodeIndex.cluster(result.items[i + 1]);
int cx = endCluster % cwidth, cy = endCluster / cwidth;
int[] field = flowField(0, cx, cy, 0, dest);
for(int y = 0; y < clusterSize; y++){
for(int x = 0; x < clusterSize; x++){
int value = field[x + y *clusterSize];
Tmp.c1.a = 1f;
Lines.stroke(0.8f, Tmp.c1.fromHsv(value * 3f, 1f, 1f));
Draw.alpha(0.5f);
Lines.rect((x + cx * clusterSize) * tilesize - tilesize/2f, (y + cy * clusterSize) * tilesize - tilesize/2f, tilesize, tilesize);
}
}
}
Lines.stroke(3f);
Draw.color(Color.orange);
2023-11-06 22:51:47 -05:00
for(int i = -1; i < result.size - 1; i++){
int current = i == -1 ? node : result.items[i], next = result.items[i + 1];
portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), Tmp.v1);
portalToVec(0, NodeIndex.cluster(next), NodeIndex.dir(next), NodeIndex.portal(next), Tmp.v2);
Lines.line(Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y);
}
2023-11-07 00:11:44 -05:00
//flowField(0, )
2023-11-06 22:51:47 -05:00
}
nodeToVec(dest, Tmp.v1);
Fonts.outline.draw(clusterNodeHeuristic(0, node, dest) + "", Tmp.v1.x, Tmp.v1.y);
}
2023-11-04 14:00:17 -04:00
Draw.reset();
});
});
}
}
2023-11-06 22:51:47 -05:00
Vec2 nodeToVec(int current, Vec2 out){
portalToVec(0, NodeIndex.cluster(current), NodeIndex.dir(current), NodeIndex.portal(current), out);
return out;
}
void portalToVec(int pathCost, int cluster, int direction, int portalIndex, Vec2 out){
portalToVec(clusters[pathCost][cluster], cluster % cwidth, cluster / cwidth, direction, portalIndex, out);
}
void portalToVec(Cluster cluster, int cx, int cy, int direction, int portalIndex, Vec2 out){
int pos = cluster.portals[direction].items[portalIndex];
2023-11-04 17:37:29 -04:00
int from = Point2.x(pos), to = Point2.y(pos);
2023-11-06 22:51:47 -05:00
int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1];
2023-11-04 17:37:29 -04:00
float average = (from + to) / 2f;
float
2023-11-06 22:51:47 -05:00
x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1) + nextOffsets[direction * 2] / 2f) * tilesize,
y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1) + nextOffsets[direction * 2 + 1] / 2f) * tilesize;
2023-11-04 17:37:29 -04:00
out.set(x, y);
}
2023-11-04 14:00:17 -04:00
void createCluster(int team, int pathCost, int cx, int cy){
if(clusters[pathCost] == null) clusters[pathCost] = new Cluster[cwidth * cheight];
Cluster cluster = clusters[pathCost][cy * cwidth + cx];
if(cluster == null){
cluster = clusters[pathCost][cy * cwidth + cx] = new Cluster();
}else{
//reset data
for(var p : cluster.portals){
p.clear();
}
}
2023-11-04 17:37:29 -04:00
//clear all connections, since portals changed, they need to be recomputed.
cluster.portalConnections = new LongSeq[4][];
2023-11-04 17:18:53 -04:00
//TODO: other cluster inner edges should be recomputed if changed.
2023-11-04 14:00:17 -04:00
//TODO look it up based on number.
PathCost cost = ControlPathfinder.costGround;
for(int direction = 0; direction < 4; direction++){
int otherX = cx + Geometry.d4x(direction), otherY = cy + Geometry.d4y(direction);
//out of bounds, no portals in this direction
if(otherX < 0 || otherY < 0 || otherX >= cwidth || otherY >= cheight){
continue;
}
Cluster other = clusters[pathCost][otherX + otherY * cwidth];
IntSeq portals;
if(other == null){
//create new portals at direction
2023-11-04 17:18:53 -04:00
portals = cluster.portals[direction] = new IntSeq(4);
2023-11-04 14:00:17 -04:00
}else{
//share portals with the other cluster
portals = cluster.portals[direction] = other.portals[(direction + 2) % 4];
2023-11-04 17:37:29 -04:00
//clear the portals, they're being recalculated now
portals.clear();
2023-11-04 14:00:17 -04:00
}
int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1];
int
baseX = cx * clusterSize + offsets[direction * 2] * (clusterSize - 1),
baseY = cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1),
nextBaseX = baseX + Geometry.d4[direction].x,
nextBaseY = baseY + Geometry.d4[direction].y;
int lastPortal = -1;
boolean prevSolid = true;
for(int i = 0; i < clusterSize; i++){
int x = baseX + addX * i, y = baseY + addY * i;
//scan for portals
if(solid(team, cost, x, y) || solid(team, cost, nextBaseX + addX * i, nextBaseY + addY * i)){
int previous = i - 1;
//hit a wall, create portals between the two points
if(!prevSolid && previous >= lastPortal){
//portals are an inclusive range
portals.add(Point2.pack(previous, lastPortal));
}
prevSolid = true;
}else{
//empty area encountered, mark the location of portal start
if(prevSolid){
lastPortal = i;
}
prevSolid = false;
}
}
//at the end of the loop, close any un-initialized portals; this is copy pasted code
int previous = clusterSize - 1;
if(!prevSolid && previous >= lastPortal){
//portals are an inclusive range
portals.add(Point2.pack(previous, lastPortal));
}
}
2023-11-04 17:18:53 -04:00
connectInnerEdges(cx, cy, team, cost, cluster);
}
void connectInnerEdges(int cx, int cy, int team, PathCost cost, Cluster cluster){
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
usedEdges.clear();
//TODO: how the hell to identify a vertex?
//cluster (i16) | direction (i2) | index (i14)
2023-11-06 22:51:47 -05:00
//TODO: clear portal connections
2023-11-04 17:37:29 -04:00
2023-11-04 17:18:53 -04:00
for(int direction = 0; direction < 4; direction++){
var portals = cluster.portals[direction];
if(portals == null) continue;
int addX = moveDirs[direction * 2], addY = moveDirs[direction * 2 + 1];
for(int i = 0; i < portals.size; i++){
usedEdges.add(Point2.pack(direction, i));
int
portal = portals.items[i],
from = Point2.x(portal), to = Point2.y(portal),
average = (from + to) / 2,
x = (addX * average + cx * clusterSize + offsets[direction * 2] * (clusterSize - 1)),
y = (addY * average + cy * clusterSize + offsets[direction * 2 + 1] * (clusterSize - 1));
for(int otherDir = 0; otherDir < 4; otherDir++){
var otherPortals = cluster.portals[otherDir];
2023-11-04 17:37:29 -04:00
if(otherPortals == null) continue;
2023-11-04 17:18:53 -04:00
for(int j = 0; j < otherPortals.size; j++){
//TODO redundant calculations?
if(!usedEdges.contains(Point2.pack(otherDir, j))){
int
other = otherPortals.items[j],
otherFrom = Point2.x(other), otherTo = Point2.y(other),
otherAverage = (otherFrom + otherTo) / 2,
ox = cx * clusterSize + offsets[otherDir * 2] * (clusterSize - 1),
oy = cy * clusterSize + offsets[otherDir * 2 + 1] * (clusterSize - 1),
otherX = (moveDirs[otherDir * 2] * otherAverage + ox),
otherY = (moveDirs[otherDir * 2 + 1] * otherAverage + oy);
2023-11-06 22:51:47 -05:00
//duplicate portal; should never happen.
2023-11-04 17:18:53 -04:00
if(Point2.pack(x, y) == Point2.pack(otherX, otherY)){
2023-11-04 17:37:29 -04:00
continue;
2023-11-04 17:18:53 -04:00
}
2023-11-04 17:37:29 -04:00
float connectionCost = innerAstar(
2023-11-06 19:21:29 -05:00
team, cost,
minX, minY, maxX, maxY,
x + y * wwidth,
otherX + otherY * wwidth,
(moveDirs[otherDir * 2] * otherFrom + ox),
(moveDirs[otherDir * 2 + 1] * otherFrom + oy),
(moveDirs[otherDir * 2] * otherTo + ox),
(moveDirs[otherDir * 2 + 1] * otherTo + oy)
2023-11-04 17:18:53 -04:00
);
if(connectionCost != -1f){
2023-11-04 17:37:29 -04:00
if(cluster.portalConnections[direction] == null) cluster.portalConnections[direction] = new LongSeq[cluster.portals[direction].size];
if(cluster.portalConnections[otherDir] == null) cluster.portalConnections[otherDir] = new LongSeq[cluster.portals[otherDir].size];
if(cluster.portalConnections[direction][i] == null) cluster.portalConnections[direction][i] = new LongSeq(8);
if(cluster.portalConnections[otherDir][j] == null) cluster.portalConnections[otherDir][j] = new LongSeq(8);
2023-11-04 17:18:53 -04:00
2023-11-04 17:37:29 -04:00
//TODO: can there be duplicate edges??
cluster.portalConnections[direction][i].add(IntraEdge.get(otherDir, j, connectionCost));
cluster.portalConnections[otherDir][j].add(IntraEdge.get(direction, i, connectionCost));
2023-11-04 17:18:53 -04:00
}
}
}
}
}
}
}
//distance heuristic: manhattan
private static float heuristic(int a, int b){
int x = a % wwidth, x2 = b % wwidth, y = a / wwidth, y2 = b / wwidth;
return Math.abs(x - x2) + Math.abs(y - y2);
}
private static int tcost(int team, PathCost cost, int tilePos){
return cost.getCost(team, pathfinder.tiles[tilePos]);
}
private static float tileCost(int team, PathCost type, int a, int b){
//currently flat cost
return cost(team, type, b);
}
/** @return -1 if no path was found */
2023-11-04 17:37:29 -04:00
float innerAstar(int team, PathCost cost, int minX, int minY, int maxX, int maxY, int startPos, int goalPos, int goalX1, int goalY1, int goalX2, int goalY2){
2023-11-04 17:18:53 -04:00
frontier.clear();
costs.clear();
2023-11-06 22:51:47 -05:00
//TODO: this can be faster and more memory efficient by making costs a NxN array... probably?
2023-11-04 17:18:53 -04:00
costs.put(startPos, 0);
frontier.add(startPos, 0);
if(debug && false){
Fx.debugLine.at(Point2.x(startPos) * tilesize, Point2.y(startPos) * tilesize, 0f, Color.purple,
new Vec2[]{new Vec2(Point2.x(startPos), Point2.y(startPos)).scl(tilesize), new Vec2(Point2.x(goalPos), Point2.y(goalPos)).scl(tilesize)});
}
while(frontier.size > 0){
int current = frontier.poll();
int cx = current % wwidth, cy = current / wwidth;
//found the goal (it's in the portal rectangle)
//TODO portal rectangle approach does not work.
if((cx >= goalX1 && cy >= goalY1 && cx <= goalX2 && cy <= goalY2) || current == goalPos){
return costs.get(current);
}
for(Point2 point : Geometry.d4){
int newx = cx + point.x, newy = cy + point.y;
int next = newx + wwidth * newy;
if(newx > maxX || newy > maxY || newx < minX || newy < minY) continue;
//TODO fallback mode for enemy walls or whatever
if(tcost(team, cost, next) == impassable) continue;
float add = tileCost(team, cost, current, next);
if(add < 0) continue;
2023-11-06 19:21:29 -05:00
float newCost = costs.get(current) + add;
2023-11-04 17:18:53 -04:00
2023-11-06 19:21:29 -05:00
if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){
2023-11-04 17:18:53 -04:00
costs.put(next, newCost);
float priority = newCost + heuristic(next, goalPos);
frontier.add(next, priority);
}
}
}
return -1f;
2023-11-04 14:00:17 -04:00
}
2023-11-06 19:21:29 -05:00
int makeNodeIndex(int cx, int cy, int dir, int portal){
//to make sure there's only one way to refer to each node, the direction must be 0 or 1 (referring to portals on the top or right edge)
//direction can only be 2 if cluster X is 0 (left edge of map)
if(dir == 2 && cx != 0){
dir = 0;
cx --;
}
//direction can only be 3 if cluster Y is 0 (bottom edge of map)
if(dir == 3 && cy != 0){
dir = 1;
cy --;
}
2023-11-06 22:51:47 -05:00
2023-11-06 19:21:29 -05:00
return NodeIndex.get(cx + cy * cwidth, dir, portal);
}
2023-11-07 00:11:44 -05:00
//uses A* to find the closest node index to specified coordinates
2023-11-06 22:51:47 -05:00
//this node is used in cluster A*
/** @return MAX_VALUE if no node is found */
private int findClosestNode(int team, int pathCost, int tileX, int tileY){
int cx = tileX / clusterSize, cy = tileY / clusterSize;
if(cx < 0 || cy < 0 || cx >= cwidth || cy >= cheight){
return Integer.MAX_VALUE;
}
//TODO
PathCost cost = ControlPathfinder.costGround;
Cluster cluster = clusters[pathCost][cx + cy * cwidth];
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
int bestPortalPair = Integer.MAX_VALUE;
float bestCost = Float.MAX_VALUE;
if(cluster != null){ //TODO create on demand??
//A* to every node, find the best one (I know there's a better algorithm for this, probably dijkstra)
for(int dir = 0; dir < 4; dir++){
var portals = cluster.portals[dir];
if(portals == null) continue;
for(int j = 0; j < portals.size; j++){
int
other = portals.items[j],
otherFrom = Point2.x(other), otherTo = Point2.y(other),
otherAverage = (otherFrom + otherTo) / 2,
ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1),
oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1),
otherX = (moveDirs[dir * 2] * otherAverage + ox),
otherY = (moveDirs[dir * 2 + 1] * otherAverage + oy);
float connectionCost = innerAstar(
team, cost,
minX, minY, maxX, maxY,
tileX + tileY * wwidth,
otherX + otherY * wwidth,
2023-11-07 00:11:44 -05:00
//TODO these are wrong and never actually trigger
2023-11-06 22:51:47 -05:00
(moveDirs[dir * 2] * otherFrom + ox),
(moveDirs[dir * 2 + 1] * otherFrom + oy),
(moveDirs[dir * 2] * otherTo + ox),
(moveDirs[dir * 2 + 1] * otherTo + oy)
);
//better cost found, update and return
if(connectionCost != -1f && connectionCost < bestCost){
bestPortalPair = Point2.pack(dir, j);
bestCost = connectionCost;
}
}
}
if(bestPortalPair != Integer.MAX_VALUE){
return makeNodeIndex(cx, cy, Point2.x(bestPortalPair), Point2.y(bestPortalPair));
}
}
return Integer.MAX_VALUE;
}
2023-11-06 19:21:29 -05:00
//distance heuristic: manhattan
private float clusterNodeHeuristic(int pathCost, int nodeA, int nodeB){
int
clusterA = NodeIndex.cluster(nodeA),
dirA = NodeIndex.dir(nodeA),
portalA = NodeIndex.portal(nodeA),
clusterB = NodeIndex.cluster(nodeB),
dirB = NodeIndex.dir(nodeB),
2023-11-06 22:51:47 -05:00
portalB = NodeIndex.portal(nodeB),
rangeA = clusters[pathCost][clusterA].portals[dirA].items[portalA],
rangeB = clusters[pathCost][clusterB].portals[dirB].items[portalB];
2023-11-06 19:21:29 -05:00
float
averageA = (Point2.x(rangeA) + Point2.y(rangeA)) / 2f,
x1 = (moveDirs[dirA * 2] * averageA + (clusterA % cwidth) * clusterSize + offsets[dirA * 2] * (clusterSize - 1) + nextOffsets[dirA * 2] / 2f),
y1 = (moveDirs[dirA * 2 + 1] * averageA + (clusterA / cwidth) * clusterSize + offsets[dirA * 2 + 1] * (clusterSize - 1) + nextOffsets[dirA * 2 + 1] / 2f),
averageB = (Point2.x(rangeB) + Point2.y(rangeB)) / 2f,
x2 = (moveDirs[dirB * 2] * averageB + (clusterB % cwidth) * clusterSize + offsets[dirB * 2] * (clusterSize - 1) + nextOffsets[dirB * 2] / 2f),
y2 = (moveDirs[dirB * 2 + 1] * averageB + (clusterB / cwidth) * clusterSize + offsets[dirB * 2 + 1] * (clusterSize - 1) + nextOffsets[dirB * 2 + 1] / 2f);
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
@Nullable IntSeq clusterAstar(int pathCost, int startNodeIndex, int endNodeIndex){
2023-11-06 22:51:47 -05:00
var v1 = nodeToVec(startNodeIndex, Tmp.v1);
var v2 = nodeToVec(endNodeIndex, Tmp.v2);
Fx.placeBlock.at(v1.x, v1.y, 1);
Fx.placeBlock.at(v2.x, v2.y, 1);
if(startNodeIndex == endNodeIndex){
//TODO alloc
return IntSeq.with(startNodeIndex);
}
2023-11-06 19:21:29 -05:00
frontier.clear();
costs.clear();
2023-11-06 22:51:47 -05:00
cameFrom.clear();
2023-11-06 19:21:29 -05:00
2023-11-06 22:51:47 -05:00
cameFrom.put(startNodeIndex, startNodeIndex);
2023-11-06 19:21:29 -05:00
costs.put(startNodeIndex, 0);
2023-11-06 22:51:47 -05:00
frontier.add(startNodeIndex, 0);
2023-11-06 19:21:29 -05:00
boolean foundEnd = false;
while(frontier.size > 0){
int current = frontier.poll();
if(current == endNodeIndex){
foundEnd = true;
break;
}
int cluster = NodeIndex.cluster(current), dir = NodeIndex.dir(current), portal = NodeIndex.portal(current);
2023-11-06 22:51:47 -05:00
int cx = cluster % cwidth, cy = cluster / cwidth;
2023-11-06 19:21:29 -05:00
Cluster clust = clusters[pathCost][cluster];
2023-11-06 22:51:47 -05:00
LongSeq innerCons = clust.portalConnections[dir] == null || portal >= clust.portalConnections[dir].length ? null : clust.portalConnections[dir][portal];
2023-11-06 19:21:29 -05:00
//edges for the cluster the node is 'in'
if(innerCons != null){
2023-11-06 23:02:26 -05:00
checkEdges(pathCost, current, endNodeIndex, cx, cy, innerCons);
2023-11-06 19:21:29 -05:00
}
2023-11-06 22:51:47 -05:00
//edges that this node 'faces' from the other side
2023-11-06 19:21:29 -05:00
int nextCx = cx + Geometry.d4[dir].x, nextCy = cy + Geometry.d4[dir].y;
if(nextCx >= 0 && nextCy >= 0 && nextCx < cwidth && nextCy < cheight){
int nextClusteri = nextCx + nextCy * cwidth;
Cluster nextCluster = clusters[pathCost][nextClusteri];
int relativeDir = (dir + 2) % 4;
LongSeq outerCons = nextCluster.portalConnections[relativeDir] == null ? null : nextCluster.portalConnections[relativeDir][portal];
if(outerCons != null){
2023-11-06 23:02:26 -05:00
checkEdges(pathCost, current, endNodeIndex, nextCx, nextCy, outerCons);
2023-11-06 19:21:29 -05:00
}
}
}
if(foundEnd){
IntSeq result = new IntSeq();
int cur = endNodeIndex;
while(cur != startNodeIndex){
result.add(cur);
cur = cameFrom.get(cur);
}
result.reverse();
return result;
}
return null;
}
2023-11-06 22:51:47 -05:00
static void line(Vec2 a, Vec2 b){
Fx.debugLine.at(a.x, a.y, 0f, Color.blue.cpy().a(0.1f), new Vec2[]{a.cpy(), b.cpy()});
}
2023-11-06 23:02:26 -05:00
void checkEdges(int pathCost, int current, int goal, int cx, int cy, LongSeq connections){
2023-11-06 19:21:29 -05:00
for(int i = 0; i < connections.size; i++){
long con = connections.items[i];
float cost = IntraEdge.cost(con);
int otherDir = IntraEdge.dir(con), otherPortal = IntraEdge.portal(con);
int next = makeNodeIndex(cx, cy, otherDir, otherPortal);
float newCost = costs.get(current) + cost;
if(newCost < costs.get(next, Float.POSITIVE_INFINITY)){
costs.put(next, newCost);
2023-11-06 22:51:47 -05:00
2023-11-06 23:02:26 -05:00
frontier.add(next, newCost + clusterNodeHeuristic(pathCost, next, goal));
2023-11-06 19:21:29 -05:00
cameFrom.put(next, current);
2023-11-06 22:51:47 -05:00
//TODO debug
line(nodeToVec(current, Tmp.v1), nodeToVec(next, Tmp.v2));
2023-11-06 19:21:29 -05:00
}
}
}
2023-11-07 00:11:44 -05:00
//both nodes must be inside the same flow field (cx, cy)
int[] flowField(int pathCost, int cx, int cy, int nodeFrom, int nodeTo){
Cluster cluster = clusters[pathCost][cx + cy * cwidth];
int[] weights = new int[clusterSize * clusterSize];
byte[] searches = new byte[clusterSize * clusterSize];
PathCost pcost = ControlPathfinder.costGround;
int team = Team.sharded.id;
IntQueue frontier = new IntQueue();
int search = 1;
//TODO actually set up the frontier and destinations (where are you going?)
{
int
dir = NodeIndex.dir(nodeTo),
other = cluster.portals[dir].items[NodeIndex.portal(nodeTo)],
otherFrom = Point2.x(other), otherTo = Point2.y(other),
ox = cx * clusterSize + offsets[dir * 2] * (clusterSize - 1),
oy = cy * clusterSize + offsets[dir * 2 + 1] * (clusterSize - 1),
maxX = (moveDirs[dir * 2] * otherFrom + ox),
maxY = (moveDirs[dir * 2 + 1] * otherFrom + oy),
minX = (moveDirs[dir * 2] * otherTo + ox),
minY = (moveDirs[dir * 2 + 1] * otherTo + oy)
;
//TODO: being zero INSIDE the cluster means that the unit will stop at the edge and not move between clusters - bad!
for(int x = minX; x <= maxX; x++){
for(int y = minY; y <= maxY; y++){
frontier.addFirst(x + y * wwidth);
}
}
}
int minX = cx * clusterSize, minY = cy * clusterSize, maxX = Math.min(minX + clusterSize - 1, wwidth - 1), maxY = Math.min(minY + clusterSize - 1, wheight - 1);
//TODO spread this out across many frames
while(frontier.size > 0){
int tile = frontier.removeLast();
int baseX = tile % wwidth, baseY = tile / wwidth;
int cost = weights[(baseX - minX) + (baseY - minY) * clusterSize];
if(cost != impassable){
for(Point2 point : Geometry.d4){
int dx = baseX + point.x, dy = baseY + point.y;
if(dx < minX || dy < minY || dx > maxX || dy > maxY) continue;
int newPos = tile + point.x + point.y * wwidth;
int newPosArray = (dx - minX) + (dy - minY) * clusterSize;
int otherCost = pcost.getCost(team, pathfinder.tiles[newPos]);
if((weights[newPosArray] > cost + otherCost || searches[newPosArray] < search) && otherCost != impassable){
frontier.addFirst(newPos);
weights[newPosArray] = cost + otherCost;
searches[newPosArray] = (byte)search;
}
}
}
}
return weights;
2023-11-04 14:00:17 -04:00
}
private static boolean solid(int team, PathCost type, int x, int y){
return x < 0 || y < 0 || x >= wwidth || y >= wheight || solid(team, type, x + y * wwidth, true);
}
private static boolean solid(int team, PathCost type, int tilePos, boolean checkWall){
int cost = cost(team, type, tilePos);
return cost == impassable || (checkWall && cost >= 6000);
}
private static int cost(int team, PathCost cost, int tilePos){
if(state.rules.limitMapArea && !Team.get(team).isAI()){
int x = tilePos % wwidth, y = tilePos / wwidth;
if(x < state.rules.limitX || y < state.rules.limitY || x > state.rules.limitX + state.rules.limitWidth || y > state.rules.limitY + state.rules.limitHeight){
return impassable;
}
}
return cost.getCost(team, pathfinder.tiles[tilePos]);
}
static class Cluster{
IntSeq[] portals = new IntSeq[4];
2023-11-04 17:37:29 -04:00
//maps rotation + index of portal to list of IntraEdge objects
LongSeq[][] portalConnections = new LongSeq[4][];
2023-11-04 17:18:53 -04:00
}
2023-11-04 14:00:17 -04:00
2023-11-04 17:37:29 -04:00
@Struct
static class IntraEdgeStruct{
@StructField(8)
int dir;
@StructField(8)
int portal;
2023-11-04 14:00:17 -04:00
2023-11-04 17:37:29 -04:00
float cost;
2023-11-04 14:00:17 -04:00
}
2023-11-06 19:21:29 -05:00
@Struct
static class NodeIndexStruct{
@StructField(22)
int cluster;
@StructField(2)
int dir;
@StructField(8)
int portal;
}
2023-11-04 14:00:17 -04:00
}