diff --git a/core/src/mindustry/ai/BaseAI.java b/core/src/mindustry/ai/BaseAI.java new file mode 100644 index 0000000000..44320c162d --- /dev/null +++ b/core/src/mindustry/ai/BaseAI.java @@ -0,0 +1,14 @@ +package mindustry.ai; + +import mindustry.game.Teams.*; + +public class BaseAI{ + + public void update(TeamData data){ + + //only schedule when there's something to build. + if(data.blocks.isEmpty()){ + //TODO + } + } +} diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java new file mode 100644 index 0000000000..0dcf000035 --- /dev/null +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -0,0 +1,79 @@ +package mindustry.ai.types; + +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import mindustry.entities.units.*; +import mindustry.game.Teams.*; +import mindustry.gen.*; +import mindustry.world.*; +import mindustry.world.blocks.BuildBlock.*; + +import static mindustry.Vars.*; + +public class BuilderAI extends AIController{ + + @Override + public void update(){ + Builderc builder = (Builderc)unit; + + //approach request if building + if(builder.building()){ + BuildRequest req = builder.buildRequest(); + + boolean valid = + (req.tile().entity instanceof BuildEntity && req.tile().ent().cblock == req.block) || + (req.breaking ? + Build.validBreak(unit.team(), req.x, req.y) : + Build.validPlace(req.block, unit.team(), req.x, req.y, req.rotation)); + + if(valid){ + //move toward the request + moveTo(req.tile(), buildingRange - 20f); + }else{ + //discard invalid request + builder.requests().removeFirst(); + } + }else{ + //find new request + if(!unit.team().data().blocks.isEmpty()){ + Queue blocks = unit.team().data().blocks; + BrokenBlock block = blocks.first(); + + //check if it's already been placed + if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block().id == block.block){ + blocks.removeFirst(); + }else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation)){ //it's valid. + //add build request. + BuildRequest req = new BuildRequest(block.x, block.y, block.rotation, content.block(block.block)); + if(block.config != null){ + req.configure(block.config); + } + builder.addBuild(req); + }else{ + //shift head of queue to tail, try something else next time + blocks.removeFirst(); + blocks.addLast(block); + } + }else{ + //TODO implement AI base building + } + } + } + + protected void moveTo(Position target, float circleLength){ + vec.set(target).sub(unit); + + float length = circleLength <= 0.001f ? 1f : Mathf.clamp((unit.dst(target) - circleLength) / 100f, -1f, 1f); + + vec.setLength(unit.type().speed * Time.delta() * length); + if(length < -0.5f){ + vec.rotate(180f); + }else if(length < 0){ + vec.setZero(); + } + + unit.moveAt(vec); + } +} diff --git a/core/src/mindustry/ai/types/FlyingAI.java b/core/src/mindustry/ai/types/FlyingAI.java index d360fbff2e..faf2a543bd 100644 --- a/core/src/mindustry/ai/types/FlyingAI.java +++ b/core/src/mindustry/ai/types/FlyingAI.java @@ -46,6 +46,8 @@ public class FlyingAI extends AIController{ unit.controlWeapons(shoot, shoot); } + //TODO clean up + protected void circle(float circleLength){ circle(circleLength, unit.type().speed); } diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index cbeb59fbd8..60d85667c5 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -3,6 +3,7 @@ package mindustry.content; import arc.graphics.*; import arc.math.*; import arc.struct.*; +import mindustry.ai.types.*; import mindustry.annotations.Annotations.*; import mindustry.ctype.*; import mindustry.entities.bullet.*; @@ -430,6 +431,8 @@ public class UnitTypes implements ContentList{ }}; phantom = new UnitType("phantom"){{ + defaultController = BuilderAI::new; + flying = true; drag = 0.05f; speed = 3f; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 0f01ee8948..9d1ac55546 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -65,21 +65,21 @@ public class Logic implements ApplicationListener{ //remove existing blocks that have been placed here. //painful O(n) iteration + copy - for(int i = 0; i < data.brokenBlocks.size; i++){ - BrokenBlock b = data.brokenBlocks.get(i); + for(int i = 0; i < data.blocks.size; i++){ + BrokenBlock b = data.blocks.get(i); if(b.x == tile.x && b.y == tile.y){ - data.brokenBlocks.removeIndex(i); + data.blocks.removeIndex(i); break; } } - data.brokenBlocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config())); + data.blocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config())); }); Events.on(BlockBuildEndEvent.class, event -> { if(!event.breaking){ TeamData data = state.teams.get(event.team); - Iterator it = data.brokenBlocks.iterator(); + Iterator it = data.blocks.iterator(); while(it.hasNext()){ BrokenBlock b = it.next(); Block block = content.block(b.block); @@ -309,6 +309,12 @@ public class Logic implements ApplicationListener{ //weather is serverside if(!net.client()){ updateWeather(); + + for(TeamData data : state.teams.getActive()){ + if(data.hasAI()){ + data.ai.update(data); + } + } } if(state.rules.waves && state.rules.waveTimer && !state.gameOver){ diff --git a/core/src/mindustry/entities/comp/BuilderComp.java b/core/src/mindustry/entities/comp/BuilderComp.java index e939794b17..003536df39 100644 --- a/core/src/mindustry/entities/comp/BuilderComp.java +++ b/core/src/mindustry/entities/comp/BuilderComp.java @@ -80,11 +80,11 @@ abstract class BuilderComp implements Unitc{ } if(!(tile.block() instanceof BuildBlock)){ - if(!current.initialized && !current.breaking && Build.validPlace(team(), current.x, current.y, current.block, current.rotation)){ + if(!current.initialized && !current.breaking && Build.validPlace(current.block, team(), current.x, current.y, current.rotation)){ boolean hasAll = !Structs.contains(current.block.requirements, i -> !core.items().has(i.item)); if(hasAll || state.rules.infiniteResources){ - Build.beginPlace(team(), current.x, current.y, current.block, current.rotation); + Build.beginPlace(current.block, team(), current.x, current.y, current.rotation); }else{ current.stuck = true; } @@ -138,7 +138,7 @@ abstract class BuilderComp implements Unitc{ control.input.drawBreaking(request); }else{ request.block.drawRequest(request, control.input.allRequests(), - Build.validPlace(team(), request.x, request.y, request.block, request.rotation) || control.input.requestMatches(request)); + Build.validPlace(request.block, team(), request.x, request.y, request.rotation) || control.input.requestMatches(request)); } } diff --git a/core/src/mindustry/entities/comp/PayloadComp.java b/core/src/mindustry/entities/comp/PayloadComp.java index 04a8f9daa7..3a02a6ae25 100644 --- a/core/src/mindustry/entities/comp/PayloadComp.java +++ b/core/src/mindustry/entities/comp/PayloadComp.java @@ -90,7 +90,7 @@ abstract class PayloadComp implements Posc, Rotc{ Tilec tile = payload.entity; int tx = Vars.world.toTile(x - tile.block().offset()), ty = Vars.world.toTile(y - tile.block().offset()); Tile on = Vars.world.tile(tx, ty); - if(on != null && Build.validPlace(tile.team(), tx, ty, tile.block(), tile.rotation())){ + if(on != null && Build.validPlace(tile.block(), tile.team(), tx, ty, tile.rotation())){ int rot = (int)((rotation() + 45f) / 90f) % 4; payload.place(on, rot); diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index 04e6792dee..e28544bd91 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -5,6 +5,7 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.ArcAnnotate.*; import arc.util.*; +import mindustry.ai.*; import mindustry.gen.*; import mindustry.world.blocks.storage.CoreBlock.*; @@ -147,7 +148,8 @@ public class Teams{ public final Array cores = new Array<>(); public final Array enemies = new Array<>(); public final Team team; - public Queue brokenBlocks = new Queue<>(); + public Queue blocks = new Queue<>(); + public BaseAI ai = new BaseAI(); public TeamData(Team team){ this.team = team; @@ -169,6 +171,11 @@ public class Teams{ return cores.isEmpty() ? null : cores.first(); } + /** @return whether this team is controlled by the AI and builds bases. */ + public boolean hasAI(){ + return state.rules.attackMode && team == state.rules.waveTeam; + } + @Override public String toString(){ return "TeamData{" + diff --git a/core/src/mindustry/graphics/BlockRenderer.java b/core/src/mindustry/graphics/BlockRenderer.java index 3957ea04d4..ec19c91dc7 100644 --- a/core/src/mindustry/graphics/BlockRenderer.java +++ b/core/src/mindustry/graphics/BlockRenderer.java @@ -120,7 +120,7 @@ public class BlockRenderer implements Disposable{ } if(brokenFade > 0.001f){ - for(BrokenBlock block : state.teams.get(player.team()).brokenBlocks){ + for(BrokenBlock block : state.teams.get(player.team()).blocks){ Block b = content.block(block.block); if(!camera.bounds(Tmp.r1).grow(tilesize * 2f).overlaps(Tmp.r2.setSize(b.size * tilesize).setCenter(block.x * tilesize + b.offset(), block.y * tilesize + b.offset()))) continue; diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index c8bec8be8c..d1b2a253be 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -507,7 +507,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } } - for(BrokenBlock req : player.team().data().brokenBlocks){ + for(BrokenBlock req : player.team().data().blocks){ Block block = content.block(req.block); if(block.bounds(req.x, req.y, Tmp.r2).overlaps(Tmp.r1)){ drawSelected(req.x, req.y, content.block(req.block), Pal.remove); @@ -629,7 +629,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ } //remove blocks to rebuild - Iterator broken = state.teams.get(player.team()).brokenBlocks.iterator(); + Iterator broken = state.teams.get(player.team()).blocks.iterator(); while(broken.hasNext()){ BrokenBlock req = broken.next(); Block block = content.block(req.block); @@ -907,7 +907,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ return false; } } - return Build.validPlace(player.team(), x, y, type, rotation); + return Build.validPlace(type, player.team(), x, y, rotation); } public boolean validBreak(int x, int y){ diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 8637d6ef86..0d6979c051 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -269,8 +269,8 @@ public abstract class SaveVersion extends SaveFileReader{ stream.writeInt(data.size); for(TeamData team : data){ stream.writeInt(team.team.id); - stream.writeInt(team.brokenBlocks.size); - for(BrokenBlock block : team.brokenBlocks){ + stream.writeInt(team.blocks.size); + for(BrokenBlock block : team.blocks){ stream.writeShort(block.x); stream.writeShort(block.y); stream.writeShort(block.rotation); @@ -297,7 +297,7 @@ public abstract class SaveVersion extends SaveFileReader{ TeamData data = team.data(); int blocks = stream.readInt(); for(int j = 0; j < blocks; j++){ - data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, TypeIO.readObject(Reads.get(stream)))); + data.blocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, TypeIO.readObject(Reads.get(stream)))); } } diff --git a/core/src/mindustry/io/legacy/Save3.java b/core/src/mindustry/io/legacy/Save3.java index 3924c18b3d..7ee1141bfa 100644 --- a/core/src/mindustry/io/legacy/Save3.java +++ b/core/src/mindustry/io/legacy/Save3.java @@ -21,7 +21,7 @@ public class Save3 extends LegacySaveVersion{ TeamData data = team.data(); int blocks = stream.readInt(); for(int j = 0; j < blocks; j++){ - data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt())); + data.blocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), content.block(stream.readShort()).id, stream.readInt())); } } diff --git a/core/src/mindustry/world/Build.java b/core/src/mindustry/world/Build.java index 701ddb20d1..30dc86f5ea 100644 --- a/core/src/mindustry/world/Build.java +++ b/core/src/mindustry/world/Build.java @@ -42,8 +42,8 @@ public class Build{ /** Places a BuildBlock at this location. */ @Remote(called = Loc.server) - public static void beginPlace(Team team, int x, int y, Block result, int rotation){ - if(!validPlace(team, x, y, result, rotation)){ + public static void beginPlace(Block result, Team team, int x, int y, int rotation){ + if(!validPlace(result, team, x, y, rotation)){ return; } @@ -62,7 +62,7 @@ public class Build{ } /** Returns whether a tile can be placed at this location by this team. */ - public static boolean validPlace(Team team, int x, int y, Block type, int rotation){ + public static boolean validPlace(Block type, Team team, int x, int y, int rotation){ //the wave team can build whatever they want as long as it's visible - banned blocks are not applicable if(type == null || (!type.isPlaceable() && !(state.rules.waves && team == state.rules.waveTeam && type.isVisible()))){ return false;