From 027d03723319c5c35b347184f52c72aaafd0757f Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 18 Feb 2022 11:27:20 -0500 Subject: [PATCH] Basic AI defending --- core/src/mindustry/ai/RtsAI.java | 200 ++++++++++++++---- core/src/mindustry/ai/types/CommandAI.java | 10 +- core/src/mindustry/entities/Damage.java | 16 +- .../mindustry/entities/bullet/BulletType.java | 2 +- .../mindustry/entities/comp/BuildingComp.java | 21 ++ core/src/mindustry/game/EventType.java | 15 ++ core/src/mindustry/game/Rules.java | 3 +- 7 files changed, 210 insertions(+), 57 deletions(-) diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index fb34b78478..17fabfcb15 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -3,6 +3,7 @@ package mindustry.ai; import arc.*; import arc.graphics.g2d.*; import arc.math.*; +import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.*; @@ -12,11 +13,17 @@ import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.ui.*; +import mindustry.world.*; import mindustry.world.blocks.defense.turrets.Turret.*; +import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; public class RtsAI{ static final Seq targets = new Seq<>(); + static final Seq squad = new Seq<>(false); + static final IntSet used = new IntSet(); + static final IntSet assignedTargets = new IntSet(); + static final float squadRadius = 120f; static final int timeUpdate = 0; static final float minWeight = 0.9f; @@ -24,73 +31,181 @@ public class RtsAI{ static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery}; static final ObjectFloatMap weights = new ObjectFloatMap<>(); static final int minSquadSize = 4; - static boolean debug = true; + //TODO max squad size + static final boolean debug = true; final Interval timer = new Interval(10); final TeamData data; + final ObjectSet damagedSet = new ObjectSet<>(); + final Seq damaged = new Seq<>(false); + + //must be static, as this class can get instantiated many times; event listeners are hard to clean up + static{ + Events.on(BuildDamageEvent.class, e -> { + if(e.build.team.rules().rtsAi){ + var ai = e.build.team.data().rtsAi; + if(ai != null){ + ai.damagedSet.add(e.build); + } + } + }); + } public RtsAI(TeamData data){ this.data = data; + timer.reset(0, Mathf.random(60f * 2f)); //TODO remove: debugging! - Events.run(Trigger.draw, () -> { - if(!debug) return; + if(debug){ + Events.run(Trigger.draw, () -> { - Draw.draw(Layer.overlayUI, () -> { + Draw.draw(Layer.overlayUI, () -> { - float s = Fonts.outline.getScaleX(); - Fonts.outline.getData().setScale(0.5f); - for(var target : targets){ - if(weights.containsKey(target)){ - float w = weights.get(target, 0f); - - Fonts.outline.draw("[sky]" + Strings.fixed(w, 2), target.x, target.y, Align.center); + float s = Fonts.outline.getScaleX(); + Fonts.outline.getData().setScale(0.5f); + for(var target : weights){ + Fonts.outline.draw("[sky]" + Strings.fixed(target.value, 2), target.key.x, target.key.y, Align.center); } - } - Fonts.outline.getData().setScale(s); - }); + Fonts.outline.getData().setScale(s); + }); - }); + }); + } } public void update(){ if(timer.get(timeUpdate, 60f * 2f)){ - var build = findAttackPoint(); + assignSquads(); + } + } - if(build != null){ - for(var unit : data.units){ - if(unit.isCommandable() && !unit.command().hasCommand()){ - unit.command().commandTarget(build); + void assignSquads(){ + assignedTargets.clear(); + used.clear(); + damaged.addAll(damagedSet); + damagedSet.clear(); + + boolean didDefend = false; + + for(var unit : data.units){ + if(unit.isCommandable() && !unit.command().hasCommand() && used.add(unit.id)){ + squad.clear(); + data.tree().intersect(unit.x - squadRadius/2f, unit.y - squadRadius/2f, squadRadius, squadRadius, squad); + //remove overlapping squads + squad.removeAll(u -> (u != unit && used.contains(u.id)) || !u.isCommandable() || u.command().hasCommand()); + //mark used so other squads can't steal them + for(var item : squad){ + used.add(item.id); + } + + //TODO flawed, squads + if(handleSquad(squad, !didDefend)){ + didDefend = true; + } + } + } + + damaged.clear(); + } + + boolean handleSquad(Seq units, boolean noDefenders){ + float health = 0f, dps = 0f; + float ax = 0f, ay = 0f; + + for(var unit : units){ + ax += unit.x; + ay += unit.y; + health += unit.health; + dps += unit.type.dpsEstimate; + } + ax /= units.size; + ay /= units.size; + + if(debug){ + Vars.ui.showLabel("Squad: " + units.size, 2f, ax, ay); + } + + Building defend = null; + + //there is something to defend, see if it's worth the time + if(damaged.size > 0){ + //TODO do the weights matter at all? + //for(var build : damaged){ + //float w = estimateStats(ax, ay, dps, health); + //weights.put(build, w); + //} + + //screw you java + float aax = ax, aay = ay; + + Building best = damaged.min(b -> { + //rush to core IMMEDIATELY + if(b instanceof CoreBuild){ + return -999999f; + } + + return b.dst(aax, aay); + }); + + //defend when close, or this is the only squad defending + if(best instanceof CoreBuild || (units.size >= minSquadSize && (noDefenders || best.within(ax, ay, 400f)))){ + defend = best; + + if(debug){ + Vars.ui.showLabel("Defend, dst = " + (int)(best.dst(ax, ay)), 8f, best.x, best.y); + } + } + } + + //find aggressor, or else, the thing being attacked + Vec2 defendPos = null; + Teamc defendTarget = null; + if(defend != null){ + //TODO could be made faster by storing bullet shooter + Unit aggressor = Units.closestEnemy(data.team, defend.x, defend.y, 250f, u -> true); + if(aggressor != null){ + defendTarget = aggressor; + }else if(false){ //TODO currently ignored, no use defending against nothing + //should it even go there if there's no aggressor found? + Tile closest = defend.findClosestEdge(units.first(), Tile::solid); + if(closest != null){ + defendPos = new Vec2(closest.worldx(), closest.worldy()); + } + } + } + + boolean anyDefend = defendPos != null || defendTarget != null; + + var build = anyDefend ? null : findTarget(ax, ay, units.size, dps, health); + + if(build != null || anyDefend){ + for(var unit : units){ + if(unit.isCommandable() && !unit.command().hasCommand()){ + if(defendPos != null){ + unit.command().commandPosition(defendPos); + }else{ + unit.command().commandTarget(defendTarget == null ? build : defendTarget); } } } } + + return anyDefend; } - @Nullable Building findAttackPoint(){ - float health = 0f, dps = 0f; - int total = 0; - for(var unit : data.units){ - if(unit.isCommandable() && !unit.command().hasCommand()){ - health += unit.health; - dps += unit.type.dpsEstimate; - total ++; - } - } + @Nullable Building findTarget(float x, float y, int total, float dps, float health){ + if(total < minSquadSize) return null; //flag priority? //1. generator - //2. factor + //2. factory //3. core - - //TODO split into "squads" that each find the best target for them - if(total < minSquadSize) return null; - targets.clear(); for(var flag : flags){ targets.addAll(Vars.indexer.getEnemy(data.team, flag)); } + targets.removeAll(b -> assignedTargets.contains(b.id)); if(targets.size == 0) return null; @@ -100,18 +215,21 @@ public class RtsAI{ weights.put(target, estimateStats(target.x, target.y, dps, health)); } - var result = targets.min(b -> { - float w = 1f - weights.get(b, 0f); - - //TODO more weighting, e.g. distance - return w; - }); + var result = targets.min( + Structs.comps( + //weight is most important? + Structs.comparingFloat(b -> (1f - weights.get(b, 0f)) + b.dst(x, y)/10000f), + //then distance TODO why weight above + Structs.comparingFloat(b -> b.dst2(x, y)) + ) + ); float weight = weights.get(result, 0f); if(weight < minWeight && total < Units.getCap(data.team)){ return null; } + assignedTargets.add(result.id); return result; } diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 4af2a98868..ee85c18074 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -31,15 +31,7 @@ public class CommandAI extends AIController{ targetPos.set(attackTarget); if(unit.isGrounded() && attackTarget instanceof Building build && build.tile.solid() && unit.pathType() != Pathfinder.costLegs){ - Tile best = null; - float mindst = 0f; - for(var point : Edges.getEdges(build.block.size)){ - Tile tile = Vars.world.tile(build.tile.x + point.x, build.tile.y + point.y); - if(tile != null && !tile.solid() && (best == null || unit.dst2(tile) < mindst)){ - best = tile; - mindst = unit.dst2(tile); - } - } + Tile best = build.findClosestEdge(unit, Tile::solid); if(best != null){ targetPos.set(best); } diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index fc6647e87b..00f69dc6b8 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -415,11 +415,11 @@ public class Damage{ /** Damages all entities and blocks in a radius that are enemies of the team. */ public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground){ - damage(team, x, y, radius, damage, complete, air, ground, false); + damage(team, x, y, radius, damage, complete, air, ground, false, null); } /** Damages all entities and blocks in a radius that are enemies of the team. */ - public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground, boolean scaled){ + public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground, boolean scaled, Bullet source){ Cons cons = entity -> { if(entity.team == team || !entity.within(x, y, radius + (scaled ? entity.hitSize / 2f : 0f)) || (entity.isFlying() && !air) || (entity.isGrounded() && !ground)){ return; @@ -445,7 +445,7 @@ public class Damage{ if(ground){ if(!complete){ - tileDamage(team, World.toTile(x), World.toTile(y), radius / tilesize, damage); + tileDamage(team, World.toTile(x), World.toTile(y), radius / tilesize, damage, source); }else{ completeDamage(team, x, y, radius, damage); } @@ -453,6 +453,10 @@ public class Damage{ } public static void tileDamage(Team team, int x, int y, float baseRadius, float damage){ + tileDamage(team, x, y, baseRadius, damage, null); + } + + public static void tileDamage(Team team, int x, int y, float baseRadius, float damage, @Nullable Bullet source){ Core.app.post(() -> { @@ -519,7 +523,11 @@ public class Damage{ int cx = Point2.x(e.key), cy = Point2.y(e.key); var build = world.build(cx, cy); if(build != null){ - build.damage(team, e.value); + if(source != null){ + build.damage(source, team, e.value); + }else{ + build.damage(team, e.value); + } } } }); diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index d88ed83135..375a534647 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -319,7 +319,7 @@ public class BulletType extends Content implements Cloneable{ } if(splashDamageRadius > 0 && !b.absorbed){ - Damage.damage(b.team, x, y, splashDamageRadius, splashDamage * b.damageMultiplier(), false, collidesAir, collidesGround, scaledSplashDamage); + Damage.damage(b.team, x, y, splashDamageRadius, splashDamage * b.damageMultiplier(), false, collidesAir, collidesGround, scaledSplashDamage, b); if(status != StatusEffects.none){ Damage.status(b.team, x, y, splashDamageRadius, status, statusDuration, collidesAir, collidesGround); diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 2c76a3bf1b..098341f1d0 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -54,6 +54,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, static final ObjectSet tmpTiles = new ObjectSet<>(); static final Seq tempBuilds = new Seq<>(); static final BuildTeamChangeEvent teamChangeEvent = new BuildTeamChangeEvent(); + static final BuildDamageEvent bulletDamageEvent = new BuildDamageEvent(); static int sleepingEntities = 0; @Import float x, y, health, maxHealth; @@ -262,6 +263,19 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, data.blocks.addFirst(new BlockPlan(tile.x, tile.y, (short)rotation, block.id, overrideConfig == null ? config() : overrideConfig)); } + public @Nullable Tile findClosestEdge(Position to, Boolf solid){ + Tile best = null; + float mindst = 0f; + for(var point : Edges.getEdges(block.size)){ + Tile other = Vars.world.tile(tile.x + point.x, tile.y + point.y); + if(other != null && !solid.get(other) && (best == null || to.dst2(other) < mindst)){ + best = other; + mindst = other.dst2(other); + } + } + return best; + } + /** Configure with the current, local player. */ public void configure(Object value){ //save last used config @@ -1498,6 +1512,7 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, * @return whether the bullet should be removed. */ public boolean collision(Bullet other){ damage(other.team, other.damage() * other.type().buildingDamageMultiplier); + Events.fire(bulletDamageEvent.set(self(), other)); return true; } @@ -1507,6 +1522,12 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, damage(damage); } + /** Handles splash damage with a bullet source. */ + public void damage(Bullet bullet, Team source, float damage){ + damage(source, damage); + Events.fire(bulletDamageEvent.set(self(), bullet)); + } + /** Changes this building's team in a safe manner. */ public void changeTeam(Team next){ Team last = this.team; diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index 81e492e11e..cf3e2104f9 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -249,6 +249,21 @@ public class EventType{ } } + /** + * Called when a bullet damages a building. May not be called for all damage events! + * This event is re-used! Never do anything to re-raise this event in the listener. + * */ + public static class BuildDamageEvent{ + public Building build; + public Bullet source; + + public BuildDamageEvent set(Building build, Bullet source){ + this.build = build; + this.source = source; + return this; + } + } + /** * Called *before* a tile has changed. * WARNING! This event is special: its instance is reused! Do not cache or use with a timer. diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 4dbdd9b6a5..cfa1196db1 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -237,8 +237,7 @@ public class Rules{ public TeamRule get(Team team){ TeamRule out = values[team.id]; - if(out == null) values[team.id] = (out = new TeamRule()); - return out; + return out == null ? (values[team.id] = new TeamRule()) : out; } @Override