Mindustry/core/src/mindustry/ai/RtsAI.java
2022-07-11 18:18:22 -04:00

320 lines
11 KiB
Java

package mindustry.ai;
import arc.*;
import arc.graphics.g2d.*;
import arc.math.*;
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.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.logic.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.defense.turrets.BaseTurret.*;
import mindustry.world.blocks.storage.*;
import mindustry.world.blocks.storage.CoreBlock.*;
import mindustry.world.meta.*;
public class RtsAI{
static final Seq<Building> targets = new Seq<>();
static final Seq<Unit> 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, timerSpawn = 1;
//in order of priority??
static final BlockFlag[] flags = {BlockFlag.generator, BlockFlag.factory, BlockFlag.core, BlockFlag.battery};
static final ObjectFloatMap<Building> weights = new ObjectFloatMap<>();
static final boolean debug = OS.hasProp("mindustry.debug");
final Interval timer = new Interval(10);
final TeamData data;
final ObjectSet<Building> damagedSet = new ObjectSet<>();
final Seq<Building> 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!
if(debug){
Events.run(Trigger.draw, () -> {
Draw.draw(Layer.overlayUI, () -> {
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);
});
});
}
}
public void update(){
if(timer.get(timeUpdate, 60f * 2f)){
assignSquads();
checkBuilding();
}
}
//TODO atrocious implementation
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);
}
}
}
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);
squad.truncate(data.team.rules().rtsMaxSquad);
//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<Unit> units, boolean noDefenders){
float health = 0f, dps = 0f;
float ax = 0f, ay = 0f;
boolean targetAir = true, targetGround = true;
for(var unit : units){
if(!unit.type.targetAir) targetAir = false;
if(!unit.type.targetGround) targetGround = false;
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
//TODO will always rush to defense no matter what
if(best instanceof CoreBuild || units.size >= data.team.rules().rtsMinSquad || best.within(ax, ay, 500f)){
defend = best;
if(debug){
Vars.ui.showLabel("Defend, dst = " + (int)(best.dst(ax, ay)), 8f, best.x, best.y);
}
}
}
boolean tair = targetAir, tground = targetGround;
//find aggressor, or else, the thing being attacked
Vec2 defendPos = null;
Teamc defendTarget = null;
if(defend != null){
float checkRange = 260f;
//TODO could be made faster by storing bullet shooter
Unit aggressor = Units.closestEnemy(data.team, defend.x, defend.y, checkRange, u -> u.checkTarget(tair, tground));
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());
}
}else{
float mindst = Float.MAX_VALUE;
Building build = null;
//find closest turret to attack.
for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){
if(turret.within(defend, ((Ranged)turret).range())){
float dst = turret.dst2(defend);
if(dst < mindst){
mindst = dst;
build = turret;
}
}
}
if(build != null){
defendTarget = build;
}
}
}
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{
//TODO stopAtTarget parameter could be false, could be tweaked
unit.command().commandTarget(defendTarget == null ? build : defendTarget, defendTarget != null);
}
}
}
}
return anyDefend;
}
@Nullable Building findTarget(float x, float y, int total, float dps, float health){
if(total < data.team.rules().rtsMinSquad) return null;
//flag priority?
//1. generator
//2. factory
//3. core
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;
weights.clear();
for(var target : targets){
weights.put(target, estimateStats(x, y, target.x, target.y, dps, health));
}
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 < data.team.rules().rtsMinWeight && total < Units.getCap(data.team)){
return null;
}
assignedTargets.add(result.id);
return result;
}
float estimateStats(float fromX, float fromY, float x, float y, float selfDps, float selfHealth){
float[] health = {0f}, dps = {0f};
float extraRadius = 50f;
for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){
if(turret instanceof BaseTurretBuild t && Intersector.distanceSegmentPoint(fromX, fromY, x, y, t.x, t.y) <= t.range() + extraRadius){
health[0] += t.health;
dps[0] += t.estimateDps();
}
}
Tmp.r1.set(fromX, fromY, x - fromX, y - fromY).normalize().grow(140f * 2f);
//add on extra radius, assume unit range is below that...?
Units.nearbyEnemies(data.team, Tmp.r1, other -> {
if(Intersector.distanceSegmentPoint(fromX, fromY, x, y, other.x, other.y) <= other.range() + extraRadius){
health[0] += other.health;
dps[0] += other.type.dpsEstimate;
}
});
float hp = health[0], dp = dps[0];
float timeDestroyOther = Mathf.zero(selfDps, 0.001f) ? Float.POSITIVE_INFINITY : hp / selfDps;
float timeDestroySelf = Mathf.zero(dp) ? Float.POSITIVE_INFINITY : selfHealth / dp;
//other can never be destroyed | other destroys self instantly
if(Float.isInfinite(timeDestroyOther) | Mathf.zero(timeDestroySelf)) return 0f;
//self can never be destroyed | self destroys other instantly
if(Float.isInfinite(timeDestroySelf) | Mathf.zero(timeDestroyOther)) return 1f;
//examples:
// self 10 sec / other 10 sec -> can destroy target with 100 % losses -> returns 1
// self 5 sec / other 10 sec -> can destroy about half of other -> returns 0.5 (needs to be 2x stronger to defeat)
return timeDestroySelf / timeDestroyOther;
}
}