Made unit shields absorb lasers/bullets too

This commit is contained in:
Anuken
2026-09-21 00:35:56 -04:00
parent c92cd927d1
commit 59d11e56a5
10 changed files with 237 additions and 29 deletions
+1 -1
View File
@@ -482,7 +482,7 @@ public class UnitTypes{
shootSound = Sounds.shootLancer;
bullet = new LaserBulletType(){{
damage = 45f;
damage = 55f;
recoil = 0f;
sideAngle = 45f;
sideWidth = 1f;
+54 -2
View File
@@ -10,6 +10,7 @@ import arc.util.*;
import arc.util.pooling.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.abilities.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
@@ -26,7 +27,7 @@ public class Damage{
private static final UnitDamageEvent bulletDamageEvent = new UnitDamageEvent();
private static final Rect rect = new Rect();
private static final Rect hitrect = new Rect();
private static final Vec2 vec = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2();
private static final Vec2 vec = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2(), polyHit = new Vec2();
private static final IntSet collidedBlocks = new IntSet();
private static final IntFloatMap damages = new IntFloatMap();
private static final Seq<Collided> collided = new Seq<>(), shieldHits = new Seq<>();
@@ -236,6 +237,31 @@ public class Damage{
return findShieldLength(b, result, laser, absorb);
}
/** @return the first point where a segment enters a regular polygon, stored in a shared vector, or null if it doesn't. */
public static @Nullable Vec2 raycastRegularPolygon(int sides, float cx, float cy, float radius, float rotation, float x1, float y1, float x2, float y2){
if(radius <= 0f) return null;
if(Intersector.isInRegularPolygon(sides, cx, cy, radius, rotation, x1, y1)){
return polyHit.set(x1, y1);
}
float best = Float.MAX_VALUE;
for(int i = 0; i < sides; i++){
Tmp.v1.trns(rotation + i * 360f / sides, radius).add(cx, cy);
Tmp.v2.trns(rotation + (i + 1) * 360f / sides, radius).add(cx, cy);
if(Intersector.intersectSegments(x1, y1, x2, y2, Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y, Tmp.v3)){
float dst = Tmp.v3.dst2(x1, y1);
if(dst < best){
best = dst;
polyHit.set(Tmp.v3);
}
}
}
return best == Float.MAX_VALUE ? null : polyHit;
}
/**
* Finds the enemy shields hit by a laser, in order, until one absorbs the rest of its damage.
* @param absorb whether to actually apply the hits to the shields; otherwise the first shield is assumed to absorb everything.
@@ -261,13 +287,31 @@ public class Damage{
}
}
var units = Units.enemyShields(b.team, rect.x, rect.y, rect.width, rect.height);
for(int i = 0; i < units.size; i++){
Unit unit = units.get(i);
if(unit.dead) continue;
for(Ability ability : unit.abilities){
if(ability instanceof UnitShieldProvider shield){
Vec2 hit = shield.intersectLaser(unit, seg1.x, seg1.y, seg2.x, seg2.y, damage);
if(hit != null){
shieldHits.add(collidePool.obtain().set(hit.x, hit.y, unit, shield));
}
}
}
}
float result = length, remaining = damage;
shieldHits.sort(c -> Mathf.dst2(seg1.x, seg1.y, c.x, c.y));
for(int i = 0; i < shieldHits.size; i++){
Collided c = shieldHits.get(i);
float absorbed = absorb ? ((ShieldProvider)c.target).absorbLaser(c.x, c.y, remaining) : remaining;
float absorbed = remaining;
if(absorb){
absorbed = c.ability != null ? c.ability.absorbLaser((Unit)c.target, c.x, c.y, remaining) : ((ShieldProvider)c.target).absorbLaser(c.x, c.y, remaining);
}
remaining -= absorbed;
if(remaining <= 0f){
@@ -739,6 +783,8 @@ public class Damage{
public static class Collided implements Pool.Poolable{
public float x, y;
public Teamc target;
/** Set when the target is a unit hit through one of its shield abilities. */
public @Nullable UnitShieldProvider ability;
public Collided set(float x, float y, Teamc target){
this.x = x;
@@ -747,9 +793,15 @@ public class Damage{
return this;
}
public Collided set(float x, float y, Teamc target, UnitShieldProvider ability){
this.ability = ability;
return set(x, y, target);
}
@Override
public void reset(){
target = null;
ability = null;
}
}
}
+15
View File
@@ -18,6 +18,7 @@ import static mindustry.Vars.*;
/** Utility class for unit and team interactions.*/
public class Units{
private static final Rect hitrect = new Rect();
private static final Seq<Unit> shieldReturn = new Seq<>();
private static Unit result;
private static float cdist, cpriority;
private static int intResult;
@@ -441,6 +442,20 @@ public class Units{
}
}
/** @return all enemy units with shield abilities that may reach into a rectangle (bottom left corner origin). The result is reused between calls. */
public static Seq<Unit> enemyShields(Team team, float x, float y, float width, float height){
shieldReturn.clear();
Seq<TeamData> data = state.teams.present;
for(int i = 0; i < data.size; i++){
var tree = data.items[i].unitShieldTree;
if(data.items[i].team != team && tree != null){
tree.intersect(x, y, width, height, shieldReturn);
}
}
return shieldReturn;
}
/** Iterates over all units in a circle around this position. */
public static void nearby(@Nullable Team team, float x, float y, float radius, Cons<Unit> cons){
nearby(team, x - radius, y - radius, radius*2f, radius*2f, unit -> {
@@ -11,13 +11,14 @@ import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.entities.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
import static mindustry.Vars.*;
public class ForceFieldAbility extends Ability{
public class ForceFieldAbility extends Ability implements UnitShieldProvider{
/** Shield radius. */
public float radius = 60f;
/** Shield regen speed in damage/tick. */
@@ -74,6 +75,27 @@ public class ForceFieldAbility extends Ability{
return max * Vars.state.rules.unitHealth(unit.team);
}
@Override
public float shieldBounds(){
return radius;
}
@Override
public @Nullable Vec2 intersectLaser(Unit unit, float x1, float y1, float x2, float y2, float damage){
return unit.shield > 0f ? Damage.raycastRegularPolygon(sides, unit.x, unit.y, radiusScale * radius, rotation, x1, y1, x2, y2) : null;
}
@Override
public float absorbLaser(Unit unit, float x, float y, float damage){
float absorbed = Math.min(damage, Math.max(unit.shield, 0f));
if(absorbed > 0f){
Fx.absorb.at(x, y);
unit.shield -= damage;
alpha = 1f;
}
return absorbed;
}
@Override
public void addStats(Table t){
super.addStats(t);
@@ -15,11 +15,12 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.ui.*;
public class ShieldArcAbility extends Ability{
public class ShieldArcAbility extends Ability implements UnitShieldProvider{
private static Unit paramUnit;
private static ShieldArcAbility paramField;
private static Vec2 paramPos = new Vec2();
private static final Vec2 laserHit = new Vec2();
private static final Cons<Bullet> shieldConsumer = b -> {
if(b.team != paramUnit.team && b.type.absorbable && paramField.data > 0 &&
!(b.within(paramPos, paramField.radius - paramField.width) && paramPos.within(b.x - b.deltaX, b.y - b.deltaY, paramField.radius - paramField.width)) &&
@@ -169,6 +170,84 @@ public class ShieldArcAbility extends Ability{
return max * Vars.state.rules.unitHealth(unit.team);
}
@Override
public float shieldBounds(){
return Mathf.len(x, y) + radius + width;
}
@Override
public @Nullable Vec2 intersectLaser(Unit unit, float x1, float y1, float x2, float y2, float damage){
if(data <= 0f || !(unit.isShooting || !whenShooting)) return null;
Tmp.v1.set(x, y).rotate(unit.rotation - 90f).add(unit);
float cx = Tmp.v1.x, cy = Tmp.v1.y;
float rot = unit.rotation + angleOffset, half = angle / 2f;
float inner = Math.max(radius - width, 0f), outer = radius + width;
float dx = x2 - x1, dy = y2 - y1, a = dx * dx + dy * dy;
float fx = x1 - cx, fy = y1 - cy, start = fx * fx + fy * fy;
//starts inside the band
if(start >= inner * inner && start <= outer * outer && inSpan(cx, cy, x1, y1, rot, half)){
return laserHit.set(x1, y1);
}
float best = Float.MAX_VALUE;
//crossings of the outer and inner arcs, as fractions along the segment
if(a > 0f){
float b = 2f * (fx * dx + fy * dy);
for(int i = 0; i < 2; i++){
float r = i == 0 ? outer : inner;
float disc = b * b - 4f * a * (start - r * r);
if(disc < 0f) continue;
float sqrt = Mathf.sqrt(disc);
for(int s : Mathf.signs){
float t = (-b + sqrt * s) / (2f * a);
if(t >= 0f && t <= 1f && t < best && inSpan(cx, cy, x1 + dx * t, y1 + dy * t, rot, half)){
best = t;
}
}
}
}
//crossings of the radial end edges
if(angle < 360f){
for(int s : Mathf.signs){
float ex = Angles.trnsx(rot + half * s, 1f), ey = Angles.trnsy(rot + half * s, 1f);
if(Intersector.intersectSegments(x1, y1, x2, y2, cx + ex * inner, cy + ey * inner, cx + ex * outer, cy + ey * outer, Tmp.v2)){
best = Math.min(best, Tmp.v2.dst(x1, y1) / Mathf.sqrt(a));
}
}
}
return best == Float.MAX_VALUE ? null : laserHit.set(x1 + dx * best, y1 + dy * best);
}
@Override
public float absorbLaser(Unit unit, float lx, float ly, float damage){
float absorbed = Math.min(damage, Math.max(data, 0f));
if(absorbed > 0f){
Fx.absorb.at(lx, ly);
if(data <= damage){
Tmp.v1.set(x, y).rotate(unit.rotation - 90f).add(unit);
data -= cooldown * regen;
Fx.arcShieldBreak.at(Tmp.v1.x, Tmp.v1.y, 0, color == null ? unit.type.shieldColor(unit) : color, unit);
breakSound.at(Tmp.v1.x, Tmp.v1.y);
}
data -= damage;
alpha = 1f;
}
return absorbed;
}
protected boolean inSpan(float cx, float cy, float px, float py, float rotation, float half){
return angle >= 360f || Angles.within(Angles.angle(cx, cy, px, py), rotation, half);
}
@Override
public void addStats(Table t){
super.addStats(t);
@@ -0,0 +1,17 @@
package mindustry.entities.abilities;
import arc.math.geom.*;
import arc.util.*;
import mindustry.gen.*;
/** Implemented by abilities that shield their unit from lasers. Ability instances are per-unit copies, so the owner is passed in. */
public interface UnitShieldProvider{
/** @return maximum distance the shield can reach from the unit's center. */
float shieldBounds();
/** @return the first point where the segment hits this shield, stored in a shared vector (copy it), or null if it doesn't. */
@Nullable Vec2 intersectLaser(Unit unit, float x1, float y1, float x2, float y2, float damage);
/** Applies a laser hit at x, y. @return how much of the damage was absorbed; anything less than the full damage means the laser continues through. */
float absorbLaser(Unit unit, float x, float y, float damage);
}
+31
View File
@@ -195,6 +195,9 @@ public class Teams{
if(data.unitTree != null){
data.unitTree.clear();
}
if(data.unitShieldTree != null){
data.unitShieldTree.clear();
}
if(data.typeCounts != null){
Arrays.fill(data.typeCounts, 0);
@@ -215,6 +218,9 @@ public class Teams{
if(unit.type == null) continue;
TeamData data = unit.team.data();
data.tree().insert(unit);
if(unit.type.shieldBounds > 0f && !unit.dead){
data.unitShieldTree().insert(unit);
}
data.units.add(unit);
data.presentFlag = true;
@@ -299,6 +305,8 @@ public class Teams{
public @Nullable QuadTree<Building> shieldTree;
/** Quadtree for units of this team. Do not access directly. */
public @Nullable QuadTree<Unit> unitTree;
/** Quadtree for units of this team that have shield abilities. Rebuilt each frame; do not access directly. */
public @Nullable QuadTree<Unit> unitShieldTree;
/** Current unit cap. Do not modify externally. */
public int unitCap;
/** Total unit count. */
@@ -446,6 +454,11 @@ public class Teams{
return unitTree;
}
public QuadTree<Unit> unitShieldTree(){
if(unitShieldTree == null) unitShieldTree = new UnitShieldQuadtree(Vars.world.getQuadBounds(new Rect()));
return unitShieldTree;
}
public int countType(UnitType type){
return typeCounts == null || typeCounts.length <= type.id ? 0 : typeCounts[type.id];
}
@@ -514,6 +527,24 @@ public class Teams{
}
}
static class UnitShieldQuadtree extends QuadTree<Unit>{
public UnitShieldQuadtree(Rect bounds){
super(bounds);
}
@Override
public void hitbox(Unit unit){
//padded by speed, as the tree is built before units move each frame
tmp.setCentered(unit.x, unit.y, (unit.type.shieldBounds + unit.type.speed * 2f) * 2f);
}
@Override
protected QuadTree<Unit> newChild(Rect rect){
return new UnitShieldQuadtree(rect);
}
}
/** Represents a block made by this team that was destroyed somewhere on the map.
* This does not include deconstructed blocks.*/
public static class BlockPlan{
+14
View File
@@ -285,6 +285,8 @@ public class UnitType extends UnlockableContent implements Senseable{
/** list of "abilities", which are various behaviors that update each frame */
public Seq<Ability> abilities = new Seq<>();
/** Maximum reach of shield abilities from the unit's center; 0 if there are none. Set in init(). */
public float shieldBounds;
/** All weapons that this unit will shoot with. */
public Seq<Weapon> weapons = new Seq<>();
/** None of the status effects in this set can be applied to this unit. */
@@ -1033,6 +1035,8 @@ public class UnitType extends UnlockableContent implements Senseable{
ab.init(this);
}
updateShieldBounds();
//add mirrored weapon variants
Seq<Weapon> mapped = new Seq<>();
for(Weapon w : weapons){
@@ -1303,6 +1307,16 @@ public class UnitType extends UnlockableContent implements Senseable{
pathCost = null;
pathCostId = -1;
initPathType();
updateShieldBounds();
}
public void updateShieldBounds(){
shieldBounds = 0f;
for(Ability ab : abilities){
if(ab instanceof UnitShieldProvider shield){
shieldBounds = Math.max(shieldBounds, shield.shieldBounds());
}
}
}
public void beforeParse(){
@@ -520,7 +520,7 @@ public class SettingsMenuDialog extends BaseDialog{
graphics.checkPref("mouseposition", false);
}
graphics.checkPref("fps", false);
graphics.checkPref("playerindicators", true);
graphics.checkPref("playerindicators", false);
graphics.checkPref("showpings", true);
graphics.checkPref("showotherbuildplans", true);
graphics.checkPref("indicators", true);
@@ -57,7 +57,6 @@ public class ForceProjector extends Block{
//lambdas need to be static to prevent GC
protected static ForceProjector paramBlock;
protected static ForceBuild paramEntity;
protected static final Vec2 laserHit = new Vec2();
protected static final Cons<Bullet> shieldConsumer = bullet -> {
if(bullet.team != paramEntity.team && bullet.type.absorbable && !bullet.absorbed &&
Intersector.isInRegularPolygon(paramBlock.sides, paramEntity.x, paramEntity.y, paramEntity.realRadius(), paramBlock.shieldRotation, bullet.x, bullet.y)){
@@ -280,28 +279,7 @@ public class ForceProjector extends Block{
@Override
public @Nullable Vec2 intersectLaser(float x1, float y1, float x2, float y2, float damage){
float radius = realRadius();
if(broken || radius <= 0f) return null;
if(Intersector.isInRegularPolygon(sides, x, y, radius, shieldRotation, x1, y1)){
return laserHit.set(x1, y1);
}
float best = Float.MAX_VALUE;
for(int i = 0; i < sides; i++){
Tmp.v1.trns(shieldRotation + i * 360f / sides, radius).add(x, y);
Tmp.v2.trns(shieldRotation + (i + 1) * 360f / sides, radius).add(x, y);
if(Intersector.intersectSegments(x1, y1, x2, y2, Tmp.v1.x, Tmp.v1.y, Tmp.v2.x, Tmp.v2.y, Tmp.v3)){
float dst = Tmp.v3.dst2(x1, y1);
if(dst < best){
best = dst;
laserHit.set(Tmp.v3);
}
}
}
return best == Float.MAX_VALUE ? null : laserHit;
return broken ? null : Damage.raycastRegularPolygon(sides, x, y, realRadius(), shieldRotation, x1, y1, x2, y2);
}
@Override