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