From d1839701bb635b2e69beec9e3c5ffd76f3ae09e7 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 17:44:14 +0100 Subject: [PATCH 01/11] Add Plug and Pluggable for building actor-specific upgrades. --- OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 2 + .../Orders/PlaceBuildingOrderGenerator.cs | 55 ++++++++++--- .../Traits/Player/PlaceBuilding.cs | 23 +++++- OpenRA.Mods.Common/Traits/Plug.cs | 24 ++++++ OpenRA.Mods.Common/Traits/Pluggable.cs | 79 +++++++++++++++++++ 5 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 OpenRA.Mods.Common/Traits/Plug.cs create mode 100644 OpenRA.Mods.Common/Traits/Pluggable.cs diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index b4ca63d1aa..3bb01cbe2c 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -619,6 +619,8 @@ + + diff --git a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs index 1de95bee5c..41faa7cf3f 100644 --- a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs +++ b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs @@ -24,6 +24,7 @@ namespace OpenRA.Mods.Common.Orders readonly string building; readonly BuildingInfo buildingInfo; readonly PlaceBuildingInfo placeBuildingInfo; + readonly BuildingInfluence buildingInfluence; readonly string race; readonly Sprite buildOk; readonly Sprite buildBlocked; @@ -52,6 +53,8 @@ namespace OpenRA.Mods.Common.Orders buildOk = map.SequenceProvider.GetSequence("overlay", "build-valid-{0}".F(tileset)).GetSprite(0); buildBlocked = map.SequenceProvider.GetSequence("overlay", "build-invalid").GetSprite(0); + + buildingInfluence = producer.World.WorldActor.Trait(); } public IEnumerable Order(World world, CPos xy, MouseInput mi) @@ -73,16 +76,33 @@ namespace OpenRA.Mods.Common.Orders if (mi.Button == MouseButton.Left) { + var orderType = "PlaceBuilding"; var topLeft = xy - FootprintUtils.AdjustForBuildingSize(buildingInfo); - if (!world.CanPlaceBuilding(building, buildingInfo, topLeft, null) - || !buildingInfo.IsCloseEnoughToBase(world, producer.Owner, building, topLeft)) + + var plugInfo = world.Map.Rules.Actors[building].Traits.GetOrDefault(); + if (plugInfo != null) { - Sound.PlayNotification(world.Map.Rules, producer.Owner, "Speech", "BuildingCannotPlaceAudio", producer.Owner.Country.Race); - yield break; + orderType = "PlacePlug"; + if (!AcceptsPlug(topLeft, plugInfo)) + { + Sound.PlayNotification(world.Map.Rules, producer.Owner, "Speech", "BuildingCannotPlaceAudio", producer.Owner.Country.Race); + yield break; + } + } + else + { + if (!world.CanPlaceBuilding(building, buildingInfo, topLeft, null) + || !buildingInfo.IsCloseEnoughToBase(world, producer.Owner, building, topLeft)) + { + Sound.PlayNotification(world.Map.Rules, producer.Owner, "Speech", "BuildingCannotPlaceAudio", producer.Owner.Country.Race); + yield break; + } + + if (world.Map.Rules.Actors[building].Traits.Contains()) + orderType = "LineBuild"; } - var isLineBuild = world.Map.Rules.Actors[building].Traits.Contains(); - yield return new Order(isLineBuild ? "LineBuild" : "PlaceBuilding", producer.Owner.PlayerActor, false) + yield return new Order(orderType, producer.Owner.PlayerActor, false) { TargetLocation = topLeft, TargetActor = producer, @@ -101,6 +121,16 @@ namespace OpenRA.Mods.Common.Orders p.Tick(); } + bool AcceptsPlug(CPos cell, PlugInfo plug) + { + var host = buildingInfluence.GetBuildingAt(cell); + if (host == null) + return false; + + var location = host.Location; + return host.TraitsImplementing().Any(p => location + p.Info.Offset == cell && p.AcceptsPlug(host, plug.Type)); + } + public IEnumerable Render(WorldRenderer wr, World world) { yield break; } public IEnumerable RenderAfterWorld(WorldRenderer wr, World world) { @@ -116,10 +146,17 @@ namespace OpenRA.Mods.Common.Orders var cells = new Dictionary(); - // Linebuild for walls. - // Requires a 1x1 footprint - if (rules.Actors[building].Traits.Contains()) + var plugInfo = rules.Actors[building].Traits.GetOrDefault(); + if (plugInfo != null) { + if (buildingInfo.Dimensions.X != 1 || buildingInfo.Dimensions.Y != 1) + throw new InvalidOperationException("Plug requires a 1x1 sized Building"); + + cells.Add(topLeft, AcceptsPlug(topLeft, plugInfo)); + } + else if (rules.Actors[building].Traits.Contains()) + { + // Linebuild for walls. if (buildingInfo.Dimensions.X != 1 || buildingInfo.Dimensions.Y != 1) throw new InvalidOperationException("LineBuild requires a 1x1 sized Building"); diff --git a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs index 91d118f0de..2c435d9196 100644 --- a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs +++ b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs @@ -26,7 +26,7 @@ namespace OpenRA.Mods.Common.Traits { public void ResolveOrder(Actor self, Order order) { - if (order.OrderString == "PlaceBuilding" || order.OrderString == "LineBuild") + if (order.OrderString == "PlaceBuilding" || order.OrderString == "LineBuild" || order.OrderString == "PlacePlug") { self.World.AddFrameEndTask(w => { @@ -69,6 +69,27 @@ namespace OpenRA.Mods.Common.Traits playSounds = false; } } + else if (order.OrderString == "PlacePlug") + { + var host = self.World.WorldActor.Trait().GetBuildingAt(order.TargetLocation); + if (host == null) + return; + + var plugInfo = unit.Traits.GetOrDefault(); + if (plugInfo == null) + return; + + var location = host.Location; + var pluggable = host.TraitsImplementing() + .FirstOrDefault(p => location + p.Info.Offset == order.TargetLocation && p.AcceptsPlug(host, plugInfo.Type)); + + if (pluggable == null) + return; + + pluggable.EnablePlug(host, plugInfo.Type); + foreach (var s in buildingInfo.BuildSounds) + Sound.PlayToPlayer(order.Player, s, host.CenterPosition); + } else { if (!self.World.CanPlaceBuilding(order.TargetString, buildingInfo, order.TargetLocation, null) diff --git a/OpenRA.Mods.Common/Traits/Plug.cs b/OpenRA.Mods.Common/Traits/Plug.cs new file mode 100644 index 0000000000..625d8b4389 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Plug.cs @@ -0,0 +1,24 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * This file is part of 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 System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class PlugInfo : TraitInfo + { + [Desc("Plug type (matched against Upgrades in Pluggable)")] + public readonly string Type = null; + } + + public class Plug { } +} diff --git a/OpenRA.Mods.Common/Traits/Pluggable.cs b/OpenRA.Mods.Common/Traits/Pluggable.cs new file mode 100644 index 0000000000..34804b99d3 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Pluggable.cs @@ -0,0 +1,79 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * This file is part of 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 System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class PluggableInfo : ITraitInfo, Requires + { + [Desc("Footprint cell offset where a plug can be placed.")] + public readonly CVec Offset = CVec.Zero; + + [FieldLoader.LoadUsing("LoadUpgrades")] + [Desc("Upgrades to grant for each accepted plug type.")] + public readonly Dictionary Upgrades = null; + + static object LoadUpgrades(MiniYaml y) + { + MiniYaml upgrades; + + if (!y.ToDictionary().TryGetValue("Upgrades", out upgrades)) + return new Dictionary(); + + return upgrades.Nodes.ToDictionary( + kv => kv.Key, + kv => FieldLoader.GetValue("(value)", kv.Value.Value)); + } + + public object Create(ActorInitializer init) { return new Pluggable(init.Self, this); } + } + + public class Pluggable + { + public readonly PluggableInfo Info; + readonly UpgradeManager upgradeManager; + string active; + + public Pluggable(Actor self, PluggableInfo info) + { + Info = info; + upgradeManager = self.Trait(); + } + + public bool AcceptsPlug(Actor self, string type) + { + return active == null && Info.Upgrades.ContainsKey(type); + } + + public void EnablePlug(Actor self, string type) + { + string[] upgrades; + if (!Info.Upgrades.TryGetValue(type, out upgrades)) + return; + + foreach (var u in upgrades) + upgradeManager.GrantUpgrade(self, u, this); + + active = type; + } + + public void DisablePlug(Actor self, string type) + { + if (type != active) + return; + + foreach (var u in Info.Upgrades[type]) + upgradeManager.RevokeUpgrade(self, u, this); + } + } +} From 4597895ea3a9a846fec8fec9c7dee3b95a68e9bd Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:43:24 +0100 Subject: [PATCH 02/11] Add upgrade support to AttackBase. --- .../Traits/Attack/AttackBase.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index 3b3fdd383f..ae9ba6267a 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -18,7 +18,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - public abstract class AttackBaseInfo : ITraitInfo + public abstract class AttackBaseInfo : UpgradableTraitInfo, ITraitInfo { [Desc("Armament names")] public readonly string[] Armaments = { "primary", "secondary" }; @@ -35,11 +35,10 @@ namespace OpenRA.Mods.Common.Traits public abstract object Create(ActorInitializer init); } - public abstract class AttackBase : IIssueOrder, IResolveOrder, IOrderVoice, ISync + public abstract class AttackBase : UpgradableTrait, IIssueOrder, IResolveOrder, IOrderVoice, ISync { [Sync] public bool IsAttacking { get; internal set; } public IEnumerable Armaments { get { return getArmaments(); } } - public readonly AttackBaseInfo Info; protected Lazy facing; protected Lazy building; @@ -49,9 +48,9 @@ namespace OpenRA.Mods.Common.Traits readonly Actor self; public AttackBase(Actor self, AttackBaseInfo info) + : base(info) { this.self = self; - Info = info; var armaments = Exts.Lazy(() => self.TraitsImplementing() .Where(a => info.Armaments.Contains(a.Info.Name))); @@ -65,7 +64,7 @@ namespace OpenRA.Mods.Common.Traits protected virtual bool CanAttack(Actor self, Target target) { - if (!self.IsInWorld) + if (!self.IsInWorld || IsTraitDisabled) return false; if (!HasAnyValidWeapons(target)) @@ -101,7 +100,7 @@ namespace OpenRA.Mods.Common.Traits get { var armament = Armaments.FirstOrDefault(a => a.Weapon.Warheads.Any(w => (w is DamageWarhead))); - if (armament == null) + if (armament == null || IsTraitDisabled) yield break; var negativeDamage = (armament.Weapon.Warheads.FirstOrDefault(w => (w is DamageWarhead)) as DamageWarhead).Damage < 0; @@ -149,6 +148,9 @@ namespace OpenRA.Mods.Common.Traits public bool HasAnyValidWeapons(Target t) { + if (IsTraitDisabled) + return false; + if (Info.AttackRequiresEnteringCell && !positionable.Value.CanEnterCell(t.Actor.Location, null, false)) return false; @@ -157,14 +159,19 @@ namespace OpenRA.Mods.Common.Traits public WRange GetMaximumRange() { - return Armaments.Select(a => a.Weapon.Range).Append(WRange.Zero).Max(); + if (IsTraitDisabled) + return WRange.Zero; + + return Armaments.Where(a => !a.IsTraitDisabled) + .Select(a => a.Weapon.Range) + .Append(WRange.Zero).Max(); } public Armament ChooseArmamentForTarget(Target t) { return Armaments.FirstOrDefault(a => a.Weapon.IsValidAgainst(t, self.World, self)); } public void AttackTarget(Target target, bool queued, bool allowMove) { - if (self.IsDisabled()) + if (self.IsDisabled() || IsTraitDisabled) return; if (!target.IsValidFor(self)) From a9e1c09d82b6b5724276c048ea2517270bbf3370 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:43:45 +0100 Subject: [PATCH 03/11] Add upgrade support to DetectCloaked. --- OpenRA.Mods.Common/Traits/Cloak.cs | 2 +- OpenRA.Mods.Common/Traits/DetectCloaked.cs | 11 +++-------- .../Traits/Render/RenderDetectionCircle.cs | 11 ++++++++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Cloak.cs b/OpenRA.Mods.Common/Traits/Cloak.cs index efa06c1532..43b71c9fcb 100644 --- a/OpenRA.Mods.Common/Traits/Cloak.cs +++ b/OpenRA.Mods.Common/Traits/Cloak.cs @@ -123,7 +123,7 @@ namespace OpenRA.Mods.Common.Traits if (!Cloaked || self.Owner.IsAlliedWith(viewer)) return true; - return self.World.ActorsWithTrait().Any(a => a.Actor.Owner.IsAlliedWith(viewer) + return self.World.ActorsWithTrait().Any(a => !a.Trait.IsTraitDisabled && a.Actor.Owner.IsAlliedWith(viewer) && Info.CloakTypes.Intersect(a.Trait.Info.CloakTypes).Any() && (self.CenterPosition - a.Actor.CenterPosition).Length <= WRange.FromCells(a.Trait.Info.Range).Range); } diff --git a/OpenRA.Mods.Common/Traits/DetectCloaked.cs b/OpenRA.Mods.Common/Traits/DetectCloaked.cs index c66d7ab825..cd5fcb1d6f 100644 --- a/OpenRA.Mods.Common/Traits/DetectCloaked.cs +++ b/OpenRA.Mods.Common/Traits/DetectCloaked.cs @@ -13,7 +13,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Actor can reveal Cloak actors in a specified range.")] - public class DetectCloakedInfo : ITraitInfo + public class DetectCloakedInfo : UpgradableTraitInfo, ITraitInfo { [Desc("Specific cloak classifications I can reveal.")] public readonly string[] CloakTypes = { "Cloak" }; @@ -24,13 +24,8 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new DetectCloaked(this); } } - public class DetectCloaked + public class DetectCloaked : UpgradableTrait { - public readonly DetectCloakedInfo Info; - - public DetectCloaked(DetectCloakedInfo info) - { - Info = info; - } + public DetectCloaked(DetectCloakedInfo info) : base(info) { } } } diff --git a/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs b/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs index 5d38a587e9..96ab52071e 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderDetectionCircle.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Drawing; +using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Graphics; using OpenRA.Traits; @@ -32,9 +33,17 @@ namespace OpenRA.Mods.Common.Traits if (self.Owner != self.World.LocalPlayer) yield break; + var range = self.TraitsImplementing() + .Where(a => !a.IsTraitDisabled) + .Select(a => WRange.FromCells(a.Info.Range)) + .Append(WRange.Zero).Max(); + + if (range == WRange.Zero) + yield break; + yield return new RangeCircleRenderable( self.CenterPosition, - WRange.FromCells(self.Info.Traits.Get().Range), + range, 0, Color.FromArgb(128, Color.LimeGreen), Color.FromArgb(96, Color.Black)); From ee12257d249276f5454b5ef9d70a74e12a1914f5 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:44:15 +0100 Subject: [PATCH 04/11] Fix RenderRangeCircle for disabled Armaments. --- OpenRA.Mods.Common/Traits/Render/RenderRangeCircle.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Render/RenderRangeCircle.cs b/OpenRA.Mods.Common/Traits/Render/RenderRangeCircle.cs index 4788c85955..8cbc3d8150 100644 --- a/OpenRA.Mods.Common/Traits/Render/RenderRangeCircle.cs +++ b/OpenRA.Mods.Common/Traits/Render/RenderRangeCircle.cs @@ -27,7 +27,8 @@ namespace OpenRA.Mods.Common.Traits public IEnumerable Render(WorldRenderer wr, World w, ActorInfo ai, WPos centerPosition) { - var armaments = ai.Traits.WithInterface(); + var armaments = ai.Traits.WithInterface() + .Where(a => a.UpgradeMinEnabledLevel == 0); var range = FallbackRange; if (armaments.Any()) @@ -69,9 +70,13 @@ namespace OpenRA.Mods.Common.Traits if (self.Owner != self.World.LocalPlayer) yield break; + var range = attack.GetMaximumRange(); + if (range == WRange.Zero) + yield break; + yield return new RangeCircleRenderable( self.CenterPosition, - attack.GetMaximumRange(), + range, 0, Color.FromArgb(128, Color.Yellow), Color.FromArgb(96, Color.Black)); From 42f3fa74395c28e2fcf4f7d3b464a4b9cfb6d75b Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:44:27 +0100 Subject: [PATCH 05/11] Add upgrade support to WithMuzzleFlash. --- OpenRA.Mods.Common/Traits/Render/WithMuzzleFlash.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Render/WithMuzzleFlash.cs b/OpenRA.Mods.Common/Traits/Render/WithMuzzleFlash.cs index 8e8a4eea81..253739ce95 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithMuzzleFlash.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithMuzzleFlash.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Renders the MuzzleSequence from the Armament trait.")] - class WithMuzzleFlashInfo : ITraitInfo, Requires, Requires, Requires + class WithMuzzleFlashInfo : UpgradableTraitInfo, ITraitInfo, Requires, Requires, Requires { [Desc("Ignore the weapon position, and always draw relative to the center of the actor")] public readonly bool IgnoreOffset = false; @@ -25,13 +25,14 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new WithMuzzleFlash(init.Self, this); } } - class WithMuzzleFlash : INotifyAttack, IRender, ITick + class WithMuzzleFlash : UpgradableTrait, INotifyAttack, IRender, ITick { Dictionary visible = new Dictionary(); Dictionary anims = new Dictionary(); Func getFacing; public WithMuzzleFlash(Actor self, WithMuzzleFlashInfo info) + : base(info) { var render = self.Trait(); var facing = self.TraitOrDefault(); @@ -64,7 +65,7 @@ namespace OpenRA.Mods.Common.Traits anims.Add(barrel, new AnimationWithOffset(muzzleFlash, () => info.IgnoreOffset ? WVec.Zero : armClosure.MuzzleOffset(self, barrel), - () => !visible[barrel], + () => IsTraitDisabled || !visible[barrel], () => false, p => WithTurret.ZOffsetFromCenter(self, p, 2))); } From 5c18220636d581a895791514e7aac0be9eab317b Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:44:43 +0100 Subject: [PATCH 06/11] Add upgrade support to WithTurret. --- .../Traits/Render/WithTurret.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Render/WithTurret.cs b/OpenRA.Mods.Common/Traits/Render/WithTurret.cs index bd1138950a..e13708d5f0 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithTurret.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithTurret.cs @@ -17,7 +17,8 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Renders turrets for units with the Turreted trait.")] - public class WithTurretInfo : ITraitInfo, IRenderActorPreviewSpritesInfo, Requires, Requires, Requires + public class WithTurretInfo : UpgradableTraitInfo, ITraitInfo, IRenderActorPreviewSpritesInfo, + Requires, Requires, Requires { [Desc("Sequence name to use")] public readonly string Sequence = "turret"; @@ -35,6 +36,9 @@ namespace OpenRA.Mods.Common.Traits public IEnumerable RenderPreviewSprites(ActorPreviewInitializer init, RenderSpritesInfo rs, string image, int facings, PaletteReference p) { + if (UpgradeMinEnabledLevel > 0) + yield break; + var body = init.Actor.Traits.Get(); var t = init.Actor.Traits.WithInterface() .First(tt => tt.Turret == Turret); @@ -52,9 +56,8 @@ namespace OpenRA.Mods.Common.Traits } } - public class WithTurret : ITick + public class WithTurret : UpgradableTrait, ITick { - WithTurretInfo info; RenderSprites rs; IBodyOrientation body; AttackBase ab; @@ -63,8 +66,8 @@ namespace OpenRA.Mods.Common.Traits Animation anim; public WithTurret(Actor self, WithTurretInfo info) + : base(info) { - this.info = info; rs = self.Trait(); body = self.Trait(); @@ -76,8 +79,8 @@ namespace OpenRA.Mods.Common.Traits anim = new Animation(self.World, rs.GetImage(self), () => t.TurretFacing); anim.Play(info.Sequence); - rs.Add("turret_{0}".F(info.Turret), new AnimationWithOffset( - anim, () => TurretOffset(self), null, () => false, p => ZOffsetFromCenter(self, p, 1))); + rs.Add("turret_{0}_{1}".F(info.Turret, info.Sequence), new AnimationWithOffset( + anim, () => TurretOffset(self), () => IsTraitDisabled, () => false, p => ZOffsetFromCenter(self, p, 1))); // Restrict turret facings to match the sprite t.QuantizedFacings = anim.CurrentSequence.Facings; @@ -85,7 +88,7 @@ namespace OpenRA.Mods.Common.Traits WVec TurretOffset(Actor self) { - if (!info.Recoils) + if (!Info.Recoils) return t.Position(self); var recoil = arms.Aggregate(WRange.Zero, (a, b) => a + b.Recoil); @@ -97,10 +100,10 @@ namespace OpenRA.Mods.Common.Traits public void Tick(Actor self) { - if (info.AimSequence == null) + if (Info.AimSequence == null) return; - var sequence = ab.IsAttacking ? info.AimSequence : info.Sequence; + var sequence = ab.IsAttacking ? Info.AimSequence : Info.Sequence; anim.ReplaceAnim(sequence); } From e440d00585248832743819e1e3c93bf25dfd119a Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:34:32 +0100 Subject: [PATCH 07/11] Add upgrade support to WithIdleOverlay. --- OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs b/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs index f428294f70..41d427f999 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("Renders a decorative animation on units and buildings.")] - public class WithIdleOverlayInfo : ITraitInfo, IRenderActorPreviewSpritesInfo, Requires, Requires + public class WithIdleOverlayInfo : UpgradableTraitInfo, ITraitInfo, IRenderActorPreviewSpritesInfo, Requires, Requires { [Desc("Sequence name to use")] public readonly string Sequence = "idle-overlay"; @@ -37,6 +37,9 @@ namespace OpenRA.Mods.Common.Traits public IEnumerable RenderPreviewSprites(ActorPreviewInitializer init, RenderSpritesInfo rs, string image, int facings, PaletteReference p) { + if (UpgradeMinEnabledLevel > 0) + yield break; + var body = init.Actor.Traits.Get(); var facing = init.Contains() ? init.Get() : 0; var anim = new Animation(init.World, image, () => facing); @@ -48,12 +51,13 @@ namespace OpenRA.Mods.Common.Traits } } - public class WithIdleOverlay : INotifyDamageStateChanged, INotifyBuildComplete, INotifySold, INotifyTransform + public class WithIdleOverlay : UpgradableTrait, INotifyDamageStateChanged, INotifyBuildComplete, INotifySold, INotifyTransform { Animation overlay; bool buildComplete; public WithIdleOverlay(Actor self, WithIdleOverlayInfo info) + : base(info) { var rs = self.Trait(); var body = self.Trait(); @@ -65,7 +69,7 @@ namespace OpenRA.Mods.Common.Traits rs.Add("idle_overlay_{0}".F(info.Sequence), new AnimationWithOffset(overlay, () => body.LocalToWorld(info.Offset.Rotate(body.QuantizeOrientation(self, self.Orientation))), - () => !buildComplete, + () => IsTraitDisabled || !buildComplete, () => info.PauseOnLowPower && disabled.Any(d => d.Disabled), p => WithTurret.ZOffsetFromCenter(self, p, 1)), info.Palette, info.IsPlayerPalette); From ebd09f196cbfa4517b38a1073022afbee84d4619 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:50:20 +0100 Subject: [PATCH 08/11] Add upgrade support to SupportPower. --- .../Traits/SupportPowers/SupportPower.cs | 7 +++---- .../SupportPowers/SupportPowerManager.cs | 20 +++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs index 6edb273fdc..a1fb516512 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPower.cs @@ -12,7 +12,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { - public abstract class SupportPowerInfo : ITraitInfo + public abstract class SupportPowerInfo : UpgradableTraitInfo, ITraitInfo { [Desc("Measured in seconds.")] public readonly int ChargeTime = 0; @@ -53,15 +53,14 @@ namespace OpenRA.Mods.Common.Traits public SupportPowerInfo() { OrderName = GetType().Name + "Order"; } } - public class SupportPower + public class SupportPower : UpgradableTrait { public readonly Actor Self; - public readonly SupportPowerInfo Info; protected RadarPing ping; public SupportPower(Actor self, SupportPowerInfo info) + : base(info) { - Info = info; Self = self; } diff --git a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs index 395833905e..6f49b1c926 100644 --- a/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs +++ b/OpenRA.Mods.Common/Traits/SupportPowers/SupportPowerManager.cs @@ -132,7 +132,7 @@ namespace OpenRA.Mods.Common.Traits if (!Powers.TryGetValue(key, out sp)) return; - sp.Disabled = false; + sp.PrerequisitesAvailable(true); } public void PrerequisitesUnavailable(string key) @@ -141,7 +141,7 @@ namespace OpenRA.Mods.Common.Traits if (!Powers.TryGetValue(key, out sp)) return; - sp.Disabled = true; + sp.PrerequisitesAvailable(false); sp.RemainingTime = sp.TotalTime; } @@ -158,17 +158,25 @@ namespace OpenRA.Mods.Common.Traits public int RemainingTime; public int TotalTime; public bool Active { get; private set; } - public bool Disabled { get; set; } + public bool Disabled { get { return !prereqsAvailable || !upgradeAvailable; } } public SupportPowerInfo Info { get { return Instances.Select(i => i.Info).FirstOrDefault(); } } public bool Ready { get { return Active && RemainingTime == 0; } } + bool upgradeAvailable; + bool prereqsAvailable = true; + public SupportPowerInstance(string key, SupportPowerManager manager) { this.manager = manager; this.key = key; } + public void PrerequisitesAvailable(bool available) + { + prereqsAvailable = available; + } + static bool InstanceDisabled(SupportPower sp) { return sp.Self.TraitsImplementing().Any(d => d.Disabled); @@ -178,6 +186,10 @@ namespace OpenRA.Mods.Common.Traits bool notifiedReady; public void Tick() { + upgradeAvailable = Instances.Any(i => !i.IsTraitDisabled); + if (!upgradeAvailable) + RemainingTime = TotalTime; + Active = !Disabled && Instances.Any(i => !i.Self.IsDisabled()); if (!Active) return; @@ -226,7 +238,7 @@ namespace OpenRA.Mods.Common.Traits notifiedCharging = notifiedReady = false; if (Info.OneShot) - Disabled = true; + PrerequisitesAvailable(false); } } From 0c5be6183f0240d1dae425f95572d5e4af2e8cf3 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 14:25:14 +0100 Subject: [PATCH 09/11] Implement GDI component tower upgrades. --- mods/ts/rules/defaults.yaml | 7 ++ mods/ts/rules/structures.yaml | 174 +++++++++++++++--------------- mods/ts/sequences/structures.yaml | 82 ++++---------- 3 files changed, 111 insertions(+), 152 deletions(-) diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index b344357523..b0e98a2e10 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -141,6 +141,13 @@ CloakDelay: 90 MustBeDestroyed: +^BuildingPlug: + Building: + BuildSounds: place2.aud + KillsSelf: + RemoveInstead: true + RenderSprites: + ^Infantry: AppearsOnRadar: Health: diff --git a/mods/ts/rules/structures.yaml b/mods/ts/rules/structures.yaml index 1dead3b5a8..62aca5e90f 100644 --- a/mods/ts/rules/structures.yaml +++ b/mods/ts/rules/structures.yaml @@ -430,7 +430,7 @@ GAWALL: Inherits: ^Wall Buildable: Queue: Defense - BuildPaletteOrder: 1001 + BuildPaletteOrder: 20 Prerequisites: ~structures.gdi SoundOnDamageTransition: DamagedSounds: @@ -910,14 +910,13 @@ NASTLH: Selectable: Bounds: 124, 64, 15, 13 -#TODO: Placeholder, replace with Component Tower + Vulcan Upgrade -GAVULC: +GACTWR: Inherits: ^Building Valued: Cost: 600 Tooltip: - Name: Vulcan Tower - Description: Basic base defense. \nRequires no power to operate.\n Strong vs infantry and light armor\n Cannot target Aircraft + Name: Component Tower + Description: Modular tower for base defenses. Buildable: Queue: Defense BuildPaletteOrder: 30 @@ -938,26 +937,60 @@ GAVULC: RenderRangeCircle: RenderDetectionCircle: DetectCloaked: + UpgradeTypes: tower + UpgradeMinEnabledLevel: 1 Range: 5 AutoTarget: Turreted: ROT: 10 InitialFacing: 50 AttackTurreted: - WithTurret: + UpgradeTypes: tower + UpgradeMinEnabledLevel: 1 + CanPowerDown: + WithTurret@VULC: + UpgradeTypes: tower.vulcan + UpgradeMinEnabledLevel: 1 Recoils: no - Armament@PRIMARY: + Sequence: turret-vulcan + WithTurret@ROCKET: + UpgradeTypes: tower.rocket + UpgradeMinEnabledLevel: 1 + Recoils: no + Sequence: turret-rocket + WithTurret@SAM: + UpgradeTypes: tower.sam + UpgradeMinEnabledLevel: 1 + Recoils: no + Sequence: turret-sam + Armament@VULCPRIMARY: + UpgradeTypes: tower.vulcan + UpgradeMinEnabledLevel: 1 Weapon: VulcanTower LocalOffset: 768,85,512 MuzzleSequence: muzzle MuzzleSplitFacings: 8 - Armament@SECONDARY: + Armament@VULCSECONDARY: + UpgradeTypes: tower.vulcan + UpgradeMinEnabledLevel: 1 Name: secondary Weapon: VulcanTower LocalOffset: 768,-85,512 MuzzleSequence: muzzle MuzzleSplitFacings: 8 + Armament@ROCKET: + UpgradeTypes: tower.rocket + UpgradeMinEnabledLevel: 1 + Weapon: RPGTower + LocalOffset: 512,-128,512 + Armament@SAM: + UpgradeTypes: tower.sam + UpgradeMinEnabledLevel: 1 + Weapon: SAMTower + LocalOffset: 512,0,512 WithMuzzleFlash: + UpgradeTypes: tower.vulcan + UpgradeMinEnabledLevel: 1 WithIdleOverlay@LIGHTS: Sequence: idle-lights LineBuildNode: @@ -965,106 +998,67 @@ GAVULC: -RenderBuilding: RenderBuildingWall: Type: wall + Power@base: + Amount: -10 + Power@turrets: + UpgradeTypes: tower + UpgradeMinEnabledLevel: 1 + Amount: -20 + Power@samextra: + UpgradeTypes: tower.sam + UpgradeMinEnabledLevel: 1 + Amount: -10 + Pluggable: + Upgrades: + tower.vulcan: tower, tower.vulcan + tower.rocket: tower, tower.rocket + tower.sam: tower, tower.sam + +GAVULC: + Inherits: ^BuildingPlug + Valued: + Cost: 150 + Tooltip: + Name: Vulcan Tower + Description: Basic base defense. \nRequires no power to operate.\n Strong vs infantry and light armor\n Cannot target Aircraft + Buildable: + Queue: Defense + BuildPaletteOrder: 40 + Prerequisites: gactwr, gapile, ~structures.gdi + Plug: + Type: tower.vulcan Power: Amount: -20 -#TODO: Placeholder, replace with Component Tower + RPG Upgrade GAROCK: - Inherits: ^Building + Inherits: ^BuildingPlug Valued: - Cost: 1000 + Cost: 600 Tooltip: - Name: RPG Tower + Name: RPG Upgrade Description: GDI Advanced base defense.\nRequires power to operate.\n Strong vs armored ground units\n Cannot target Aircraft Buildable: Queue: Defense BuildPaletteOrder: 40 - Prerequisites: gapile, ~structures.gdi - Building: - Selectable: - Bounds: 48, 48, 0, -12 - RequiresPower: - DisabledOverlay: - -GivesBuildableArea: - Health: - HP: 500 - Armor: - Type: Light - RevealsShroud: - Range: 6c0 - BodyOrientation: - QuantizedFacings: 32 - RenderRangeCircle: - RenderDetectionCircle: - DetectCloaked: - Range: 5 - AutoTarget: - Turreted: - ROT: 10 - InitialFacing: 50 - AttackTurreted: - WithTurret: - Recoils: no - Armament: - Weapon: RPGTower - LocalOffset: 512,-128,512 - WithIdleOverlay@LIGHTS: - Sequence: idle-lights - LineBuildNode: - Types: turret - -RenderBuilding: - RenderBuildingWall: - Type: wall + Prerequisites: gactwr, gapile, ~structures.gdi + Plug: + Type: tower.rocket Power: - Amount: -50 + Amount: -20 -#TODO: Placeholder, replace with Component Tower + SAM Upgrade GACSAM: - Inherits: ^Building + Inherits: ^BuildingPlug Valued: - Cost: 600 + Cost: 300 Tooltip: - Name: S.A.M. Tower + Name: SAM. Upgrade Description: GDI Anti-Air base defense. \nRequires power to operate.\n Strong vs all Aircraft\n Cannot target ground units Buildable: Queue: Defense - BuildPaletteOrder: 60 - Prerequisites: garadr, ~structures.gdi - Building: - Selectable: - Bounds: 48, 48, 0, -12 - RequiresPower: - DisabledOverlay: - -GivesBuildableArea: - Health: - HP: 500 - Armor: - Type: Light - RevealsShroud: - Range: 6c0 - BodyOrientation: - QuantizedFacings: 32 - RenderRangeCircle: - RenderDetectionCircle: - DetectCloaked: - Range: 5 - AutoTarget: - Turreted: - ROT: 10 - InitialFacing: 50 - AttackTurreted: - WithTurret: - Recoils: no - Armament: - Weapon: SAMTower - LocalOffset: 512,0,512 - WithIdleOverlay@LIGHTS: - Sequence: idle-lights - LineBuildNode: - Types: turret - -RenderBuilding: - RenderBuildingWall: - Type: wall + BuildPaletteOrder: 40 + Prerequisites: gactwr, garadr, ~structures.gdi + Plug: + Type: tower.sam Power: Amount: -30 diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index 2ba5c581c6..033d8bb621 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -654,7 +654,7 @@ nastlh: Offset: 0, 0 UseTilesetCode: false -gavulc: +gactwr: Defaults: Offset: 0, -12 UseTilesetCode: true @@ -667,7 +667,21 @@ gavulc: Start: 2 ShadowStart: 5 Tick: 400 - turret: gtctwr_b + idle-lights: gtctwr_a + Length: 6 + Tick: 200 + damaged-idle-lights: gtctwr_a + Length: 6 + Tick: 200 + make: gtctwrmk + Length: 11 + ShadowStart: 11 + turret-vulcan: gtctwr_b + Facings: 32 + turret-rocket: gtctwr_c + Facings: 32 + UseTilesetCode: false + turret-sam: gtctwr_d Facings: 32 muzzle0: mgun-n Length: * @@ -693,74 +707,18 @@ gavulc: muzzle7: mgun-ne Length: * UseTilesetCode: false - idle-lights: gtctwr_a - Length: 6 - Tick: 200 - damaged-idle-lights: gtctwr_a - Length: 6 - Tick: 200 - make: gtctwrmk - Length: 11 - ShadowStart: 11 - icon: twr1icon + icon: towricon Offset: 0, 0 UseTilesetCode: false +gavulc: + icon: twr1icon + garock: - Defaults: - Offset: 0, -12 - UseTilesetCode: true - idle: gtctwr - ShadowStart: 3 - damaged-idle: gtctwr - Start: 1 - ShadowStart: 4 - dead: gtctwr - Start: 2 - ShadowStart: 5 - Tick: 400 - turret: gtctwr_c - Facings: 32 - idle-lights: gtctwr_a - Length: 6 - Tick: 200 - damaged-idle-lights: gtctwr_a - Length: 6 - Tick: 200 - make: gtctwrmk - Length: 11 - ShadowStart: 11 icon: twr2icon - Offset: 0, 0 - UseTilesetCode: false gacsam: - Defaults: - Offset: 0, -12 - UseTilesetCode: true - idle: gtctwr - ShadowStart: 3 - damaged-idle: gtctwr - Start: 1 - ShadowStart: 4 - dead: gtctwr - Start: 2 - ShadowStart: 5 - Tick: 400 - turret: gtctwr_d - Facings: 32 - idle-lights: gtctwr_a - Length: 6 - Tick: 200 - damaged-idle-lights: gtctwr_a - Length: 6 - Tick: 200 - make: gtctwrmk - Length: 11 - ShadowStart: 11 icon: twr3icon - Offset: 0, 0 - UseTilesetCode: false gahpad: Defaults: From a89029acf031ce44f87ab41eb80b3554a787ddac Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 18:34:53 +0100 Subject: [PATCH 10/11] Implement GDI power turbines upgrade. --- mods/ts/rules/structures.yaml | 40 +++++++++++++++++++++++++++++++ mods/ts/sequences/structures.yaml | 19 +++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/mods/ts/rules/structures.yaml b/mods/ts/rules/structures.yaml index 62aca5e90f..a839f00463 100644 --- a/mods/ts/rules/structures.yaml +++ b/mods/ts/rules/structures.yaml @@ -86,6 +86,46 @@ GAPOWR: TargetTypes: Ground, C4, DetonateAttack, SpyInfiltrate ScalePowerWithHealth: DisabledOverlay: + Pluggable@pluga: + Offset: 0,1 + Upgrades: + powrup: powrup.a + Power@pluga: + UpgradeTypes: powrup.a + UpgradeMinEnabledLevel: 1 + Amount: 50 + WithIdleOverlay@pluga: + UpgradeTypes: powrup.a + UpgradeMinEnabledLevel: 1 + Sequence: idle-powrupa + Pluggable@plugb: + Offset: 1,1 + Upgrades: + powrup: powrup.b + WithIdleOverlay@plugb: + UpgradeTypes: powrup.b + UpgradeMinEnabledLevel: 1 + Sequence: idle-powrupb + Power@plugb: + UpgradeTypes: powrup.b + UpgradeMinEnabledLevel: 1 + Amount: 50 + +GAPOWRUP: + Inherits: ^BuildingPlug + Valued: + Cost: 150 + Tooltip: + Name: Power Turbine + Description: Provides extra power generation + Buildable: + Queue: Defense + BuildPaletteOrder: 10 + Prerequisites: gapowr, ~structures.gdi + Plug: + Type: powrup + Power: + Amount: 50 GAPILE: Inherits: ^Building diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index 033d8bb621..4b6236e2a9 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -67,6 +67,22 @@ gapowr: damaged-idle-plug: gtpowr_b Length: 12 Tick: 200 + idle-powrupa: gtpowr_b + Length: 12 + Tick: 200 + Offset: -48, -24 + damaged-idle-powrupa: gtpowr_b + Length: 12 + Tick: 200 + Offset: -48, -24 + idle-powrupb: gtpowr_b + Length: 12 + Tick: 200 + Offset: -24, -12 + damaged-idle-powrupb: gtpowr_b + Length: 12 + Tick: 200 + Offset: -24, -12 make: gtpowrmk Length: 20 ShadowStart: 20 @@ -74,6 +90,9 @@ gapowr: Offset: 0, 0 UseTilesetCode: false +gapowrup: + icon: turbicon + gapile: Defaults: Offset: 0, -24 From 704e3656b42ae4c3f5cbdef640a91ae989aa7c76 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 29 Mar 2015 19:33:44 +0100 Subject: [PATCH 11/11] Implement Ion Cannon Uplink upgrade. --- mods/ts/rules/structures.yaml | 44 ++++++++++++++++++++++++++++--- mods/ts/sequences/structures.yaml | 12 +++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/mods/ts/rules/structures.yaml b/mods/ts/rules/structures.yaml index a839f00463..bd4c842663 100644 --- a/mods/ts/rules/structures.yaml +++ b/mods/ts/rules/structures.yaml @@ -1325,16 +1325,16 @@ NAMISL: GAPLUG: Inherits: ^Building Valued: - Cost: 1800 + Cost: 1000 Tooltip: Name: GDI Upgrade Center - Description: Provides the Orbital Ion Cannon support power.\nRequires power to operate. + Description: Can be upgraded for additional technology Selectable: Bounds: 115,120,0,-20 Buildable: BuildPaletteOrder: 100 Prerequisites: proc, gatech - Queue: Defense + Queue: Building Building: Footprint: xxx xxx Dimensions: 2,3 @@ -1355,6 +1355,8 @@ GAPLUG: RevealsShroud: Range: 6c0 IonCannonPower: + UpgradeTypes: plug.ioncannon + UpgradeMinEnabledLevel: 1 Icon: ioncannon Effect: ionbeam ChargeTime: 180 @@ -1370,6 +1372,42 @@ GAPLUG: SupportPowerChargeBar: Power: Amount: -150 + Power@ioncannon: + UpgradeTypes: plug.ioncannon + UpgradeMinEnabledLevel: 1 + Amount: -100 + Pluggable@pluga: + Offset: 0,2 + Upgrades: + plug.ioncannon: plug.ioncannon, plug.ioncannona + WithIdleOverlay@ioncannona: + UpgradeTypes: plug.ioncannona + UpgradeMinEnabledLevel: 1 + Sequence: idle-ioncannona + Pluggable@plugb: + Offset: 1,2 + Upgrades: + plug.ioncannon: plug.ioncannon, plug.ioncannonb + WithIdleOverlay@ioncannonb: + UpgradeTypes: plug.ioncannonb + UpgradeMinEnabledLevel: 1 + Sequence: idle-ioncannonb + +GAPLUG3: + Inherits: ^BuildingPlug + Valued: + Cost: 1500 + Tooltip: + Name: Ion Cannon Uplink + Description: Enables use of the Ion Cannon + Buildable: + Queue: Defense + BuildPaletteOrder: 1000 + Prerequisites: gaplug, gatech, ~structures.gdi + Plug: + Type: plug.ioncannon + Power: + Amount: -100 ANYPOWER: Tooltip: diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index 4b6236e2a9..ce81145fc3 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -1015,9 +1015,21 @@ gaplug: Start: 8 Length: 8 Tick: 120 + idle-ioncannona: gaplug_f + Length: 15 + Tick: 120 + Reverses: true + Offset: -12, -42 + idle-ioncannonb: gaplug_f + Length: 15 + Reverses: true + Tick: 120 make: gtplugmk Length: 17 ShadowStart: 17 icon: plugicon Offset: 0, 0 UseTilesetCode: false + +gaplug3: + icon: rad3icon \ No newline at end of file