From 520b60770c6a72f805efe7ff727637a927c65da2 Mon Sep 17 00:00:00 2001 From: Anuken Date: Thu, 17 Feb 2022 23:00:51 -0500 Subject: [PATCH] WIP RTS AI functionality --- core/src/mindustry/ai/RtsAI.java | 138 ++++++++++++++++++ core/src/mindustry/ai/types/CommandAI.java | 4 + core/src/mindustry/graphics/Pal.java | 2 +- core/src/mindustry/maps/SectorDamage.java | 4 +- .../world/blocks/defense/turrets/Turret.java | 5 + gradle.properties | 2 +- 6 files changed, 151 insertions(+), 4 deletions(-) diff --git a/core/src/mindustry/ai/RtsAI.java b/core/src/mindustry/ai/RtsAI.java index ca2b9c7537..fb34b78478 100644 --- a/core/src/mindustry/ai/RtsAI.java +++ b/core/src/mindustry/ai/RtsAI.java @@ -1,15 +1,153 @@ package mindustry.ai; +import arc.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.entities.*; +import mindustry.game.EventType.*; import mindustry.game.Teams.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.ui.*; +import mindustry.world.blocks.defense.turrets.Turret.*; +import mindustry.world.meta.*; public class RtsAI{ + static final Seq targets = new Seq<>(); + static final int timeUpdate = 0; + static final float minWeight = 0.9f; + + //in order of priority?? + 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; + + final Interval timer = new Interval(10); final TeamData data; public RtsAI(TeamData data){ this.data = data; + + //TODO remove: debugging! + + Events.run(Trigger.draw, () -> { + if(!debug) return; + + 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); + } + } + Fonts.outline.getData().setScale(s); + }); + + }); } public void update(){ + if(timer.get(timeUpdate, 60f * 2f)){ + var build = findAttackPoint(); + if(build != null){ + for(var unit : data.units){ + if(unit.isCommandable() && !unit.command().hasCommand()){ + unit.command().commandTarget(build); + } + } + } + } + } + + @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 ++; + } + } + + //flag priority? + //1. generator + //2. factor + //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)); + } + + if(targets.size == 0) return null; + + weights.clear(); + + for(var target : targets){ + 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; + }); + + float weight = weights.get(result, 0f); + if(weight < minWeight && total < Units.getCap(data.team)){ + return null; + } + + return result; + } + + float estimateStats(float x, float y, float selfDps, float selfHealth){ + float[] health = {0f}, dps = {0f}; + float extraRadius = 15f; + + //TODO this does not take into account the path to this object + for(var turret : Vars.indexer.getEnemy(data.team, BlockFlag.turret)){ + if(turret.within(x, y, ((TurretBuild)turret).range() + extraRadius)){ + health[0] += turret.health; + dps[0] += ((TurretBuild)turret).estimateDps(); + } + } + + //add on extra radius, assume unit range is below that...? + Units.nearbyEnemies(data.team, x, y, extraRadius + 140f, other -> { + if(other.within(x, 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; } } diff --git a/core/src/mindustry/ai/types/CommandAI.java b/core/src/mindustry/ai/types/CommandAI.java index 20fcaae230..4af2a98868 100644 --- a/core/src/mindustry/ai/types/CommandAI.java +++ b/core/src/mindustry/ai/types/CommandAI.java @@ -94,6 +94,10 @@ public class CommandAI extends AIController{ return attackTarget != null || timer.get(timerTarget, 20); } + public boolean hasCommand(){ + return targetPos != null; + } + public void commandPosition(Vec2 pos){ targetPos = pos; attackTarget = null; diff --git a/core/src/mindustry/graphics/Pal.java b/core/src/mindustry/graphics/Pal.java index 1780680b9d..b10065fa90 100644 --- a/core/src/mindustry/graphics/Pal.java +++ b/core/src/mindustry/graphics/Pal.java @@ -133,5 +133,5 @@ public class Pal{ techBlue = Color.valueOf("8ca9e8"), vent = Color.valueOf("6b4e4e"), - vent2 = Color.valueOf("261d1d"); + vent2 = Color.valueOf("3b2a2a"); } diff --git a/core/src/mindustry/maps/SectorDamage.java b/core/src/mindustry/maps/SectorDamage.java index a37e153f95..84fcc13036 100644 --- a/core/src/mindustry/maps/SectorDamage.java +++ b/core/src/mindustry/maps/SectorDamage.java @@ -285,8 +285,8 @@ public class SectorDamage{ if(e > 0.08f){ if(build.team == state.rules.defaultTeam && build instanceof Ranged ranged && sparse.contains(t -> t.within(build, ranged.range() + 4*tilesize))){ //TODO make sure power turret network supports the turrets? - if(build.block instanceof Turret t && build instanceof TurretBuild b && b.hasAmmo()){ - sumDps += t.shots / t.reloadTime * 60f * b.peekAmmo().estimateDPS() * e * build.timeScale; + if(build instanceof TurretBuild b && b.hasAmmo()){ + sumDps += b.estimateDps(); } if(build.block instanceof MendProjector m){ diff --git a/core/src/mindustry/world/blocks/defense/turrets/Turret.java b/core/src/mindustry/world/blocks/defense/turrets/Turret.java index 1a82b3f2e9..7f758dcf69 100644 --- a/core/src/mindustry/world/blocks/defense/turrets/Turret.java +++ b/core/src/mindustry/world/blocks/defense/turrets/Turret.java @@ -198,6 +198,11 @@ public class Turret extends ReloadTurret{ public float heatReq; public float[] sideHeat = new float[4]; + public float estimateDps(){ + if(!hasAmmo()) return 0f; + return shots / reloadTime * 60f * peekAmmo().estimateDPS() * efficiency() * timeScale; + } + @Override public float range(){ if(hasAmmo()){ diff --git a/gradle.properties b/gradle.properties index 461ef6c3be..1581b2309d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,4 +24,4 @@ android.useAndroidX=true #used for slow jitpack builds; TODO see if this actually works org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 -archash=ea5e9c0c40 +archash=6f8e68b7db