diff --git a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj index 6ba96ea6dc..b648538d9d 100644 --- a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj +++ b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj @@ -109,6 +109,7 @@ + diff --git a/OpenRA.Mods.Cnc/Traits/EnergyWall.cs b/OpenRA.Mods.Cnc/Traits/EnergyWall.cs new file mode 100644 index 0000000000..067c0fd88b --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/EnergyWall.cs @@ -0,0 +1,112 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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.GameRules; +using OpenRA.Mods.Common.Traits; +using OpenRA.Support; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits +{ + [Desc("Will open and be passable for actors that appear friendly when there are no enemies in range.")] + public class EnergyWallInfo : BuildingInfo, IObservesVariablesInfo, IRulesetLoaded + { + [FieldLoader.Require] + [WeaponReference] + [Desc("The weapon to attack units on top of the wall with when activated.")] + public readonly string Weapon = null; + + [ConsumedConditionReference] + [Desc("Boolean expression defining the condition to activate this trait.")] + public readonly BooleanExpression ActiveCondition = null; + + public override object Create(ActorInitializer init) { return new EnergyWall(init, this); } + + public WeaponInfo WeaponInfo { get; private set; } + + public void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + WeaponInfo weaponInfo; + + var weaponToLower = Weapon.ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out weaponInfo)) + throw new YamlException("Weapons Ruleset does not contain an entry '{0}'".F(weaponToLower)); + + WeaponInfo = weaponInfo; + } + } + + public class EnergyWall : Building, IObservesVariables, ITick, ITemporaryBlocker + { + readonly EnergyWallInfo info; + IEnumerable blockedPositions; + + // Initial state is active to match Building adding the influence to the ActorMap + // This will be updated by ConditionsChanged at actor creation. + bool active = true; + + public EnergyWall(ActorInitializer init, EnergyWallInfo info) + : base(init, info) + { + this.info = info; + } + + public virtual IEnumerable GetVariableObservers() + { + if (info.ActiveCondition != null) + yield return new VariableObserver(ActiveConditionChanged, info.ActiveCondition.Variables); + } + + void ActiveConditionChanged(Actor self, IReadOnlyDictionary conditions) + { + if (info.ActiveCondition == null) + return; + + var wasActive = active; + active = info.ActiveCondition.Evaluate(conditions); + + if (!wasActive && active) + self.World.ActorMap.AddInfluence(self, this); + else if (wasActive && !active) + self.World.ActorMap.RemoveInfluence(self, this); + } + + void ITick.Tick(Actor self) + { + if (!active) + return; + + foreach (var loc in blockedPositions) + { + var blockers = self.World.ActorMap.GetActorsAt(loc).Where(a => !a.IsDead && a != self); + foreach (var blocker in blockers) + info.WeaponInfo.Impact(Target.FromActor(blocker), self, Enumerable.Empty()); + } + } + + bool ITemporaryBlocker.IsBlocking(Actor self, CPos cell) + { + return active && blockedPositions.Contains(cell); + } + + bool ITemporaryBlocker.CanRemoveBlockage(Actor self, Actor blocking) + { + return !active; + } + + public override void AddedToWorld(Actor self) + { + base.AddedToWorld(self); + blockedPositions = FootprintUtils.Tiles(self); + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 61f03891a0..ce4c980fff 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -800,6 +800,8 @@ + + diff --git a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs index babeb42abf..a25706fd36 100644 --- a/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs +++ b/OpenRA.Mods.Common/Orders/PlaceBuildingOrderGenerator.cs @@ -22,6 +22,9 @@ namespace OpenRA.Mods.Common.Orders { public class PlaceBuildingOrderGenerator : IOrderGenerator { + [Flags] + enum CellType { Valid = 0, Invalid = 1, LineBuild = 2 } + readonly ProductionQueue queue; readonly string building; readonly BuildingInfo buildingInfo; @@ -62,6 +65,15 @@ namespace OpenRA.Mods.Common.Orders buildingInfluence = world.WorldActor.Trait(); } + CellType MakeCellType(bool valid, bool lineBuild = false) + { + var cell = valid ? CellType.Valid : CellType.Invalid; + if (lineBuild) + cell |= CellType.LineBuild; + + return cell; + } + public IEnumerable Order(World world, CPos cell, int2 worldPixel, MouseInput mi) { if (mi.Button == MouseButton.Right) @@ -160,7 +172,7 @@ namespace OpenRA.Mods.Common.Orders foreach (var r in dec.Render(wr, world, actorInfo, offset)) yield return r; - var cells = new Dictionary(); + var cells = new Dictionary(); var plugInfo = rules.Actors[building].TraitInfoOrDefault(); if (plugInfo != null) @@ -168,7 +180,7 @@ namespace OpenRA.Mods.Common.Orders if (buildingInfo.Dimensions.X != 1 || buildingInfo.Dimensions.Y != 1) throw new InvalidOperationException("Plug requires a 1x1 sized Building"); - cells.Add(topLeft, AcceptsPlug(topLeft, plugInfo)); + cells.Add(topLeft, MakeCellType(AcceptsPlug(topLeft, plugInfo))); } else if (rules.Actors[building].HasTraitInfo()) { @@ -178,9 +190,9 @@ namespace OpenRA.Mods.Common.Orders if (!Game.GetModifierKeys().HasModifier(Modifiers.Shift)) foreach (var t in BuildingUtils.GetLineBuildCells(world, topLeft, building, buildingInfo)) - cells.Add(t, buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, building, t)); - else - cells.Add(topLeft, buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, building, topLeft)); + cells.Add(t.First, MakeCellType(buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, building, t.First), true)); + + cells[topLeft] = MakeCellType(buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, building, topLeft)); } else { @@ -211,14 +223,16 @@ namespace OpenRA.Mods.Common.Orders var res = world.WorldActor.Trait(); var isCloseEnough = buildingInfo.IsCloseEnoughToBase(world, world.LocalPlayer, building, topLeft); foreach (var t in FootprintUtils.Tiles(rules, building, buildingInfo, topLeft)) - cells.Add(t, isCloseEnough && world.IsCellBuildable(t, buildingInfo) && res.GetResource(t) == null); + cells.Add(t, MakeCellType(isCloseEnough && world.IsCellBuildable(t, buildingInfo) && res.GetResource(t) == null)); } - var pal = wr.Palette(placeBuildingInfo.Palette); + var cellPalette = wr.Palette(placeBuildingInfo.Palette); + var linePalette = wr.Palette(placeBuildingInfo.LineBuildSegmentPalette); var topLeftPos = world.Map.CenterOfCell(topLeft); foreach (var c in cells) { - var tile = c.Value ? buildOk : buildBlocked; + var tile = !c.Value.HasFlag(CellType.Invalid) ? buildOk : buildBlocked; + var pal = c.Value.HasFlag(CellType.LineBuild) ? linePalette : cellPalette; var pos = world.Map.CenterOfCell(c.Key); yield return new SpriteRenderable(tile, pos, new WVec(0, 0, topLeftPos.Z - pos.Z), -511, pal, 1f, true); diff --git a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs index 64807449b1..8bae043e68 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; +using OpenRA.Primitives; namespace OpenRA.Mods.Common.Traits { @@ -48,18 +49,20 @@ namespace OpenRA.Mods.Common.Traits world.IsCellBuildable(t, building, toIgnore)); } - public static IEnumerable GetLineBuildCells(World world, CPos location, string name, BuildingInfo bi) + public static IEnumerable> GetLineBuildCells(World world, CPos location, string name, BuildingInfo bi) { var lbi = world.Map.Rules.Actors[name].TraitInfo(); var topLeft = location; // 1x1 assumption! if (world.IsCellBuildable(topLeft, bi)) - yield return topLeft; + yield return Pair.New(topLeft, null); // Start at place location, search outwards // TODO: First make it work, then make it nice var vecs = new[] { new CVec(1, 0), new CVec(0, 1), new CVec(-1, 0), new CVec(0, -1) }; int[] dirs = { 0, 0, 0, 0 }; + Actor[] connectors = { null, null, null, null }; + for (var d = 0; d < 4; d++) { for (var i = 1; i < lbi.Range; i++) @@ -72,17 +75,17 @@ namespace OpenRA.Mods.Common.Traits continue; // Cell is empty; continue search // Cell contains an actor. Is it the type we want? - var hasConnector = world.ActorMap.GetActorsAt(cell) - .Any(a => a.Info.TraitInfos() + connectors[d] = world.ActorMap.GetActorsAt(cell) + .FirstOrDefault(a => a.Info.TraitInfos() .Any(info => info.Types.Overlaps(lbi.NodeTypes) && info.Connections.Contains(vecs[d]))); - dirs[d] = hasConnector ? i : -1; + dirs[d] = connectors[d] != null ? i : -1; } // Place intermediate-line sections if (dirs[d] > 0) for (var i = 1; i < dirs[d]; i++) - yield return topLeft + i * vecs[d]; + yield return Pair.New(topLeft + i * vecs[d], connectors[d]); } } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/LineBuild.cs b/OpenRA.Mods.Common/Traits/Buildings/LineBuild.cs index 255f0a312b..f31642e77c 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/LineBuild.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/LineBuild.cs @@ -10,19 +10,116 @@ #endregion using System.Collections.Generic; +using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { + public enum LineBuildDirection { Unset, X, Y } + public class LineBuildDirectionInit : IActorInit + { + [FieldFromYamlKey] readonly LineBuildDirection value = LineBuildDirection.Unset; + public LineBuildDirectionInit() { } + public LineBuildDirectionInit(LineBuildDirection init) { value = init; } + public LineBuildDirection Value(World world) { return value; } + } + + public class LineBuildParentInit : IActorInit + { + [FieldFromYamlKey] public readonly string[] ParentNames = new string[0]; + readonly Actor[] parents = null; + + public LineBuildParentInit() { } + public LineBuildParentInit(Actor[] init) { parents = init; } + public Actor[] Value(World world) + { + if (parents != null) + return parents; + + var sma = world.WorldActor.Trait(); + return ParentNames.Select(n => sma.Actors[n]).ToArray(); + } + } + + public interface INotifyLineBuildSegmentsChanged + { + void SegmentAdded(Actor self, Actor segment); + void SegmentRemoved(Actor self, Actor segment); + } + [Desc("Place the second actor in line to build more of the same at once (used for walls).")] - public class LineBuildInfo : TraitInfo + public class LineBuildInfo : ITraitInfo { [Desc("The maximum allowed length of the line.")] public readonly int Range = 5; [Desc("LineBuildNode 'Types' to attach to.")] public readonly HashSet NodeTypes = new HashSet { "wall" }; + + [ActorReference(typeof(LineBuildInfo))] + [Desc("Actor type for line-built segments (defaults to same actor).")] + public readonly string SegmentType = null; + + [Desc("Delete generated segments when destroyed or sold.")] + public readonly bool SegmentsRequireNode = false; + + public object Create(ActorInitializer init) { return new LineBuild(init, this); } } - public class LineBuild { } + public class LineBuild : INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyLineBuildSegmentsChanged + { + readonly LineBuildInfo info; + readonly Actor[] parentNodes = new Actor[0]; + HashSet segments; + + public LineBuild(ActorInitializer init, LineBuildInfo info) + { + this.info = info; + if (init.Contains()) + parentNodes = init.Get().Value(init.World); + } + + void INotifyLineBuildSegmentsChanged.SegmentAdded(Actor self, Actor segment) + { + if (segments == null) + segments = new HashSet(); + + segments.Add(segment); + } + + void INotifyLineBuildSegmentsChanged.SegmentRemoved(Actor self, Actor segment) + { + if (segments == null) + return; + + segments.Remove(segment); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + foreach (var parent in parentNodes) + if (!parent.Disposed) + foreach (var n in parent.TraitsImplementing()) + n.SegmentAdded(parent, self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + foreach (var parent in parentNodes) + if (!parent.Disposed) + foreach (var n in parent.TraitsImplementing()) + n.SegmentRemoved(parent, self); + + if (info.SegmentsRequireNode && segments != null) + foreach (var s in segments) + s.Dispose(); + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (info.SegmentsRequireNode && segments != null) + foreach (var s in segments) + s.Kill(e.Attacker); + } + } } diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDisabled.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDisabled.cs index 0b5c16226d..aa778ba772 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDisabled.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnDisabled.cs @@ -15,7 +15,7 @@ namespace OpenRA.Mods.Common.Traits { [Desc("Applies a condition to the actor when it is disabled.", "This is a temporary shim to help migration away from the legacy IDisable code")] - public class GrantConditionOnDisabledInfo : ITraitInfo, Requires + public class GrantConditionOnDisabledInfo : ITraitInfo { [FieldLoader.Require] [GrantedConditionReference] diff --git a/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLineBuildDirection.cs b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLineBuildDirection.cs new file mode 100644 index 0000000000..e8ee3eaf8a --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/GrantConditionOnLineBuildDirection.cs @@ -0,0 +1,51 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public class GrantConditionOnLineBuildDirectionInfo : ITraitInfo, Requires + { + [FieldLoader.Require] + [GrantedConditionReference] + [Desc("Condition to grant.")] + public readonly string Condition = null; + + [FieldLoader.Require] + [Desc("Line build direction to trigger the condition.")] + public readonly LineBuildDirection Direction = LineBuildDirection.X; + + public object Create(ActorInitializer init) { return new GrantConditionOnLineBuildDirection(init, this); } + } + + public class GrantConditionOnLineBuildDirection : INotifyCreated + { + readonly GrantConditionOnLineBuildDirectionInfo info; + readonly LineBuildDirection direction; + + public GrantConditionOnLineBuildDirection(ActorInitializer init, GrantConditionOnLineBuildDirectionInfo info) + { + this.info = info; + direction = init.Get().Value(init.World); + } + + void INotifyCreated.Created(Actor self) + { + if (direction != info.Direction) + return; + + var conditionManager = self.TraitOrDefault(); + if (conditionManager != null && !string.IsNullOrEmpty(info.Condition)) + conditionManager.GrantCondition(self, info.Condition); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Conditions/LineBuildSegmentExternalCondition.cs b/OpenRA.Mods.Common/Traits/Conditions/LineBuildSegmentExternalCondition.cs new file mode 100644 index 0000000000..c78414ef0d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Conditions/LineBuildSegmentExternalCondition.cs @@ -0,0 +1,88 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Applies a condition to connected line build segments.")] + public class LineBuildSegmentExternalConditionInfo : ConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")] + public readonly string Condition = null; + + public override object Create(ActorInitializer init) { return new LineBuildSegmentExternalCondition(init.Self, this); } + } + + public class LineBuildSegmentExternalCondition : ConditionalTrait, INotifyLineBuildSegmentsChanged + { + readonly HashSet segments = new HashSet(); + readonly Dictionary tokens = new Dictionary(); + + public LineBuildSegmentExternalCondition(Actor self, LineBuildSegmentExternalConditionInfo info) + : base(info) { } + + void GrantCondition(Actor self, Actor segment) + { + if (tokens.ContainsKey(segment)) + return; + + var external = segment.TraitsImplementing() + .FirstOrDefault(t => t.Info.Condition == Info.Condition && t.CanGrantCondition(segment, self)); + + if (external != null) + tokens[segment] = external.GrantCondition(segment, self); + } + + void RevokeCondition(Actor self, Actor segment) + { + int token; + if (!tokens.TryGetValue(segment, out token)) + return; + + tokens.Remove(segment); + if (segment.Disposed) + return; + + foreach (var external in segment.TraitsImplementing()) + external.TryRevokeCondition(segment, self, token); + } + + void INotifyLineBuildSegmentsChanged.SegmentAdded(Actor self, Actor segment) + { + segments.Add(segment); + if (!IsTraitDisabled) + GrantCondition(self, segment); + } + + void INotifyLineBuildSegmentsChanged.SegmentRemoved(Actor self, Actor segment) + { + if (!IsTraitDisabled) + RevokeCondition(self, segment); + segments.Remove(segment); + } + + protected override void TraitEnabled(Actor self) + { + foreach (var s in segments) + GrantCondition(self, s); + } + + protected override void TraitDisabled(Actor self) + { + foreach (var s in segments) + RevokeCondition(self, s); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs index a05b643d6a..8f7eac28c3 100644 --- a/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs +++ b/OpenRA.Mods.Common/Traits/Player/PlaceBuilding.cs @@ -22,6 +22,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Palette to use for rendering the placement sprite.")] [PaletteReference] public readonly string Palette = TileSet.TerrainPaletteInternalName; + [Desc("Palette to use for rendering the placement sprite for line build segments.")] + [PaletteReference] public readonly string LineBuildSegmentPalette = TileSet.TerrainPaletteInternalName; + [Desc("Play NewOptionsNotification this many ticks after building placement.")] public readonly int NewOptionsNotificationDelay = 10; @@ -74,21 +77,35 @@ namespace OpenRA.Mods.Common.Traits if (os == "LineBuild") { - var playSounds = true; + // Build the parent actor first + var placed = w.CreateActor(order.TargetString, new TypeDictionary + { + new LocationInit(order.TargetLocation), + new OwnerInit(order.Player), + new FactionInit(faction), + }); + + foreach (var s in buildingInfo.BuildSounds) + Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, placed.CenterPosition); + + // Build the connection segments + var segmentType = unit.TraitInfo().SegmentType; + if (string.IsNullOrEmpty(segmentType)) + segmentType = order.TargetString; + foreach (var t in BuildingUtils.GetLineBuildCells(w, order.TargetLocation, order.TargetString, buildingInfo)) { - var building = w.CreateActor(order.TargetString, new TypeDictionary + if (t.First == order.TargetLocation) + continue; + + w.CreateActor(t.First == order.TargetLocation ? order.TargetString : segmentType, new TypeDictionary { - new LocationInit(t), + new LocationInit(t.First), new OwnerInit(order.Player), - new FactionInit(faction) + new FactionInit(faction), + new LineBuildDirectionInit(t.First.X == order.TargetLocation.X ? LineBuildDirection.Y : LineBuildDirection.X), + new LineBuildParentInit(new[] { t.Second, placed }) }); - - if (playSounds) - foreach (var s in buildingInfo.BuildSounds) - Game.Sound.PlayToPlayer(SoundType.World, order.Player, s, building.CenterPosition); - - playSounds = false; } } else if (os == "PlacePlug") diff --git a/OpenRA.Mods.Common/Traits/Render/WithMakeAnimation.cs b/OpenRA.Mods.Common/Traits/Render/WithMakeAnimation.cs index 0382db908c..be03da03e6 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithMakeAnimation.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithMakeAnimation.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using OpenRA.Activities; using OpenRA.Traits; @@ -20,36 +21,79 @@ namespace OpenRA.Mods.Common.Traits.Render [Desc("Sequence name to use")] [SequenceReference] public readonly string Sequence = "make"; + [GrantedConditionReference] + [Desc("The condition to grant to self while the make animation is playing.")] + public readonly string Condition = null; + public object Create(ActorInitializer init) { return new WithMakeAnimation(init, this); } } - public class WithMakeAnimation + public class WithMakeAnimation : INotifyCreated { readonly WithMakeAnimationInfo info; readonly WithSpriteBody wsb; + ConditionManager conditionManager; + int token = ConditionManager.InvalidConditionToken; + public WithMakeAnimation(ActorInitializer init, WithMakeAnimationInfo info) { this.info = info; var self = init.Self; wsb = self.Trait(); + } + void INotifyCreated.Created(Actor self) + { + conditionManager = self.TraitOrDefault(); var building = self.TraitOrDefault(); if (building != null && !building.SkipMakeAnimation) + Forward(self, () => building.NotifyBuildingComplete(self)); + } + + void Forward(Actor self, Action onComplete) + { + if (conditionManager != null && !string.IsNullOrEmpty(info.Condition) && token == ConditionManager.InvalidConditionToken) + token = conditionManager.GrantCondition(self, info.Condition); + + wsb.PlayCustomAnimation(self, info.Sequence, () => { - wsb.PlayCustomAnimation(self, info.Sequence, () => - { - building.NotifyBuildingComplete(self); - }); - } + if (token != ConditionManager.InvalidConditionToken) + token = conditionManager.RevokeCondition(self, token); + + // TODO: Rewrite this to use a trait notification for save game support + onComplete(); + }); + } + + void Reverse(Actor self, Action onComplete) + { + if (conditionManager != null && !string.IsNullOrEmpty(info.Condition) && token == ConditionManager.InvalidConditionToken) + token = conditionManager.GrantCondition(self, info.Condition); + + wsb.PlayCustomAnimationBackwards(self, info.Sequence, () => + { + if (token != ConditionManager.InvalidConditionToken) + token = conditionManager.RevokeCondition(self, token); + + // TODO: Rewrite this to use a trait notification for save game support + onComplete(); + }); } public void Reverse(Actor self, Activity activity, bool queued = true) { - wsb.PlayCustomAnimationBackwards(self, info.Sequence, () => + Reverse(self, () => { - // avoids visual glitches as we wait for the actor to get destroyed + // HACK: The actor remains alive and active for one tick before the followup activity + // (sell/transform/etc) runs. This causes visual glitches that we attempt to minimize + // by forcing the animation to frame 0 and regranting the make condition. + // These workarounds will break the actor if the followup activity doesn't dispose it! wsb.DefaultAnimation.PlayFetchIndex(info.Sequence, () => 0); + + if (conditionManager != null && !string.IsNullOrEmpty(info.Condition)) + token = conditionManager.GrantCondition(self, info.Condition); + self.QueueActivity(queued, activity); }); } diff --git a/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs b/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs index 179522dfc7..eb17f98b95 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs @@ -159,6 +159,8 @@ namespace OpenRA.Mods.Common.Traits.Render { UpdateNeighbours(self); } + + protected override void TraitEnabled(Actor self) { dirty = true; } } public class RuntimeNeighbourInit : IActorInit>, ISuppressInitExport diff --git a/mods/ts/rules/nod-structures.yaml b/mods/ts/rules/nod-structures.yaml index c7664a5d2e..3062ce436a 100644 --- a/mods/ts/rules/nod-structures.yaml +++ b/mods/ts/rules/nod-structures.yaml @@ -75,6 +75,7 @@ NAAPWR: PowerTooltip: SelectionDecorations: VisualBounds: 100, 74, 0, -12 + ProvidesPrerequisite@buildingname: NAHAND: Inherits: ^Building diff --git a/mods/ts/rules/nod-support.yaml b/mods/ts/rules/nod-support.yaml index 2ded7c09ab..c7d6536aec 100644 --- a/mods/ts/rules/nod-support.yaml +++ b/mods/ts/rules/nod-support.yaml @@ -41,6 +41,111 @@ NAGATE_B: Tooltip: Name: Nod Gate +NAPOST: + Inherits: ^Building + Buildable: + Queue: Defense + BuildPaletteOrder: 150 + Prerequisites: naapwr, ~structures.nod + Description: Stops infantry and blocks enemy fire.\nCan NOT be crushed by tanks. + Valued: + Cost: 200 + Tooltip: + Name: Laser Fence + Health: + HP: 300 + Armor: + Type: Concrete + LineBuild: + Range: 10 + NodeTypes: laserfencenode + SegmentType: nafnce + SegmentsRequireNode: true + WithMakeAnimation: + Condition: make-animation-playing + Selectable: + Bounds: 42, 44, 0, -12 + LineBuildNode: + Types: laserfencenode + Power: + Amount: -25 + RevealsShroud: + Range: 4c0 + WithIdleOverlay@LIGHTS: + RequiresCondition: !disabled + Sequence: lights + WithIdleOverlay@CHAINOFLIGHTS: + RequiresCondition: !disabled + Sequence: chainoflights + CanPowerDown: + IndicatorPalette: mouse + PowerupSpeech: EnablePower + PowerdownSpeech: DisablePower + RequiresPower: + GrantConditionOnDisabled: + Condition: disabled + LineBuildSegmentExternalCondition: + RequiresCondition: !disabled && !make-animation-playing + Condition: active-posts + +NAFNCE: + Inherits: ^Wall + Valued: + Cost: 50 + CustomSellValue: + Value: 0 + Tooltip: + Name: Laser Fence + LineBuild: + NodeTypes: laserfence + LineBuildNode: + Types: laserfence + -Crushable: + -Sellable: + -Targetable: + -Building: + EnergyWall: + ActiveCondition: active-posts == 2 + Weapon: LaserFence + GrantConditionOnLineBuildDirection@X: + Direction: X + Condition: laserfence-direction-x + GrantConditionOnLineBuildDirection@Y: + Direction: Y + Condition: laserfence-direction-y + -WithWallSpriteBody: + CustomSelectionSize: + CustomBounds: 48, 24 + ExternalCondition@ACTIVE: + Condition: active-posts + WithWallSpriteBody@XENABLED: + RequiresCondition: laserfence-direction-x && active-posts == 2 + Type: laserfence + Sequence: enabled-x + WithWallSpriteBody@YENABLED: + RequiresCondition: laserfence-direction-y && active-posts == 2 + Type: laserfence + Sequence: enabled-y + WithSpriteBody@XDISABLED: + RequiresCondition: laserfence-direction-x && active-posts < 2 + Sequence: disabled-x + WithSpriteBody@YDISABLED: + RequiresCondition: laserfence-direction-y && active-posts < 2 + Sequence: disabled-y + BlocksProjectiles: + RequiresCondition: active-posts == 2 + Height: 640 + DamageMultiplier: # Prevent all normal damage, but still allows direct kills from the post + Modifier: 0 + Explodes: + Weapon: BuildingExplosions + EmptyWeapon: BuildingExplosions + Type: Footprint + ThrowsShrapnel@SMALL: + Weapons: SmallDebris + Pieces: 0, 1 + Range: 2c0, 5c0 + NALASR: Inherits: ^Defense Valued: @@ -89,7 +194,7 @@ NAOBEL: Name: Obelisk of Light Buildable: Queue: Defense - BuildPaletteOrder: 150 + BuildPaletteOrder: 160 Prerequisites: natech, ~structures.nod, ~techlevel.high Description: Advanced base defense.\nRequires power to operate.\n Strong vs Ground units\n Weak vs Aircraft Building: @@ -170,7 +275,7 @@ NASTLH: Name: Stealth Generator Buildable: Queue: Defense - BuildPaletteOrder: 160 + BuildPaletteOrder: 170 Prerequisites: proc, natech, ~structures.nod, ~techlevel.high Description: Generates a cloaking field\nto hide your forces from the enemy. Building: @@ -211,7 +316,7 @@ NAMISL: Inherits: ^Building Buildable: Queue: Defense - BuildPaletteOrder: 190 + BuildPaletteOrder: 180 Prerequisites: natech, ~structures.nod, ~techlevel.superweapons BuildLimit: 1 Description: Launches a devastating missile\nat a target location.\nRequires power to operate.\nMaximum 1 can be built. @@ -272,7 +377,7 @@ NAWAST: Name: Waste Refinery Buildable: Queue: Defense - BuildPaletteOrder: 200 + BuildPaletteOrder: 190 Prerequisites: namisl, ~structures.nod, ~techlevel.superweapons BuildLimit: 1 Description: Processes Veins\ninto useable resources.\nMaximum 1 can be built. diff --git a/mods/ts/rules/palettes.yaml b/mods/ts/rules/palettes.yaml index 555467c58f..5c96fd2e1f 100644 --- a/mods/ts/rules/palettes.yaml +++ b/mods/ts/rules/palettes.yaml @@ -113,6 +113,10 @@ Name: placebuilding BasePalette: ra Alpha: 0.75 + PaletteFromPaletteWithAlpha@placelinesegment: + Name: placelinesegment + BasePalette: ra + Alpha: 0.4 TSShroudPalette@shroud: Name: shroud Type: Shroud diff --git a/mods/ts/rules/player.yaml b/mods/ts/rules/player.yaml index e226adf553..75d30071d9 100644 --- a/mods/ts/rules/player.yaml +++ b/mods/ts/rules/player.yaml @@ -33,6 +33,7 @@ Player: SpeedUp: True PlaceBuilding: Palette: placebuilding + LineBuildSegmentPalette: placelinesegment SupportPowerManager: ScriptTriggers: MissionObjectives: diff --git a/mods/ts/rules/world.yaml b/mods/ts/rules/world.yaml index e1bc34cc61..131bcac616 100644 --- a/mods/ts/rules/world.yaml +++ b/mods/ts/rules/world.yaml @@ -189,4 +189,5 @@ EditorWorld: EditorResourceLayer: EditorSelectionLayer: Palette: placebuilding + LineBuildSegmentPalette: placelinesegment LoadWidgetAtGameStart: diff --git a/mods/ts/sequences/misc.yaml b/mods/ts/sequences/misc.yaml index 7f9e95257c..555bdaca70 100644 --- a/mods/ts/sequences/misc.yaml +++ b/mods/ts/sequences/misc.yaml @@ -21,6 +21,7 @@ poweroff: Length: * Tick: 160 ZOffset: 2047 + Offset: 0, -12 allyrepair: repair: wrench diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index e4fe52fd68..b96c8a2d0f 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -859,6 +859,90 @@ nagate_b: UseTilesetCode: false -DepthSprite: +napost: + Defaults: + Offset: 0, -12, 13 + UseTilesetCode: true + DepthSprite: isodepth.shp + idle: + Start: 0 + ShadowStart: 3 + damaged-idle: + Start: 1 + ShadowStart: 4 + lights: napost_b + Start: 0 + Length: 7 + ShadowStart: 14 + Tick: 80 + damaged-lights: napost_b + Start: 7 + Length: 7 + ShadowStart: 21 + Tick: 80 + chainoflights: napost_a + Start: 0 + Length: 12 + ShadowStart: 24 + Tick: 80 + damaged-chainoflights: napost_a + Start: 12 + Length: 12 + ShadowStart: 36 + Tick: 80 + dead: + Start: 2 + Tick: 400 + ShadowStart: 5 + make: napostmk + Start: 0 + Length: 12 + ShadowStart: 12 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0, 25 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + -DepthSprite: + icon: lasricon + Offset: 0, 0 + UseTilesetCode: false + -DepthSprite: + +nafnce: + Defaults: + Offset: 0, -12, 13 + UseTilesetCode: true + DepthSprite: isodepth.shp + idle: + Length: 1 + disabled-x: + Start: 8 + Length: 1 + Offset: 0, -12, 1 + ZRamp: 1 + -DepthSprite: + disabled-y: + Start: 12 + Length: 1 + Offset: 0, -12, 1 + ZRamp: 1 + -DepthSprite: + enabled-x: + Frames: 3,3,1,1,3,3,1,1,2,2,0,0,2,2,0,0 + Length: 16 + enabled-y: + Frames: 7,5,7,5,6,4,6,4,7,5,7,5,6,4,6,4 + Length: 16 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0, 25 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + -DepthSprite: + nawall: Defaults: Offset: 0, -12, 12 diff --git a/mods/ts/weapons/energyweapons.yaml b/mods/ts/weapons/energyweapons.yaml index aae4a7e719..5f61791298 100644 --- a/mods/ts/weapons/energyweapons.yaml +++ b/mods/ts/weapons/energyweapons.yaml @@ -198,3 +198,10 @@ TurretLaserFire: SecondaryBeamColor: FF000030 Warhead@1Dam: SpreadDamage Damage: 30 + +LaserFence: + Projectile: InstantHit + Warhead@1Dam: TargetDamage + DebugOverlayColor: FF0000 + Damage: 99999 + DamageTypes: FireDeath