it is done

This commit is contained in:
Anuken
2019-12-25 01:39:38 -05:00
parent 5b21873f3c
commit 514d4817c8
488 changed files with 4572 additions and 4574 deletions

View File

@@ -0,0 +1,357 @@
package mindustry.ai;
import arc.*;
import arc.struct.*;
import arc.func.*;
import arc.math.*;
import arc.math.geom.*;
import mindustry.content.*;
import mindustry.entities.type.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.game.Teams.*;
import mindustry.type.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
/** Class used for indexing special target blocks for AI. */
@SuppressWarnings("unchecked")
public class BlockIndexer{
/** Size of one quadrant. */
private final static int quadrantSize = 16;
/** Set of all ores that are being scanned. */
private final ObjectSet<Item> scanOres = new ObjectSet<>();
private final ObjectSet<Item> itemSet = new ObjectSet<>();
/** Stores all ore quadtrants on the map. */
private ObjectMap<Item, ObjectSet<Tile>> ores = new ObjectMap<>();
/** Tags all quadrants. */
private GridBits[] structQuadrants;
/** Stores all damaged tile entities by team. */
private ObjectSet<Tile>[] damagedTiles = new ObjectSet[Team.all.length];
/**All ores available on this map.*/
private ObjectSet<Item> allOres = new ObjectSet<>();
/** Maps teams to a map of flagged tiles by type. */
private ObjectSet<Tile>[][] flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
/** Maps tile positions to their last known tile index data. */
private IntMap<TileIndex> typeMap = new IntMap<>();
/** Empty set used for returning. */
private ObjectSet<Tile> emptySet = new ObjectSet<>();
/** Array used for returning and reusing. */
private Array<Tile> returnArray = new Array<>();
public BlockIndexer(){
Events.on(TileChangeEvent.class, event -> {
if(typeMap.get(event.tile.pos()) != null){
TileIndex index = typeMap.get(event.tile.pos());
for(BlockFlag flag : index.flags){
getFlagged(index.team)[flag.ordinal()].remove(event.tile);
}
}
process(event.tile);
updateQuadrant(event.tile);
});
Events.on(WorldLoadEvent.class, event -> {
scanOres.clear();
scanOres.addAll(Item.getAllOres());
damagedTiles = new ObjectSet[Team.all.length];
flagMap = new ObjectSet[Team.all.length][BlockFlag.all.length];
for(int i = 0; i < flagMap.length; i++){
for(int j = 0; j < BlockFlag.all.length; j++){
flagMap[i][j] = new ObjectSet<>();
}
}
typeMap.clear();
allOres.clear();
ores = null;
//create bitset for each team type that contains each quadrant
structQuadrants = new GridBits[Team.all.length];
for(int i = 0; i < Team.all.length; i++){
structQuadrants[i] = new GridBits(Mathf.ceil(world.width() / (float)quadrantSize), Mathf.ceil(world.height() / (float)quadrantSize));
}
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
Tile tile = world.tile(x, y);
process(tile);
if(tile.entity != null && tile.entity.damaged()){
notifyTileDamaged(tile.entity);
}
if(tile.drop() != null) allOres.add(tile.drop());
}
}
for(int x = 0; x < quadWidth(); x++){
for(int y = 0; y < quadHeight(); y++){
updateQuadrant(world.tile(x * quadrantSize, y * quadrantSize));
}
}
scanOres();
});
}
private ObjectSet<Tile>[] getFlagged(Team team){
return flagMap[team.ordinal()];
}
/** @return whether this item is present on this map.*/
public boolean hasOre(Item item){
return allOres.contains(item);
}
/** Returns all damaged tiles by team. */
public ObjectSet<Tile> getDamaged(Team team){
returnArray.clear();
if(damagedTiles[team.ordinal()] == null){
damagedTiles[team.ordinal()] = new ObjectSet<>();
}
ObjectSet<Tile> set = damagedTiles[team.ordinal()];
for(Tile tile : set){
if((tile.entity == null || tile.entity.getTeam() != team || !tile.entity.damaged()) || tile.block() instanceof BuildBlock){
returnArray.add(tile);
}
}
for(Tile tile : returnArray){
set.remove(tile);
}
return set;
}
/** Get all allied blocks with a flag. */
public ObjectSet<Tile> getAllied(Team team, BlockFlag type){
return flagMap[team.ordinal()][type.ordinal()];
}
/** Get all enemy blocks with a flag. */
public Array<Tile> getEnemy(Team team, BlockFlag type){
returnArray.clear();
for(Team enemy : state.teams.enemiesOf(team)){
if(state.teams.isActive(enemy)){
ObjectSet<Tile> set = getFlagged(enemy)[type.ordinal()];
if(set != null){
for(Tile tile : set){
returnArray.add(tile);
}
}
}
}
return returnArray;
}
public void notifyTileDamaged(TileEntity entity){
if(damagedTiles[entity.getTeam().ordinal()] == null){
damagedTiles[entity.getTeam().ordinal()] = new ObjectSet<>();
}
ObjectSet<Tile> set = damagedTiles[entity.getTeam().ordinal()];
set.add(entity.tile);
}
public TileEntity findTile(Team team, float x, float y, float range, Boolf<Tile> pred){
return findTile(team, x, y, range, pred, false);
}
public TileEntity findTile(Team team, float x, float y, float range, Boolf<Tile> pred, boolean usePriority){
TileEntity closest = null;
float dst = 0;
for(int rx = Math.max((int)((x - range) / tilesize / quadrantSize), 0); rx <= (int)((x + range) / tilesize / quadrantSize) && rx < quadWidth(); rx++){
for(int ry = Math.max((int)((y - range) / tilesize / quadrantSize), 0); ry <= (int)((y + range) / tilesize / quadrantSize) && ry < quadHeight(); ry++){
if(!getQuad(team, rx, ry)) continue;
for(int tx = rx * quadrantSize; tx < (rx + 1) * quadrantSize && tx < world.width(); tx++){
for(int ty = ry * quadrantSize; ty < (ry + 1) * quadrantSize && ty < world.height(); ty++){
Tile other = world.ltile(tx, ty);
if(other == null) continue;
if(other.entity == null || other.getTeam() != team || !pred.get(other) || !other.block().targetable)
continue;
TileEntity e = other.entity;
float ndst = Mathf.dst(x, y, e.x, e.y);
if(ndst < range && (closest == null || ndst < dst || (usePriority && closest.block.priority.ordinal() < e.block.priority.ordinal()))){
dst = ndst;
closest = e;
}
}
}
}
}
return closest;
}
/**
* Returns a set of tiles that have ores of the specified type nearby.
* While each tile in the set is not guaranteed to have an ore directly on it,
* each tile will at least have an ore within {@link #quadrantSize} / 2 blocks of it.
* Only specific ore types are scanned. See {@link #scanOres}.
*/
public ObjectSet<Tile> getOrePositions(Item item){
return ores.get(item, emptySet);
}
/** Find the closest ore block relative to a position. */
public Tile findClosestOre(float xp, float yp, Item item){
Tile tile = Geometry.findClosest(xp, yp, getOrePositions(item));
if(tile == null) return null;
for(int x = Math.max(0, tile.x - quadrantSize / 2); x < tile.x + quadrantSize / 2 && x < world.width(); x++){
for(int y = Math.max(0, tile.y - quadrantSize / 2); y < tile.y + quadrantSize / 2 && y < world.height(); y++){
Tile res = world.tile(x, y);
if(res.block() == Blocks.air && res.drop() == item){
return res;
}
}
}
return null;
}
private void process(Tile tile){
if(tile.block().flags.size() > 0 && tile.getTeam() != Team.derelict){
ObjectSet<Tile>[] map = getFlagged(tile.getTeam());
for(BlockFlag flag : tile.block().flags){
ObjectSet<Tile> arr = map[flag.ordinal()];
arr.add(tile);
map[flag.ordinal()] = arr;
}
typeMap.put(tile.pos(), new TileIndex(tile.block().flags, tile.getTeam()));
}
if(ores == null) return;
int quadrantX = tile.x / quadrantSize;
int quadrantY = tile.y / quadrantSize;
itemSet.clear();
Tile rounded = world.tile(Mathf.clamp(quadrantX * quadrantSize + quadrantSize / 2, 0, world.width() - 1), Mathf.clamp(quadrantY * quadrantSize + quadrantSize / 2, 0, world.height() - 1));
//find all items that this quadrant contains
for(int x = Math.max(0, rounded.x - quadrantSize / 2); x < rounded.x + quadrantSize / 2 && x < world.width(); x++){
for(int y = Math.max(0, rounded.y - quadrantSize / 2); y < rounded.y + quadrantSize / 2 && y < world.height(); y++){
Tile result = world.tile(x, y);
if(result == null || result.drop() == null || !scanOres.contains(result.drop()) || result.block() != Blocks.air) continue;
itemSet.add(result.drop());
}
}
//update quadrant at this position
for(Item item : scanOres){
ObjectSet<Tile> set = ores.get(item);
//update quadrant status depending on whether the item is in it
if(!itemSet.contains(item)){
set.remove(rounded);
}else{
set.add(rounded);
}
}
}
private void updateQuadrant(Tile tile){
if(structQuadrants == null) return;
//this quadrant is now 'dirty', re-scan the whole thing
int quadrantX = tile.x / quadrantSize;
int quadrantY = tile.y / quadrantSize;
int index = quadrantX + quadrantY * quadWidth();
for(Team team : Team.all){
TeamData data = state.teams.get(team);
//fast-set this quadrant to 'occupied' if the tile just placed is already of this team
if(tile.getTeam() == data.team && tile.entity != null && tile.block().targetable){
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY);
continue; //no need to process futher
}
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY, false);
outer:
for(int x = quadrantX * quadrantSize; x < world.width() && x < (quadrantX + 1) * quadrantSize; x++){
for(int y = quadrantY * quadrantSize; y < world.height() && y < (quadrantY + 1) * quadrantSize; y++){
Tile result = world.ltile(x, y);
//when a targetable block is found, mark this quadrant as occupied and stop searching
if(result.entity != null && result.getTeam() == data.team){
structQuadrants[data.team.ordinal()].set(quadrantX, quadrantY);
break outer;
}
}
}
}
}
private boolean getQuad(Team team, int quadrantX, int quadrantY){
return structQuadrants[team.ordinal()].get(quadrantX, quadrantY);
}
private int quadWidth(){
return Mathf.ceil(world.width() / (float)quadrantSize);
}
private int quadHeight(){
return Mathf.ceil(world.height() / (float)quadrantSize);
}
private void scanOres(){
ores = new ObjectMap<>();
//initialize ore map with empty sets
for(Item item : scanOres){
ores.put(item, new ObjectSet<>());
}
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
int qx = (x / quadrantSize);
int qy = (y / quadrantSize);
Tile tile = world.tile(x, y);
//add position of quadrant to list when an ore is found
if(tile.drop() != null && scanOres.contains(tile.drop()) && tile.block() == Blocks.air){
ores.get(tile.drop()).add(world.tile(
//make sure to clamp quadrant middle position, since it might go off bounds
Mathf.clamp(qx * quadrantSize + quadrantSize / 2, 0, world.width() - 1),
Mathf.clamp(qy * quadrantSize + quadrantSize / 2, 0, world.height() - 1)));
}
}
}
}
private class TileIndex{
public final EnumSet<BlockFlag> flags;
public final Team team;
public TileIndex(EnumSet<BlockFlag> flags, Team team){
this.flags = flags;
this.team = team;
}
}
}

View File

@@ -0,0 +1,372 @@
package mindustry.ai;
import arc.*;
import mindustry.annotations.Annotations.*;
import arc.struct.*;
import arc.func.*;
import arc.math.geom.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import arc.util.async.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
public class Pathfinder implements Runnable{
private static final long maxUpdate = Time.millisToNanos(4);
private static final int updateFPS = 60;
private static final int updateInterval = 1000 / updateFPS;
private static final int impassable = -1;
/** tile data, see PathTileStruct */
private int[][] tiles;
/** unordered array of path data for iteration only. DO NOT iterate ot access this in the main thread.*/
private Array<PathData> list = new Array<>();
/** Maps teams + flags to a valid path to get to that flag for that team. */
private PathData[][] pathMap = new PathData[Team.all.length][PathTarget.all.length];
/** Grid map of created path data that should not be queued again. */
private GridBits created = new GridBits(Team.all.length, PathTarget.all.length);
/** handles task scheduling on the update thread. */
private TaskQueue queue = new TaskQueue();
/** current pathfinding thread */
private @Nullable
Thread thread;
public Pathfinder(){
Events.on(WorldLoadEvent.class, event -> {
stop();
//reset and update internal tile array
tiles = new int[world.width()][world.height()];
pathMap = new PathData[Team.all.length][PathTarget.all.length];
created = new GridBits(Team.all.length, PathTarget.all.length);
list = new Array<>();
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
tiles[x][y] = packTile(world.rawTile(x, y));
}
}
//special preset which may help speed things up; this is optional
preloadPath(waveTeam, PathTarget.enemyCores);
start();
});
Events.on(ResetEvent.class, event -> stop());
Events.on(TileChangeEvent.class, event -> updateTile(event.tile));
}
/** Packs a tile into its internal representation. */
private int packTile(Tile tile){
return PathTile.get(tile.cost, tile.getTeamID(), (byte)0, !tile.solid() && tile.floor().drownTime <= 0f);
}
/** Starts or restarts the pathfinding thread. */
private void start(){
stop();
thread = Threads.daemon(this);
}
/** Stops the pathfinding thread. */
private void stop(){
if(thread != null){
thread.interrupt();
thread = null;
}
queue.clear();
}
public int debugValue(Team team, int x, int y){
if(pathMap[team.ordinal()][PathTarget.enemyCores.ordinal()] == null) return 0;
return pathMap[team.ordinal()][PathTarget.enemyCores.ordinal()].weights[x][y];
}
/** Update a tile in the internal pathfinding grid. Causes a complete pathfinding reclaculation. */
public void updateTile(Tile tile){
if(net.client()) return;
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);
}
});
//can't iterate through array so use the map, which should not lead to problems
for(PathData[] arr : pathMap){
for(PathData path : arr){
if(path != null){
synchronized(path.targets){
path.targets.clear();
path.target.getTargets(path.team, path.targets);
}
}
}
}
queue.post(() -> {
for(PathData data : list){
updateTargets(data, x, y);
}
});
}
/** Thread implementation. */
@Override
public void run(){
while(true){
if(net.client()) return;
try{
queue.run();
//total update time no longer than maxUpdate
for(PathData data : list){
updateFrontier(data, maxUpdate / list.size);
}
try{
Thread.sleep(updateInterval);
}catch(InterruptedException e){
//stop looping when interrupted externally
return;
}
}catch(Exception e){
e.printStackTrace();
}
}
}
/** Gets next tile to travel to. Main thread only. */
public Tile getTargetTile(Tile tile, Team team, PathTarget target){
if(tile == null) return null;
PathData data = pathMap[team.ordinal()][target.ordinal()];
if(data == null){
//if this combination is not found, create it on request
if(!created.get(team.ordinal(), target.ordinal())){
created.set(team.ordinal(), target.ordinal());
//grab targets since this is run on main thread
IntArray targets = target.getTargets(team, new IntArray());
queue.post(() -> createPath(team, target, targets));
}
return tile;
}
int[][] values = data.weights;
int value = values[tile.x][tile.y];
Tile current = null;
int tl = 0;
for(Point2 point : Geometry.d8){
int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy);
if(other == null) continue;
if(values[dx][dy] < value && (current == null || values[dx][dy] < tl) && !other.solid() && other.floor().drownTime <= 0 &&
!(point.x != 0 && point.y != 0 && (world.solid(tile.x + point.x, tile.y) || world.solid(tile.x, tile.y + point.y)))){ //diagonal corner trap
current = other;
tl = values[dx][dy];
}
}
if(current == null || tl == impassable) return tile;
return current;
}
/** @return whether a tile can be passed through by this team. Pathfinding thread only.*/
private boolean passable(int x, int y, Team team){
int tile = tiles[x][y];
return PathTile.passable(tile) || (PathTile.team(tile) != team.ordinal() && PathTile.team(tile) != Team.derelict.ordinal());
}
/**
* Clears the frontier, increments the search and sets up all flow sources.
* This only occurs for active teams.
*/
private void updateTargets(PathData path, int x, int y){
if(!Structs.inBounds(x, y, path.weights)) return;
if(path.weights[x][y] == 0){
//this was a previous target
path.frontier.clear();
}else if(!path.frontier.isEmpty()){
//skip if this path is processing
return;
}
//assign impassability to the tile
if(!passable(x, y, path.team)){
path.weights[x][y] = impassable;
}
//increment search, clear frontier
path.search++;
path.frontier.clear();
synchronized(path.targets){
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
int tx = Pos.x(pos), ty = Pos.y(pos);
path.weights[tx][ty] = 0;
path.searches[tx][ty] = (short)path.search;
path.frontier.addFirst(pos);
}
}
}
private void preloadPath(Team team, PathTarget target){
updateFrontier(createPath(team, target, target.getTargets(team, new IntArray())), -1);
}
/** Created a new flowfield that aims to get to a certain target for a certain team.
* Pathfinding thread only. */
private PathData createPath(Team team, PathTarget target, IntArray targets){
PathData path = new PathData(team, target, world.width(), world.height());
list.add(path);
pathMap[team.ordinal()][target.ordinal()] = path;
//grab targets from passed array
synchronized(path.targets){
path.targets.clear();
path.targets.addAll(targets);
}
//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;
}
}
//add targets
for(int i = 0; i < path.targets.size; i++){
int pos = path.targets.get(i);
path.weights[Pos.x(pos)][Pos.y(pos)] = 0;
path.frontier.addFirst(pos);
}
return path;
}
/** Update the frontier for a path. Pathfinding thread only. */
private void updateFrontier(PathData 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];
//pathfinding overflowed for some reason, time to bail. the next block update will handle this, hopefully
if(path.frontier.size >= world.width() * world.height()){
path.frontier.clear();
return;
}
if(cost != impassable){
for(Point2 point : Geometry.d4){
int dx = tile.x + point.x, dy = tile.y + point.y;
Tile other = world.tile(dx, dy);
if(other != null && (path.weights[dx][dy] > cost + other.cost || path.searches[dx][dy] < path.search) && passable(dx, dy, path.team)){
if(other.cost < 0) throw new IllegalArgumentException("Tile cost cannot be negative! " + other);
path.frontier.addFirst(Pos.get(dx, dy));
path.weights[dx][dy] = cost + other.cost;
path.searches[dx][dy] = (short)path.search;
}
}
}
}
}
/** A path target defines a set of targets for a path.*/
public enum PathTarget{
enemyCores((team, out) -> {
for(Tile other : indexer.getEnemy(team, BlockFlag.core)){
out.add(other.pos());
}
//spawn points are also enemies.
if(state.rules.waves && team == defaultTeam){
for(Tile other : spawner.getGroundSpawns()){
out.add(other.pos());
}
}
}),
rallyPoints((team, out) -> {
for(Tile other : indexer.getAllied(team, BlockFlag.rally)){
out.add(other.pos());
}
});
public static final PathTarget[] all = values();
private final Cons2<Team, IntArray> targeter;
PathTarget(Cons2<Team, IntArray> targeter){
this.targeter = targeter;
}
/** Get targets. This must run on the main thread.*/
public IntArray getTargets(Team team, IntArray out){
targeter.get(team, out);
return out;
}
}
/** Data for a specific flow field to some set of destinations. */
class PathData{
/** Team this path is for. */
final Team team;
/** Flag that is being targeted. */
final PathTarget target;
/** costs of getting to a specific tile */
final int[][] weights;
/** search IDs of each position - the highest, most recent search is prioritized and overwritten */
final short[][] searches;
/** search frontier, these are Pos objects */
final IntQueue frontier = new IntQueue();
/** all target positions; these positions have a cost of 0, and must be synchronized on! */
final IntArray targets = new IntArray();
/** current search ID */
int search = 1;
PathData(Team team, PathTarget target, int width, int height){
this.team = team;
this.target = target;
this.weights = new int[width][height];
this.searches = new short[width][height];
this.frontier.ensureCapacity((width + height) * 3);
}
}
/** Holds a copy of tile data for a specific tile position. */
@Struct
class PathTileStruct{
//traversal cost
byte cost;
//team of block, if applicable (0 by default)
byte team;
//type of target; TODO remove
byte type;
//whether it's viable to pass this block
boolean passable;
}
}

View File

@@ -0,0 +1,159 @@
package mindustry.ai;
import arc.Events;
import arc.struct.Array;
import arc.func.Floatc2;
import arc.math.Angles;
import arc.math.Mathf;
import arc.util.Time;
import arc.util.Tmp;
import mindustry.content.Blocks;
import mindustry.content.Fx;
import mindustry.entities.Damage;
import mindustry.entities.Effects;
import mindustry.entities.type.BaseUnit;
import mindustry.game.EventType.WorldLoadEvent;
import mindustry.game.SpawnGroup;
import mindustry.world.Tile;
import static mindustry.Vars.*;
public class WaveSpawner{
private static final float margin = 40f, coreMargin = tilesize * 3; //how far away from the edge flying units spawn
private Array<FlyerSpawn> flySpawns = new Array<>();
private Array<Tile> groundSpawns = new Array<>();
private boolean spawning = false;
public WaveSpawner(){
Events.on(WorldLoadEvent.class, e -> reset());
}
public int countSpawns(){
return groundSpawns.size;
}
public Array<Tile> getGroundSpawns(){
return groundSpawns;
}
/** @return true if the player is near a ground spawn point. */
public boolean playerNear(){
return groundSpawns.contains(g -> Mathf.dst(g.x * tilesize, g.y * tilesize, player.x, player.y) < state.rules.dropZoneRadius);
}
public void spawnEnemies(){
spawning = true;
for(SpawnGroup group : state.rules.spawns){
int spawned = group.getUnitsSpawned(state.wave - 1);
if(group.type.flying){
float spread = margin / 1.5f;
eachFlyerSpawn((spawnX, spawnY) -> {
for(int i = 0; i < spawned; i++){
BaseUnit unit = group.createUnit(waveTeam);
unit.set(spawnX + Mathf.range(spread), spawnY + Mathf.range(spread));
unit.add();
}
});
}else{
float spread = tilesize * 2;
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
for(int i = 0; i < spawned; i++){
Tmp.v1.rnd(spread);
BaseUnit unit = group.createUnit(waveTeam);
unit.set(spawnX + Tmp.v1.x, spawnY + Tmp.v1.y);
Time.run(Math.min(i * 5, 60 * 2), () -> spawnEffect(unit));
}
});
}
}
eachGroundSpawn((spawnX, spawnY, doShockwave) -> {
if(doShockwave){
Time.run(20f, () -> Effects.effect(Fx.spawnShockwave, spawnX, spawnY, state.rules.dropZoneRadius));
Time.run(40f, () -> Damage.damage(waveTeam, spawnX, spawnY, state.rules.dropZoneRadius, 99999999f, true));
}
});
Time.runTask(121f, () -> spawning = false);
}
private void eachGroundSpawn(SpawnConsumer cons){
for(Tile spawn : groundSpawns){
cons.accept(spawn.worldx(), spawn.worldy(), true);
}
if(state.rules.attackMode && state.teams.isActive(waveTeam) && !state.teams.get(defaultTeam).cores.isEmpty()){
Tile firstCore = state.teams.get(defaultTeam).cores.first();
for(Tile core : state.teams.get(waveTeam).cores){
Tmp.v1.set(firstCore).sub(core.worldx(), core.worldy()).limit(coreMargin + core.block().size*tilesize);
cons.accept(core.worldx() + Tmp.v1.x, core.worldy() + Tmp.v1.y, false);
}
}
}
private void eachFlyerSpawn(Floatc2 cons){
for(FlyerSpawn spawn : flySpawns){
float trns = (world.width() + world.height()) * tilesize;
float spawnX = Mathf.clamp(world.width() * tilesize / 2f + Angles.trnsx(spawn.angle, trns), -margin, world.width() * tilesize + margin);
float spawnY = Mathf.clamp(world.height() * tilesize / 2f + Angles.trnsy(spawn.angle, trns), -margin, world.height() * tilesize + margin);
cons.get(spawnX, spawnY);
}
if(state.rules.attackMode && state.teams.isActive(waveTeam)){
for(Tile core : state.teams.get(waveTeam).cores){
cons.get(core.worldx(), core.worldy());
}
}
}
public boolean isSpawning(){
return spawning && !net.client();
}
private void reset(){
flySpawns.clear();
groundSpawns.clear();
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
if(world.tile(x, y).overlay() == Blocks.spawn){
addSpawns(x, y);
}
}
}
}
private void addSpawns(int x, int y){
groundSpawns.add(world.tile(x, y));
FlyerSpawn fspawn = new FlyerSpawn();
fspawn.angle = Angles.angle(world.width() / 2f, world.height() / 2f, x, y);
flySpawns.add(fspawn);
}
private void spawnEffect(BaseUnit unit){
Effects.effect(Fx.unitSpawn, unit.x, unit.y, 0f, unit);
Time.run(30f, () -> {
unit.add();
Effects.effect(Fx.spawn, unit);
});
}
private interface SpawnConsumer{
void accept(float x, float y, boolean shockwave);
}
private class FlyerSpawn{
float angle;
}
}