Unit pathfinding
This commit is contained in:
@@ -232,6 +232,7 @@ public class Vars implements Loadable{
|
||||
public static WaveSpawner spawner;
|
||||
public static BlockIndexer indexer;
|
||||
public static Pathfinder pathfinder;
|
||||
public static ControlPathfinder controlPath;
|
||||
|
||||
public static Control control;
|
||||
public static Logic logic;
|
||||
@@ -298,6 +299,7 @@ public class Vars implements Loadable{
|
||||
spawner = new WaveSpawner();
|
||||
indexer = new BlockIndexer();
|
||||
pathfinder = new Pathfinder();
|
||||
controlPath = new ControlPathfinder();
|
||||
bases = new BaseRegistry();
|
||||
constants = new GlobalConstants();
|
||||
javaPath =
|
||||
|
||||
@@ -96,17 +96,17 @@ public class BaseAI{
|
||||
var field = pathfinder.getField(data.team, Pathfinder.costGround, Pathfinder.fieldCore);
|
||||
|
||||
if(field.weights != null){
|
||||
int[][] weights = field.weights;
|
||||
int[] weights = field.weights;
|
||||
for(int i = 0; i < pathStep; i++){
|
||||
int minCost = Integer.MAX_VALUE;
|
||||
int cx = calcTile.x, cy = calcTile.y;
|
||||
boolean foundAny = false;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int nx = cx + p.x, ny = cy + p.y;
|
||||
int nx = cx + p.x, ny = cy + p.y, packed = world.packArray(nx, ny);
|
||||
|
||||
Tile other = world.tile(nx, ny);
|
||||
if(other != null && weights[nx][ny] < minCost && weights[nx][ny] != -1){
|
||||
minCost = weights[nx][ny];
|
||||
if(other != null && weights[packed] < minCost && weights[packed] != -1){
|
||||
minCost = weights[packed];
|
||||
calcTile = other;
|
||||
foundAny = true;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,125 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import arc.util.async.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.game.EventType.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.graphics.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
import static mindustry.ai.Pathfinder.*;
|
||||
|
||||
//TODO I'm sure this class has countless problems
|
||||
public class ControlPathfinder implements Runnable{
|
||||
private static final long maxUpdate = Time.millisToNanos(7);
|
||||
private static final int updateFPS = 60;
|
||||
private static final long maxUpdate = Time.millisToNanos(20);
|
||||
private static final int updateFPS = 40;
|
||||
private static final int updateInterval = 1000 / updateFPS;
|
||||
|
||||
public static boolean showDebug = false;
|
||||
|
||||
public static final Seq<PathCost> costTypes = Seq.with(
|
||||
//ground
|
||||
(team, tile) -> (PathTile.allDeep(tile) || PathTile.solid(tile)) ? impassable : 1 +
|
||||
(PathTile.nearSolid(tile) ? 6 : 0) +
|
||||
(PathTile.nearLiquid(tile) ? 8 : 0) +
|
||||
(PathTile.deep(tile) ? 6000 : 0) +
|
||||
(PathTile.damages(tile) ? 40 : 0),
|
||||
|
||||
//legs
|
||||
(team, tile) -> PathTile.legSolid(tile) ? impassable : 1 +
|
||||
(PathTile.deep(tile) ? 6000 : 0) +
|
||||
(PathTile.nearSolid(tile) || PathTile.solid(tile) ? 3 : 0),
|
||||
|
||||
//water
|
||||
(team, tile) -> (PathTile.solid(tile) || !PathTile.liquid(tile) ? impassable : 1) +
|
||||
(PathTile.nearGround(tile) || PathTile.nearSolid(tile) ? 2 : 0) +
|
||||
(PathTile.deep(tile) ? 0 : 1)
|
||||
);
|
||||
|
||||
//static access probably faster than object access
|
||||
static int wwidth, wheight;
|
||||
|
||||
//MAIN THREAD DATA
|
||||
|
||||
static volatile int worldUpdateId;
|
||||
|
||||
/** Current pathfinding thread */
|
||||
@Nullable Thread thread;
|
||||
/** for unique target IDs */
|
||||
int lastTargetId;
|
||||
int lastTargetId = 1;
|
||||
/** handles task scheduling on the update thread. */
|
||||
TaskQueue queue = new TaskQueue();
|
||||
ObjectMap<Unit, PathRequest> requests = new ObjectMap<>();
|
||||
|
||||
//PATHFINDING THREAD DATA
|
||||
ObjectMap<Unit, PathRequest> requests = new ObjectMap<>();
|
||||
Seq<PathRequest> threadRequests = new Seq<>();
|
||||
|
||||
public ControlPathfinder(){
|
||||
Events.on(WorldLoadEvent.class, event -> {
|
||||
stop();
|
||||
|
||||
wwidth = world.width();
|
||||
wheight = world.height();
|
||||
|
||||
start();
|
||||
});
|
||||
|
||||
Events.on(TileChangeEvent.class, e -> {
|
||||
worldUpdateId ++;
|
||||
});
|
||||
|
||||
Events.on(ResetEvent.class, event -> stop());
|
||||
|
||||
//invalidate paths
|
||||
Events.run(Trigger.update, () -> {
|
||||
for(var req : requests.values()){
|
||||
//skipped N update -> drop it
|
||||
if(req.lastUpdateId <= state.updateId - 10){
|
||||
requests.remove(req.unit);
|
||||
queue.post(() -> threadRequests.remove(req));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Events.run(Trigger.draw, () -> {
|
||||
if(!showDebug) return;
|
||||
|
||||
for(var req : requests.values()){
|
||||
if(req.frontier == null) continue;
|
||||
//TODO will this even work
|
||||
Draw.draw(Layer.overlayUI, () -> {
|
||||
if(req.done){
|
||||
int len = req.result.size;
|
||||
for(int i = 0; i < len; i++){
|
||||
Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f));
|
||||
int pos = req.result.items[i];
|
||||
Fill.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 3f);
|
||||
|
||||
if(i == req.pathIndex){
|
||||
Draw.color(Color.green);
|
||||
Lines.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 5f);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
int len = req.frontier.size;
|
||||
float[] weights = req.frontier.weights;
|
||||
int[] poses = req.frontier.queue;
|
||||
for(int i = 0; i < len; i++){
|
||||
Draw.color(Tmp.c1.set(Color.white).fromHsv((weights[i] * 4f) % 360f, 1f, 0.9f));
|
||||
int pos = poses[i];
|
||||
Lines.square(pos % wwidth * tilesize, pos / wwidth * tilesize, 4f);
|
||||
}
|
||||
}
|
||||
Draw.reset();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,10 +128,104 @@ public class ControlPathfinder implements Runnable{
|
||||
return lastTargetId ++;
|
||||
}
|
||||
|
||||
public void getPathPosition(Unit unit, int pathId, Vec2 destination){
|
||||
/** @return whether a path is ready */
|
||||
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
|
||||
|
||||
int pathType = unit.pathType();
|
||||
|
||||
//if the destination can be trivially reached in a straight line, do that.
|
||||
if((!requests.containsKey(unit) || requests.get(unit).curId != pathId) && !raycast(pathType, unit.tileX(), unit.tileY(), World.toTile(destination.x), World.toTile(destination.y))){
|
||||
out.set(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
//destination is impassable, can't go there.
|
||||
if(solid(pathType, world.packArray(World.toTile(destination.x), World.toTile(destination.y)))){
|
||||
return false;
|
||||
}
|
||||
|
||||
//check for request existence
|
||||
if(!requests.containsKey(unit)){
|
||||
var req = new PathRequest();
|
||||
req.unit = unit;
|
||||
req.pathType = pathType;
|
||||
req.destination = destination;
|
||||
req.curId = pathId;
|
||||
req.lastUpdateId = state.updateId;
|
||||
req.lastWorldUpdate = worldUpdateId;
|
||||
|
||||
requests.put(unit, req);
|
||||
|
||||
//add to thread so it gets processed next update
|
||||
queue.post(() -> threadRequests.add(req));
|
||||
}else{
|
||||
var req = requests.get(unit);
|
||||
req.lastUpdateId = state.updateId;
|
||||
if(req.curId != req.lastId || req.curId != pathId){
|
||||
req.pathIndex = 0;
|
||||
req.rayPathIndex = -1;
|
||||
req.done = false;
|
||||
req.foundEnd = false;
|
||||
}
|
||||
|
||||
req.destination = destination;
|
||||
req.curId = pathId;
|
||||
|
||||
if(req.done){
|
||||
int[] items = req.result.items;
|
||||
int len = req.result.size;
|
||||
int tileX = unit.tileX(), tileY = unit.tileY();
|
||||
float range = 4f;
|
||||
|
||||
float minDst = req.pathIndex < len ? unit.dst2(world.tiles.geti(items[req.pathIndex])) : 0f;
|
||||
int idx = req.pathIndex;
|
||||
|
||||
//find closest node that is in front of the path index and hittable with raycast
|
||||
for(int i = len - 1; i >= idx; i--){
|
||||
Tile tile = tile(items[i]);
|
||||
float dst = unit.dst2(tile);
|
||||
if(dst < minDst && !permissiveRaycast(pathType, tileX, tileY, tile.x, tile.y)){
|
||||
req.pathIndex = Math.max(dst <= range * range ? i + 1 : i, req.pathIndex);
|
||||
minDst = Math.min(dst, minDst);
|
||||
}
|
||||
}
|
||||
|
||||
if(req.rayPathIndex < 0){
|
||||
req.rayPathIndex = req.pathIndex;
|
||||
}
|
||||
|
||||
//TODO indecision dance: moving forward blocks the raycasted node from view, so it moves back.
|
||||
if((req.raycastTimer += Time.delta) >= 50f){
|
||||
for(int i = len - 1; i > req.pathIndex; i--){
|
||||
int val = items[i];
|
||||
//TODO this raycasting is flawed, it assumes units can move through corners even when they can't.
|
||||
if(!raycast(pathType, tileX, tileY, val % wwidth, val / wwidth)){
|
||||
req.rayPathIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
req.raycastTimer = 0;
|
||||
}
|
||||
|
||||
if(req.rayPathIndex < len){
|
||||
Tile tile = tile(items[req.rayPathIndex]);
|
||||
out.set(tile);
|
||||
|
||||
if(unit.within(tile, range)){
|
||||
req.pathIndex = req.rayPathIndex = Math.max(req.pathIndex, req.rayPathIndex + 1);
|
||||
}
|
||||
}else{
|
||||
//implicit done
|
||||
out.set(unit);
|
||||
//end of path, we're done here? reset path? what???
|
||||
}
|
||||
}
|
||||
|
||||
return req.done;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Starts or restarts the pathfinding thread. */
|
||||
private void start(){
|
||||
stop();
|
||||
@@ -63,30 +241,110 @@ public class ControlPathfinder implements Runnable{
|
||||
requests.clear();
|
||||
}
|
||||
|
||||
//distance heuristic: manhattan
|
||||
private static float dstCost(float x, float y, float x2, float y2){
|
||||
return Math.abs(x - x2) + Math.abs(x2 - y2);
|
||||
private static boolean raycast(int type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
while(x >= 0 && y >= 0 && x < ww && y < wh){
|
||||
if(avoid(type, x + y * wwidth)) return true;
|
||||
if(x == x2 && y == y2) return false;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
|
||||
|
||||
//no diagonals allowed here, mimics how units actually move
|
||||
/*
|
||||
if(2 * err + dy > dx - 2 * err){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}else{
|
||||
err += dx;
|
||||
y += sy;
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static float tileCost(Tile a, Tile b){
|
||||
return 1f;
|
||||
private static boolean permissiveRaycast(int type, int x1, int y1, int x2, int y2){
|
||||
int ww = world.width(), wh = world.height();
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
while(x >= 0 && y >= 0 && x < ww && y < wh){
|
||||
if(solid(type, x + y * wwidth)) return true;
|
||||
if(x == x2 && y == y2) return false;
|
||||
|
||||
//no diagonals
|
||||
if(2 * err + dy > dx - 2 * err){
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}else{
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean cast(int pathType, int from, int to){
|
||||
return raycast(pathType, from % wwidth, from / wwidth, to % wwidth, to / wwidth);
|
||||
}
|
||||
|
||||
private Tile tile(int pos){
|
||||
return world.tiles.geti(pos);
|
||||
}
|
||||
|
||||
//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 cost(int type, int tilePos){
|
||||
return costTypes.items[type].getCost(null, pathfinder.tiles[tilePos]);
|
||||
}
|
||||
|
||||
private static boolean avoid(int type, int tilePos){
|
||||
int cost = cost(type, tilePos);
|
||||
return cost == impassable || cost >= 2;
|
||||
}
|
||||
|
||||
private static boolean solid(int type, int tilePos){
|
||||
return cost(type, tilePos) == impassable;
|
||||
}
|
||||
|
||||
private static float tileCost(int type, int a, int b){
|
||||
//currently flat cost
|
||||
return cost(type, b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(){
|
||||
while(true){
|
||||
//stop on client, no updating
|
||||
if(net.client()) return;
|
||||
try{
|
||||
if(state.isPlaying()){
|
||||
queue.run();
|
||||
|
||||
//total update time no longer than maxUpdate
|
||||
//for(Flowfield data : threadList){
|
||||
// updateFrontier(data, maxUpdate / threadList.size);
|
||||
//}
|
||||
|
||||
for(var entry : requests){
|
||||
entry.value.update(maxUpdate / requests.size);
|
||||
for(var req : threadRequests){
|
||||
req.update(maxUpdate / requests.size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,35 +361,159 @@ public class ControlPathfinder implements Runnable{
|
||||
}
|
||||
}
|
||||
|
||||
class PathRequest{
|
||||
GridBits closed;
|
||||
PQueue<Tile> queue;
|
||||
//TODO each one of these could run in its own thread.
|
||||
static class PathRequest{
|
||||
volatile boolean done = false;
|
||||
volatile boolean foundEnd = false;
|
||||
|
||||
//TODO how will costs be computed? where will they be stored...?
|
||||
volatile Unit unit;
|
||||
volatile int pathType;
|
||||
volatile Vec2 destination;
|
||||
volatile int lastWorldUpdate;
|
||||
volatile float stuckTime;
|
||||
|
||||
int lastId;
|
||||
int curId;
|
||||
int lastFrame;
|
||||
//TODO only access on main thread??
|
||||
int pathIndex;
|
||||
int rayPathIndex = -1;
|
||||
IntSeq result = new IntSeq();
|
||||
float raycastTimer;
|
||||
|
||||
PathRequest(){
|
||||
clear();
|
||||
PathfindQueue frontier = new PathfindQueue();
|
||||
//node index -> node it came from
|
||||
IntIntMap cameFrom = new IntIntMap();
|
||||
//node index -> total cost
|
||||
IntFloatMap costs = new IntFloatMap();
|
||||
|
||||
lastId = curId;
|
||||
}
|
||||
int start, goal;
|
||||
|
||||
long lastTime;
|
||||
|
||||
volatile int lastId, curId;
|
||||
|
||||
//TODO invalidate when not request for a while
|
||||
long lastUpdateId;
|
||||
|
||||
void update(long maxUpdateNs){
|
||||
if(curId != lastId){
|
||||
clear();
|
||||
}
|
||||
lastId = curId;
|
||||
|
||||
//re-do everything when world updates
|
||||
if(Time.timeSinceMillis(lastTime) > 1000 * 1 && worldUpdateId != lastWorldUpdate){
|
||||
lastTime = Time.millis();
|
||||
lastWorldUpdate = worldUpdateId;
|
||||
pathIndex = 0;
|
||||
rayPathIndex = -1;
|
||||
result.clear();
|
||||
clear();
|
||||
}
|
||||
|
||||
if(done) return;
|
||||
|
||||
long ns = Time.nanos();
|
||||
int counter = 0;
|
||||
//Log.info("running; @ in frontier", frontier.size);
|
||||
|
||||
while(frontier.size > 0){
|
||||
int current = frontier.poll();
|
||||
|
||||
if(current == goal){
|
||||
foundEnd = true;
|
||||
break;
|
||||
}
|
||||
|
||||
int cx = current % wwidth, cy = current / wwidth;
|
||||
|
||||
//TODO corner traps? d8 vs d4 here?
|
||||
for(Point2 point : Geometry.d4){
|
||||
int newx = cx + point.x, newy = cy + point.y;
|
||||
int next = newx + wwidth * newy;
|
||||
|
||||
if(newx >= wwidth || newy >= wheight || newx < 0 || newy < 0) continue;
|
||||
|
||||
if(cost(pathType, next) == impassable) continue;
|
||||
|
||||
float newCost = costs.get(current) + tileCost(pathType, current, next);
|
||||
//a cost of 0 means "not set"
|
||||
if(!costs.containsKey(next) || newCost < costs.get(next)){
|
||||
costs.put(next, newCost);
|
||||
float priority = newCost + heuristic(next, goal);
|
||||
frontier.add(next, priority);
|
||||
cameFrom.put(next, current);
|
||||
}
|
||||
}
|
||||
|
||||
//only check every N iterations to prevent nanoTime spam (slow)
|
||||
if((counter ++) >= 100){
|
||||
counter = 0;
|
||||
|
||||
//exit when out of time.
|
||||
if(Time.timeSinceNanos(ns) > maxUpdateNs){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastTime = Time.millis();
|
||||
result.clear();
|
||||
|
||||
if(foundEnd){
|
||||
int cur = goal;
|
||||
while(cur != start){
|
||||
result.add(cur);
|
||||
cur = cameFrom.get(cur);
|
||||
}
|
||||
|
||||
result.reverse();
|
||||
|
||||
smoothPath();
|
||||
}
|
||||
|
||||
//TODO free resources?
|
||||
|
||||
done = true;
|
||||
}
|
||||
|
||||
void smoothPath(){
|
||||
int len = result.size;
|
||||
if(len <= 2) return;
|
||||
|
||||
int output = 1, input = 2;
|
||||
|
||||
while(input < len){
|
||||
if(cast(pathType, result.get(output - 1), result.get(input))){
|
||||
result.swap(output, input - 1);
|
||||
output++;
|
||||
}
|
||||
input++;
|
||||
}
|
||||
|
||||
result.swap(output, input - 1);
|
||||
result.size = output + 1;
|
||||
}
|
||||
|
||||
void clear(){
|
||||
//TODO
|
||||
done = false;
|
||||
|
||||
closed = new GridBits(world.width(), world.height());
|
||||
queue = new PQueue<>(16, (a, b) -> 0);
|
||||
//TODO horribly expensive
|
||||
frontier = new PathfindQueue(20);
|
||||
cameFrom.clear();
|
||||
costs.clear();
|
||||
|
||||
start = world.packArray(unit.tileX(), unit.tileY());
|
||||
goal = world.packArray(World.toTile(destination.x), World.toTile(destination.y));
|
||||
|
||||
cameFrom.put(start, start);
|
||||
costs.put(start, 0);
|
||||
|
||||
frontier.add(start, 0);
|
||||
|
||||
foundEnd = false;
|
||||
result.clear();
|
||||
|
||||
//closed = new GridBits(world.width(), world.height());
|
||||
//queue = new PathfindQueue(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
146
core/src/mindustry/ai/PathfindQueue.java
Normal file
146
core/src/mindustry/ai/PathfindQueue.java
Normal file
@@ -0,0 +1,146 @@
|
||||
package mindustry.ai;
|
||||
|
||||
import arc.util.*;
|
||||
|
||||
/** A priority queue. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public class PathfindQueue{
|
||||
private static final double CAPACITY_RATIO_LOW = 1.5f;
|
||||
private static final double CAPACITY_RATIO_HI = 2f;
|
||||
|
||||
/**
|
||||
* Priority queue represented as a balanced binary heap: the two children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
|
||||
* priority queue is ordered by the elements' natural ordering: For each node n in the heap and each descendant d of n, n <= d.
|
||||
* The element with the lowest value is in queue[0], assuming the queue is nonempty.
|
||||
*/
|
||||
public int[] queue;
|
||||
/** Weights of each object in the queue. */
|
||||
public float[] weights;
|
||||
/** The number of elements in the priority queue. */
|
||||
public int size = 0;
|
||||
|
||||
public PathfindQueue(){
|
||||
this(12);
|
||||
}
|
||||
|
||||
public PathfindQueue(int initialCapacity){
|
||||
this.queue = new int[initialCapacity];
|
||||
this.weights = new float[initialCapacity];
|
||||
}
|
||||
|
||||
public boolean empty(){
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the specified element into this priority queue. If {@code uniqueness} is enabled and this priority queue already
|
||||
* contains the element, the call leaves the queue unchanged and returns false.
|
||||
* @return true if the element was added to this queue, else false
|
||||
* @throws ClassCastException if the specified element cannot be compared with elements currently in this priority queue
|
||||
* according to the priority queue's ordering
|
||||
* @throws IllegalArgumentException if the specified element is null
|
||||
*/
|
||||
public boolean add(int e, float weight){
|
||||
int i = size;
|
||||
if(i >= queue.length) growToSize(i + 1);
|
||||
size = i + 1;
|
||||
if(i == 0){
|
||||
queue[0] = e;
|
||||
weights[0] = weight;
|
||||
}else{
|
||||
siftUp(i, e, weight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, but does not remove, the head of this queue. If this queue is empty, {@code 0} is returned.
|
||||
* @return the head of this queue
|
||||
*/
|
||||
public int peek(){
|
||||
return size == 0 ? 0 : queue[0];
|
||||
}
|
||||
|
||||
/** Removes all of the elements from this priority queue. The queue will be empty after this call returns. */
|
||||
public void clear(){
|
||||
size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and removes the head of this queue, or returns {@code null} if this queue is empty.
|
||||
* @return the head of this queue, or {@code null} if this queue is empty.
|
||||
*/
|
||||
public int poll(){
|
||||
if(size == 0) return 0;
|
||||
int s = --size;
|
||||
int result = queue[0];
|
||||
int x = queue[s];
|
||||
if(s != 0) siftDown(0, x, weights[s]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts item x at position k, maintaining heap invariant by promoting x up the tree until it is greater than or equal to its
|
||||
* parent, or is the root.
|
||||
* @param k the position to fill
|
||||
* @param x the item to insert
|
||||
*/
|
||||
private void siftUp(int k, int x, float weight){
|
||||
while(k > 0){
|
||||
int parent = (k - 1) >>> 1;
|
||||
int e = queue[parent];
|
||||
if(weight >= weights[parent]) break;
|
||||
queue[k] = e;
|
||||
weights[k] = weights[parent];
|
||||
k = parent;
|
||||
}
|
||||
queue[k] = x;
|
||||
weights[k] = weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts item x at position k, maintaining heap invariant by demoting x down the tree repeatedly until it is less than or
|
||||
* equal to its children or is a leaf.
|
||||
* @param k the position to fill
|
||||
* @param x the item to insert
|
||||
*/
|
||||
private void siftDown(int k, int x, float weight){
|
||||
int half = size >>> 1; // loop while a non-leaf
|
||||
while(k < half){
|
||||
int child = (k << 1) + 1; // assume left child is least
|
||||
int c = queue[child];
|
||||
int right = child + 1;
|
||||
if(right < size && weights[child] > weights[right]){
|
||||
c = queue[child = right];
|
||||
}
|
||||
if(weight <= weights[child]) break;
|
||||
queue[k] = c;
|
||||
weights[k] = weights[child];
|
||||
k = child;
|
||||
}
|
||||
queue[k] = x;
|
||||
weights[k] = weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the capacity of the array.
|
||||
* @param minCapacity the desired minimum capacity
|
||||
*/
|
||||
private void growToSize(int minCapacity){
|
||||
if(minCapacity < 0) // overflow
|
||||
throw new ArcRuntimeException("Capacity upper limit exceeded.");
|
||||
int oldCapacity = queue.length;
|
||||
// Double size if small; else grow by 50%
|
||||
int newCapacity = (int)((oldCapacity < 64) ? ((oldCapacity + 1) * CAPACITY_RATIO_HI) : (oldCapacity * CAPACITY_RATIO_LOW));
|
||||
if(newCapacity < 0) // overflow
|
||||
newCapacity = Integer.MAX_VALUE;
|
||||
if(newCapacity < minCapacity) newCapacity = minCapacity;
|
||||
|
||||
int[] newQueue = new int[newCapacity];
|
||||
float[] newWeights = new float[newCapacity];
|
||||
System.arraycopy(queue, 0, newQueue, 0, size);
|
||||
System.arraycopy(weights, 0, newWeights, 0, size);
|
||||
queue = newQueue;
|
||||
weights = newWeights;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ public class Pathfinder implements Runnable{
|
||||
private static final long maxUpdate = Time.millisToNanos(7);
|
||||
private static final int updateFPS = 60;
|
||||
private static final int updateInterval = 1000 / updateFPS;
|
||||
private static final int impassable = -1;
|
||||
|
||||
static final int impassable = -1;
|
||||
|
||||
public static final int
|
||||
fieldCore = 0,
|
||||
@@ -60,11 +61,13 @@ public class Pathfinder implements Runnable{
|
||||
(PathTile.damages(tile) ? 35 : 0)
|
||||
);
|
||||
|
||||
//maps team, cost, type to flow field
|
||||
Flowfield[][][] cache;
|
||||
/** tile data, see PathTileStruct - kept as a separate array for threading reasons */
|
||||
int[] tiles = new int[0];
|
||||
|
||||
/** tile data, see PathTileStruct */
|
||||
int[][] tiles = new int[0][0];
|
||||
/** maps team, cost, type to flow field*/
|
||||
Flowfield[][][] cache;
|
||||
/** cached world size */
|
||||
int wwidth, wheight;
|
||||
/** unordered array of path data for iteration only. DO NOT iterate or access this in the main thread. */
|
||||
Seq<Flowfield> threadList = new Seq<>(), mainList = new Seq<>();
|
||||
/** handles task scheduling on the update thread. */
|
||||
@@ -80,13 +83,16 @@ public class Pathfinder implements Runnable{
|
||||
stop();
|
||||
|
||||
//reset and update internal tile array
|
||||
tiles = new int[world.width()][world.height()];
|
||||
tiles = new int[world.width() * world.height()];
|
||||
wwidth = world.width();
|
||||
wheight = world.height();
|
||||
threadList = new Seq<>();
|
||||
mainList = new Seq<>();
|
||||
clearCache();
|
||||
|
||||
for(Tile tile : world.tiles){
|
||||
tiles[tile.x][tile.y] = packTile(tile);
|
||||
for(int i = 0; i < tiles.length; i++){
|
||||
Tile tile = world.tiles.geti(i);
|
||||
tiles[i] = packTile(tile);
|
||||
}
|
||||
|
||||
preloadPath(getField(state.rules.waveTeam, costGround, fieldCore));
|
||||
@@ -163,8 +169,9 @@ public class Pathfinder implements Runnable{
|
||||
int x = tile.x, y = tile.y;
|
||||
|
||||
tile.getLinkedTiles(t -> {
|
||||
if(Structs.inBounds(t.x, t.y, tiles)){
|
||||
tiles[t.x][t.y] = packTile(t);
|
||||
int pos = t.array();
|
||||
if(pos < tiles.length){
|
||||
tiles[pos] = packTile(t);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -208,7 +215,9 @@ public class Pathfinder implements Runnable{
|
||||
return;
|
||||
}
|
||||
}catch(Throwable e){
|
||||
e.printStackTrace();
|
||||
//TODO remove in production!
|
||||
Threads.throwAppException(e);
|
||||
//e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,8 +264,9 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
}
|
||||
|
||||
int[][] values = path.weights;
|
||||
int value = values[tile.x][tile.y];
|
||||
int[] values = path.weights;
|
||||
int apos = tile.array();
|
||||
int value = values[apos];
|
||||
|
||||
Tile current = null;
|
||||
int tl = 0;
|
||||
@@ -266,10 +276,12 @@ public class Pathfinder implements Runnable{
|
||||
Tile other = world.tile(dx, dy);
|
||||
if(other == null) continue;
|
||||
|
||||
if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && path.passable(dx, dy) &&
|
||||
!(point.x != 0 && point.y != 0 && (!path.passable(tile.x + point.x, tile.y) || !path.passable(tile.x, tile.y + point.y)))){ //diagonal corner trap
|
||||
int packed = world.packArray(dx, dy);
|
||||
|
||||
if(values[packed] < value && (current == null || values[packed] < tl) && path.passable(packed) &&
|
||||
!(point.x != 0 && point.y != 0 && (!path.passable(world.packArray(tile.x + point.x, tile.y)) || !path.passable(world.packArray(tile.x, tile.y + point.y))))){ //diagonal corner trap
|
||||
current = other;
|
||||
tl = values[dx][dy];
|
||||
tl = values[packed];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,9 +295,11 @@ public class Pathfinder implements Runnable{
|
||||
* This only occurs for active teams.
|
||||
*/
|
||||
private void updateTargets(Flowfield path, int x, int y){
|
||||
if(!Structs.inBounds(x, y, path.weights)) return;
|
||||
int packed = world.packArray(x, y);
|
||||
|
||||
if(path.weights[x][y] == 0){
|
||||
if(packed > path.weights.length) return;
|
||||
|
||||
if(path.weights[packed] == 0){
|
||||
//this was a previous target
|
||||
path.frontier.clear();
|
||||
}else if(!path.frontier.isEmpty()){
|
||||
@@ -294,7 +308,7 @@ public class Pathfinder implements Runnable{
|
||||
}
|
||||
|
||||
//update cost of the tile TODO maybe only update the cost when it's not passable
|
||||
path.weights[x][y] = path.cost.getCost(path.team, tiles[x][y]);
|
||||
path.weights[packed] = path.cost.getCost(path.team, tiles[packed]);
|
||||
|
||||
//clear frontier to prevent contamination
|
||||
path.frontier.clear();
|
||||
@@ -312,10 +326,9 @@ public class Pathfinder implements Runnable{
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
int tx = Point2.x(pos), ty = Point2.y(pos);
|
||||
|
||||
path.weights[tx][ty] = 0;
|
||||
path.searches[tx][ty] = path.search;
|
||||
path.weights[pos] = 0;
|
||||
path.searches[pos] = path.search;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
}
|
||||
@@ -335,7 +348,7 @@ public class Pathfinder implements Runnable{
|
||||
*/
|
||||
private void registerPath(Flowfield path){
|
||||
path.lastUpdateTime = Time.millis();
|
||||
path.setup(tiles.length, tiles[0].length);
|
||||
path.setup(tiles.length);
|
||||
|
||||
threadList.add(path);
|
||||
|
||||
@@ -343,16 +356,14 @@ public class Pathfinder implements Runnable{
|
||||
Core.app.post(() -> mainList.add(path));
|
||||
|
||||
//fill with impassables by default
|
||||
for(int x = 0; x < world.width(); x++){
|
||||
for(int y = 0; y < world.height(); y++){
|
||||
path.weights[x][y] = impassable;
|
||||
}
|
||||
for(int i = 0; i < tiles.length; i++){
|
||||
path.weights[i] = impassable;
|
||||
}
|
||||
|
||||
//add targets
|
||||
for(int i = 0; i < path.targets.size; i++){
|
||||
int pos = path.targets.get(i);
|
||||
path.weights[Point2.x(pos)][Point2.y(pos)] = 0;
|
||||
path.weights[pos] = 0;
|
||||
path.frontier.addFirst(pos);
|
||||
}
|
||||
}
|
||||
@@ -361,10 +372,12 @@ public class Pathfinder implements Runnable{
|
||||
private void updateFrontier(Flowfield path, long nsToRun){
|
||||
long start = Time.nanos();
|
||||
|
||||
while(path.frontier.size > 0 && (nsToRun < 0 || Time.timeSinceNanos(start) <= nsToRun)){
|
||||
Tile tile = world.tile(path.frontier.removeLast());
|
||||
if(tile == null || path.weights == null) return; //something went horribly wrong, bail
|
||||
int cost = path.weights[tile.x][tile.y];
|
||||
int counter = 0;
|
||||
|
||||
while(path.frontier.size > 0){
|
||||
int tile = path.frontier.removeLast();
|
||||
if(path.weights == null) return; //something went horribly wrong, bail
|
||||
int cost = path.weights[tile];
|
||||
|
||||
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
|
||||
if(path.frontier.size >= world.width() * world.height()){
|
||||
@@ -375,19 +388,28 @@ public class Pathfinder implements Runnable{
|
||||
if(cost != impassable){
|
||||
for(Point2 point : Geometry.d4){
|
||||
|
||||
int dx = tile.x + point.x, dy = tile.y + point.y;
|
||||
int dx = (tile % wwidth) + point.x, dy = (tile / wheight) + point.y;
|
||||
|
||||
if(dx < 0 || dy < 0 || dx >= tiles.length || dy >= tiles[0].length) continue;
|
||||
if(dx < 0 || dy < 0 || dx >= wwidth || dy >= wheight) continue;
|
||||
|
||||
int otherCost = path.cost.getCost(path.team, tiles[dx][dy]);
|
||||
int newPos = tile + point.x + point.y * wwidth;
|
||||
int otherCost = path.cost.getCost(path.team, tiles[newPos]);
|
||||
|
||||
if((path.weights[dx][dy] > cost + otherCost || path.searches[dx][dy] < path.search) && otherCost != impassable){
|
||||
path.frontier.addFirst(Point2.pack(dx, dy));
|
||||
path.weights[dx][dy] = cost + otherCost;
|
||||
path.searches[dx][dy] = (short)path.search;
|
||||
if((path.weights[newPos] > cost + otherCost || path.searches[newPos] < path.search) && otherCost != impassable){
|
||||
path.frontier.addFirst(newPos);
|
||||
path.weights[newPos] = cost + otherCost;
|
||||
path.searches[newPos] = (short)path.search;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//every 100 iterations, check the time spent - this prevents extra calls to nano time, which itself is slow
|
||||
if(nsToRun >= 0 && (counter++) >= 200){
|
||||
counter = 0;
|
||||
if(Time.timeSinceNanos(start) >= nsToRun){
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,13 +417,13 @@ public class Pathfinder implements Runnable{
|
||||
@Override
|
||||
protected void getPositions(IntSeq out){
|
||||
for(Building other : indexer.getEnemy(team, BlockFlag.core)){
|
||||
out.add(other.pos());
|
||||
out.add(other.tile.array());
|
||||
}
|
||||
|
||||
//spawn points are also enemies.
|
||||
if(state.rules.waves && team == state.rules.defaultTeam){
|
||||
for(Tile other : spawner.getSpawns()){
|
||||
out.add(other.pos());
|
||||
out.add(other.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,7 +433,7 @@ public class Pathfinder implements Runnable{
|
||||
@Override
|
||||
protected void getPositions(IntSeq out){
|
||||
for(Building other : indexer.getFlagged(team, BlockFlag.rally)){
|
||||
out.add(other.pos());
|
||||
out.add(other.tile.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,7 +448,7 @@ public class Pathfinder implements Runnable{
|
||||
|
||||
@Override
|
||||
public void getPositions(IntSeq out){
|
||||
out.add(Point2.pack(World.toTile(position.getX()), World.toTile(position.getY())));
|
||||
out.add(world.packArray(World.toTile(position.getX()), World.toTile(position.getY())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,9 +465,9 @@ public class Pathfinder implements Runnable{
|
||||
protected PathCost cost = costTypes.get(costGround);
|
||||
|
||||
/** costs of getting to a specific tile */
|
||||
public int[][] weights;
|
||||
public int[] weights;
|
||||
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
|
||||
public int[][] searches;
|
||||
public int[] searches;
|
||||
/** search frontier, these are Pos objects */
|
||||
IntQueue frontier = new IntQueue();
|
||||
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
|
||||
@@ -457,15 +479,15 @@ public class Pathfinder implements Runnable{
|
||||
/** whether this flow field is ready to be used */
|
||||
boolean initialized;
|
||||
|
||||
void setup(int width, int height){
|
||||
this.weights = new int[width][height];
|
||||
this.searches = new int[width][height];
|
||||
this.frontier.ensureCapacity((width + height) * 3);
|
||||
void setup(int length){
|
||||
this.weights = new int[length];
|
||||
this.searches = new int[length];
|
||||
this.frontier.ensureCapacity((length) / 4);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
protected boolean passable(int x, int y){
|
||||
return cost.getCost(team, pathfinder.tiles[x][y]) != impassable;
|
||||
protected boolean passable(int pos){
|
||||
return cost.getCost(team, pathfinder.tiles[pos]) != impassable;
|
||||
}
|
||||
|
||||
/** Gets targets to pathfind towards. This must run on the main thread. */
|
||||
|
||||
@@ -2,25 +2,67 @@ package mindustry.ai.types;
|
||||
|
||||
import arc.math.geom.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.*;
|
||||
|
||||
public class CommandAI extends AIController{
|
||||
static Vec2 vecOut = new Vec2();
|
||||
|
||||
public @Nullable Vec2 targetPos;
|
||||
public @Nullable Teamc attackTarget;
|
||||
|
||||
private int pathId = -1;
|
||||
|
||||
@Override
|
||||
public void updateUnit(){
|
||||
updateVisuals();
|
||||
updateTargeting();
|
||||
|
||||
if(invalid(attackTarget)){
|
||||
attackTarget = null;
|
||||
targetPos = null;
|
||||
}
|
||||
|
||||
if(attackTarget != null){
|
||||
if(targetPos == null) targetPos = new Vec2();
|
||||
targetPos.set(attackTarget);
|
||||
|
||||
if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs){
|
||||
Tile best = null;
|
||||
float mindst = 0f;
|
||||
for(var point : Edges.getEdges(build.block.size)){
|
||||
Tile tile = Vars.world.tile(build.tile.x + point.x, build.tile.y + point.y);
|
||||
if(tile != null && !tile.solid() && (best == null || unit.dst2(tile) < mindst)){
|
||||
best = tile;
|
||||
mindst = unit.dst2(tile);
|
||||
}
|
||||
}
|
||||
if(best != null){
|
||||
targetPos.set(best);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(targetPos != null){
|
||||
moveTo(targetPos, attackTarget != null ? unit.type.range - 10f : 0f);
|
||||
boolean move = true;
|
||||
vecOut.set(targetPos);
|
||||
|
||||
if(unit.isGrounded()){
|
||||
move = Vars.controlPath.getPathPosition(unit, pathId, targetPos, vecOut);
|
||||
}
|
||||
|
||||
float engageRange = unit.type.range - 10f;
|
||||
|
||||
if(move){
|
||||
moveTo(vecOut,
|
||||
attackTarget != null && unit.within(attackTarget, engageRange) ? engageRange :
|
||||
unit.isGrounded() ? 0f :
|
||||
attackTarget != null ? engageRange :
|
||||
0f, 100f, false);
|
||||
}
|
||||
|
||||
if(unit.isFlying()){
|
||||
unit.lookAt(targetPos);
|
||||
@@ -28,7 +70,7 @@ public class CommandAI extends AIController{
|
||||
faceTarget();
|
||||
}
|
||||
|
||||
if(attackTarget == null && unit.within(targetPos, Math.max(5f, unit.hitSize) / 2.9f)){
|
||||
if(attackTarget == null && unit.within(targetPos, Math.max(5f, unit.hitSize) / 2.5f)){
|
||||
targetPos = null;
|
||||
}
|
||||
}else if(target != null){
|
||||
@@ -37,17 +79,25 @@ public class CommandAI extends AIController{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Teamc findMainTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
return attackTarget == null ? super.findMainTarget(x, y, range, air, ground) : attackTarget;
|
||||
public Teamc findTarget(float x, float y, float range, boolean air, boolean ground){
|
||||
return attackTarget == null ? super.findTarget(x, y, range, air, ground) : attackTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retarget(){
|
||||
//retarget instantly when there is an explicit target, there is no performance cost
|
||||
return attackTarget != null || timer.get(timerTarget, 20);
|
||||
}
|
||||
|
||||
public void commandPosition(Vec2 pos){
|
||||
targetPos = pos;
|
||||
attackTarget = null;
|
||||
pathId = Vars.controlPath.nextTargetId();
|
||||
}
|
||||
|
||||
public void commandTarget(Teamc moveTo){
|
||||
//TODO
|
||||
attackTarget = moveTo;
|
||||
pathId = Vars.controlPath.nextTargetId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1846,6 +1846,7 @@ public class UnitTypes{
|
||||
x = y = shootX = shootY = 0f;
|
||||
shootSound = Sounds.mineDeploy;
|
||||
rotateSpeed = 180f;
|
||||
targetAir = false;
|
||||
|
||||
bullet = new BasicBulletType(){{
|
||||
sprite = "mine-bullet";
|
||||
|
||||
@@ -18,6 +18,8 @@ public class GameState{
|
||||
public float wavetime;
|
||||
/** Logic tick. */
|
||||
public double tick;
|
||||
/** Continuously ticks up every non-paused update. */
|
||||
public long updateId;
|
||||
/** Whether the game is in game over state. */
|
||||
public boolean gameOver = false, serverPaused = false;
|
||||
/** Server ticks/second. Only valid in multiplayer. */
|
||||
|
||||
@@ -397,7 +397,7 @@ public class Logic implements ApplicationListener{
|
||||
if(!state.isPaused()){
|
||||
float delta = Core.graphics.getDeltaTime();
|
||||
state.tick += Float.isNaN(delta) || Float.isInfinite(delta) ? 0f : delta * 60f;
|
||||
|
||||
state.updateId ++;
|
||||
state.teams.updateTeamStats();
|
||||
|
||||
if(state.isCampaign()){
|
||||
|
||||
@@ -170,6 +170,10 @@ public class World{
|
||||
return Math.round(coord / tilesize);
|
||||
}
|
||||
|
||||
public int packArray(int x, int y){
|
||||
return x + y * tiles.width;
|
||||
}
|
||||
|
||||
private void clearTileEntities(){
|
||||
for(Tile tile : tiles){
|
||||
if(tile != null && tile.build != null){
|
||||
@@ -373,52 +377,46 @@ public class World{
|
||||
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
|
||||
}
|
||||
|
||||
public void raycastEach(int x0f, int y0f, int x1, int y1, Raycaster cons){
|
||||
int x0 = x0f, dx = Math.abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
|
||||
int y0 = y0f, dy = Math.abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
|
||||
public void raycastEach(int x1, int y1, int x2, int y2, Raycaster cons){
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
while(true){
|
||||
if(cons.accept(x0, y0)) break;
|
||||
if(x0 == x1 && y0 == y1) break;
|
||||
if(cons.accept(x, y)) break;
|
||||
if(x == x2 && y == y2) break;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err -= dy;
|
||||
x0 += sx;
|
||||
x += sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean raycast(int x0f, int y0f, int x1, int y1, Raycaster cons){
|
||||
int x0 = x0f;
|
||||
int y0 = y0f;
|
||||
int dx = Math.abs(x1 - x0);
|
||||
int dy = Math.abs(y1 - y0);
|
||||
public boolean raycast(int x1, int y1, int x2, int y2, Raycaster cons){
|
||||
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
|
||||
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
|
||||
int e2, err = dx - dy;
|
||||
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
|
||||
int err = dx - dy;
|
||||
int e2;
|
||||
while(true){
|
||||
if(cons.accept(x0, y0)) return true;
|
||||
if(x0 == x1 && y0 == y1) return false;
|
||||
if(cons.accept(x, y)) return true;
|
||||
if(x == x2 && y == y2) return false;
|
||||
|
||||
e2 = 2 * err;
|
||||
if(e2 > -dy){
|
||||
err = err - dy;
|
||||
x0 = x0 + sx;
|
||||
x = x + sx;
|
||||
}
|
||||
|
||||
if(e2 < dx){
|
||||
err = err + dx;
|
||||
y0 = y0 + sy;
|
||||
y = y + sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,10 @@ public class AIController implements UnitController{
|
||||
}
|
||||
|
||||
public void moveTo(Position target, float circleLength, float smooth){
|
||||
moveTo(target, circleLength, smooth, unit.isFlying());
|
||||
}
|
||||
|
||||
public void moveTo(Position target, float circleLength, float smooth, boolean keepDistance){
|
||||
if(target == null) return;
|
||||
|
||||
vec.set(target).sub(unit);
|
||||
@@ -256,7 +260,11 @@ public class AIController implements UnitController{
|
||||
|
||||
vec.setLength(unit.speed() * length);
|
||||
if(length < -0.5f){
|
||||
vec.rotate(180f);
|
||||
if(keepDistance){
|
||||
vec.rotate(180f);
|
||||
}else{
|
||||
vec.setZero();
|
||||
}
|
||||
}else if(length < 0){
|
||||
vec.setZero();
|
||||
}
|
||||
@@ -264,7 +272,15 @@ public class AIController implements UnitController{
|
||||
//do not move when infinite vectors are used.
|
||||
if(vec.isNaN() || vec.isInfinite()) return;
|
||||
|
||||
unit.movePref(vec);
|
||||
if(!unit.type.omniMovement && unit.type.rotateMoveFirst){
|
||||
float angle = vec.angle();
|
||||
unit.lookAt(angle);
|
||||
if(Angles.within(unit.rotation, angle, 3f)){
|
||||
unit.movePref(vec);
|
||||
}
|
||||
}else{
|
||||
unit.movePref(vec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,6 @@ import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import mindustry.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
import mindustry.entities.units.*;
|
||||
import mindustry.game.EventType.*;
|
||||
@@ -122,14 +121,19 @@ public class DesktopInput extends InputHandler{
|
||||
CommandAI ai = (CommandAI)unit.controller();
|
||||
//draw target line
|
||||
if(ai.targetPos != null){
|
||||
Tmp.v1.set(ai.targetPos).sub(unit).setLength(unit.hitSize / 2f);
|
||||
Position lineDest = ai.attackTarget != null ? ai.attackTarget : ai.targetPos;
|
||||
|
||||
Drawf.line(Pal.accent, unit.x + Tmp.v1.x, unit.y + Tmp.v1.y, ai.targetPos.x, ai.targetPos.y);
|
||||
Tmp.v1.set(lineDest).sub(unit).setLength(unit.hitSize / 2f);
|
||||
|
||||
Drawf.line(Pal.accent, unit.x + Tmp.v1.x, unit.y + Tmp.v1.y, lineDest.getX(), lineDest.getY());
|
||||
}
|
||||
|
||||
Drawf.square(unit.x, unit.y, unit.hitSize / 1.4f + 1f);
|
||||
}
|
||||
|
||||
if(ai.attackTarget != null){
|
||||
Drawf.target(ai.attackTarget.getX(), ai.attackTarget.getY(), 6f, Pal.remove);
|
||||
}
|
||||
}
|
||||
|
||||
if(commandMode && !commandRect){
|
||||
Unit sel = selectedCommandUnit(input.mouseWorldX(), input.mouseWorldY());
|
||||
@@ -689,29 +693,20 @@ public class DesktopInput extends InputHandler{
|
||||
}
|
||||
}else if(selectedUnits.size > 0){
|
||||
//move to location - TODO right click instead?
|
||||
//TODO all this needs to be synced, done with packets, etc
|
||||
Vec2 target = input.mouseWorld().cpy();
|
||||
|
||||
Teamc build = world.buildWorld(target.x, target.y);
|
||||
Teamc attack = world.buildWorld(target.x, target.y);
|
||||
|
||||
if(build == null || build.team() == player.team()){
|
||||
build = selectedEnemyUnit(target.x, target.y);
|
||||
if(attack == null || attack.team() == player.team()){
|
||||
attack = selectedEnemyUnit(target.x, target.y);
|
||||
}
|
||||
|
||||
if(build != null && build.team() != player.team()){
|
||||
for(var sel : selectedUnits){
|
||||
((CommandAI)sel.controller()).commandTarget(build);
|
||||
}
|
||||
|
||||
Fx.attackCommand.at(build);
|
||||
}else{
|
||||
for(var sel : selectedUnits){
|
||||
((CommandAI)sel.controller()).commandPosition(target);
|
||||
}
|
||||
|
||||
Fx.moveCommand.at(target);
|
||||
int[] ids = new int[selectedUnits.size];
|
||||
for(int i = 0; i < ids.length; i++){
|
||||
ids[i] = selectedUnits.get(i).id;
|
||||
}
|
||||
|
||||
Call.commandUnits(player, ids, attack instanceof Building b ? b : null, attack instanceof Unit u ? u : null, target);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import arc.scene.ui.layout.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.ai.formations.patterns.*;
|
||||
import mindustry.ai.types.*;
|
||||
import mindustry.annotations.Annotations.*;
|
||||
import mindustry.content.*;
|
||||
import mindustry.core.*;
|
||||
@@ -189,6 +190,36 @@ public abstract class InputHandler implements InputProcessor, GestureListener{
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.both, targets = Loc.both, forward = true)
|
||||
public static void commandUnits(Player player, int[] unitIds, @Nullable Building buildTarget, @Nullable Unit unitTarget, @Nullable Vec2 posTarget){
|
||||
if(player == null || unitIds == null) return;
|
||||
|
||||
if(net.server() && !netServer.admins.allowAction(player, ActionType.commandUnits, event -> {
|
||||
event.unitIDs = unitIds;
|
||||
})){
|
||||
throw new ValidateException(player, "Player cannot command units.");
|
||||
}
|
||||
|
||||
Teamc teamTarget = buildTarget == null ? unitTarget : buildTarget;
|
||||
|
||||
for(int id : unitIds){
|
||||
Unit unit = Groups.unit.getByID(id);
|
||||
if(unit.team == player.team() && unit.controller() instanceof CommandAI ai){
|
||||
if(teamTarget != null && teamTarget.team() != player.team()){
|
||||
ai.commandTarget(teamTarget);
|
||||
}else if(posTarget != null){
|
||||
ai.commandPosition(posTarget);
|
||||
}
|
||||
}
|
||||
|
||||
if(teamTarget != null){
|
||||
Fx.attackCommand.at(teamTarget);
|
||||
}else{
|
||||
Fx.moveCommand.at(posTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Remote(called = Loc.server, targets = Loc.both, forward = true)
|
||||
public static void requestItem(Player player, Building build, Item item, int amount){
|
||||
if(player == null || build == null || !build.interactable(player.team()) || !player.within(build, buildingRange) || player.dead()) return;
|
||||
|
||||
@@ -391,6 +391,23 @@ public class TypeIO{
|
||||
}else if(control instanceof LogicAI logic && logic.controller != null){
|
||||
write.b(3);
|
||||
write.i(logic.controller.pos());
|
||||
}else if(control instanceof CommandAI ai){
|
||||
write.b(4);
|
||||
write.bool(ai.attackTarget != null);
|
||||
write.bool(ai.targetPos != null);
|
||||
|
||||
if(ai.targetPos != null){
|
||||
write.f(ai.targetPos.x);
|
||||
write.f(ai.targetPos.y);
|
||||
}
|
||||
if(ai.attackTarget != null){
|
||||
write.b(ai.attackTarget instanceof Building ? 1 : 0);
|
||||
if(ai.attackTarget instanceof Building b){
|
||||
write.i(b.pos());
|
||||
}else{
|
||||
write.i(((Unit)ai.attackTarget).id);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
write.b(2);
|
||||
}
|
||||
@@ -424,6 +441,29 @@ public class TypeIO{
|
||||
out.controller = world.build(pos);
|
||||
return out;
|
||||
}
|
||||
}else if(type == 4){
|
||||
CommandAI ai = prev instanceof CommandAI pai ? pai : new CommandAI();
|
||||
|
||||
boolean hasAttack = read.bool(), hasPos = read.bool();
|
||||
if(hasPos){
|
||||
if(ai.targetPos == null) ai.targetPos = new Vec2();
|
||||
ai.targetPos.set(read.f(), read.f());
|
||||
}else{
|
||||
ai.targetPos = null;
|
||||
}
|
||||
|
||||
if(hasAttack){
|
||||
byte entityType = read.b();
|
||||
if(entityType == 1){
|
||||
ai.attackTarget = world.build(read.i());
|
||||
}else{
|
||||
ai.attackTarget = Groups.unit.getByID(read.i());
|
||||
}
|
||||
}else{
|
||||
ai.attackTarget = null;
|
||||
}
|
||||
|
||||
return ai;
|
||||
}else{
|
||||
//there are two cases here:
|
||||
//1: prev controller was not a player, carry on
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package mindustry.logic;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.*;
|
||||
import arc.math.geom.*;
|
||||
import arc.struct.*;
|
||||
@@ -1055,9 +1054,9 @@ public class LExecutor{
|
||||
exec.var(varCounter).numval --;
|
||||
}
|
||||
|
||||
if(Core.graphics.getFrameId() != frameId){
|
||||
if(state.updateId != frameId){
|
||||
curTime += Time.delta / 60f;
|
||||
frameId = Core.graphics.getFrameId();
|
||||
frameId = state.updateId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,18 +200,18 @@ public class SectorDamage{
|
||||
boolean found = false;
|
||||
|
||||
if(field != null && field.weights != null){
|
||||
int[][] weights = field.weights;
|
||||
int[] weights = field.weights;
|
||||
int count = 0;
|
||||
Tile current = start;
|
||||
while(count < world.width() * world.height()){
|
||||
while(count < weights.length){
|
||||
int minCost = Integer.MAX_VALUE;
|
||||
int cx = current.x, cy = current.y;
|
||||
for(Point2 p : Geometry.d4){
|
||||
int nx = cx + p.x, ny = cy + p.y;
|
||||
int nx = cx + p.x, ny = cy + p.y, packed = world.packArray(nx, ny);
|
||||
|
||||
Tile other = world.tile(nx, ny);
|
||||
if(other != null && weights[nx][ny] < minCost && weights[nx][ny] != -1){
|
||||
minCost = weights[nx][ny];
|
||||
if(other != null && weights[packed] < minCost && weights[packed] != -1){
|
||||
minCost = weights[packed];
|
||||
current = other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,6 +621,9 @@ public class Administration{
|
||||
/** valid only for removePlanned events only; contains packed positions. */
|
||||
public @Nullable int[] plans;
|
||||
|
||||
/** valid only for command unit events */
|
||||
public @Nullable int[] unitIDs;
|
||||
|
||||
public PlayerAction set(Player player, ActionType type, Tile tile){
|
||||
this.player = player;
|
||||
this.type = type;
|
||||
@@ -650,7 +653,7 @@ public class Administration{
|
||||
}
|
||||
|
||||
public enum ActionType{
|
||||
breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned
|
||||
breakBlock, placeBlock, rotate, configure, withdrawItem, depositItem, control, buildSelect, command, removePlanned, commandUnits
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ public class UnitType extends UnlockableContent{
|
||||
/** If true, this unit cannot drown, and will not be affected by the floor under it. */
|
||||
public boolean hovering = false;
|
||||
public boolean omniMovement = true;
|
||||
/** If true, the unit faces its moving direction before actually moving. */
|
||||
public boolean rotateMoveFirst = false;
|
||||
public boolean showHeal = true;
|
||||
public Color healColor = Pal.heal;
|
||||
public Effect fallEffect = Fx.fallSmoke;
|
||||
|
||||
@@ -9,6 +9,7 @@ public class TankUnitType extends ErekirUnitType{
|
||||
|
||||
squareShape = true;
|
||||
omniMovement = false;
|
||||
rotateMoveFirst = true;
|
||||
rotateSpeed = 1.3f;
|
||||
envDisabled = Env.none;
|
||||
speed = 0.8f;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package mindustry.world.blocks.defense;
|
||||
|
||||
import arc.*;
|
||||
import arc.graphics.*;
|
||||
import arc.graphics.g2d.*;
|
||||
import arc.math.*;
|
||||
@@ -147,8 +146,8 @@ public class RegenProjector extends Block{
|
||||
}
|
||||
}
|
||||
|
||||
if(lastUpdateFrame != Core.graphics.getFrameId()){
|
||||
lastUpdateFrame = Core.graphics.getFrameId();
|
||||
if(lastUpdateFrame != state.updateId){
|
||||
lastUpdateFrame = state.updateId;
|
||||
|
||||
for(var entry : mendMap.entries()){
|
||||
var build = world.build(entry.key);
|
||||
|
||||
@@ -28,6 +28,7 @@ public abstract class BlockProducer extends PayloadBlock{
|
||||
update = true;
|
||||
outputsPayload = true;
|
||||
hasItems = true;
|
||||
solid = true;
|
||||
hasPower = true;
|
||||
rotate = true;
|
||||
regionRotated1 = 1;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package mindustry.world.blocks.power;
|
||||
|
||||
import arc.*;
|
||||
import arc.math.*;
|
||||
import arc.struct.*;
|
||||
import arc.util.*;
|
||||
import mindustry.gen.*;
|
||||
import mindustry.world.consumers.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
public class PowerGraph{
|
||||
private static final Queue<Building> queue = new Queue<>();
|
||||
private static final Seq<Building> outArray1 = new Seq<>();
|
||||
@@ -206,7 +207,7 @@ public class PowerGraph{
|
||||
}
|
||||
|
||||
public void update(){
|
||||
if(Core.graphics.getFrameId() == lastFrameUpdated){
|
||||
if(state.updateId == lastFrameUpdated){
|
||||
return;
|
||||
}else if(!consumers.isEmpty() && consumers.first().cheating()){
|
||||
//when cheating, just set status to 1
|
||||
@@ -218,7 +219,7 @@ public class PowerGraph{
|
||||
return;
|
||||
}
|
||||
|
||||
lastFrameUpdated = Core.graphics.getFrameId();
|
||||
lastFrameUpdated = state.updateId;
|
||||
|
||||
float powerNeeded = getPowerNeeded();
|
||||
float powerProduced = getPowerProduced();
|
||||
|
||||
Reference in New Issue
Block a user