diff --git a/core/src/mindustry/ai/BaseAI.java b/core/src/mindustry/ai/BaseAI.java index 44320c162d..466a3fad95 100644 --- a/core/src/mindustry/ai/BaseAI.java +++ b/core/src/mindustry/ai/BaseAI.java @@ -1,14 +1,164 @@ 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.game.*; +import mindustry.game.Schematic.*; import mindustry.game.Teams.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.defense.*; +import mindustry.world.blocks.production.*; +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 float step = 5; + private static final int attempts = 5; + private static final float emptyChance = 0.01f; - public void update(TeamData data){ + private static int correct = 0, incorrect = 0; + + private int lastX, lastY, lastW, lastH; + private boolean triedWalls; + + TeamData data; + Interval timer = new Interval(); + + public BaseAI(TeamData data){ + this.data = data; + } + + public void update(){ //only schedule when there's something to build. - if(data.blocks.isEmpty()){ - //TODO + if(data.blocks.isEmpty() && timer.get(step)){ + if(!triedWalls){ + tryWalls(); + triedWalls = true; + } + + for(int i = 0; i < attempts; i++){ + int range = 150; + CoreEntity core = data.cores.random(); + + Tmp.v1.rnd(Mathf.random(range)); + int wx = (int)(core.tileX() + Tmp.v1.x), wy = (int)(core.tileY() + Tmp.v1.y); + Tile tile = world.tiles.getc(wx, wy); + + Array 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; + } + } + } + } + } + + 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; + + //chekc 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; + } + } + + //make sure at least X% of resource requirements are met + correct = incorrect = 0; + + if(part.required instanceof Item){ + for(Stile tile : result.tiles){ + if(tile.block instanceof Drill){ + + tile.block.iterateTaken(tile.x + cx, tile.y + cy, (ex, ey) -> { + Tile res = world.rawTile(ex, ey); + if(res.drop() == part.required){ + correct ++; + }else{ + incorrect ++; + } + }); + } + } + } + + //fail if not enough fit requirements + if((float)correct / incorrect < correctPercent){ + 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; + } + + void tryWalls(){ + Block wall = Blocks.copperWall; + Tile spawn = state.rules.defaultTeam.core() != null ? state.rules.defaultTeam.core().tile : data.team.core().tile; + + 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.team() == data.team && !(o.block() instanceof Wall)){ + any = true; + break; + } + } + + if(any && Build.validPlace(wall, data.team, tile.x, tile.y, 0)){ + data.blocks.add(new BlockPlan(tile.x, tile.y, (short)0, wall.id, null)); + } + } } } } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index 4974b97999..4fd4667c58 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -18,8 +18,12 @@ public class BuilderAI extends AIController{ public void update(){ Builderc builder = (Builderc)unit; + if(builder.moving()){ + builder.lookAt(builder.vel().angle()); + } + //approach request if building - if(builder.building()){ + if(builder.buildRequest() != null){ BuildRequest req = builder.buildRequest(); boolean valid = diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index c895cabf8a..2ee47743ea 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -1708,7 +1708,7 @@ public class Blocks implements ContentList{ requirements(Category.turret, ItemStack.with(Items.silicon, 80, Items.thorium, 80, Items.surgealloy, 50)); hasPower = true; - consumes.power(2f); + consumes.power(3f); size = 2; shootLength = 5f; bulletDamage = 12f; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 3b4c6745f5..871979bcaf 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -311,8 +311,16 @@ public class Logic implements ApplicationListener{ updateWeather(); for(TeamData data : state.teams.getActive()){ - if(data.hasAI()){ - data.ai.update(data); + if(data.hasAI() && data.hasCore()){ + data.ai.update(); + } + + //TODO this is terrible + //fills enemy core with resources + if(state.rules.enemyInfiniteResources && state.rules.waves && data.team == state.rules.waveTeam && data.hasCore()){ + for(Item item : content.items()){ + data.core().items.set(item, data.core().block.itemCapacity); + } } } } diff --git a/core/src/mindustry/game/Gamemode.java b/core/src/mindustry/game/Gamemode.java index bdd7ee6ba6..55bd7e0249 100644 --- a/core/src/mindustry/game/Gamemode.java +++ b/core/src/mindustry/game/Gamemode.java @@ -19,6 +19,8 @@ public enum Gamemode{ }), attack(rules -> { rules.attackMode = true; + rules.waves = true; + rules.waveTimer = true; }, map -> map.teams.contains(state.rules.waveTeam.id)), pvp(rules -> { rules.pvp = true; diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 8ddee5f377..a4f2921eb3 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -22,6 +22,8 @@ public class Rules{ public boolean waves; /** Whether the enemy AI has infinite resources in most of their buildings and turrets. */ public boolean enemyCheat; + /** Whether the enemy AI has infinite resources in their core only. TODO remove */ + public boolean enemyInfiniteResources = true; /** Whether the game objective is PvP. Note that this enables automatic hosting. */ public boolean pvp; /** Whether reactors can explode and damage other blocks. */ @@ -69,6 +71,8 @@ public class Rules{ /** Whether to draw shadows of blocks at map edges and static blocks. * Do not change unless you know exactly what you are doing.*/ public boolean drawDarkness = true; + /** EXPERIMENTAL building AI. TODO remove */ + public boolean buildAI = true; /** Starting items put in cores */ public Array loadout = Array.with(ItemStack.with(Items.copper, 100)); /** Weather events that occur here. */ diff --git a/core/src/mindustry/game/Schematics.java b/core/src/mindustry/game/Schematics.java index 2b92574490..4d74a13dae 100644 --- a/core/src/mindustry/game/Schematics.java +++ b/core/src/mindustry/game/Schematics.java @@ -6,12 +6,14 @@ import arc.files.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.graphics.gl.*; +import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.ArcAnnotate.*; import arc.util.*; import arc.util.io.*; import arc.util.io.Streams.*; +import arc.util.pooling.*; import arc.util.serialization.*; import mindustry.*; import mindustry.content.*; @@ -38,6 +40,8 @@ import static mindustry.Vars.*; /** Handles schematics.*/ public class Schematics implements Loadable{ + private static final Schematic tmpSchem = new Schematic(new Array<>(), new StringMap(), 0, 0); + private static final Schematic tmpSchem2 = new Schematic(new Array<>(), new StringMap(), 0, 0); public static final String base64Header = "bXNjaAB"; private static final byte[] header = {'m', 's', 'c', 'h'}; @@ -526,5 +530,69 @@ public class Schematics implements Loadable{ return null; } + //endregion + //region misc utility + + /** @return a temporary schematic representing the input rotated 90 degrees counterclockwise N times. */ + public static Schematic rotate(Schematic input, int times){ + if(times == 0) return input; + + boolean sign = times > 0; + for(int i = 0; i < Math.abs(times); i++){ + input = rotated(input, sign); + } + return input; + } + + private static Schematic rotated(Schematic input, boolean counter){ + int direction = Mathf.sign(counter); + Schematic schem = input == tmpSchem ? tmpSchem2 : tmpSchem2; + schem.width = input.width; + schem.height = input.height; + Pools.freeAll(schem.tiles); + schem.tiles.clear(); + for(Stile tile : input.tiles){ + schem.tiles.add(Pools.obtain(Stile.class, Stile::new).set(tile)); + } + + int ox = schem.width/2, oy = schem.height/2; + + schem.tiles.each(req -> { + req.config = BuildRequest.pointConfig(req.config, p -> { + int cx = p.x, cy = p.y; + int lx = cx; + + if(direction >= 0){ + cx = -cy; + cy = lx; + }else{ + cx = cy; + cy = -lx; + } + p.set(cx, cy); + }); + + //rotate actual request, centered on its multiblock position + float wx = (req.x - ox) * tilesize + req.block.offset(), wy = (req.y - oy) * tilesize + req.block.offset(); + float x = wx; + if(direction >= 0){ + wx = -wy; + wy = x; + }else{ + wx = wy; + wy = -x; + } + req.x = (short)(world.toTile(wx - req.block.offset()) + ox); + req.y = (short)(world.toTile(wy - req.block.offset()) + oy); + req.rotation = (byte)Mathf.mod(req.rotation + direction, 4); + }); + + //assign flipped values, since it's rotated + schem.width = input.height; + schem.height = input.width; + + return schem; + } + //endregion } diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 059f483087..60a534d9a8 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -148,11 +148,12 @@ public class Teams{ public final Array cores = new Array<>(); public final Array enemies = new Array<>(); public final Team team; + public final BaseAI ai; public Queue blocks = new Queue<>(); - public BaseAI ai = new BaseAI(); public TeamData(Team team){ this.team = team; + this.ai = new BaseAI(this); } public boolean active(){ @@ -173,7 +174,7 @@ public class Teams{ /** @return whether this team is controlled by the AI and builds bases. */ public boolean hasAI(){ - return state.rules.attackMode && team == state.rules.waveTeam; + return state.rules.attackMode && team == state.rules.waveTeam && state.rules.buildAI; } @Override @@ -191,9 +192,9 @@ public class Teams{ public final short x, y, rotation, block; public final Object config; - public BlockPlan(short x, short y, short rotation, short block, Object config){ - this.x = x; - this.y = y; + public BlockPlan(int x, int y, short rotation, short block, Object config){ + this.x = (short)x; + this.y = (short)y; this.rotation = rotation; this.block = block; this.config = config; diff --git a/core/src/mindustry/maps/generators/BaseGenerator.java b/core/src/mindustry/maps/generators/BaseGenerator.java index 1e6bb24475..98d68b26d4 100644 --- a/core/src/mindustry/maps/generators/BaseGenerator.java +++ b/core/src/mindustry/maps/generators/BaseGenerator.java @@ -4,10 +4,8 @@ import arc.func.*; import arc.math.*; import arc.math.geom.*; import arc.struct.*; -import arc.util.pooling.*; import mindustry.ai.BaseRegistry.*; import mindustry.content.*; -import mindustry.entities.units.*; import mindustry.game.*; import mindustry.game.Schematic.*; import mindustry.gen.*; @@ -21,8 +19,6 @@ import mindustry.world.blocks.production.*; import static mindustry.Vars.*; public class BaseGenerator{ - private static final Schematic tmpSchem = new Schematic(new Array<>(), new StringMap(), 0, 0); - private static final Schematic tmpSchem2 = new Schematic(new Array<>(), new StringMap(), 0, 0); private static final Vec2 axis = new Vec2(), rotator = new Vec2(); private final static int range = 180; @@ -138,15 +134,13 @@ public class BaseGenerator{ void pass(Cons cons){ Tile core = cores.first(); - //for(Tile core : cores){ core.circle(range, (x, y) -> cons.get(tiles.getn(x, y))); - //} } 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 = rotate(part.schematic, rotation); + Schematic result = Schematics.rotate(part.schematic, rotation); int rotdeg = rotation*90; rotator.set(part.centerX, part.centerY).rotateAround(axis, rotdeg); @@ -211,64 +205,4 @@ public class BaseGenerator{ return tile == null || !tile.block().alwaysReplace || world.getDarkness(x, y) > 0; } - - Schematic rotate(Schematic input, int times){ - if(times == 0) return input; - - boolean sign = times > 0; - for(int i = 0; i < Math.abs(times); i++){ - input = rotated(input, sign); - } - return input; - } - - Schematic rotated(Schematic input, boolean counter){ - int direction = Mathf.sign(counter); - Schematic schem = input == tmpSchem ? tmpSchem2 : tmpSchem2; - schem.width = input.width; - schem.height = input.height; - Pools.freeAll(schem.tiles); - schem.tiles.clear(); - for(Stile tile : input.tiles){ - schem.tiles.add(Pools.obtain(Stile.class, Stile::new).set(tile)); - } - - int ox = schem.width/2, oy = schem.height/2; - - schem.tiles.each(req -> { - req.config = BuildRequest.pointConfig(req.config, p -> { - int cx = p.x, cy = p.y; - int lx = cx; - - if(direction >= 0){ - cx = -cy; - cy = lx; - }else{ - cx = cy; - cy = -lx; - } - p.set(cx, cy); - }); - - //rotate actual request, centered on its multiblock position - float wx = (req.x - ox) * tilesize + req.block.offset(), wy = (req.y - oy) * tilesize + req.block.offset(); - float x = wx; - if(direction >= 0){ - wx = -wy; - wy = x; - }else{ - wx = wy; - wy = -x; - } - req.x = (short)(world.toTile(wx - req.block.offset()) + ox); - req.y = (short)(world.toTile(wy - req.block.offset()) + oy); - req.rotation = (byte)Mathf.mod(req.rotation + direction, 4); - }); - - //assign flipped values, since it's rotated - schem.width = input.height; - schem.height = input.width; - - return schem; - } }