diff --git a/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs b/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs new file mode 100644 index 000000000000..79adabeb92e0 --- /dev/null +++ b/OpenRA.Mods.AS/Traits/World/WeaponTriggerCells.cs @@ -0,0 +1,150 @@ +#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.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.AS.Traits +{ + [Desc("A layer that support weapon like infernal cannon like in cnc General")] + [TraitLocation(SystemActors.World | SystemActors.EditorWorld)] + public sealed class WeaponTriggerCellsInfo : TraitInfo + { + [Desc("Name of the layer type")] + public readonly string Name = ""; + + [Desc("Speed of restore per tick to 0")] + public readonly int RestorePerTick = 4; + + [Desc("Show debug overlay of this WeaponTriggerCells")] + public readonly bool ShowDebugOverlay = false; + + public override object Create(ActorInitializer init) { return new WeaponTriggerCells(init.Self, this); } + } + + sealed class TriggerCell + { + public int Level; + } + + public sealed class WeaponTriggerCells : INotifyActorDisposing, ITick, ITickRender + { + readonly World world; + public readonly WeaponTriggerCellsInfo Info; + + readonly Dictionary tiles = new Dictionary(); + + public WeaponTriggerCells(Actor self, WeaponTriggerCellsInfo info) + { + world = self.World; + Info = info; + } + + void ITick.Tick(Actor self) + { + var remove = new List(); + + // Apply half life to each cell. + foreach (var kv in tiles) + { + // has to be decreased by at least 1 so that it disappears eventually. + var level = kv.Value.Level; + if (level > 0) + { + if ((level -= Info.RestorePerTick) <= 0) + remove.Add(kv.Key); + else + kv.Value.Level = level; + } + else + { + if ((level += Info.RestorePerTick) >= 0) + remove.Add(kv.Key); + else + kv.Value.Level = level; + } + } + + foreach (var r in remove) + tiles.Remove(r); + } + + public int GetLevel(CPos cell) + { + if (!tiles.ContainsKey(cell)) + return 0; + + return tiles[cell].Level; + } + + public void SetLevel(CPos cell, int level) + { + if (!tiles.ContainsKey(cell)) + return; + + tiles[cell].Level = level; + } + + public void IncreaseLevel(CPos cell, int add_level, int max_level) + { + if (add_level == 0) + return; + + var currentLevel = 0; + + // Initialize, on fresh impact. + if (tiles.ContainsKey(cell)) + currentLevel = tiles[cell].Level; + + // the given weapon can't saturate the cell anymore. + if ((add_level > 0 && currentLevel >= max_level) || + (add_level < 0 && currentLevel <= max_level)) + return; + + var new_level = currentLevel + add_level; + + if ((add_level > 0 && new_level > max_level) || + (add_level < 0 && new_level < max_level)) + return; + + currentLevel = new_level; + + if (!tiles.ContainsKey(cell)) + tiles[cell] = new TriggerCell() { Level = currentLevel }; + else + tiles[cell].Level = currentLevel; + } + + bool disposed = false; + void INotifyActorDisposing.Disposing(Actor self) + { + if (disposed) + return; + + disposed = true; + } + + // Debug only, require enabling the `ITickRender` interface in this class + public void TickRender(WorldRenderer wr, Actor self) + { + if (Info.ShowDebugOverlay) + { + foreach (var kv in tiles) + { + var i = new FloatingText(world.Map.CenterOfCell(kv.Key), Color.Gold, kv.Value.Level.ToString(), 1); + world.Add(i); + } + } + } + } +} diff --git a/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs b/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs new file mode 100644 index 000000000000..a68178b44aea --- /dev/null +++ b/OpenRA.Mods.AS/Warheads/TriggerLayerWeaponWarhead.cs @@ -0,0 +1,136 @@ +using System; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Mods.AS.Traits; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.Common.Warheads; +using OpenRA.Traits; + +namespace OpenRA.Mods.TA.Warheads +{ + [Desc("Works like infernal cannon like in cnc General, used by TA")] + public sealed class TriggerLayerWeaponWarhead : Warhead, IRulesetLoaded + { + [Desc("Range between falloff steps in cells.")] + public readonly WDist Spread = new WDist(1024); + + [Desc("Level percentage at each range step.")] + public readonly int[] Falloff = { 100, 75, 50 }; + + [Desc("The name of the layer we want to increase the level of.")] + public readonly string LayerName = ""; + + [Desc("Ranges at which each Falloff step is defined (in cells). Overrides Spread.")] + public WDist[] Range = null; + + [Desc("Level this weapon puts on the ground. Accumulates over previously trigger area.")] + public int Level = 200; + + [Desc("It saturates at this level, by this weapon.")] + public int MaxLevel = 600; + + [Desc("Allow triggering effects when the impacted cell has the value in [triggerAtLevelMax, triggerAtLevelMin]")] + public bool AllowtriggerLevel = true; + + [Desc("Allows a triggering effect: cells (affected by Falloff and Range) set to a specific level defined by triggerSetLevel")] + public bool AllowSetLevelWhenTrigger = true; + + [Desc("Allows a triggering effect: impacted cell explode a weapon")] + public bool AllowtriggerWeaponWhenTrigger = true; + + [Desc("Impacted cell has the value in [triggerAtLevelMax, triggerAtLevelMin] to trigger effect. Requires \"AllowtriggerLevel = true\".")] + public int TriggerAtLevelMax = int.MaxValue; + + [Desc("Impacted cell has the value in [triggerAtLevelMax, triggerAtLevelMin] to trigger effect. Requires \"AllowtriggerLevel = true\".")] + public int TriggerAtLevelMin = int.MinValue; + + [Desc("Cells (affected by Falloff and Range) set to this level when trigger. Requires \"AllowtriggerLevel = true\" and \"AllowSetLevelWhenTrigger = true\"")] + public int TriggerSetLevel = 0; + + [WeaponReference] + [Desc("Impacted cell explode a weapon when trigger. Has to be defined in weapons.yaml as well.")] + public readonly string TriggerWeapon = null; + + WeaponInfo weapon; + + public void RulesetLoaded(Ruleset rules, WeaponInfo info) + { + if (Range == null) + Range = Exts.MakeArray(Falloff.Length, i => i * Spread); + else + { + if (Range.Length != 1 && Range.Length != Falloff.Length) + throw new YamlException("Number of range values must be 1 or equal to the number of Falloff values."); + + for (var i = 0; i < Range.Length - 1; i++) + if (Range[i] > Range[i + 1]) + throw new YamlException("Range values must be specified in an increasing order."); + } + + if (AllowtriggerLevel && AllowtriggerWeaponWhenTrigger && !rules.Weapons.TryGetValue(TriggerWeapon.ToLowerInvariant(), out weapon)) + throw new YamlException($"Weapons Ruleset does not contain an entry '{TriggerWeapon.ToLowerInvariant()}'"); + } + + public override void DoImpact(in Target target, WarheadArgs args) + { + var firedBy = args.SourceActor; + var world = firedBy.World; + + if (world.LocalPlayer != null) + { + var devMode = world.LocalPlayer.PlayerActor.TraitOrDefault(); + if (devMode != null && devMode.CombatGeometry) + { + var rng = Exts.MakeArray(Range.Length, i => WDist.FromCells(Range[i].Length)); + world.WorldActor.Trait().AddImpact(target.CenterPosition, rng, Primitives.Color.Gold); + } + } + + var targetTile = world.Map.CellContaining(target.CenterPosition); + var raLayer = world.WorldActor.TraitsImplementing() + .First(l => l.Info.Name == LayerName); + + var triggeredSetLevel = false; + if (AllowtriggerLevel && + raLayer.GetLevel(targetTile) >= TriggerAtLevelMin && + raLayer.GetLevel(targetTile) <= TriggerAtLevelMax) + { + if (AllowtriggerWeaponWhenTrigger) + weapon.Impact(Target.FromPos(target.CenterPosition), firedBy); + + var affectedCells = world.Map.FindTilesInCircle(targetTile, (int)Math.Ceiling((decimal)Range[Range.Length - 1].Length / 1024)); + if (AllowSetLevelWhenTrigger) + { + triggeredSetLevel = true; + foreach (var cell in affectedCells) + raLayer.SetLevel(cell, TriggerSetLevel); + } + } + + if (!triggeredSetLevel && Level != 0) + { + var affectedCells = world.Map.FindTilesInCircle(targetTile, (int)Math.Ceiling((decimal)Range[Range.Length - 1].Length / 1024)); + foreach (var cell in affectedCells) + { + var mul = GetIntensityFalloff((target.CenterPosition - world.Map.CenterOfCell(cell)).Length); + raLayer.IncreaseLevel(cell, Level * mul / 100, MaxLevel); + } + } + } + + int GetIntensityFalloff(int distance) + { + var inner = Range[0].Length; + for (var i = 1; i < Range.Length; i++) + { + var outer = Range[i].Length; + if (outer > distance) + return int2.Lerp(Falloff[i - 1], Falloff[i], distance - inner, outer - inner); + + inner = outer; + } + + return 0; + } + } +}