From cc71da6dfe9cefcbb64959e7e9089ec41fba1d8b Mon Sep 17 00:00:00 2001 From: Anuken Date: Sat, 10 May 2025 15:18:27 -0400 Subject: [PATCH] WIP base prebuild AI (not functional) --- core/assets/bundles/bundle.properties | 2 +- core/src/mindustry/ai/BlockIndexer.java | 9 + core/src/mindustry/ai/types/BuilderAI.java | 6 + core/src/mindustry/ai/types/MinerAI.java | 1 + core/src/mindustry/ai/types/PrebuildAI.java | 250 ++++++++++++++++++++ core/src/mindustry/content/UnitTypes.java | 9 +- core/src/mindustry/core/Logic.java | 24 +- core/src/mindustry/game/Rules.java | 2 + core/src/mindustry/game/Teams.java | 9 +- 9 files changed, 300 insertions(+), 12 deletions(-) create mode 100644 core/src/mindustry/ai/types/PrebuildAI.java diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 0cdad46949..2f9f520307 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -1425,7 +1425,7 @@ rules.wavespawnatcores.info = When enabled in attack mode, waves spawn near all rules.attack = Attack Mode rules.buildai = Base Builder AI rules.buildaitier = Builder AI Tier -rules.rtsai = RTS AI [red](WIP) +rules.rtsai = RTS AI rules.rtsai.campaign = RTS Attack AI rules.rtsai.campaign.info = In attack maps, makes units group up and attack player bases in a more intelligent manner. rules.rtsminsquadsize = Min Squad Size diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index 4a87c1df67..e6a498808a 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -6,6 +6,7 @@ import arc.math.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; +import mindustry.ai.types.*; import mindustry.content.*; import mindustry.entities.*; import mindustry.entities.Units.*; @@ -115,6 +116,14 @@ public class BlockIndexer{ } updatePresentOres(); + + for(Team team : Team.all){ + var data = state.teams.get(team); + + if(team.rules().prebuildAi && data.hasCore()){ + PrebuildAI.sortPlans(data.plans); + } + } }); } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index fc09a607f6..0cef52fb53 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -209,11 +209,17 @@ public class BuilderAI extends AIController{ @Override public AIController fallback(){ + if(unit.team.isAI() && unit.team.rules().prebuildAi){ + return new PrebuildAI(); + } return unit.type.flying ? new FlyingAI() : new GroundAI(); } @Override public boolean useFallback(){ + if(unit.team.isAI() && unit.team.rules().prebuildAi){ + return true; + } return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().rtsAi; } diff --git a/core/src/mindustry/ai/types/MinerAI.java b/core/src/mindustry/ai/types/MinerAI.java index 598e479515..69043b3c70 100644 --- a/core/src/mindustry/ai/types/MinerAI.java +++ b/core/src/mindustry/ai/types/MinerAI.java @@ -76,6 +76,7 @@ public class MinerAI extends AIController{ circle(core, unit.type.range / 1.8f); } + if(!unit.type.flying){ unit.updateBoosting(unit.type.boostWhenMining || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep()); } diff --git a/core/src/mindustry/ai/types/PrebuildAI.java b/core/src/mindustry/ai/types/PrebuildAI.java new file mode 100644 index 0000000000..1319843383 --- /dev/null +++ b/core/src/mindustry/ai/types/PrebuildAI.java @@ -0,0 +1,250 @@ +package mindustry.ai.types; + +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import mindustry.entities.units.*; +import mindustry.game.Teams.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.ConstructBlock.*; +import mindustry.world.blocks.storage.CoreBlock.*; + +import static mindustry.Vars.*; + +//don't use this yet, it's not functional! +public class PrebuildAI extends AIController{ + static float[] priorities = new float[Category.all.length]; + + static Seq tmpCopy = new Seq<>(); + + @Nullable BlockPlan lastPlan; + @Nullable Block collectBlock; + + boolean collectingItems; + boolean mining; + @Nullable Item lastTargetItem; + @Nullable Tile ore; + + static{ + priorities[Category.production.ordinal()] = 11f; + priorities[Category.distribution.ordinal()] = 10f; + priorities[Category.liquid.ordinal()] = 9f; + priorities[Category.crafting.ordinal()] = 8f; + + } + + + //TODO move this function? + public static void sortPlans(Queue plans){ + var copy = Seq.with(plans); + + copy.sort(Structs.comps( + Structs.comparingFloat(plan -> priorities[plan.block.category.ordinal()]), + Structs.comparingFloat(plan -> plan.block.buildTime) + )); + + plans.clear(); + + for(var plan : copy){ + plans.addFirst(plan); + } + } + + private boolean canBuild(CoreBuild core, Block block){ + return state.rules.infiniteResources || unit.team.rules().infiniteResources || core.items.has(block.requirements, state.rules.buildCostMultiplier); + } + + private @Nullable BlockPlan findNextPlan(){ + var data = unit.team.data(); + var core = unit.core(); + if(data.buildingTree == null || core == null) return null; + var plans = data.plans; + + //TODO super slow just inline the search + tmpCopy.clear(); + tmpCopy.addAll(plans); + + //TODO: this search is really slow + var min = tmpCopy.min(plan -> + (canBuild(core, plan.block) || !Structs.contains(plan.block.requirements, it -> !indexer.hasOre(it.item))) && + (plan.block.category == Category.production || data.buildingTree.any(plan.x * tilesize + plan.block.offset - (plan.block.size * tilesize + 1f)/2f, plan.y * tilesize + plan.block.offset- (plan.block.size * tilesize + 1f)/2f, plan.block.size * tilesize + 1f, plan.block.size * tilesize + 1f)), + plan -> unit.dst(plan.x * tilesize, plan.y * tilesize) - priorities[plan.block.category.ordinal()] * 200f); + + if(min != null){ + return min; + } + + return plans.first(); + /* + for(int i = searchIndex; i < Math.min(maxSearches, size); i++){ + searchIndex ++; + int index = (i + startIndex) % size + head; + if(index >= values.length){ + index -= values.length; + } + + var plan = plans.get((i + startIndex) % plans.size); + if(plan != null){ + var block = plan.block; + + if(data.buildingTree != null && data.buildingTree.any(plan.x * tilesize + block.offset, plan.y * tilesize + block.offset, block.size * tilesize + 1f, block.size * tilesize + 1f)){ + return plan; + } + } + }*/ + //return null; + } + + @Override + public void updateMovement(){ + + if(target != null && shouldShoot()){ + unit.lookAt(target); + }else if(!unit.type.flying){ + unit.lookAt(unit.prefRotation()); + } + + unit.updateBuilding = !collectingItems; + + boolean moving = false; + + if(collectingItems){ + doMining(); + }else if(unit.buildPlan() != null){ + //approach plan if building + BuildPlan req = unit.buildPlan(); + + boolean valid = + !(lastPlan != null && lastPlan.removed) && + ((req.tile() != null && req.tile().build instanceof ConstructBuild cons && cons.current == 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){ + float range = Math.min(unit.type.buildRange - 20f, 100f); + //move toward the plan + moveTo(req.tile(), range - 10f, 20f); + moving = !unit.within(req.tile(), range); + }else{ + //discard invalid plan + unit.plans.removeFirst(); + lastPlan = null; + } + }else{ + + //find new plan + if(!unit.team.data().plans.isEmpty() && timer.get(timerTarget3, 2f)){ + //Queue blocks = unit.team.data().plans; + BlockPlan plan = findNextPlan(); + + //check if it's already been placed + //if(world.tile(block.x, block.y) != null && world.tile(block.x, block.y).block() == block.block){ + // blocks.removeFirst(); + //}else + if(plan != null && Build.validPlace(plan.block, unit.team(), plan.x, plan.y, plan.rotation)){ //it's valid + if(!canBuild(unit.core(), plan.block)){ + collectingItems = true; + collectBlock = plan.block; + lastTargetItem = null; + ore = null; + timer.reset(timerTarget, 0f); + } + lastPlan = plan; + unit.addBuild(new BuildPlan(plan.x, plan.y, plan.rotation, plan.block, plan.config)); + + + //shift build plan to tail so next unit builds something else + //blocks.addLast(blocks.removeFirst()); + }//else{ + //shift head of queue to tail, try something else next time + // blocks.addLast(blocks.removeFirst()); + //} + } + } + + if(!unit.type.flying){ + unit.updateBoosting(unit.type.boostWhenBuilding || moving || unit.floorOn().isDuct || unit.floorOn().damageTaken > 0f || unit.floorOn().isDeep()); + } + } + + void doMining(){ + var core = unit.closestCore(); + + if(!unit.canMine() || core == null || collectBlock == null) return; + + if(!unit.validMine(unit.mineTile)){ + unit.mineTile(null); + } + + if(mining){ + var targetStack = Structs.find(collectBlock.requirements, i -> !core.items.has(i.item, Mathf.ceil(state.rules.buildCostMultiplier * i.amount))); + Item targetItem = targetStack == null ? null : targetStack.item; + + if(targetItem != null){ + lastTargetItem = targetItem; + }else{ + targetItem = lastTargetItem; + //hacky way to check if the unit just deposited something + if(!unit.hasItem() && canBuild(core, collectBlock)){ + collectingItems = false; + return; + } + } + + //core full of the target item, do nothing + if(targetItem != null && core.acceptStack(targetItem, 1, unit) == 0){ + unit.clearItem(); + unit.mineTile = null; + return; + } + + //if inventory is full, drop it off. + if(targetItem == null || unit.stack.amount >= unit.type.itemCapacity || (targetItem != null && !unit.acceptsItem(targetItem))){ + mining = false; + }else{ + if(timer.get(timerTarget3, 60) && targetItem != null){ + ore = null; + if(unit.type.mineFloor) ore = indexer.findClosestOre(unit, targetItem); + if(ore == null && unit.type.mineWalls) ore = indexer.findClosestWallOre(unit, targetItem); + } + + if(ore != null){ + moveTo(ore, unit.type.mineRange / 2f, 20f); + + if(unit.within(ore, unit.type.mineRange) && unit.validMine(ore)){ + unit.mineTile = ore; + } + } + } + }else{ + unit.mineTile = null; + + if(unit.stack.amount == 0){ + mining = true; + + if(canBuild(core, collectBlock)){ + collectingItems = false; + } + return; + } + + if(unit.within(core, unit.type.range)){ + if(core.acceptStack(unit.stack.item, unit.stack.amount, unit) > 0){ + Call.transferItemTo(unit, unit.stack.item, unit.stack.amount, unit.x, unit.y, core); + } + + unit.clearItem(); + mining = true; + + if(canBuild(core, collectBlock)){ + collectingItems = false; + } + } + + circle(core, unit.type.range / 1.8f); + } + } +} diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 64f8d6633f..94e043a5de 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -2368,8 +2368,7 @@ public class UnitTypes{ //region core alpha = new UnitType("alpha"){{ - aiController = () -> new BuilderAI(true, 400f); - controller = u -> u.team.isAI() ? aiController.get() : new CommandAI(); + controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI(); isEnemy = false; targetBuildingsMobile = false; @@ -2408,8 +2407,7 @@ public class UnitTypes{ }}; beta = new UnitType("beta"){{ - aiController = () -> new BuilderAI(true, 400f); - controller = u -> u.team.isAI() ? aiController.get() : new CommandAI(); + controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI(); isEnemy = false; targetBuildingsMobile = false; @@ -2451,8 +2449,7 @@ public class UnitTypes{ }}; gamma = new UnitType("gamma"){{ - aiController = () -> new BuilderAI(true, 400f); - controller = u -> u.team.isAI() ? aiController.get() : new CommandAI(); + controller = u -> u.team.isAI() ? new BuilderAI(true, 400f) : new CommandAI(); isEnemy = false; targetBuildingsMobile = false; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 6a0dd376ba..f9f56cf13b 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -6,8 +6,10 @@ import arc.util.*; import mindustry.*; import mindustry.ai.*; import mindustry.annotations.Annotations.*; +import mindustry.content.*; import mindustry.core.GameState.*; import mindustry.ctype.*; +import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; @@ -16,6 +18,7 @@ import mindustry.maps.*; import mindustry.type.*; import mindustry.type.Weather.*; import mindustry.world.*; +import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import java.util.*; @@ -455,7 +458,8 @@ public class Logic implements ApplicationListener{ updateWeather(); for(TeamData data : state.teams.getActive()){ - if(data.team.rules().fillItems && data.cores.size > 0){ + var rules = data.team.rules(); + if(rules.fillItems && data.cores.size > 0){ var core = data.cores.first(); content.items().each(i -> { if(i.isOnPlanet(Vars.state.getPlanet())){ @@ -464,15 +468,29 @@ public class Logic implements ApplicationListener{ }); } //does not work on PvP so built-in attack maps can have it on by default without issues - if(data.team.rules().buildAi && !state.rules.pvp){ + if(rules.buildAi && !state.rules.pvp){ if(data.buildAi == null) data.buildAi = new BaseBuilderAI(data); data.buildAi.update(); } - if(data.team.rules().rtsAi){ + if(rules.rtsAi){ if(data.rtsAi == null) data.rtsAi = new RtsAI(data); data.rtsAi.update(); } + + //spawn units for prebuild AI cores + if(rules.prebuildAi && !state.isEditor()){ + for(var core : data.cores){ + var units = data.getUnits(((CoreBlock)core.block).unitType); + if(units == null || !units.contains(u -> u.flag == core.pos())){ + Unit unit = ((CoreBlock)core.block).unitType.spawn(core, data.team); + unit.flag = core.pos(); + unit.add(); + Units.notifyUnitSpawn(unit); + Fx.spawn.at(unit); + } + } + } } } diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 14b2880cb2..5656c695be 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -305,6 +305,8 @@ public class Rules{ public boolean infiniteResources; /** If true, this team has infinite unit ammo. */ public boolean infiniteAmmo; + /** EXPERIMENTAL, DO NOT USE: Pre-built base AI. Gives the illusion of intelligent design of pre-building an attack base. */ + public boolean prebuildAi; /** AI that builds random schematics. */ public boolean buildAi; diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index e4c5a9e190..d43150efb3 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -94,7 +94,12 @@ public class Teams{ /** Returns team data by type. */ public TeamData get(Team team){ - return map[team.id] == null ? (map[team.id] = new TeamData(team)) : map[team.id]; + var data = map[team.id]; + if(data != null){ + return data; + }else{ + return map[team.id] = new TeamData(team); + } } public @Nullable TeamData getOrNull(Team team){ @@ -276,7 +281,7 @@ public class Teams{ /** Enemies with cores or spawn points. */ public Team[] coreEnemies = {}; /** Planned blocks for drones. This is usually only blocks that have been broken. */ - public Queue plans = new Queue<>(); + public Queue plans = new Queue<>(16, BlockPlan.class); /** List of live cores of this team. */ public final Seq cores = new Seq<>();