diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index 786821eadb..2610dd5939 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -683,7 +683,7 @@ public class BlockIndexer{ @Override public void hitbox(T build){ - tmp.setCentered(build.x, build.y, build.getShieldBounds()); + tmp.setCentered(build.x, build.y, build.getShieldBounds() * 2f); } @Override diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index 4a99e4df30..57c3cb5944 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -29,7 +29,7 @@ public class Damage{ private static final Vec2 vec = new Vec2(), seg1 = new Vec2(), seg2 = new Vec2(); private static final IntSet collidedBlocks = new IntSet(); private static final IntFloatMap damages = new IntFloatMap(); - private static final Seq collided = new Seq<>(); + private static final Seq collided = new Seq<>(), shieldHits = new Seq<>(); private static final Pool collidePool = Pools.get(Collided.class, Collided::new); private static final Seq builds = new Seq<>(); private static final FloatSeq distances = new FloatSeq(); @@ -153,16 +153,26 @@ public class Damage{ } public static float findLength(Bullet b, float length, boolean laser, int pierceCap){ + return findLength(b, length, laser, pierceCap, false); + } + + private static float findLength(Bullet b, float length, boolean laser, int pierceCap, boolean absorb){ if(pierceCap > 0){ - length = findPierceLength(b, pierceCap, laser, length); + length = findPierceLength(b, pierceCap, laser, length, absorb); }else if(laser){ - length = findLaserLength(b, length); + length = findLaserLength(b, length, absorb); + }else{ + length = findShieldLength(b, length, false, absorb); } return length; } public static float findLaserLength(Bullet b, float length){ + return findLaserLength(b, length, false); + } + + private static float findLaserLength(Bullet b, float length, boolean absorb){ vec.trnsExact(b.rotation(), length); furthest = null; @@ -170,7 +180,9 @@ public class Damage{ boolean found = World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + vec.x), World.toTile(b.y + vec.y), (x, y) -> (furthest = world.tile(x, y)) != null && furthest.team() != b.team && (furthest.build != null && furthest.build.absorbLasers())); - return found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length; + float result = found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length; + + return findShieldLength(b, result, true, absorb); } public static float findPierceLength(Bullet b, int pierceCap, float length){ @@ -178,6 +190,10 @@ public class Damage{ } public static float findPierceLength(Bullet b, int pierceCap, boolean laser, float length){ + return findPierceLength(b, pierceCap, laser, length, false); + } + + private static float findPierceLength(Bullet b, int pierceCap, boolean laser, float length, boolean absorb){ vec.trnsExact(b.rotation(), length); rect.setPosition(b.x, b.y).setSize(vec.x, vec.y).normalize().grow(3f); @@ -215,14 +231,66 @@ public class Damage{ //return either the length when not enough things were pierced, //or the last pierced object if there were enough blockages - return Math.min(distances.size < pierceCap || pierceCap <= 0 ? length : Math.max(6f, distances.get(pierceCap - 1)), maxDst); + float result = Math.min(distances.size < pierceCap || pierceCap <= 0 ? length : Math.max(6f, distances.get(pierceCap - 1)), maxDst); + + return findShieldLength(b, result, laser, absorb); + } + + /** + * 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. + * If the shields only absorb part of the damage, the bullet's damage is reduced accordingly. + * @return the length of the laser, cut short if a shield absorbed it. + */ + private static float findShieldLength(Bullet b, float length, boolean laser, boolean absorb){ + float damage = b.type.shieldDamage(b); + if(!(laser || b.type.shieldAbsorb) || length <= 0f || damage <= 0f) return length; + + seg1.set(b.x, b.y); + seg2.trnsExact(b.rotation(), length).add(seg1); + rect.setPosition(seg1.x, seg1.y).setSize(seg2.x - seg1.x, seg2.y - seg1.y).normalize(); + + var shields = indexer.getEnemyShields(b.team, rect.x, rect.y, rect.width, rect.height); + for(int i = 0; i < shields.size; i++){ + Building build = shields.get(i); + if(build instanceof ShieldProvider shield){ + Vec2 hit = shield.intersectLaser(seg1.x, seg1.y, seg2.x, seg2.y, damage); + if(hit != null){ + shieldHits.add(collidePool.obtain().set(hit.x, hit.y, build)); + } + } + } + + 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; + remaining -= absorbed; + + if(remaining <= 0f){ + result = Mathf.dst(seg1.x, seg1.y, c.x, c.y); + break; + } + } + + if(absorb && remaining > 0f && remaining < damage){ + b.damage *= remaining / damage; + } + + collidePool.freeAll(shieldHits); + shieldHits.clear(); + + return result; } /** Collides a bullet with blocks in a laser, taking into account absorption blocks. Resulting length is stored in the bullet's fdata. */ public static float collideLaser(Bullet b, float length, boolean large, boolean laser, int pierceCap){ - float resultLength = findPierceLength(b, pierceCap, laser, length); + float resultLength = findPierceLength(b, pierceCap, laser, length, true); - collideLine(b, b.team, b.x, b.y, b.rotation(), resultLength, large, laser, pierceCap); + collideLine(b, b.team, b.x, b.y, b.rotation(), resultLength, large, laser, pierceCap, false); b.fdata = resultLength; @@ -254,7 +322,11 @@ public class Damage{ * Only enemies of the specified team are damaged. */ public static void collideLine(Bullet hitter, Team team, float x, float y, float angle, float length, boolean large, boolean laser, int pierceCap){ - length = findLength(hitter, length, laser, pierceCap); + collideLine(hitter, team, x, y, angle, length, large, laser, pierceCap, true); + } + + private static void collideLine(Bullet hitter, Team team, float x, float y, float angle, float length, boolean large, boolean laser, int pierceCap, boolean absorb){ + length = findLength(hitter, length, laser, pierceCap, absorb); hitter.fdata = length; collidedBlocks.clear(); diff --git a/core/src/mindustry/entities/bullet/BulletType.java b/core/src/mindustry/entities/bullet/BulletType.java index 7086570513..ebc0ac6700 100644 --- a/core/src/mindustry/entities/bullet/BulletType.java +++ b/core/src/mindustry/entities/bullet/BulletType.java @@ -65,6 +65,8 @@ public class BulletType extends Content implements Cloneable{ public boolean removeAfterPierce = true; /** For piercing lasers, setting this to true makes it get absorbed by plastanium walls. */ public boolean laserAbsorb = true; + /** Whether force shields absorb this bullet when it hits as a line, even if it isn't a laser. Used by rails. */ + public boolean shieldAbsorb = false; /** Whether this bullet is considered a laser bullet and thus absorbed by plastanium walls. */ public boolean laserBullet = false; /** Life fraction at which this bullet has the best range/damage/etc. Used for lasers and continuous turrets. */ diff --git a/core/src/mindustry/entities/bullet/RailBulletType.java b/core/src/mindustry/entities/bullet/RailBulletType.java index ea5f336d22..f1ffc1a95c 100644 --- a/core/src/mindustry/entities/bullet/RailBulletType.java +++ b/core/src/mindustry/entities/bullet/RailBulletType.java @@ -25,6 +25,7 @@ public class RailBulletType extends BulletType{ keepVelocity = false; lifetime = 1f; delayFrags = true; + shieldAbsorb = true; } @Override diff --git a/core/src/mindustry/world/blocks/ShieldProvider.java b/core/src/mindustry/world/blocks/ShieldProvider.java index 3b9ead0478..ab34a97a65 100644 --- a/core/src/mindustry/world/blocks/ShieldProvider.java +++ b/core/src/mindustry/world/blocks/ShieldProvider.java @@ -1,8 +1,21 @@ package mindustry.world.blocks; +import arc.math.geom.*; +import arc.util.*; + public interface ShieldProvider{ /** @return whether the shield was able to absorb the explosion; this should apply damage to the shield if true is returned. */ boolean absorbExplosion(float x, float y, float damage); /** @return maximum (not current size!) half-size of the shield square bounding box */ float getShieldBounds(); + + /** @return the first point where the segment hits this shield, stored in a shared vector (copy it), or null if it doesn't. Must not have side effects. */ + default @Nullable Vec2 intersectLaser(float x1, float y1, float x2, float y2, float damage){ + return null; + } + + /** 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. */ + default float absorbLaser(float x, float y, float damage){ + return 0f; + } } diff --git a/core/src/mindustry/world/blocks/defense/ForceProjector.java b/core/src/mindustry/world/blocks/defense/ForceProjector.java index 9f94ddae2b..cb11484470 100644 --- a/core/src/mindustry/world/blocks/defense/ForceProjector.java +++ b/core/src/mindustry/world/blocks/defense/ForceProjector.java @@ -57,6 +57,7 @@ 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 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)){ @@ -277,6 +278,43 @@ public class ForceProjector extends Block{ return absorb; } + @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; + } + + @Override + public float absorbLaser(float lx, float ly, float damage){ + float absorbed = broken ? 0f : Math.min(damage, Math.max(shieldHealth + phaseShieldBoost * phaseHeat - buildup, 0f)); + if(absorbed > 0f){ + absorbEffect.at(lx, ly); + hit = 1f; + buildup += damage; + } + return absorbed; + } + @Override public float getShieldBounds(){ return radius + phaseRadiusBoost;