diff --git a/core/src/mindustry/ai/BaseAI.java b/core/src/mindustry/ai/BaseAI.java index c362b68551..d33bcae145 100644 --- a/core/src/mindustry/ai/BaseAI.java +++ b/core/src/mindustry/ai/BaseAI.java @@ -57,9 +57,9 @@ public class BaseAI{ wallType = BaseGenerator.getDifficultyWall(1, data.team.rules().aiTier / 0.8f); } - if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 2.5f) && data.hasCore()){ + if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 6f) && data.hasCore()){ CoreBlock block = (CoreBlock)data.core().block; - int coreUnits = Groups.unit.count(u -> u.team == data.team && u.type == block.unitType); + int coreUnits = data.countType(block.unitType); //create AI core unit(s) if(!state.isEditor() && coreUnits < data.cores.size){ diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index 67371def66..72bb503566 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -11,6 +11,7 @@ import mindustry.game.EventType.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; +import mindustry.logic.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -38,9 +39,6 @@ public class BlockIndexer{ private Seq[][] flagMap = new Seq[Team.all.length][BlockFlag.all.length]; /** Counts whether a certain floor is present in the world upon load. */ private boolean[] blocksPresent; - - /** Array used for returning and reusing. */ - private Seq returnArray = new Seq<>(); /** Array used for returning and reusing. */ private Seq breturnArray = new Seq<>(Building.class); @@ -114,6 +112,11 @@ public class BlockIndexer{ data.buildings.remove(build); } + //remove indexed turret + if(data.turrets != null && build.block.attacks){ + data.turrets.remove(build); + } + //is no longer registered build.wasDamaged = false; @@ -442,6 +445,14 @@ public class BlockIndexer{ } data.buildings.insert(tile.build); + if(tile.block().attacks && tile.build instanceof Ranged){ + if(data.turrets == null){ + data.turrets = new TurretQuadtree(new Rect(0, 0, world.unitWidth(), world.unitHeight())); + } + + data.turrets.insert(tile.build); + } + notifyBuildDamaged(tile.build); } @@ -452,4 +463,21 @@ public class BlockIndexer{ //bounds checks only needed in very specific scenarios if(tile.blockID() < blocksPresent.length) blocksPresent[tile.blockID()] = true; } + + static class TurretQuadtree extends QuadTree{ + + public TurretQuadtree(Rect bounds){ + super(bounds); + } + + @Override + public void hitbox(Building build){ + tmp.setCentered(build.x, build.y, ((Ranged)build).range() * 2f); + } + + @Override + protected QuadTree newChild(Rect rect){ + return new TurretQuadtree(rect); + } + } } diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index 17fabfcb15..5cc2ab9794 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -7,6 +7,7 @@ import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.*; +import mindustry.content.*; import mindustry.entities.*; import mindustry.game.EventType.*; import mindustry.game.Teams.*; @@ -15,6 +16,7 @@ import mindustry.graphics.*; import mindustry.ui.*; import mindustry.world.*; import mindustry.world.blocks.defense.turrets.Turret.*; +import mindustry.world.blocks.storage.*; import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; @@ -24,7 +26,7 @@ public class RtsAI{ static final IntSet used = new IntSet(); static final IntSet assignedTargets = new IntSet(); static final float squadRadius = 120f; - static final int timeUpdate = 0; + static final int timeUpdate = 0, timerSpawn = 1; static final float minWeight = 0.9f; //in order of priority?? @@ -32,7 +34,7 @@ public class RtsAI{ static final ObjectFloatMap weights = new ObjectFloatMap<>(); static final int minSquadSize = 4; //TODO max squad size - static final boolean debug = true; + static final boolean debug = OS.hasProp("mindustry.debug"); final Interval timer = new Interval(10); final TeamData data; @@ -77,6 +79,22 @@ public class RtsAI{ public void update(){ if(timer.get(timeUpdate, 60f * 2f)){ assignSquads(); + checkBuilding(); + } + } + + void checkBuilding(){ + if(data.team.rules().aiCoreSpawn && timer.get(timerSpawn, 60 * 7f) && data.hasCore()){ + CoreBlock block = (CoreBlock)data.core().block; + int coreUnits = data.countType(block.unitType); + + //create AI core unit(s) at random cores + if(coreUnits < data.cores.size){ + Unit unit = block.unitType.create(data.team); + unit.set(data.cores.random()); + unit.add(); + Fx.spawn.at(unit); + } } } diff --git a/core/src/mindustry/ai/types/BuilderAI.java b/core/src/mindustry/ai/types/BuilderAI.java index b083216573..43c340e737 100644 --- a/core/src/mindustry/ai/types/BuilderAI.java +++ b/core/src/mindustry/ai/types/BuilderAI.java @@ -13,15 +13,26 @@ import mindustry.world.blocks.ConstructBlock.*; import static mindustry.Vars.*; public class BuilderAI extends AIController{ - public static float buildRadius = 1500, retreatDst = 110f, fleeRange = 370f, retreatDelay = Time.toSeconds * 2f; + public static float buildRadius = 1500, retreatDst = 110f, retreatDelay = Time.toSeconds * 2f; public @Nullable Unit following; public @Nullable Teamc enemy; public @Nullable BlockPlan lastPlan; + public float fleeRange = 370f; + public boolean alwaysFlee; + boolean found = false; float retreatTimer; + public BuilderAI(boolean alwaysFlee, float fleeRange){ + this.alwaysFlee = alwaysFlee; + this.fleeRange = fleeRange; + } + + public BuilderAI(){ + } + @Override public void updateMovement(){ @@ -46,15 +57,16 @@ public class BuilderAI extends AIController{ unit.plans.clear(); unit.plans.addFirst(following.buildPlan()); lastPlan = null; - }else if(unit.buildPlan() == null){ + }else if(unit.buildPlan() == null || alwaysFlee){ //not following anyone or building if(timer.get(timerTarget4, 40)){ enemy = target(unit.x, unit.y, fleeRange, true, true); } //fly away from enemy when not doing anything, but only after a delay - if((retreatTimer += Time.delta) >= retreatDelay){ + if((retreatTimer += Time.delta) >= retreatDelay || alwaysFlee){ if(enemy != null){ + unit.clearBuilding(); var core = unit.closestCore(); if(core != null && !unit.within(core, retreatDst)){ moveTo(core, retreatDst); @@ -64,7 +76,7 @@ public class BuilderAI extends AIController{ } if(unit.buildPlan() != null){ - retreatTimer = 0f; + if(!alwaysFlee) retreatTimer = 0f; //approach request if building BuildPlan req = unit.buildPlan(); @@ -131,7 +143,7 @@ public class BuilderAI extends AIController{ //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 + }else if(Build.validPlace(content.block(block.block), unit.team(), block.x, block.y, block.rotation) && (!alwaysFlee || !nearEnemy(block.x, block.y))){ //it's valid lastPlan = block; //add build request unit.addBuild(new BuildPlan(block.x, block.y, block.rotation, content.block(block.block), block.config)); @@ -145,6 +157,10 @@ public class BuilderAI extends AIController{ } } + protected boolean nearEnemy(int x, int y){ + return Units.nearEnemy(unit.team, x * tilesize - fleeRange/2f, y * tilesize - fleeRange/2f, fleeRange, fleeRange); + } + @Override public AIController fallback(){ return unit.type.flying ? new FlyingAI() : new GroundAI(); @@ -152,7 +168,7 @@ public class BuilderAI extends AIController{ @Override public boolean useFallback(){ - return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().ai; + return state.rules.waves && unit.team == state.rules.waveTeam && !unit.team.rules().ai && !unit.team.rules().rtsAi; } @Override diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 4e290046c8..0d1724332b 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -3156,10 +3156,12 @@ public class UnitTypes{ //endregion //region erekir - core + float coreFleeRange = 500f; + //TODO bad name evoke = new ErekirUnitType("evoke"){{ coreUnitDock = true; - aiController = BuilderAI::new; + defaultController = u -> new BuilderAI(true, coreFleeRange); isCounted = false; envDisabled = 0; @@ -3216,7 +3218,7 @@ public class UnitTypes{ incite = new ErekirUnitType("incite"){{ coreUnitDock = true; - aiController = BuilderAI::new; + defaultController = u -> new BuilderAI(true, coreFleeRange); isCounted = false; envDisabled = 0; @@ -3285,7 +3287,7 @@ public class UnitTypes{ emanate = new ErekirUnitType("emanate"){{ coreUnitDock = true; - aiController = BuilderAI::new; + defaultController = u -> new BuilderAI(true, coreFleeRange); isCounted = false; envDisabled = 0; diff --git a/core/src/mindustry/entities/Units.java b/core/src/mindustry/entities/Units.java index 095c380c86..397597f1b5 100644 --- a/core/src/mindustry/entities/Units.java +++ b/core/src/mindustry/entities/Units.java @@ -457,6 +457,23 @@ public class Units{ nearbyEnemies(team, rect.x, rect.y, rect.width, rect.height, cons); } + /** @return whether there is an enemy in this rectangle. */ + public static boolean nearEnemy(Team team, float x, float y, float width, float height){ + Seq data = state.teams.present; + for(int i = 0; i < data.size; i++){ + var other = data.items[i]; + if(other.team != team){ + if(other.tree().any(x, y, width, height)){ + return true; + } + if(other.turrets != null && other.turrets.any(x, y, width, height)){ + return true; + } + } + } + return false; + } + public interface Sortf{ float cost(Unit unit, float x, float y); } diff --git a/core/src/mindustry/entities/units/AIController.java b/core/src/mindustry/entities/units/AIController.java index cd79041957..f4f3d99c8e 100644 --- a/core/src/mindustry/entities/units/AIController.java +++ b/core/src/mindustry/entities/units/AIController.java @@ -2,11 +2,13 @@ package mindustry.entities.units; import arc.math.*; import arc.math.geom.*; +import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.ai.*; import mindustry.entities.*; import mindustry.game.*; +import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index fb17734058..d8d8b98d79 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -237,6 +237,8 @@ public class Teams{ /** Quadtree for all buildings of this team. Null if not active. */ public @Nullable QuadTree buildings; + /** Turrets by range. Null if not active. */ + public @Nullable QuadTree turrets; /** Current unit cap. Do not modify externally. */ public int unitCap; /** Total unit count. */ diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 924d982271..e7588195c6 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -210,6 +210,8 @@ public class Block extends UnlockableContent implements Senseable{ public boolean hasColor = false; /** Whether units target this block. */ public boolean targetable = true; + /** If true, this block attacks and is considered a turret in the indexer. Building must implement Ranged. */ + public boolean attacks = false; /** If true, this block is mending-related and can be suppressed with special units/missiles. */ public boolean suppressable = false; /** Whether the overdrive core has any effect on this block. */ diff --git a/core/src/mindustry/world/blocks/defense/turrets/BaseTurret.java b/core/src/mindustry/world/blocks/defense/turrets/BaseTurret.java index 530ea47040..2fbc9ec7f4 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/BaseTurret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/BaseTurret.java @@ -32,6 +32,7 @@ public class BaseTurret extends Block{ update = true; solid = true; outlineIcon = true; + attacks = true; priority = TargetPriority.turret; group = BlockGroup.turrets; flags = EnumSet.of(BlockFlag.turret);