diff --git a/core/assets-raw/sprites/units/quell-cell.png b/core/assets-raw/sprites/units/quell-cell.png new file mode 100644 index 0000000000..9c381e0415 Binary files /dev/null and b/core/assets-raw/sprites/units/quell-cell.png differ diff --git a/core/assets-raw/sprites/units/quell.png b/core/assets-raw/sprites/units/quell.png new file mode 100644 index 0000000000..1ce9a121c2 Binary files /dev/null and b/core/assets-raw/sprites/units/quell.png differ diff --git a/core/assets/icons/icons.properties b/core/assets/icons/icons.properties index 51936e1650..0346ad6892 100755 --- a/core/assets/icons/icons.properties +++ b/core/assets/icons/icons.properties @@ -491,3 +491,4 @@ 63215=basic-assembler-module|block-basic-assembler-module-ui 63214=beryllium-wall|block-beryllium-wall-ui 63213=beryllium-wall-large|block-beryllium-wall-large-ui +63212=quell|unit-quell-ui diff --git a/core/assets/logicids.dat b/core/assets/logicids.dat index 478711e7fc..e61fa09a6d 100644 Binary files a/core/assets/logicids.dat and b/core/assets/logicids.dat differ diff --git a/core/src/mindustry/ai/BlockIndexer.java b/core/src/mindustry/ai/BlockIndexer.java index f39358d853..a7564437d8 100644 --- a/core/src/mindustry/ai/BlockIndexer.java +++ b/core/src/mindustry/ai/BlockIndexer.java @@ -238,6 +238,8 @@ public class BlockIndexer{ /** Does not work with null teams. */ public boolean eachBlock(Team team, Rect rect, Boolf pred, Cons cons){ + if(team == null) return false; + breturnArray.clear(); var buildings = team.data().buildings; diff --git a/core/src/mindustry/content/Fx.java b/core/src/mindustry/content/Fx.java index 1744b616d3..fb455c11b1 100644 --- a/core/src/mindustry/content/Fx.java +++ b/core/src/mindustry/content/Fx.java @@ -1447,6 +1447,30 @@ public class Fx{ Fill.square(e.x, e.y, e.fslope() * 1.5f + 0.14f, 45f); }), + regenSuppressParticle = new Effect(30f, e -> { + color(Pal.sapBullet, e.color, e.fin()); + stroke(e.fout() * 1.4f + 0.5f); + + randLenVectors(e.id, 4, 17f * e.fin(), (x, y) -> { + lineAngle(e.x + x, e.y + y, Mathf.angle(x, y), e.fslope() * 3f + 0.5f); + }); + }), + + regenSuppressSeek = new Effect(140f, e -> { + e.lifetime = Mathf.randomSeed(e.id, 120f, 200f); + + if(!(e.data instanceof Position to)) return; + + Tmp.v2.set(to).sub(e.x, e.y).nor().rotate90(1).scl(Mathf.randomSeedRange(e.id, 1f) * 50f); + + Tmp.bz2.set(Tmp.v1.set(e.x, e.y), Tmp.v2.add(e.x, e.y), Tmp.v3.set(to)); + + Tmp.bz2.valueAt(Tmp.v4, e.fout()); + + color(Pal.sapBullet); + Fill.circle(Tmp.v4.x, Tmp.v4.y, e.fslope() * 2f + 0.1f); + }).followParent(false).rotWithParent(false), + surgeCruciSmoke = new Effect(160f, e -> { color(Pal.slagOrange); alpha(0.6f); diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index 662faa746b..513476b6ef 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -51,7 +51,7 @@ public class UnitTypes{ //air + payload public static @EntityDef({Unitc.class, Payloadc.class}) UnitType mega, - incite, emanate; + incite, emanate, quell; //air + payload, legacy public static @EntityDef(value = {Unitc.class, Payloadc.class}, legacy = true) UnitType quad; @@ -2502,7 +2502,36 @@ public class UnitTypes{ //endregion //region erekir - flying - //TODO + //TODO orb, suppress healing + quell = new UnitType("quell"){{ + envDisabled = 0; + + outlineColor = Pal.darkOutline; + lowAltitude = false; + flying = true; + drag = 0.06f; + speed = 1.1f; + rotateSpeed = 3.5f; + accel = 0.1f; + health = 3000f; + armor = 4f; + hitSize = 36f; + payloadCapacity = Mathf.sqr(3f) * tilePayload; + + engineSize = 4.8f; + engineOffset = 61 / 4f; + + abilities.add(new SuppressionFieldAbility(){{ + orbRadius = 5.3f; + }}); + + float es = 3.9f; + + setEnginesMirror( + new UnitEngine(62 / 4f, -60 / 4f, es, 315f), + new UnitEngine(72 / 4f, -29 / 4f, 3f, 315f) + ); + }}; //endregion //region erekir - neoplasm @@ -2646,9 +2675,8 @@ public class UnitTypes{ }}; emanate = new UnitType("emanate"){{ - //TODO not a real enemy, should not be counted or have flying AI - defaultController = FlyingAI::new; - //isCounted = false; + defaultController = BuilderAI::new; + isCounted = false; envDisabled = 0; outlineColor = Pal.darkOutline; diff --git a/core/src/mindustry/entities/abilities/SuppressionFieldAbility.java b/core/src/mindustry/entities/abilities/SuppressionFieldAbility.java new file mode 100644 index 0000000000..b7755f33bf --- /dev/null +++ b/core/src/mindustry/entities/abilities/SuppressionFieldAbility.java @@ -0,0 +1,111 @@ +package mindustry.entities.abilities; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import mindustry.*; +import mindustry.content.*; +import mindustry.gen.*; +import mindustry.graphics.*; + +import static mindustry.Vars.*; + +public class SuppressionFieldAbility extends Ability{ + protected static Rand rand = new Rand(); + protected static Seq builds = new Seq<>(); + + public float reload = 60f * 1.5f; + public float range = 200f; + + public float orbRadius = 4.5f, orbMidScl = 0.62f, orbSinScl = 8f, orbSinMag = 1f; + public Color color1 = Pal.sap.cpy().mul(1.6f), color2 = Pal.sap; + public float layer = Layer.effect; + + public int particles = 15; + public float particleSize = 4f; + public float particleLen = 7f; + public float rotateScl = 3f; + public float particleLife = 110f; + public Interp particleInterp = f -> Interp.circleOut.apply(Interp.slope.apply(f)); + public Color particleColor = Pal.sap.cpy().a(0.8f); + + public float applyParticleChance = 13f; + + protected boolean any; + protected float timer; + protected float heat = 0f; + + @Override + public void update(Unit unit){ + if((timer += Time.delta) >= reload){ + any = false; + builds.clear(); + Vars.indexer.eachBlock(null, unit.x,unit.y, range, build -> true, build -> { + if(build.team != unit.team){ + float prev = build.healSuppressionTime; + build.applyHealSuppression(reload + 1f); + + any = true; + + //add prev check so ability spam doesn't lead to particle spam (essentially, recently suppressed blocks don't get new particles) + if(!headless && prev - Time.time <= reload/2f){ + builds.add(build); + } + } + }); + + //to prevent particle spam, the amount of particles is to remain constant (scales with number of buildings) + float scaledChance = applyParticleChance / builds.size; + for(var build : builds){ + if(Mathf.chance(scaledChance)){ + Time.run(Mathf.random(reload), () -> { + Fx.regenSuppressSeek.at(build.x + Mathf.range(build.block.size * tilesize / 2f), build.y + Mathf.range(build.block.size * tilesize / 2f), 0f, unit); + }); + } + } + + timer = 0f; + } + + heat = Mathf.lerpDelta(heat, any ? 1f : 0f, 0.09f); + + } + + @Override + public void draw(Unit unit){ + Draw.z(layer); + + float rad = orbRadius + Mathf.absin(orbSinScl, orbSinMag); + + Draw.color(color2); + Fill.circle(unit.x, unit.y, rad); + + Draw.color(color1); + Fill.circle(unit.x, unit.y, rad * orbMidScl); + + float base = (Time.time / particleLife); + rand.setSeed(unit.id); + Draw.color(particleColor); + for(int i = 0; i < particles; i++){ + float fin = (rand.random(1f) + base) % 1f, fout = 1f - fin; + float angle = rand.random(360f) + (Time.time / rotateScl + unit.rotation) % 360f; + float len = particleLen * particleInterp.apply(fout); + Fill.circle( + unit.x + Angles.trnsx(angle, len), + unit.y + Angles.trnsy(angle, len), + particleSize * Mathf.slope(fin) + ); + } + + //TODO improve + if(heat > 0.001f){ + Draw.color(Pal.sapBullet); + Lines.stroke(1.2f * heat * Mathf.absin(10f, 1f)); + Lines.circle(unit.x, unit.y, range); + } + + Draw.reset(); + } +} diff --git a/core/src/mindustry/entities/comp/BuildingComp.java b/core/src/mindustry/entities/comp/BuildingComp.java index 7d7cf7d157..32c194356a 100644 --- a/core/src/mindustry/entities/comp/BuildingComp.java +++ b/core/src/mindustry/entities/comp/BuildingComp.java @@ -76,6 +76,8 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, LiquidModule liquids; ConsumeModule cons; + public transient float healSuppressionTime = -1f; + private transient float timeScale = 1f, timeScaleDuration; private transient float dumpAccum; @@ -329,6 +331,14 @@ abstract class BuildingComp implements Posc, Teamc, Healthc, Buildingc, Timerc, timeScale = Math.max(timeScale, intensity); } + public void applyHealSuppression(float amount){ + healSuppressionTime = Math.max(healSuppressionTime, Time.time + amount); + } + + public boolean isHealSuppressed(){ + return Time.time <= healSuppressionTime; + } + public Building nearby(int dx, int dy){ return world.build(tile.x + dx, tile.y + dy); } diff --git a/core/src/mindustry/world/blocks/defense/MendProjector.java b/core/src/mindustry/world/blocks/defense/MendProjector.java index a42d6cecb9..9fdbf01e0a 100644 --- a/core/src/mindustry/world/blocks/defense/MendProjector.java +++ b/core/src/mindustry/world/blocks/defense/MendProjector.java @@ -65,6 +65,19 @@ public class MendProjector extends Block{ indexer.eachBlock(player.team(), x * tilesize + offset, y * tilesize + offset, range, other -> true, other -> Drawf.selected(other, Tmp.c1.set(baseColor).a(Mathf.absin(4f, 1f)))); } + /** @return whether a building has regen/healing suppressed; if so, spawns particles on it. */ + public static boolean checkSuppression(Building build){ + if(build.isHealSuppressed()){ + if(Mathf.chanceDelta(0.04)){ + Fx.regenSuppressParticle.at(build.x + Mathf.range(build.block.size * tilesize/2f - 1f), build.y + Mathf.range(build.block.size * tilesize/2f - 1f)); + } + + return true; + } + + return false; + } + public class MendBuild extends Building implements Ranged{ public float heat, charge = Mathf.random(reload), phaseHeat, smoothEfficiency; @@ -75,21 +88,23 @@ public class MendProjector extends Block{ @Override public void updateTile(){ + boolean canHeal = !checkSuppression(this); + smoothEfficiency = Mathf.lerpDelta(smoothEfficiency, efficiency(), 0.08f); - heat = Mathf.lerpDelta(heat, consValid() || cheating() ? 1f : 0f, 0.08f); + heat = Mathf.lerpDelta(heat, consValid() && canHeal ? 1f : 0f, 0.08f); charge += heat * delta(); phaseHeat = Mathf.lerpDelta(phaseHeat, Mathf.num(cons.optionalValid()), 0.1f); - if(cons.optionalValid() && timer(timerUse, useTime) && efficiency() > 0){ + if(cons.optionalValid() && timer(timerUse, useTime) && efficiency() > 0 && canHeal){ consume(); } - if(charge >= reload){ + if(charge >= reload && canHeal){ float realRange = range + phaseHeat * phaseRangeBoost; charge = 0f; - indexer.eachBlock(this, realRange, Building::damaged, other -> { + indexer.eachBlock(this, realRange, b -> b.damaged() && !b.isHealSuppressed(), other -> { other.heal(other.maxHealth() * (healPercent + phaseHeat * phaseBoost) / 100f * efficiency()); Fx.healBlockFull.at(other.x, other.y, other.block.size, baseColor); }); diff --git a/core/src/mindustry/world/blocks/defense/RegenProjector.java b/core/src/mindustry/world/blocks/defense/RegenProjector.java index 5292c6d104..19f747e742 100644 --- a/core/src/mindustry/world/blocks/defense/RegenProjector.java +++ b/core/src/mindustry/world/blocks/defense/RegenProjector.java @@ -85,6 +85,8 @@ public class RegenProjector extends Block{ @Override public void updateTile(){ + //TODO particles when heal suppressed + if(lastChange != world.tileChanges){ lastChange = world.tileChanges; updateTargets(); @@ -95,10 +97,15 @@ public class RegenProjector extends Block{ totalTime += warmup * Time.delta; didRegen = false; + //no healing when suppressed + if(MendProjector.checkSuppression(this)){ + return; + } + if(consValid()){ //use Math.max to prevent stacking for(Building build : targets){ - if(!build.damaged()) continue; + if(!build.damaged() || build.isHealSuppressed()) continue; didRegen = true;