package mindustry.ai; import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.ai.BaseRegistry.*; import mindustry.content.*; import mindustry.core.*; import mindustry.game.*; import mindustry.game.Schematic.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.defense.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.production.*; import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import static mindustry.Vars.*; public class BaseAI{ private static final Vec2 axis = new Vec2(), rotator = new Vec2(); private static final float correctPercent = 0.5f; private static final int attempts = 4; private static final float emptyChance = 0.01f; private static final int timerStep = 0, timerSpawn = 1, timerRefreshPath = 2; private static final int pathStep = 50; private static final Seq tmpTiles = new Seq<>(); private static int correct = 0, incorrect = 0; private static boolean anyDrills; private int lastX, lastY, lastW, lastH; private boolean triedWalls, foundPath; TeamData data; Interval timer = new Interval(4); IntSet path = new IntSet(); IntSet calcPath = new IntSet(); @Nullable Tile calcTile; boolean calculating, startedCalculating; int calcCount = 0; int totalCalcs = 0; public BaseAI(TeamData data){ this.data = data; } public void update(){ if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 2.5f) && data.hasCore()){ CoreBlock block = (CoreBlock)data.core().block; int coreUnits = Groups.unit.count(u -> u.team == data.team && u.type == block.unitType); //create AI core unit(s) if(!state.isEditor() && coreUnits < data.cores.size){ Unit unit = block.unitType.create(data.team); unit.set(data.cores.random()); unit.add(); Fx.spawn.at(unit); } } //refresh path if(!calculating && (timer.get(timerRefreshPath, 3f * Time.toMinutes) || !startedCalculating) && data.hasCore()){ calculating = true; startedCalculating = true; calcPath.clear(); } //didn't find tile in time if(calculating && calcCount >= world.width() * world.height()){ calculating = false; calcCount = 0; calcPath.clear(); totalCalcs ++; } //calculate path for units so schematics are not placed on it if(calculating){ if(calcTile == null){ Vars.spawner.eachGroundSpawn((x, y) -> calcTile = world.tile(x, y)); if(calcTile == null){ calculating = false; } }else{ var field = pathfinder.getField(state.rules.waveTeam, Pathfinder.costGround, Pathfinder.fieldCore); 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; Tile other = world.tile(nx, ny); if(other != null && weights[nx][ny] < minCost && weights[nx][ny] != -1){ minCost = weights[nx][ny]; calcTile = other; foundAny = true; } } //didn't find anything, break out of loop, this will trigger a clear later if(!foundAny){ calcCount = Integer.MAX_VALUE; break; } calcPath.add(calcTile.pos()); for(Point2 p : Geometry.d8){ calcPath.add(Point2.pack(p.x + calcTile.x, p.y + calcTile.y)); } //found the end. if(calcTile.build instanceof CoreBuild b && b.team == state.rules.defaultTeam){ //clean up calculations and flush results calculating = false; calcCount = 0; path.clear(); path.addAll(calcPath); calcPath.clear(); calcTile = null; totalCalcs ++; foundPath = true; break; } calcCount ++; } } } //only schedule when there's something to build. if(foundPath && data.blocks.isEmpty() && timer.get(timerStep, Mathf.lerp(20f, 4f, data.team.rules().aiTier))){ if(!triedWalls){ tryWalls(); triedWalls = true; } for(int i = 0; i < attempts; i++){ int range = 150; Position pos = randomPosition(); //when there are no random positions, do nothing. if(pos == null) return; Tmp.v1.rnd(Mathf.random(range)); int wx = (int)(World.toTile(pos.getX()) + Tmp.v1.x), wy = (int)(World.toTile(pos.getY()) + Tmp.v1.y); Tile tile = world.tiles.getc(wx, wy); //try not to block the spawn point if(spawner.getSpawns().contains(t -> t.within(tile, tilesize * 40f))){ continue; } Seq parts = null; //pick a completely random base part, and place it a random location //((yes, very intelligent)) if(tile.drop() != null && Vars.bases.forResource(tile.drop()).any()){ parts = Vars.bases.forResource(tile.drop()); }else if(Mathf.chance(emptyChance)){ parts = Vars.bases.parts; } if(parts != null){ BasePart part = parts.random(); if(tryPlace(part, tile.x, tile.y)){ break; } } } } } /** @return a random position from which to seed building. */ private Position randomPosition(){ if(data.hasCore()){ return data.cores.random(); }else if(data.team == state.rules.waveTeam){ return spawner.getSpawns().random(); } return null; } private boolean tryPlace(BasePart part, int x, int y){ int rotation = Mathf.range(2); axis.set((int)(part.schematic.width / 2f), (int)(part.schematic.height / 2f)); Schematic result = Schematics.rotate(part.schematic, rotation); int rotdeg = rotation*90; rotator.set(part.centerX, part.centerY).rotateAround(axis, rotdeg); //bottom left schematic corner int cx = x - (int)rotator.x; int cy = y - (int)rotator.y; //check valid placeability for(Stile tile : result.tiles){ int realX = tile.x + cx, realY = tile.y + cy; if(!Build.validPlace(tile.block, data.team, realX, realY, tile.rotation)){ return false; } Tile wtile = world.tile(realX, realY); //may intersect AI path tmpTiles.clear(); if(tile.block.solid && wtile != null && wtile.getLinkedTilesAs(tile.block, tmpTiles).contains(t -> path.contains(t.pos()))){ return false; } } //make sure at least X% of resource requirements are met correct = incorrect = 0; anyDrills = false; if(part.required instanceof Item){ for(Stile tile : result.tiles){ if(tile.block instanceof Drill){ anyDrills = true; tile.block.iterateTaken(tile.x + cx, tile.y + cy, (ex, ey) -> { Tile res = world.rawTile(ex, ey); if(res.drop() == part.required){ correct ++; }else if(res.drop() != null){ incorrect ++; } }); } } } //fail if not enough fit requirements if(anyDrills && (incorrect != 0 || correct == 0)){ return false; } //queue it for(Stile tile : result.tiles){ data.blocks.add(new BlockPlan(cx + tile.x, cy + tile.y, tile.rotation, tile.block.id, tile.config)); } lastX = cx - 1; lastY = cy - 1; lastW = result.width + 2; lastH = result.height + 2; triedWalls = false; return true; } private void tryWalls(){ Block wall = Blocks.copperWall; Building spawnt = state.rules.defaultTeam.core() != null ? state.rules.defaultTeam.core() : data.team.core(); Tile spawn = spawnt == null ? null : spawnt.tile; if(spawn == null) return; for(int wx = lastX; wx <= lastX + lastW; wx++){ for(int wy = lastY; wy <= lastY + lastH; wy++){ Tile tile = world.tile(wx, wy); if(tile == null || !tile.block().alwaysReplace) continue; boolean any = false; for(Point2 p : Geometry.d8){ if(Angles.angleDist(Angles.angle(p.x, p.y), spawn.angleTo(tile)) > 70){ continue; } Tile o = world.tile(tile.x + p.x, tile.y + p.y); if(o != null && (o.block() instanceof PayloadAcceptor || o.block() instanceof PayloadConveyor)){ break; } if(o != null && o.team() == data.team && !(o.block() instanceof Wall)){ any = true; break; } } tmpTiles.clear(); if(any && Build.validPlace(wall, data.team, tile.x, tile.y, 0) && !tile.getLinkedTilesAs(wall, tmpTiles).contains(t -> path.contains(t.pos()))){ data.blocks.add(new BlockPlan(tile.x, tile.y, (short)0, wall.id, null)); } } } } }