diff --git a/OpenRA.Mods.CA/Projectiles/SpriteAthenaLaser.cs b/OpenRA.Mods.CA/Projectiles/SpriteAthenaLaser.cs new file mode 100644 index 0000000000..9c4451c43a --- /dev/null +++ b/OpenRA.Mods.CA/Projectiles/SpriteAthenaLaser.cs @@ -0,0 +1,204 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, which is free software. + * It is made available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. For more information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Effects; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.CA.Projectiles +{ + [Desc("Generate laser connect by image with different height offset, and trigger warheads on the ground until expires.")] + class SpriteAthenaLaserInfo : IProjectileInfo + { + [FieldLoader.Require] + [Desc("Laser Image to display.")] + public readonly string Image = null; + + [SequenceReference(nameof(Image), allowNullImage: false)] + [Desc("Laser sprite Sequence of Image from this list while this projectile is moving.")] + public readonly string Sequence = "idle"; + + [Desc("Number of the laser sprite to form the beam.")] + public readonly int SpriteNumber = 8; + + [Desc("Offset of laser sprite to form the beam.")] + public readonly int HeightOffset = 1024; + + [Desc("Laser ring image to display.")] + public readonly string RingImage = null; + + [SequenceReference(nameof(RingImage), allowNullImage: true)] + [Desc("Sequence of laser ring image from this list while this projectile is moving.")] + public readonly string RingSequence = "idle"; + + [PaletteReference(nameof(IsPlayerPalette))] + [Desc("The palette used to draw this projectile.")] + public readonly string Palette = "effect"; + + [Desc("Palette is a player palette BaseName")] + public readonly bool IsPlayerPalette = false; + + [Desc("Projectile speed in WDist / tick.")] + public readonly WDist Speed = new(90); + + [Desc("Rotation speed around the target.")] + public readonly WAngle RotSpeed = WAngle.Zero; + + [Desc("Rotation speed slowly add to max.")] + public readonly bool RotStartFromZero = true; + + [Desc("How many ticks will pass between explosions.")] + public readonly int ExplosionInterval = 3; + + [Desc("How many ticks will the projectile pierce even after reach the target location.")] + public readonly int PierceTicks = 0; + + [Desc("How many ticks will the projectile stay after motion.")] + public readonly int StayTicks = 8; + + public IProjectile Create(ProjectileArgs args) { return new SpriteAthenaLaser(this, args); } + } + + class SpriteAthenaLaser : IProjectile + { + readonly int explosionInterval; + readonly ProjectileArgs args; + readonly WarheadArgs warheadArgs; + readonly Animation[] animations; + readonly Animation ringAnim; + readonly int heightoffset; + readonly int rotAcc; + readonly int speed; + readonly World world; + readonly string paletteName; + readonly int length, flightticks, maxticks; + readonly WPos target; + readonly WPos source; + + WPos projectilepos; + WAngle rot; + int ticks; + int rotSpeed; + + protected bool FlightLengthReached => ticks > flightticks; + + protected bool LifeExpired => ticks > maxticks; + + public SpriteAthenaLaser(SpriteAthenaLaserInfo info, ProjectileArgs args) + { + this.args = args; + warheadArgs = new WarheadArgs(args); + speed = info.Speed.Length; + + world = args.SourceActor.World; + source = new WPos(args.Source.X, args.Source.Y, 0); + target = new WPos(args.PassiveTarget.X, args.PassiveTarget.Y, 0); + length = Math.Max((target - source).Length / Math.Max(speed, 1), 1); + rotSpeed = info.RotStartFromZero ? 0 : info.RotSpeed.Angle * 1000; + rotAcc = info.RotStartFromZero ? info.RotSpeed.Angle * 1000 / length : 0; + + projectilepos = source - new WVec(0, 0, world.Map.DistanceAboveTerrain(source).Length); + flightticks = length + info.PierceTicks; + maxticks = length + info.PierceTicks + info.StayTicks; + explosionInterval = Math.Max(info.ExplosionInterval, 1); + heightoffset = info.HeightOffset; + + paletteName = info.Palette; + if (paletteName != null && info.IsPlayerPalette) + paletteName += args.SourceActor.Owner.InternalName; + + if (!string.IsNullOrEmpty(info.Image)) + { + animations = new Animation[info.SpriteNumber]; + + for (var i = 0; i < animations.Length; i++) + { + animations[i] = new Animation(world, info.Image); + animations[i].PlayRepeating(info.Sequence); + } + } + + if (!string.IsNullOrEmpty(info.RingImage)) + { + ringAnim = new Animation(world, info.RingImage); + ringAnim.PlayRepeating(info.RingSequence); + } + } + + IEnumerable IEffect.Render(WorldRenderer wr) + { + if (LifeExpired) + yield break; + + foreach (var r in RenderAnimation(wr)) + yield return r; + } + + void IEffect.Tick(World world) + { + ticks++; + + if (ticks % explosionInterval == 0) + { + warheadArgs.ImpactPosition = projectilepos; + args.Weapon.Impact(Target.FromPos(projectilepos), warheadArgs); + } + + ringAnim?.Tick(); + for (var i = 0; i < animations.Length; i++) + animations[i].Tick(); + + rotSpeed += rotAcc; + if (!FlightLengthReached) + { + var pos = projectilepos; + if (speed != 0) + pos = WPos.Lerp(source, target, ticks, length); + + if (rotSpeed != 0) + { + rot += new WAngle(rotSpeed / 1000); + pos = target + (pos - target).Rotate(WRot.FromYaw(rot)); + } + + projectilepos = pos - new WVec(0, 0, world.Map.DistanceAboveTerrain(pos).Length); + } + + if (LifeExpired) + world.AddFrameEndTask(w => w.Remove(this)); + } + + protected IEnumerable RenderAnimation(WorldRenderer wr) + { + var renderpos = projectilepos; + var palette = wr.Palette(paletteName); + + if (world.FogObscures(projectilepos)) + yield break; + + if (ringAnim != null) + { + foreach (var r in ringAnim.Render(renderpos, palette)) + yield return r; + } + + for (var i = 0; i < animations.Length; i++) + { + foreach (var r in animations[i].Render(renderpos, palette)) + yield return r; + + renderpos += new WVec(0, 0, heightoffset); + } + } + } +} diff --git a/OpenRA.Mods.CA/Warheads/FireReverseRadiusWarhead.cs b/OpenRA.Mods.CA/Warheads/FireReverseRadiusWarhead.cs new file mode 100644 index 0000000000..207f5e0ae4 --- /dev/null +++ b/OpenRA.Mods.CA/Warheads/FireReverseRadiusWarhead.cs @@ -0,0 +1,114 @@ +#region Copyright & License Information +/* + * Copyright 2015- OpenRA.Mods.AS Developers (see AUTHORS) + * This file is a part of a third-party plugin for OpenRA, which is + * free software. It is made available to you under the terms of the + * GNU General Public License as published by the Free Software + * Foundation. For more information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.CA.Warheads +{ + [Desc("Fires a defined amount of weapons with their maximum range in a reverse wave pattern.")] + public class FireReverseRadiusWarhead : WarheadAS, IRulesetLoaded + { + [WeaponReference] + [FieldLoader.Require] + [Desc("Has to be defined in weapons.yaml as well.")] + public readonly string Weapon = null; + + [Desc("Amount of weapons fired.")] + public readonly int[] Amount = { 1 }; + + [Desc("Should the weapons be fired around the intended target or at the explosion's epicenter.")] + public readonly bool AroundTarget = false; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + if (!target.IsValidFor(firedBy)) + return; + + var world = firedBy.World; + var map = world.Map; + + if (!IsValidImpact(target.CenterPosition, firedBy)) + return; + + var epicenter = AroundTarget && args.WeaponTarget.Type != TargetType.Invalid + ? args.WeaponTarget.CenterPosition + : target.CenterPosition; + + var amount = Amount.Length == 2 + ? world.SharedRandom.Next(Amount[0], Amount[1]) + : Amount[0]; + + var offset = 1024 / amount; + + for (var i = 0; i < amount; i++) + { + var radiusSource = Target.Invalid; + + var rotation = WRot.FromYaw(new WAngle(i * offset)); + var targetpos = epicenter + new WVec(weapon.Range.Length, 0, 0).Rotate(rotation); + radiusSource = Target.FromPos(new WPos(targetpos.X, targetpos.Y, map.CenterOfCell(map.CellContaining(targetpos)).Z)); + + if (radiusSource.Type == TargetType.Invalid) + continue; + + // Lambdas can't use 'in' variables, so capture a copy for later + var centerPosition = target.CenterPosition; + + var projectileArgs = new ProjectileArgs + { + Weapon = weapon, + Facing = (target.CenterPosition - radiusSource.CenterPosition).Yaw, + CurrentMuzzleFacing = () => (centerPosition - radiusSource.CenterPosition).Yaw, + + DamageModifiers = args.DamageModifiers, + + InaccuracyModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetInaccuracyModifier()).ToArray() : Array.Empty(), + + RangeModifiers = !firedBy.IsDead ? firedBy.TraitsImplementing() + .Select(a => a.GetRangeModifier()).ToArray() : Array.Empty(), + + Source = radiusSource.CenterPosition, + CurrentSource = () => radiusSource.CenterPosition, + SourceActor = firedBy, + GuidedTarget = target, + PassiveTarget = target.CenterPosition + }; + + if (projectileArgs.Weapon.Projectile != null) + { + var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs); + if (projectile != null) + firedBy.World.AddFrameEndTask(w => w.Add(projectile)); + + if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Length > 0) + { + var pos = target.CenterPosition; + if ((!firedBy.World.ShroudObscures(pos) && !firedBy.World.FogObscures(pos))) + Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, pos, null); + } + } + } + } + } +} diff --git a/mods/ca/bits/tsionbeam.shp b/mods/ca/bits/tsionbeam.shp new file mode 100644 index 0000000000..a9002457b7 Binary files /dev/null and b/mods/ca/bits/tsionbeam.shp differ diff --git a/mods/ca/bits/tsionbeamstart.shp b/mods/ca/bits/tsionbeamstart.shp new file mode 100644 index 0000000000..e925931aa7 Binary files /dev/null and b/mods/ca/bits/tsionbeamstart.shp differ diff --git a/mods/ca/bits/tsring1.shp b/mods/ca/bits/tsring1.shp new file mode 100644 index 0000000000..6fd1d64fa7 Binary files /dev/null and b/mods/ca/bits/tsring1.shp differ diff --git a/mods/ca/bits/tsringmini2.shp b/mods/ca/bits/tsringmini2.shp new file mode 100644 index 0000000000..aefc5eb9ac Binary files /dev/null and b/mods/ca/bits/tsringmini2.shp differ diff --git a/mods/ca/rules/powers.yaml b/mods/ca/rules/powers.yaml index f7613cf2eb..e9be89a2b6 100644 --- a/mods/ca/rules/powers.yaml +++ b/mods/ca/rules/powers.yaml @@ -1247,6 +1247,7 @@ ^SurgicalStrikePower: IonCannonPower@SurgicalStrike: OrderName: surgicalstrike + Weapon: TSIonCannonSpawner Prerequisites: ~eye.zocom Icon: surgicalstrike IconPalette: chrometd diff --git a/mods/ca/sequences/misc.yaml b/mods/ca/sequences/misc.yaml index 2250f7a63d..7384fca747 100644 --- a/mods/ca/sequences/misc.yaml +++ b/mods/ca/sequences/misc.yaml @@ -3371,5 +3371,36 @@ laserhit: Frames: 0, 0, 0, 0 Alpha: 1, 0.8, 0.6, 0.3 idle1: + idle2: FlipX: true + +tsioncannon: + Defaults: + Length: * + ZOffset: 2047 + Tick: 35 + IgnoreWorldTint: true + ionring: + Filename: tsring1.shp + ZRamp: 1 + Alpha: 0.75 + Tick: 50 + ionbeam: + Filename: tsionbeam.shp + Offset: 0, -60, 60 + ZRamp: 1 + Alpha: 0.75 + Tick: 40 + ionbeamstart: + Filename: tsionbeamstart.shp + Offset: 0, -60, 60 + ZRamp: 1 + Alpha: 0.75 + Tick: 30 + ringmini2: + Filename: tsringmini2.shp + Alpha: 0.75 + Offset: 0,-6 + Tick: 5 + Scale: 0.8 diff --git a/mods/ca/weapons/other.yaml b/mods/ca/weapons/other.yaml index 9496acead7..18c8b783cc 100644 --- a/mods/ca/weapons/other.yaml +++ b/mods/ca/weapons/other.yaml @@ -3769,3 +3769,110 @@ TroopCrawlerDummyWeapon: Projectile: InstantHit Warhead@1Dam: Dummy ValidRelationships: Ally + +TSIonCannonSpawner: ### THIS ONE GENERATES THE MAIN ION BEAM DUMMY + ReloadDelay: 6 + Range: 12c0 + AirThreshold: 8c0 + ValidTargets: Ground, Water, Air + Warhead@4BeamEff: FireReverseRadius + ValidTargets: Ground, Water, Air + Weapon: TSIonBeamMini + ImpactActors: false + Amount: 10 + AirThreshold: 8c0 + Warhead@5BeamEff: FireShrapnel + Delay: 100 + ImpactActors: false + Weapon: TSIonBeam + ValidTargets: Ground, Water, Air + AirThreshold: 8c0 + Warhead@6BeamEff: FireShrapnel + Delay: 110 + ImpactActors: false + Weapon: TSIonBeamMiniEnd + ValidTargets: Ground, Water, Air + AirThreshold: 8c0 + Warhead@3Smu_area: LeaveSmudge + Delay: 110 + SmudgeType: Scorch + Size: 3,2 + AirThreshold: 8c0 + Warhead@3Effect: CreateEffect + Delay: 110 + Image: tsioncannon + Explosions: ionring + ExplosionPalette: tseffect + ImpactSounds: ion1.aud + AffectsParent: true + ImpactActors: false + ValidTargets: Ground, Water, BlueTiberium, Tiberium, Air + AirThreshold: 8c0 + Warhead@Shake: ShakeScreen + Duration: 15 + Intensity: 8 + Multiplier: 1,1 + AirThreshold: 8c0 + Delay: 114 + Warhead@1Dam: SpreadDamage + Spread: 420 + Delay: 112 + Damage: 37500 + DamageTypes: Prone50Percent, TriggerProne, FireDeath + Versus: + None: 100 + Light: 100 + Wood: 100 + Heavy: 100 + Concrete: 100 + Brick: 100 + +TSIonBeam: + ReloadDelay: 9999 + AirThreshold: 8c0 + ValidTargets: Ground, Water, Air + Projectile: SpriteAthenaLaser + Image: tsioncannon + Sequence: ionbeam + Palette: tseffect-ignore-lighting-alpha75 + RingImage: tsioncannon + RingSequence: ringmini2 + SpriteNumber: 8 + HeightOffset: 4146 + StayTicks: 39 + Speed: 0 + ExplosionInterval: 9999 + +TSIonBeamMini: + Inherits: TSIonBeam + Range: 5c256 + AirThreshold: 8c0 + Projectile: SpriteAthenaLaser + Sequence: ionbeamstart + RingSequence: ringmini2 + Palette: tseffect + StayTicks: 5 + ExplosionInterval: 3 + Speed: 52 + RotSpeed: 12 + Warhead@1Dam: SpreadDamage + Spread: 100 + Damage: 5500 + DamageTypes: Prone50Percent, TriggerProne, FireDeath + Versus: + None: 250 + Light: 101 + Wood: 60 + Heavy: 101 + Concrete: 90 + Brick: 50 + Warhead@3Eff: CreateEffect + Explosions: small_explosion, small_explosion_alt1, small_explosion_alt2, small_explosion_alt3 + +TSIonBeamMiniEnd: + Inherits: TSIonBeam + Projectile: SpriteAthenaLaser + StayTicks: 43 + Sequence: ionbeamstart + RingSequence: ringmini2 + Palette: tseffect