diff --git a/res/bundles/bundle.properties b/res/bundles/bundle.properties index b907fd16..64c62b92 100644 --- a/res/bundles/bundle.properties +++ b/res/bundles/bundle.properties @@ -39,6 +39,7 @@ stat.fos-hackchancemultiplier = Hack Chance Multiplier stat.fos-hackhpthreshold = Health Thresholds stat.fos-maxbeams = Maximum Beams stat.fos-damagereduction = Damage Reduction +stat.fos-healthtrigger = activates at [accent]<{0}%[] health unittype = [gray]Type: [] unittype.infantry = [accent]Infantry[] @@ -396,7 +397,7 @@ unit.fos-assault.description = Fires homing bullets at enemy targets. unit.fos-abrupt.name = Abrupt unit.fos-abrupt.description = Fires barrages of bombs at enemy targets. unit.fos-brunt.name = Brunt -unit.fos-brunt.description = Self-destructs, causing a huge explosion. +unit.fos-brunt.description = Shoots slowing missiles at enemy targets. Activates a self-destruct protocol after sustaining enough damage, causing a huge explosion. # TODO: bug names unit.fos-grain.name = Grain unit.fos-grain.description = An agile insect species that burrows underground to evade defenses and ambush ore detectors, shutting down most types in a single slash. diff --git a/res/bundles/bundle_ru.properties b/res/bundles/bundle_ru.properties index 5fe276a8..1ddbc35c 100644 --- a/res/bundles/bundle_ru.properties +++ b/res/bundles/bundle_ru.properties @@ -39,6 +39,7 @@ stat.fos-hackchancemultiplier = Множитель шанса взлома stat.fos-hackhpthreshold = Пороги прочности stat.fos-maxbeams = Максимум лучей stat.fos-damagereduction = Поглощение урона +stat.fos-healthtrigger = включается при [accent]<{0}%[] прочности unittype = [gray]Тип: [] unittype.infantry = [accent]Пехота[] @@ -396,7 +397,7 @@ unit.fos-assault.description = Стреляет самонаводящимися unit.fos-abrupt.name = Перфорация unit.fos-abrupt.description = Стреляет залпами бомб по вражеским целям. unit.fos-brunt.name = Подавление -unit.fos-brunt.description = Самоуничтожается, вызывая огромный взрыв. +unit.fos-brunt.description = Стреляет замедляющими ракетами по вражеским целям. Активирует протокол самоуничтожения при получении достаточного урона, вызывая огромный взрыв. # TODO: названия жуков unit.fos-grain.name = Песчинка unit.fos-grain.description = Ловкий вид насекомых, зарывающийся под землю, чтобы обойти оборону и атаковать детекторы руды, отключая большинство типов одним ударом. diff --git a/res/sprites/units/bosses/citadel-front.png b/res/sprites/units/bosses/citadel-front.png deleted file mode 100644 index 4cf1dcb4..00000000 Binary files a/res/sprites/units/bosses/citadel-front.png and /dev/null differ diff --git a/res/sprites/units/bosses/citadel-base.png b/res/sprites/units/bosses/citadel/citadel-base.png similarity index 100% rename from res/sprites/units/bosses/citadel-base.png rename to res/sprites/units/bosses/citadel/citadel-base.png diff --git a/res/sprites/units/bosses/citadel/citadel-front.png b/res/sprites/units/bosses/citadel/citadel-front.png new file mode 100644 index 00000000..38999a0f Binary files /dev/null and b/res/sprites/units/bosses/citadel/citadel-front.png differ diff --git a/res/sprites/units/bosses/citadel-preview.png b/res/sprites/units/bosses/citadel/citadel-full.png similarity index 100% rename from res/sprites/units/bosses/citadel-preview.png rename to res/sprites/units/bosses/citadel/citadel-full.png diff --git a/res/sprites/units/bosses/citadel-leg.png b/res/sprites/units/bosses/citadel/citadel-leg.png similarity index 100% rename from res/sprites/units/bosses/citadel-leg.png rename to res/sprites/units/bosses/citadel/citadel-leg.png diff --git a/res/sprites/units/bosses/citadel-shotgun.png b/res/sprites/units/bosses/citadel/citadel-shotgun.png similarity index 100% rename from res/sprites/units/bosses/citadel-shotgun.png rename to res/sprites/units/bosses/citadel/citadel-shotgun.png diff --git a/res/sprites/units/bosses/citadel-stickybomb-launcher.png b/res/sprites/units/bosses/citadel/citadel-stickybomb-launcher.png similarity index 100% rename from res/sprites/units/bosses/citadel-stickybomb-launcher.png rename to res/sprites/units/bosses/citadel/citadel-stickybomb-launcher.png diff --git a/res/sprites/units/bosses/citadel.png b/res/sprites/units/bosses/citadel/citadel.png similarity index 100% rename from res/sprites/units/bosses/citadel.png rename to res/sprites/units/bosses/citadel/citadel.png diff --git a/res/sprites/units/bosses/legion-sapper.png b/res/sprites/units/bosses/legion/legion-sapper.png similarity index 100% rename from res/sprites/units/bosses/legion-sapper.png rename to res/sprites/units/bosses/legion/legion-sapper.png diff --git a/res/sprites/units/bosses/legion.png b/res/sprites/units/bosses/legion/legion.png similarity index 100% rename from res/sprites/units/bosses/legion.png rename to res/sprites/units/bosses/legion/legion.png diff --git a/res/sprites/units/bosses/legionnaire-cell.png b/res/sprites/units/bosses/legion/legionnaire-cell.png similarity index 100% rename from res/sprites/units/bosses/legionnaire-cell.png rename to res/sprites/units/bosses/legion/legionnaire-cell.png diff --git a/res/sprites/units/bosses/legionnaire-replica-cell.png b/res/sprites/units/bosses/legion/legionnaire-replica-cell.png similarity index 100% rename from res/sprites/units/bosses/legionnaire-replica-cell.png rename to res/sprites/units/bosses/legion/legionnaire-replica-cell.png diff --git a/res/sprites/units/bosses/legionnaire-replica.png b/res/sprites/units/bosses/legion/legionnaire-replica.png similarity index 100% rename from res/sprites/units/bosses/legionnaire-replica.png rename to res/sprites/units/bosses/legion/legionnaire-replica.png diff --git a/res/sprites/units/bosses/legionnaire.png b/res/sprites/units/bosses/legion/legionnaire.png similarity index 100% rename from res/sprites/units/bosses/legionnaire.png rename to res/sprites/units/bosses/legion/legionnaire.png diff --git a/res/sprites/units/bosses/myriad-glow.png b/res/sprites/units/bosses/myriad/myriad-glow.png similarity index 100% rename from res/sprites/units/bosses/myriad-glow.png rename to res/sprites/units/bosses/myriad/myriad-glow.png diff --git a/res/sprites/units/bosses/myriad-point-weapon-heat.png b/res/sprites/units/bosses/myriad/myriad-point-weapon-heat.png similarity index 100% rename from res/sprites/units/bosses/myriad-point-weapon-heat.png rename to res/sprites/units/bosses/myriad/myriad-point-weapon-heat.png diff --git a/res/sprites/units/bosses/myriad-point-weapon.png b/res/sprites/units/bosses/myriad/myriad-point-weapon.png similarity index 100% rename from res/sprites/units/bosses/myriad-point-weapon.png rename to res/sprites/units/bosses/myriad/myriad-point-weapon.png diff --git a/res/sprites/units/bosses/myriad-treads.png b/res/sprites/units/bosses/myriad/myriad-treads.png similarity index 100% rename from res/sprites/units/bosses/myriad-treads.png rename to res/sprites/units/bosses/myriad/myriad-treads.png diff --git a/res/sprites/units/bosses/myriad-weapon-blade-glow.png b/res/sprites/units/bosses/myriad/myriad-weapon-blade-glow.png similarity index 100% rename from res/sprites/units/bosses/myriad-weapon-blade-glow.png rename to res/sprites/units/bosses/myriad/myriad-weapon-blade-glow.png diff --git a/res/sprites/units/bosses/myriad-weapon-blade.png b/res/sprites/units/bosses/myriad/myriad-weapon-blade.png similarity index 100% rename from res/sprites/units/bosses/myriad-weapon-blade.png rename to res/sprites/units/bosses/myriad/myriad-weapon-blade.png diff --git a/res/sprites/units/bosses/myriad-weapon-glow.png b/res/sprites/units/bosses/myriad/myriad-weapon-glow.png similarity index 100% rename from res/sprites/units/bosses/myriad-weapon-glow.png rename to res/sprites/units/bosses/myriad/myriad-weapon-glow.png diff --git a/res/sprites/units/bosses/myriad-weapon-heat.png b/res/sprites/units/bosses/myriad/myriad-weapon-heat.png similarity index 100% rename from res/sprites/units/bosses/myriad-weapon-heat.png rename to res/sprites/units/bosses/myriad/myriad-weapon-heat.png diff --git a/res/sprites/units/bosses/myriad-weapon.png b/res/sprites/units/bosses/myriad/myriad-weapon.png similarity index 100% rename from res/sprites/units/bosses/myriad-weapon.png rename to res/sprites/units/bosses/myriad/myriad-weapon.png diff --git a/res/sprites/units/bosses/myriad.png b/res/sprites/units/bosses/myriad/myriad.png similarity index 100% rename from res/sprites/units/bosses/myriad.png rename to res/sprites/units/bosses/myriad/myriad.png diff --git a/res/sprites/units/destroyers/brunt-cell.png b/res/sprites/units/destroyers/brunt-cell.png new file mode 100644 index 00000000..5d37aeb2 Binary files /dev/null and b/res/sprites/units/destroyers/brunt-cell.png differ diff --git a/res/sprites/units/destroyers/brunt.png b/res/sprites/units/destroyers/brunt.png new file mode 100644 index 00000000..7a85c78e Binary files /dev/null and b/res/sprites/units/destroyers/brunt.png differ diff --git a/res/sprites/units/dummy-submarine-cell.png b/res/sprites/units/dummy-submarine-cell.png deleted file mode 100644 index 916eff41..00000000 Binary files a/res/sprites/units/dummy-submarine-cell.png and /dev/null differ diff --git a/res/sprites/units/dummy-submarine.png b/res/sprites/units/dummy-submarine.png deleted file mode 100644 index 162d4291..00000000 Binary files a/res/sprites/units/dummy-submarine.png and /dev/null differ diff --git a/res/sprites/units/warden-treads.png b/res/sprites/units/warden-treads.png deleted file mode 100644 index 4f6c6d43..00000000 Binary files a/res/sprites/units/warden-treads.png and /dev/null differ diff --git a/src/fos/content/FOSFx.java b/src/fos/content/FOSFx.java index 1fe78a81..0f33dc78 100644 --- a/src/fos/content/FOSFx.java +++ b/src/fos/content/FOSFx.java @@ -226,5 +226,23 @@ public class FOSFx { burrowDust = new Effect(180f, e -> { burrowDustSingle.at(e.x, e.y, e.color); - }); + }), + + bruntChargeSmoke = new Effect(80f, e -> { + color(Pal.reactorPurple2); + alpha(e.fin()); + + randLenVectors(e.id, 1, e.fout() * 20f, (x, y) -> { + float rad = 12f * e.fout(); + + Fill.circle(e.x + x, e.y + y, rad); + Drawf.light(e.x + x, e.y + y, rad * 2.5f, Pal.reactorPurple, 0.5f); + }); + }).followParent(true).layer(Layer.flyingUnit + 0.01f), + + bruntCharge = new Effect(300f, e -> { + if (Mathf.chance(0.08f) && e.time < 220f) { + bruntChargeSmoke.at(e.x, e.y); + } + }).followParent(true); } diff --git a/src/fos/content/FOSUnitTypes.java b/src/fos/content/FOSUnitTypes.java index ab6abbbf..cec74364 100644 --- a/src/fos/content/FOSUnitTypes.java +++ b/src/fos/content/FOSUnitTypes.java @@ -1,30 +1,27 @@ package fos.content; import arc.graphics.Color; -import arc.graphics.g2d.Lines; +import arc.graphics.g2d.*; import arc.math.*; -import arc.math.geom.*; +import arc.math.geom.Rect; import arc.struct.Seq; -import arc.util.*; import fos.ai.*; import fos.audio.FOSSounds; import fos.gen.*; import fos.graphics.*; -import fos.type.abilities.*; +import fos.type.abilities.UnitResistanceAbility; import fos.type.bullets.*; import fos.type.content.WeaponSet; import fos.type.units.types.*; -import fos.type.units.weapons.InjectorWeapon; +import fos.type.units.weapons.*; import mindustry.ai.UnitCommand; import mindustry.ai.types.*; -import mindustry.audio.SoundLoop; import mindustry.content.*; -import mindustry.entities.*; +import mindustry.entities.Effect; import mindustry.entities.abilities.*; import mindustry.entities.bullet.*; import mindustry.entities.part.*; import mindustry.entities.pattern.*; -import mindustry.entities.units.WeaponMount; import mindustry.gen.ElevationMoveUnit; import mindustry.gen.LegsUnit; import mindustry.gen.MechUnit; @@ -38,10 +35,10 @@ import static arc.graphics.g2d.Draw.color; import static arc.graphics.g2d.Lines.stroke; +import static arc.math.Angles.randLenVectors; import static ent.anno.Annotations.EntityDef; import static fos.content.FOSItems.zinc; import static fos.content.FOSStatuses.*; -import static mindustry.Vars.*; public class FOSUnitTypes { public static @EntityDef({Unitc.class, Mechc.class}) UnitType @@ -250,17 +247,6 @@ public static void load(){ flying = false; canBoost = false; - parts.add( - new RegionPart("-front"){{ - growX = growY = -1; - //layer = Layer.groundUnit - 0.01f; - growProgress = p -> { - var unit = Groups.unit.find(u -> u.type.name.equals("fos-citadel")); - return unit == null || unit.health / unit.maxHealth > 0.5 ? 0 : 1; - }; - }} - ); - weapons.add( new Weapon("fos-citadel-shotgun"){{ x = 18; y = 0; @@ -310,179 +296,48 @@ public static void load(){ }}; }}, - new Weapon(){ - { - x = 0; y = 5; - rotate = false; - continuous = true; - shoot.firstShotDelay = 40f; - shootCone = 90f; - reload = 600f; - shake = 2f; - - chargeSound = Sounds.lasercharge2; - shootSound = Sounds.laserbig; - - bullet = new ContinuousLaserBulletType(60){{ - length = 120; - width = 8f; - lifetime = 230f; - status = StatusEffects.melting; - - incendChance = 0.2f; - incendSpread = 5f; - incendAmount = 1; - - chargeEffect = new Effect(40f, 100f, e -> { - color(Pal.lightFlame); - stroke(e.fin() * 2f); - Lines.circle(e.x, e.y, e.fout() * 50f); - }).followParent(true).rotWithParent(true); - }}; - } - - @Override - public void update(Unit unit, WeaponMount mount) { - // TODO: this copy of the super method is WAY too long - boolean can = unit.canShoot() && unit.health / unit.maxHealth < 0.5f; - float lastReload = mount.reload; - mount.reload = Math.max(mount.reload - Time.delta * unit.reloadMultiplier, 0); - mount.recoil = Mathf.approachDelta(mount.recoil, 0, unit.reloadMultiplier / recoilTime); - if(recoils > 0){ - if(mount.recoils == null) mount.recoils = new float[recoils]; - for(int i = 0; i < recoils; i++){ - mount.recoils[i] = Mathf.approachDelta(mount.recoils[i], 0, unit.reloadMultiplier / recoilTime); - } - } - mount.smoothReload = Mathf.lerpDelta(mount.smoothReload, mount.reload / reload, smoothReloadSpeed); - mount.charge = mount.charging && shoot.firstShotDelay > 0 ? Mathf.approachDelta(mount.charge, 1, 1 / shoot.firstShotDelay) : 0; - - float warmupTarget = (can && mount.shoot) || (continuous && mount.bullet != null) || mount.charging ? 1f : 0f; - if(linearWarmup){ - mount.warmup = Mathf.approachDelta(mount.warmup, warmupTarget, shootWarmupSpeed); - }else{ - mount.warmup = Mathf.lerpDelta(mount.warmup, warmupTarget, shootWarmupSpeed); - } - - //rotate if applicable - if(rotate && (mount.rotate || mount.shoot) && can){ - float axisX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), - axisY = unit.y + Angles.trnsy(unit.rotation - 90, x, y); - - mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - unit.rotation; - mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, rotateSpeed * Time.delta); - if(rotationLimit < 360){ - float dst = Angles.angleDist(mount.rotation, baseRotation); - if(dst > rotationLimit/2f){ - mount.rotation = Angles.moveToward(mount.rotation, baseRotation, dst - rotationLimit/2f); - } - } - }else if(!rotate){ - mount.rotation = baseRotation; - mount.targetRotation = unit.angleTo(mount.aimX, mount.aimY); - } - - float - weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation), - mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), - mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), - bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX, this.shootY), - bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX, this.shootY), - shootAngle = bulletRotation(unit, mount, bulletX, bulletY); - - //find a new target - if(!controllable && autoTarget){ - if((mount.retarget -= Time.delta) <= 0f){ - mount.target = findTarget(unit, mountX, mountY, bullet.range, bullet.collidesAir, bullet.collidesGround); - mount.retarget = mount.target == null ? targetInterval : targetSwitchInterval; - } - - if(mount.target != null && checkTarget(unit, mount.target, mountX, mountY, bullet.range)){ - mount.target = null; - } - - boolean shoot = false; - - if(mount.target != null){ - shoot = mount.target.within(mountX, mountY, bullet.range + Math.abs(shootY) + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && can; - - if(predictTarget){ - Vec2 to = Predict.intercept(unit, mount.target, bullet.speed); - mount.aimX = to.x; - mount.aimY = to.y; - }else{ - mount.aimX = mount.target.x(); - mount.aimY = mount.target.y(); - } - } - - mount.shoot = mount.rotate = shoot; - - //note that shooting state is not affected, as these cannot be controlled - //logic will return shooting as false even if these return true, which is fine - } - - if(alwaysShooting) mount.shoot = true; - - //update continuous state - if(continuous && mount.bullet != null){ - if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != bullet){ - mount.bullet = null; - }else{ - mount.bullet.rotation(weaponRotation + 90); - mount.bullet.set(bulletX, bulletY); - mount.reload = reload; - mount.recoil = 1f; - unit.vel.add(Tmp.v1.trns(unit.rotation + 180f, mount.bullet.type.recoil * Time.delta)); - if(shootSound != Sounds.none && !headless){ - if(mount.sound == null) mount.sound = new SoundLoop(shootSound, 1f); - mount.sound.update(bulletX, bulletY, true); - } - - if(alwaysContinuous && mount.shoot){ - mount.bullet.time = mount.bullet.lifetime * mount.bullet.type.optimalLifeFract * mount.warmup; - mount.bullet.keepAlive = true; - - unit.apply(shootStatus, shootStatusDuration); - } - } - }else{ - //heat decreases when not firing - mount.heat = Math.max(mount.heat - Time.delta * unit.reloadMultiplier / cooldownTime, 0); - - if(mount.sound != null){ - mount.sound.update(bulletX, bulletY, false); - } - } - - //flip weapon shoot side for alternating weapons - boolean wasFlipped = mount.side; - if(otherSide != -1 && alternate && mount.side == flipSprite && mount.reload <= reload / 2f && lastReload > reload / 2f){ - unit.mounts[otherSide].side = !unit.mounts[otherSide].side; - mount.side = !mount.side; - } - - //shoot if applicable - if(mount.shoot && //must be shooting - can && //must be able to shoot - (!useAmmo || unit.ammo > 0 || !state.rules.unitAmmo || unit.team.rules().infiniteAmmo) && //check ammo - (!alternate || wasFlipped == flipSprite) && - mount.warmup >= minWarmup && //must be warmed up - unit.vel.len() >= minShootVelocity && //check velocity requirements - (mount.reload <= 0.0001f || (alwaysContinuous && mount.bullet == null)) && //reload has to be 0, or it has to be an always-continuous weapon - (alwaysShooting || Angles.within(rotate ? mount.rotation : unit.rotation + baseRotation, mount.targetRotation, shootCone)) //has to be within the cone - ){ - shoot(unit, mount, bulletX, bulletY, shootAngle); - - mount.reload = reload; - - if(useAmmo){ - unit.ammo--; - if(unit.ammo < 0) unit.ammo = 0; - } - } - } - } + new HealthTriggerWeapon(){{ + x = 0; y = 5; + rotate = false; + continuous = true; + shoot.firstShotDelay = 40f; + shootCone = 90f; + reload = 600f; + shake = 2f; + healthFrac = 0.5f; + + chargeSound = Sounds.lasercharge2; + shootSound = Sounds.laserbig; + + bullet = new ContinuousLaserBulletType(60){{ + length = 120; + width = 8f; + lifetime = 230f; + status = StatusEffects.melting; + + incendChance = 0.2f; + incendSpread = 5f; + incendAmount = 1; + + chargeEffect = new Effect(40f, 100f, e -> { + color(Pal.lightFlame); + stroke(e.fin() * 2f); + Lines.circle(e.x, e.y, e.fout() * 50f); + }).followParent(true).rotWithParent(true); + }}; + + parts.add( + new RegionPart("fos-citadel-front"){{ + x = 0; y = 3; + growX = growY = -1; + //layer = Layer.groundUnit - 0.01f; + growProgress = p -> { + var unit = Groups.unit.find(u -> u.type.name.equals("fos-citadel")); + return unit == null || unit.health / unit.maxHealth > 0.5 ? 0 : 1; + }; + }} + ); + }} ); } }; @@ -1234,7 +1089,7 @@ public void update(Unit unit, WeaponMount mount) { ); }}; abrupt = new FOSUnitType("abrupt", ElevationMoveUnit.class){{ - health = 800; + health = 1600; armor = 4; speed = 1.5f; hovering = true; @@ -1299,32 +1154,87 @@ public void update(Unit unit, WeaponMount mount) { ); }}; brunt = new FOSUnitType("brunt", UnitEntity.class){{ - health = 1500; - armor = 22.5f; - speed = 0.8f; + health = 2800; + armor = 20f; + speed = 1.4f; hitSize = 24f; rotateSpeed = 2f; flying = true; - - abilities.add(new DamageFieldAbility(120f, 1f){{ - borderColor = Pal.sapBulletBack.cpy().a(0.4f); - }}); + accel = 0.05f; + drag = 0.03f; + + engineOffset = 12f; + engineSize = 4f; + engineColor = Pal.reactorPurple2; + engineColorInner = Pal.reactorPurple; + engines.add( + new UnitEngine(10f, -9f, 2f, -60f), + new UnitEngine(-10f, -9f, 2f, -120f) + ); weapons.add( new Weapon(){{ - shootCone = 360f; - range = 32f; + x = 0; y = 6; + reload = 15f; + shootCone = 25f; + mirror = false; + + shoot = new ShootAlternate(){{ + spread = 2f; + }}; + + shootSound = Sounds.missile; + + bullet = new MissileBulletType(){{ + speed = 4f; lifetime = 30f; + damage = 70f; + + weaveMag = 1.2f; + weaveScale = 4f; + + backColor = trailColor = Pal.reactorPurple; + frontColor = Pal.reactorPurple2; + trailChance = 0f; + trailWidth = 1f; + trailLength = 12; + smokeEffect = new Effect(20f, e -> { + color(Pal.reactorPurple, Pal.reactorPurple2, e.fin()); + + randLenVectors(e.id, 5, e.finpow() * 6f, e.rotation, 20f, (x, y) -> { + Fill.circle(e.x + x, e.y + y, e.fout() * 1.5f); + }); + }); + + status = StatusEffects.slow; + statusDuration = 300f; + }}; + }}, + new HealthTriggerWeapon(){{ + x = 0; y = 0; + alwaysShooting = true; + healthFrac = 0.4f; + reload = 600f; + mirror = false; + shoot.firstShotDelay = 300f; + + parentizeEffects = true; + shootSound = Sounds.none; + bullet = new BombBulletType(){{ - splashDamage = 1000f; + splashDamage = 1200f; splashDamageRadius = 80f; - status = StatusEffects.slow; - statusDuration = 300f; hittable = false; killShooter = true; instantDisappear = true; - hitEffect = Fx.reactorExplosion; buildingDamageMultiplier = 0.1f; + + shootStatus = StatusEffects.unmoving; + shootStatusDuration = 300f; + + hitEffect = Fx.reactorExplosion; + hitSound = Sounds.largeExplosion; + chargeEffect = FOSFx.bruntCharge; }}; }} ); diff --git a/src/fos/type/abilities/DamageFieldAbility.java b/src/fos/type/abilities/DamageFieldAbility.java deleted file mode 100644 index abc847ae..00000000 --- a/src/fos/type/abilities/DamageFieldAbility.java +++ /dev/null @@ -1,46 +0,0 @@ -package fos.type.abilities; - -import arc.graphics.Color; -import arc.graphics.g2d.*; -import arc.scene.ui.layout.Table; -import mindustry.entities.Units; -import mindustry.entities.abilities.Ability; -import mindustry.gen.Unit; -import mindustry.world.meta.*; - -import static mindustry.Vars.tilesize; - -public class DamageFieldAbility extends Ability { - public float range, damage; - public Color borderColor; - - public DamageFieldAbility(float range, float damage) { - this.range = range; - this.damage = damage; - } - - public DamageFieldAbility() {} - - public void addStats(Table t) { - t.add("[lightgray]" + Stat.range.localized() + ": [white]" + range / tilesize + " " + StatUnit.blocks.localized()); - t.row(); - } - - @Override - public void update(Unit unit) { - Units.nearbyEnemies(unit.team, unit.x, unit.y, range, other -> { - other.damageContinuous(damage); - }); - } - - @Override - public void draw(Unit unit) { - super.draw(unit); - - Draw.color(borderColor); - Lines.stroke(2f); - Lines.circle(unit.x, unit.y, range); - - Draw.reset(); - } -} diff --git a/src/fos/type/units/weapons/HealthTriggerWeapon.java b/src/fos/type/units/weapons/HealthTriggerWeapon.java new file mode 100644 index 00000000..7ec78f76 --- /dev/null +++ b/src/fos/type/units/weapons/HealthTriggerWeapon.java @@ -0,0 +1,176 @@ +package fos.type.units.weapons; + +import arc.Core; +import arc.math.*; +import arc.math.geom.Vec2; +import arc.scene.ui.layout.Table; +import arc.util.*; +import mindustry.audio.SoundLoop; +import mindustry.entities.*; +import mindustry.entities.units.WeaponMount; +import mindustry.gen.*; +import mindustry.type.*; + +import static mindustry.Vars.*; + +public class HealthTriggerWeapon extends Weapon { + /** Health fraction needed to reach before activating the weapon. */ + public float healthFrac = 0.5f; + + public HealthTriggerWeapon() { + super(); + } + + public HealthTriggerWeapon(String name) { + super(name); + } + + @Override + public void addStats(UnitType u, Table t) { + super.addStats(u, t); + + t.row(); + t.add("[lightgray]" + Core.bundle.format("stat.fos-healthtrigger", Mathf.round(healthFrac * 100))); + } + + @Override + public void update(Unit unit, WeaponMount mount) { + boolean can = unit.canShoot() && unit.health / unit.maxHealth < healthFrac; + float lastReload = mount.reload; + mount.reload = Math.max(mount.reload - Time.delta * unit.reloadMultiplier, 0); + mount.recoil = Mathf.approachDelta(mount.recoil, 0, unit.reloadMultiplier / recoilTime); + if(recoils > 0){ + if(mount.recoils == null) mount.recoils = new float[recoils]; + for(int i = 0; i < recoils; i++){ + mount.recoils[i] = Mathf.approachDelta(mount.recoils[i], 0, unit.reloadMultiplier / recoilTime); + } + } + mount.smoothReload = Mathf.lerpDelta(mount.smoothReload, mount.reload / reload, smoothReloadSpeed); + mount.charge = mount.charging && shoot.firstShotDelay > 0 ? Mathf.approachDelta(mount.charge, 1, 1 / shoot.firstShotDelay) : 0; + + float warmupTarget = (can && mount.shoot) || (continuous && mount.bullet != null) || mount.charging ? 1f : 0f; + if(linearWarmup){ + mount.warmup = Mathf.approachDelta(mount.warmup, warmupTarget, shootWarmupSpeed); + }else{ + mount.warmup = Mathf.lerpDelta(mount.warmup, warmupTarget, shootWarmupSpeed); + } + + //rotate if applicable + if(rotate && (mount.rotate || mount.shoot) && can){ + float axisX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + axisY = unit.y + Angles.trnsy(unit.rotation - 90, x, y); + + mount.targetRotation = Angles.angle(axisX, axisY, mount.aimX, mount.aimY) - unit.rotation; + mount.rotation = Angles.moveToward(mount.rotation, mount.targetRotation, rotateSpeed * Time.delta); + if(rotationLimit < 360){ + float dst = Angles.angleDist(mount.rotation, baseRotation); + if(dst > rotationLimit/2f){ + mount.rotation = Angles.moveToward(mount.rotation, baseRotation, dst - rotationLimit/2f); + } + } + }else if(!rotate){ + mount.rotation = baseRotation; + mount.targetRotation = unit.angleTo(mount.aimX, mount.aimY); + } + + float + weaponRotation = unit.rotation - 90 + (rotate ? mount.rotation : baseRotation), + mountX = unit.x + Angles.trnsx(unit.rotation - 90, x, y), + mountY = unit.y + Angles.trnsy(unit.rotation - 90, x, y), + bulletX = mountX + Angles.trnsx(weaponRotation, this.shootX, this.shootY), + bulletY = mountY + Angles.trnsy(weaponRotation, this.shootX, this.shootY), + shootAngle = bulletRotation(unit, mount, bulletX, bulletY); + + //find a new target + if(!controllable && autoTarget){ + if((mount.retarget -= Time.delta) <= 0f){ + mount.target = findTarget(unit, mountX, mountY, bullet.range, bullet.collidesAir, bullet.collidesGround); + mount.retarget = mount.target == null ? targetInterval : targetSwitchInterval; + } + + if(mount.target != null && checkTarget(unit, mount.target, mountX, mountY, bullet.range)){ + mount.target = null; + } + + boolean shoot = false; + + if(mount.target != null){ + shoot = mount.target.within(mountX, mountY, bullet.range + Math.abs(shootY) + (mount.target instanceof Sized s ? s.hitSize()/2f : 0f)) && can; + + if(predictTarget){ + Vec2 to = Predict.intercept(unit, mount.target, bullet.speed); + mount.aimX = to.x; + mount.aimY = to.y; + }else{ + mount.aimX = mount.target.x(); + mount.aimY = mount.target.y(); + } + } + + mount.shoot = mount.rotate = shoot; + + //note that shooting state is not affected, as these cannot be controlled + //logic will return shooting as false even if these return true, which is fine + } + + if(alwaysShooting) mount.shoot = true; + + //update continuous state + if(continuous && mount.bullet != null){ + if(!mount.bullet.isAdded() || mount.bullet.time >= mount.bullet.lifetime || mount.bullet.type != bullet){ + mount.bullet = null; + }else{ + mount.bullet.rotation(weaponRotation + 90); + mount.bullet.set(bulletX, bulletY); + mount.reload = reload; + mount.recoil = 1f; + unit.vel.add(Tmp.v1.trns(unit.rotation + 180f, mount.bullet.type.recoil * Time.delta)); + if(shootSound != Sounds.none && !headless){ + if(mount.sound == null) mount.sound = new SoundLoop(shootSound, 1f); + mount.sound.update(bulletX, bulletY, true); + } + + if(alwaysContinuous && mount.shoot){ + mount.bullet.time = mount.bullet.lifetime * mount.bullet.type.optimalLifeFract * mount.warmup; + mount.bullet.keepAlive = true; + + unit.apply(shootStatus, shootStatusDuration); + } + } + }else{ + //heat decreases when not firing + mount.heat = Math.max(mount.heat - Time.delta * unit.reloadMultiplier / cooldownTime, 0); + + if(mount.sound != null){ + mount.sound.update(bulletX, bulletY, false); + } + } + + //flip weapon shoot side for alternating weapons + boolean wasFlipped = mount.side; + if(otherSide != -1 && alternate && mount.side == flipSprite && mount.reload <= reload / 2f && lastReload > reload / 2f){ + unit.mounts[otherSide].side = !unit.mounts[otherSide].side; + mount.side = !mount.side; + } + + //shoot if applicable + if(mount.shoot && //must be shooting + can && //must be able to shoot + (!useAmmo || unit.ammo > 0 || !state.rules.unitAmmo || unit.team.rules().infiniteAmmo) && //check ammo + (!alternate || wasFlipped == flipSprite) && + mount.warmup >= minWarmup && //must be warmed up + unit.vel.len() >= minShootVelocity && //check velocity requirements + (mount.reload <= 0.0001f || (alwaysContinuous && mount.bullet == null)) && //reload has to be 0, or it has to be an always-continuous weapon + (alwaysShooting || Angles.within(rotate ? mount.rotation : unit.rotation + baseRotation, mount.targetRotation, shootCone)) //has to be within the cone + ){ + shoot(unit, mount, bulletX, bulletY, shootAngle); + + mount.reload = reload; + + if(useAmmo){ + unit.ammo--; + if(unit.ammo < 0) unit.ammo = 0; + } + } + } +}