mirror of
https://github.com/Anuken/Mindustry.git
synced 2026-10-01 10:40:38 +03:00
Made unit shields absorb lasers/bullets too
This commit is contained in:
@@ -482,7 +482,7 @@ public class UnitTypes{
|
||||
shootSound = Sounds.shootLancer;
|
||||
|
||||
bullet = new LaserBulletType(){{
|
||||
damage = 45f;
|
||||
damage = 55f;
|
||||
recoil = 0f;
|
||||
sideAngle = 45f;
|
||||
sideWidth = 1f;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user