package mindustry.entities; import arc.*; import arc.func.*; import arc.math.geom.*; import arc.struct.*; import arc.util.*; import mindustry.annotations.Annotations.*; import mindustry.content.*; import mindustry.game.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.type.*; import mindustry.world.*; import static mindustry.Vars.*; /** Utility class for unit and team interactions.*/ public class Units{ private static final Rect hitrect = new Rect(); private static Unit result; private static float cdist, cpriority; private static boolean boolResult; private static int intResult; private static Building buildResult; //prevents allocations in anyEntities private static boolean anyEntityGround; private static float aeX, aeY, aeW, aeH; private static final Cons anyEntityLambda = unit -> { if(boolResult) return; if((unit.isGrounded() && !unit.type.allowLegStep) == anyEntityGround){ unit.hitboxTile(hitrect); if(hitrect.overlaps(aeX, aeY, aeW, aeH)){ boolResult = true; } } }; @Remote(called = Loc.server) public static void unitCapDeath(Unit unit){ if(unit != null){ unit.dead = true; Fx.unitCapKill.at(unit); Core.app.post(() -> Call.unitDestroy(unit.id)); } } @Remote(called = Loc.server) public static void unitEnvDeath(Unit unit){ if(unit != null){ unit.dead = true; Fx.unitEnvKill.at(unit); Core.app.post(() -> Call.unitDestroy(unit.id)); } } @Remote(called = Loc.server) public static void unitDeath(int uid){ Unit unit = Groups.unit.getByID(uid); //if there's no unit don't add it later and get it stuck as a ghost if(netClient != null){ netClient.addRemovedEntity(uid); } if(unit != null){ unit.killed(); } } //destroys immediately @Remote(called = Loc.server) public static void unitDestroy(int uid){ Unit unit = Groups.unit.getByID(uid); //if there's no unit don't add it later and get it stuck as a ghost if(netClient != null){ netClient.addRemovedEntity(uid); } if(unit != null){ unit.destroy(); } } @Remote(called = Loc.server) public static void unitDespawn(Unit unit){ Fx.unitDespawn.at(unit.x, unit.y, 0, unit); unit.remove(); } /** @return whether a new instance of a unit of this team can be created. */ public static boolean canCreate(Team team, UnitType type){ return team.data().countType(type) < getCap(team); } public static int getCap(Team team){ //wave team has no cap if((team == state.rules.waveTeam && !state.rules.pvp) || (state.isCampaign() && team == state.rules.waveTeam)){ return Integer.MAX_VALUE; } return Math.max(0, state.rules.unitCapVariable ? state.rules.unitCap + team.data().unitCap : state.rules.unitCap); } /** @return unit cap as a string, substituting the infinity symbol instead of MAX_VALUE */ public static String getStringCap(Team team){ int cap = getCap(team); return cap >= Integer.MAX_VALUE - 1 ? "∞" : cap + ""; } /** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/ public static boolean canInteract(Player player, Building tile){ return player == null || tile == null || tile.interactable(player.team()); } /** * Validates a target. * @param target The target to validate * @param team The team of the thing doing tha targeting * @param x The X position of the thing doing the targeting * @param y The Y position of the thing doing the targeting * @param range The maximum distance from the target X/Y the targeter can be for it to be valid * @return whether the target is invalid */ public static boolean invalidateTarget(Posc target, Team team, float x, float y, float range){ return target == null || (range != Float.MAX_VALUE && !target.within(x, y, range + (target instanceof Sized hb ? hb.hitSize()/2f : 0f))) || (target instanceof Teamc t && t.team() == team) || (target instanceof Healthc h && !h.isValid()) || (target instanceof Unit u && !u.targetable(team)); } /** See {@link #invalidateTarget(Posc, Team, float, float, float)} */ public static boolean invalidateTarget(Posc target, Team team, float x, float y){ return invalidateTarget(target, team, x, y, Float.MAX_VALUE); } /** See {@link #invalidateTarget(Posc, Team, float, float, float)} */ public static boolean invalidateTarget(Teamc target, Unit targeter, float range){ return invalidateTarget(target, targeter.team(), targeter.x(), targeter.y(), range); } /** Returns whether there are any entities on this tile. */ public static boolean anyEntities(Tile tile, boolean ground){ float size = tile.block().size * tilesize; return anyEntities(tile.drawx() - size/2f, tile.drawy() - size/2f, size, size, ground); } /** Returns whether there are any entities on this tile. */ public static boolean anyEntities(Tile tile){ return anyEntities(tile, true); } public static boolean anyEntities(float x, float y, float size){ return anyEntities(x - size/2f, y - size/2f, size, size, true); } public static boolean anyEntities(float x, float y, float width, float height){ return anyEntities(x, y, width, height, true); } public static boolean anyEntities(float x, float y, float width, float height, boolean ground){ boolResult = false; anyEntityGround = ground; aeX = x; aeY = y; aeW = width; aeH = height; nearby(x, y, width, height, anyEntityLambda); return boolResult; } public static boolean anyEntities(float x, float y, float width, float height, Boolf check){ boolResult = false; nearby(x, y, width, height, unit -> { if(boolResult) return; if(check.get(unit)){ unit.hitboxTile(hitrect); if(hitrect.overlaps(x, y, width, height)){ boolResult = true; } } }); return boolResult; } /** Returns the nearest damaged tile. */ public static Building findDamagedTile(Team team, float x, float y){ return indexer.getDamaged(team).min(b -> b.dst2(x, y)); } /** Returns the nearest ally tile in a range. */ public static Building findAllyTile(Team team, float x, float y, float range, Boolf pred){ return indexer.findTile(team, x, y, range, pred); } /** Returns the nearest enemy tile in a range. */ public static Building findEnemyTile(Team team, float x, float y, float range, Boolf pred){ if(team == Team.derelict) return null; return indexer.findEnemyTile(team, x, y, range, pred); } /** @return the closest building of the provided team that matches the predicate. */ public static @Nullable Building closestBuilding(Team team, float wx, float wy, float range, Boolf pred){ buildResult = null; cdist = 0f; var buildings = team.data().buildingTree; if(buildings == null) return null; buildings.intersect(wx - range, wy - range, range*2f, range*2f, b -> { if(pred.get(b)){ float dst = b.dst(wx, wy) - b.hitSize()/2f; if(dst <= range && (buildResult == null || dst <= cdist)){ cdist = dst; buildResult = b; } } }); return buildResult; } /** Iterates through all buildings in a range. */ public static void nearbyBuildings(float x, float y, float range, Cons cons){ indexer.allBuildings(x, y, range, cons); } /** Returns the closest target enemy. First, units are checked, then tile entities. */ public static Teamc closestTarget(Team team, float x, float y, float range){ return closestTarget(team, x, y, range, Unit::isValid); } /** Returns the closest target enemy. First, units are checked, then tile entities. */ public static Teamc closestTarget(Team team, float x, float y, float range, Boolf unitPred){ return closestTarget(team, x, y, range, unitPred, t -> true); } /** Returns the closest target enemy. First, units are checked, then tile entities. */ public static Teamc closestTarget(Team team, float x, float y, float range, Boolf unitPred, Boolf tilePred){ if(team == Team.derelict) return null; Unit unit = closestEnemy(team, x, y, range, unitPred); if(unit != null){ return unit; }else{ return findEnemyTile(team, x, y, range, tilePred); } } /** Returns the closest target enemy. First, units are checked, then buildings. */ public static Teamc bestTarget(Team team, float x, float y, float range, Boolf unitPred, Boolf tilePred, Sortf sort){ if(team == Team.derelict) return null; Unit unit = bestEnemy(team, x, y, range, unitPred, sort); if(unit != null){ return unit; }else{ return findEnemyTile(team, x, y, range, tilePred); } } /** Returns the closest enemy of this team. Filter by predicate. */ public static Unit closestEnemy(Team team, float x, float y, float range, Boolf predicate){ if(team == Team.derelict) return null; result = null; cdist = 0f; cpriority = -99999f; nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> { if(e.dead() || !predicate.get(e) || e.team == Team.derelict || !e.targetable(team)) return; float dst2 = e.dst2(x, y) - (e.hitSize * e.hitSize); if(dst2 < range*range && (result == null || dst2 < cdist || e.type.targetPriority > cpriority) && e.type.targetPriority >= cpriority){ result = e; cdist = dst2; cpriority = e.type.targetPriority; } }); return result; } /** Returns the closest enemy of this team using a custom comparison function. Filter by predicate. */ public static Unit bestEnemy(Team team, float x, float y, float range, Boolf predicate, Sortf sort){ if(team == Team.derelict) return null; result = null; cdist = 0f; cpriority = -99999f; nearbyEnemies(team, x - range, y - range, range*2f, range*2f, e -> { if(e.dead() || !predicate.get(e) || e.team == Team.derelict || !e.within(x, y, range + e.hitSize/2f) || !e.targetable(team)) return; float cost = sort.cost(e, x, y); if((result == null || cost < cdist || e.type.targetPriority > cpriority) && e.type.targetPriority >= cpriority){ result = e; cdist = cost; cpriority = e.type.targetPriority; } }); return result; } /** Returns the closest ally of this team. Filter by predicate. No range. */ public static Unit closest(Team team, float x, float y, Boolf predicate){ result = null; cdist = 0f; for(Unit e : Groups.unit){ if(!predicate.get(e) || e.team() != team) continue; float dist = e.dst2(x, y); if(result == null || dist < cdist){ result = e; cdist = dist; } } return result; } /** Returns the closest ally of this team in a range. Filter by predicate. */ public static Unit closest(Team team, float x, float y, float range, Boolf predicate){ result = null; cdist = 0f; nearby(team, x, y, range, e -> { if(!predicate.get(e)) return; float dist = e.dst2(x, y); if(result == null || dist < cdist){ result = e; cdist = dist; } }); return result; } /** Returns the closest ally of this team in a range. Filter by predicate. */ public static Unit closest(Team team, float x, float y, float range, Boolf predicate, Sortf sort){ result = null; cdist = 0f; nearby(team, x, y, range, e -> { if(!predicate.get(e)) return; float dist = sort.cost(e, x, y); if(result == null || dist < cdist){ result = e; cdist = dist; } }); return result; } /** Returns the closest ally of this team. Filter by predicate. * Unlike the closest() function, this only guarantees that unit hitboxes overlap the range. */ public static Unit closestOverlap(Team team, float x, float y, float range, Boolf predicate){ result = null; cdist = 0f; nearby(team, x - range, y - range, range*2f, range*2f, e -> { if(!predicate.get(e)) return; float dist = e.dst2(x, y); if(result == null || dist < cdist){ result = e; cdist = dist; } }); return result; } /** @return whether any units exist in this square (centered) */ public static int count(float x, float y, float size, Boolf filter){ return count(x - size/2f, y - size/2f, size, size, filter); } /** @return whether any units exist in this rectangle */ public static int count(float x, float y, float width, float height, Boolf filter){ intResult = 0; Groups.unit.intersect(x, y, width, height, v -> { if(filter.get(v)){ intResult ++; } }); return intResult; } /** @return whether any units exist in this rectangle */ public static boolean any(float x, float y, float width, float height, Boolf filter){ return count(x, y, width, height, filter) > 0; } /** Iterates over all units in a rectangle. */ public static void nearby(@Nullable Team team, float x, float y, float width, float height, Cons cons){ if(team != null){ team.data().tree().intersect(x, y, width, height, cons); }else{ for(var other : state.teams.getActive()){ other.tree().intersect(x, y, width, height, cons); } } } /** Iterates over all units in a circle around this position. */ public static void nearby(@Nullable Team team, float x, float y, float radius, Cons cons){ nearby(team, x - radius, y - radius, radius*2f, radius*2f, unit -> { if(unit.within(x, y, radius + unit.hitSize/2f)){ cons.get(unit); } }); } /** Iterates over all units in a rectangle. */ public static void nearby(float x, float y, float width, float height, Cons cons){ Groups.unit.intersect(x, y, width, height, cons); } /** Iterates over all units in a rectangle. */ public static void nearby(Rect rect, Cons cons){ nearby(rect.x, rect.y, rect.width, rect.height, cons); } /** Iterates over all units that are enemies of this team. */ public static void nearbyEnemies(Team team, float x, float y, float width, float height, Cons cons){ Seq data = state.teams.present; for(int i = 0; i < data.size; i++){ if(data.items[i].team != team){ nearby(data.items[i].team, x, y, width, height, cons); } } } /** Iterates over all units that are enemies of this team. */ public static void nearbyEnemies(Team team, float x, float y, float radius, Cons cons){ nearbyEnemies(team, x - radius, y - radius, radius * 2f, radius * 2f, u -> { if(u.within(x, y, radius + u.hitSize/2f)){ cons.get(u); } }); } /** Iterates over all units that are enemies of this team. */ public static void nearbyEnemies(Team team, Rect rect, Cons cons){ 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.turretTree != null && other.turretTree.any(x, y, width, height)){ return true; } } } return false; } public interface Sortf{ float cost(Unit unit, float x, float y); } }