diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs index 7bd075e9f8..1258cdf91d 100644 --- a/OpenRA.Game/FieldLoader.cs +++ b/OpenRA.Game/FieldLoader.cs @@ -373,6 +373,28 @@ namespace OpenRA return InvalidValueAction(value, fieldType, fieldName); } + else if (fieldType == typeof(CVec[])) + { + if (value != null) + { + var parts = value.Split(','); + + if (parts.Length % 2 != 0) + return InvalidValueAction(value, fieldType, fieldName); + + var vecs = new CVec[parts.Length / 2]; + for (var i = 0; i < vecs.Length; i++) + { + int rx, ry; + if (int.TryParse(parts[2 * i], out rx) && int.TryParse(parts[2 * i + 1], out ry)) + vecs[i] = new CVec(rx, ry); + } + + return vecs; + } + + return InvalidValueAction(value, fieldType, fieldName); + } else if (fieldType.IsEnum) { try diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 04e860e609..e346d4191a 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -284,6 +284,13 @@ namespace OpenRA.Traits bool CanEnterTargetNow(Actor self, Target target); } + [RequireExplicitImplementation] + public interface ITemporaryBlocker + { + bool CanRemoveBlockage(Actor self, Actor blocking); + bool IsBlocking(Actor self, CPos cell); + } + public interface INotifyBlockingMove { void OnNotifyBlockingMove(Actor self, Actor blocking); } public interface IFacing diff --git a/OpenRA.Game/WorldUtils.cs b/OpenRA.Game/WorldUtils.cs index 90d4c6480d..44166e19f9 100644 --- a/OpenRA.Game/WorldUtils.cs +++ b/OpenRA.Game/WorldUtils.cs @@ -37,6 +37,23 @@ namespace OpenRA a => (a.CenterPosition - origin).HorizontalLengthSquared <= r.LengthSquared); } + public static bool ContainsTemporaryBlocker(this World world, CPos cell, Actor ignoreActor = null) + { + var temporaryBlockers = world.ActorMap.GetActorsAt(cell); + foreach (var temporaryBlocker in temporaryBlockers) + { + if (temporaryBlocker == ignoreActor) + continue; + + var temporaryBlockerTraits = temporaryBlocker.TraitsImplementing(); + foreach (var temporaryBlockerTrait in temporaryBlockerTraits) + if (temporaryBlockerTrait.IsBlocking(temporaryBlocker, cell)) + return true; + } + + return false; + } + public static void DoTimed(this IEnumerable e, Action a, string text) { // PERF: This is a hot path and must run with minimal added overhead. diff --git a/OpenRA.Mods.Common/Activities/Move/Move.cs b/OpenRA.Mods.Common/Activities/Move/Move.cs index 6e1fc960d7..e6395229c6 100644 --- a/OpenRA.Mods.Common/Activities/Move/Move.cs +++ b/OpenRA.Mods.Common/Activities/Move/Move.cs @@ -220,12 +220,14 @@ namespace OpenRA.Mods.Common.Activities var nextCell = path[path.Count - 1]; + var containsTemporaryBlocker = WorldUtils.ContainsTemporaryBlocker(self.World, nextCell, self); + // Next cell in the move is blocked by another actor - if (!mobile.CanMoveFreelyInto(nextCell, ignoredActor, true)) + if (containsTemporaryBlocker || !mobile.CanMoveFreelyInto(nextCell, ignoredActor, true)) { // Are we close enough? var cellRange = nearEnough.Length / 1024; - if ((mobile.ToCell - destination.Value).LengthSquared <= cellRange * cellRange) + if (!containsTemporaryBlocker && (mobile.ToCell - destination.Value).LengthSquared <= cellRange * cellRange) { path.Clear(); return null; diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 50bd7c0bb0..b0c42ec9f9 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -289,6 +289,7 @@ + @@ -731,6 +732,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs b/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs index 6ca63a390a..7238869fe8 100644 --- a/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs +++ b/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs @@ -9,9 +9,16 @@ #endregion using System.Linq; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { + [RequireExplicitImplementation] + public interface IBlocksProjectiles + { + WDist BlockingHeight { get; } + } + [Desc("This actor blocks bullets and missiles with 'Blockable' property.")] public class BlocksProjectilesInfo : UpgradableTraitInfo { @@ -20,17 +27,20 @@ namespace OpenRA.Mods.Common.Traits public override object Create(ActorInitializer init) { return new BlocksProjectiles(init.Self, this); } } - public class BlocksProjectiles : UpgradableTrait + public class BlocksProjectiles : UpgradableTrait, IBlocksProjectiles { public BlocksProjectiles(Actor self, BlocksProjectilesInfo info) : base(info) { } + WDist IBlocksProjectiles.BlockingHeight { get { return Info.Height; } } + public static bool AnyBlockingActorAt(World world, WPos pos) { var dat = world.Map.DistanceAboveTerrain(pos); + return world.ActorMap.GetActorsAt(world.Map.CellContaining(pos)) - .Any(a => a.TraitsImplementing() - .Where(t => t.Info.Height.Length >= dat.Length) + .Any(a => a.TraitsImplementing() + .Where(t => t.BlockingHeight > dat) .Any(Exts.IsTraitEnabled)); } @@ -41,7 +51,7 @@ namespace OpenRA.Mods.Common.Traits foreach (var a in actors) { - var blockers = a.TraitsImplementing() + var blockers = a.TraitsImplementing() .Where(Exts.IsTraitEnabled).ToList(); if (!blockers.Any()) @@ -49,7 +59,7 @@ namespace OpenRA.Mods.Common.Traits var hitPos = WorldExtensions.MinimumPointLineProjection(start, end, a.CenterPosition); var dat = world.Map.DistanceAboveTerrain(hitPos); - if ((hitPos - start).Length < length && blockers.Any(t => t.Info.Height.Length >= dat.Length)) + if ((hitPos - start).Length < length && blockers.Any(t => t.BlockingHeight > dat)) { hit = hitPos; return true; diff --git a/OpenRA.Mods.Common/Traits/Buildings/Building.cs b/OpenRA.Mods.Common/Traits/Buildings/Building.cs index 00eb6e3449..b9a6362558 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Building.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Building.cs @@ -36,7 +36,7 @@ namespace OpenRA.Mods.Common.Traits public readonly string[] BuildSounds = { "placbldg.aud", "build5.aud" }; public readonly string[] UndeploySounds = { "cashturn.aud" }; - public object Create(ActorInitializer init) { return new Building(init, this); } + public virtual object Create(ActorInitializer init) { return new Building(init, this); } public Actor FindBaseProvider(World world, Player p, CPos topLeft) { @@ -179,7 +179,7 @@ namespace OpenRA.Mods.Common.Traits NotifyBuildingComplete(self); } - public void AddedToWorld(Actor self) + public virtual void AddedToWorld(Actor self) { self.World.ActorMap.AddInfluence(self, this); self.World.ActorMap.AddPosition(self, this); diff --git a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs index ca10c021dc..7adc2f91b5 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/BuildingUtils.cs @@ -71,12 +71,11 @@ namespace OpenRA.Mods.Common.Traits continue; // Cell is empty; continue search // Cell contains an actor. Is it the type we want? - if (world.ActorsHavingTrait() - .Any(a => a.Location == cell - && a.Info.TraitInfo().Types.Overlaps(lbi.NodeTypes))) - dirs[d] = i; // Cell contains actor of correct type - else - dirs[d] = -1; // Cell is blocked by another actor type + var hasConnector = world.ActorMap.GetActorsAt(cell) + .Any(a => a.Info.TraitInfos() + .Any(info => info.Types.Overlaps(lbi.NodeTypes) && info.Connections.Contains(vecs[d]))); + + dirs[d] = hasConnector ? i : -1; } // Place intermediate-line sections diff --git a/OpenRA.Mods.Common/Traits/Buildings/Gate.cs b/OpenRA.Mods.Common/Traits/Buildings/Gate.cs new file mode 100644 index 0000000000..b77471592d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Buildings/Gate.cs @@ -0,0 +1,135 @@ +#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 +{ + [Desc("Will open and be passable for actors that appear friendly when there are no enemies in range.")] + public class GateInfo : BuildingInfo + { + public readonly string OpeningSound = null; + public readonly string ClosingSound = null; + + [Desc("Ticks until the gate closes.")] + public readonly int CloseDelay = 150; + + [Desc("Ticks until the gate is considered open.")] + public readonly int TransitionDelay = 33; + + [Desc("Blocks bullets scaled to open value.")] + public readonly int BlocksProjectilesHeight = 640; + + public override object Create(ActorInitializer init) { return new Gate(init, this); } + } + + public class Gate : Building, ITick, ITemporaryBlocker, IBlocksProjectiles, INotifyBlockingMove, ISync + { + readonly GateInfo info; + readonly Actor self; + IEnumerable blockedPositions; + + public readonly int OpenPosition; + [Sync] public int Position { get; private set; } + int desiredPosition; + int remainingOpenTime; + + public Gate(ActorInitializer init, GateInfo info) + : base(init, info) + { + this.info = info; + self = init.Self; + OpenPosition = info.TransitionDelay; + } + + void ITick.Tick(Actor self) + { + if (self.IsDisabled() || Locked || !BuildComplete) + return; + + if (desiredPosition < Position) + { + // Gate was fully open + if (Position == OpenPosition) + { + Game.Sound.Play(info.ClosingSound, self.CenterPosition); + self.World.ActorMap.AddInfluence(self, this); + } + + Position--; + } + else if (desiredPosition > Position) + { + // Gate was fully closed + if (Position == 0) + Game.Sound.Play(info.OpeningSound, self.CenterPosition); + + Position++; + + // Gate is now fully open + if (Position == OpenPosition) + { + self.World.ActorMap.RemoveInfluence(self, this); + remainingOpenTime = info.CloseDelay; + } + } + + if (Position == OpenPosition) + { + if (IsBlocked()) + remainingOpenTime = info.CloseDelay; + else if (--remainingOpenTime <= 0) + desiredPosition = 0; + } + } + + bool ITemporaryBlocker.IsBlocking(Actor self, CPos cell) + { + return Position != OpenPosition && blockedPositions.Contains(cell); + } + + bool ITemporaryBlocker.CanRemoveBlockage(Actor self, Actor blocking) + { + return CanRemoveBlockage(self, blocking); + } + + void INotifyBlockingMove.OnNotifyBlockingMove(Actor self, Actor blocking) + { + if (Position != OpenPosition && CanRemoveBlockage(self, blocking)) + desiredPosition = OpenPosition; + } + + bool CanRemoveBlockage(Actor self, Actor blocking) + { + return !self.IsDisabled() && BuildComplete && blocking.AppearsFriendlyTo(self); + } + + public override void AddedToWorld(Actor self) + { + base.AddedToWorld(self); + blockedPositions = FootprintUtils.Tiles(self); + } + + bool IsBlocked() + { + return blockedPositions.Any(loc => self.World.ActorMap.GetActorsAt(loc).Any(a => a != self)); + } + + WDist IBlocksProjectiles.BlockingHeight + { + get + { + return new WDist(info.BlocksProjectilesHeight * (OpenPosition - Position) / OpenPosition); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Buildings/LineBuildNode.cs b/OpenRA.Mods.Common/Traits/Buildings/LineBuildNode.cs index 5eceef6af2..4567ac2ab7 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/LineBuildNode.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/LineBuildNode.cs @@ -18,6 +18,9 @@ namespace OpenRA.Mods.Common.Traits { [Desc("This actor is of LineBuild 'NodeType'...")] public readonly HashSet Types = new HashSet { "wall" }; + + [Desc("Cells (outside the footprint) that contain cells that can connect to this actor.")] + public readonly CVec[] Connections = new[] { new CVec(1, 0), new CVec(0, 1), new CVec(-1, 0), new CVec(0, -1) }; } public class LineBuildNode { } diff --git a/OpenRA.Mods.Common/Traits/CombatDebugOverlay.cs b/OpenRA.Mods.Common/Traits/CombatDebugOverlay.cs index e016bf9620..72d30fdbea 100644 --- a/OpenRA.Mods.Common/Traits/CombatDebugOverlay.cs +++ b/OpenRA.Mods.Common/Traits/CombatDebugOverlay.cs @@ -9,7 +9,9 @@ #endregion using System; +using System.Collections.Generic; using System.Drawing; +using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Effects; using OpenRA.Traits; @@ -22,19 +24,18 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new CombatDebugOverlay(init.Self); } } - public class CombatDebugOverlay : IPostRender, INotifyDamage + public class CombatDebugOverlay : IPostRender, INotifyDamage, INotifyCreated { readonly DeveloperMode devMode; readonly HealthInfo healthInfo; - readonly BlocksProjectilesInfo blockInfo; + IBlocksProjectiles[] allBlockers; Lazy attack; Lazy coords; public CombatDebugOverlay(Actor self) { healthInfo = self.Info.TraitInfoOrDefault(); - blockInfo = self.Info.TraitInfoOrDefault(); attack = Exts.Lazy(() => self.TraitOrDefault()); coords = Exts.Lazy(() => self.Trait()); @@ -42,6 +43,11 @@ namespace OpenRA.Mods.Common.Traits devMode = localPlayer != null ? localPlayer.PlayerActor.Trait() : null; } + public void Created(Actor self) + { + allBlockers = self.TraitsImplementing().ToArray(); + } + public void RenderAfterWorld(WorldRenderer wr, Actor self) { if (devMode == null || !devMode.ShowCombatGeometry) @@ -53,10 +59,11 @@ namespace OpenRA.Mods.Common.Traits if (healthInfo != null) healthInfo.Shape.DrawCombatOverlay(wr, wcr, self); - if (blockInfo != null) + var blockers = allBlockers.Where(Exts.IsTraitEnabled).ToList(); + if (blockers.Count > 0) { var hc = Color.Orange; - var height = new WVec(0, 0, blockInfo.Height.Length); + var height = new WVec(0, 0, blockers.Max(b => b.BlockingHeight.Length)); var ha = wr.ScreenPosition(self.CenterPosition); var hb = wr.ScreenPosition(self.CenterPosition + height); wcr.DrawLine(ha, hb, iz, hc); diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index 739d2536ea..9497668b1d 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -240,6 +240,11 @@ namespace OpenRA.Mods.Common.Traits IsMovingInMyDirection(self, otherActor)) return false; + // If there is a temporary blocker in our path, but we can remove it, we are not blocked. + var temporaryBlocker = otherActor.TraitOrDefault(); + if (temporaryBlocker != null && temporaryBlocker.CanRemoveBlockage(otherActor, self)) + return false; + // If we cannot crush the other actor in our way, we are blocked. if (self == null || Crushes == null || Crushes.Count == 0) return true; diff --git a/OpenRA.Mods.Common/Traits/Render/WithGateSpriteBody.cs b/OpenRA.Mods.Common/Traits/Render/WithGateSpriteBody.cs new file mode 100644 index 0000000000..0f7eb940b1 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithGateSpriteBody.cs @@ -0,0 +1,98 @@ +#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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + class WithGateSpriteBodyInfo : WithSpriteBodyInfo, IWallConnectorInfo, Requires + { + [Desc("Cells (outside the gate footprint) that contain wall cells that can connect to the gate")] + public readonly CVec[] WallConnections = { }; + + [Desc("Wall type for connections")] + public readonly string Type = "wall"; + + public override object Create(ActorInitializer init) { return new WithGateSpriteBody(init, this); } + + public override IEnumerable RenderPreviewSprites(ActorPreviewInitializer init, RenderSpritesInfo rs, string image, int facings, PaletteReference p) + { + var anim = new Animation(init.World, image); + anim.PlayFetchIndex(RenderSprites.NormalizeSequence(anim, init.GetDamageState(), Sequence), () => 0); + + yield return new SpriteActorPreview(anim, WVec.Zero, 0, p, rs.Scale); + } + + string IWallConnectorInfo.GetWallConnectionType() + { + return Type; + } + } + + class WithGateSpriteBody : WithSpriteBody, INotifyRemovedFromWorld, INotifyBuildComplete, IWallConnector + { + readonly WithGateSpriteBodyInfo gateInfo; + readonly Gate gate; + + public WithGateSpriteBody(ActorInitializer init, WithGateSpriteBodyInfo info) + : base(init, info, () => 0) + { + gateInfo = info; + gate = init.Self.Trait(); + } + + int GetGateFrame() + { + return int2.Lerp(0, DefaultAnimation.CurrentSequence.Length - 1, gate.Position, gate.OpenPosition); + } + + public override void DamageStateChanged(Actor self, AttackInfo e) + { + DefaultAnimation.PlayFetchIndex(NormalizeSequence(self, Info.Sequence), GetGateFrame); + } + + public override void BuildingComplete(Actor self) + { + DefaultAnimation.PlayFetchIndex(NormalizeSequence(self, Info.Sequence), GetGateFrame); + UpdateNeighbours(self); + } + + void UpdateNeighbours(Actor self) + { + var footprint = FootprintUtils.Tiles(self).ToArray(); + var adjacent = Util.ExpandFootprint(footprint, true).Except(footprint) + .Where(self.World.Map.Contains).ToList(); + + var adjacentActorTraits = adjacent.SelectMany(self.World.ActorMap.GetActorsAt) + .SelectMany(a => a.TraitsImplementing()); + + foreach (var rb in adjacentActorTraits) + rb.SetDirty(); + } + + public void RemovedFromWorld(Actor self) + { + UpdateNeighbours(self); + } + + bool IWallConnector.AdjacentWallCanConnect(Actor self, CPos wallLocation, string wallType, out CVec facing) + { + facing = wallLocation - self.Location; + return wallType == gateInfo.Type && gateInfo.WallConnections.Contains(facing); + } + + void IWallConnector.SetDirty() { } + } +} diff --git a/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs b/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs index ae80d16913..1016e6a617 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithWallSpriteBody.cs @@ -8,6 +8,7 @@ */ #endregion +using System; using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; @@ -16,8 +17,14 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { + [RequireExplicitImplementation] + interface IWallConnectorInfo : ITraitInfoInterface + { + string GetWallConnectionType(); + } + [Desc("Render trait for actors that change sprites if neighbors with the same trait are present.")] - class WithWallSpriteBodyInfo : WithSpriteBodyInfo, Requires + class WithWallSpriteBodyInfo : WithSpriteBodyInfo, IWallConnectorInfo, Requires { public readonly string Type = "wall"; @@ -39,8 +46,8 @@ namespace OpenRA.Mods.Common.Traits var haveNeighbour = false; foreach (var n in kv.Value) { - var rb = init.World.Map.Rules.Actors[n].TraitInfoOrDefault(); - if (rb != null && rb.Type == Type) + var rb = init.World.Map.Rules.Actors[n].TraitInfos().FirstOrDefault(Exts.IsTraitEnabled); + if (rb != null && rb.GetWallConnectionType() == Type) { haveNeighbour = true; break; @@ -66,14 +73,27 @@ namespace OpenRA.Mods.Common.Traits yield return new SpriteActorPreview(anim, WVec.Zero, 0, p, rs.Scale); } + + string IWallConnectorInfo.GetWallConnectionType() + { + return Type; + } } - class WithWallSpriteBody : WithSpriteBody, INotifyRemovedFromWorld, ITick + class WithWallSpriteBody : WithSpriteBody, INotifyRemovedFromWorld, IWallConnector, ITick { readonly WithWallSpriteBodyInfo wallInfo; int adjacent = 0; bool dirty = true; + bool IWallConnector.AdjacentWallCanConnect(Actor self, CPos wallLocation, string wallType, out CVec facing) + { + facing = wallLocation - self.Location; + return wallInfo.Type == wallType && Math.Abs(facing.X) + Math.Abs(facing.Y) == 1; + } + + void IWallConnector.SetDirty() { dirty = true; } + public WithWallSpriteBody(ActorInitializer init, WithWallSpriteBodyInfo info) : base(init, info, () => 0) { @@ -97,20 +117,18 @@ namespace OpenRA.Mods.Common.Traits adjacent = 0; foreach (var a in adjacentActors) { - var rb = a.TraitOrDefault(); - if (rb == null || rb.wallInfo.Type != wallInfo.Type) + CVec facing; + var wc = a.TraitsImplementing().FirstOrDefault(Exts.IsTraitEnabled); + if (wc == null || !wc.AdjacentWallCanConnect(a, self.Location, wallInfo.Type, out facing)) continue; - var location = self.Location; - var otherLocation = a.Location; - - if (otherLocation == location + new CVec(0, -1)) + if (facing.Y > 0) adjacent |= 1; - else if (otherLocation == location + new CVec(+1, 0)) + else if (facing.X < 0) adjacent |= 2; - else if (otherLocation == location + new CVec(0, +1)) + else if (facing.Y < 0) adjacent |= 4; - else if (otherLocation == location + new CVec(-1, 0)) + else if (facing.X > 0) adjacent |= 8; } @@ -125,13 +143,12 @@ namespace OpenRA.Mods.Common.Traits static void UpdateNeighbours(Actor self) { - var adjacentActors = CVec.Directions.SelectMany(dir => + var adjacentActorTraits = CVec.Directions.SelectMany(dir => self.World.ActorMap.GetActorsAt(self.Location + dir)) - .Select(a => a.TraitOrDefault()) - .Where(a => a != null); + .SelectMany(a => a.TraitsImplementing()); - foreach (var rb in adjacentActors) - rb.dirty = true; + foreach (var aat in adjacentActorTraits) + aat.SetDirty(); } public void RemovedFromWorld(Actor self) diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 23ae4125e7..cac8375849 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -116,4 +116,11 @@ namespace OpenRA.Mods.Common.Traits { bool PreventsAutoTarget(Actor self, Actor attacker); } + + [RequireExplicitImplementation] + interface IWallConnector + { + bool AdjacentWallCanConnect(Actor self, CPos wallLocation, string wallType, out CVec facing); + void SetDirty(); + } } diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index ae0878062e..9b7522a03d 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -750,3 +750,52 @@ Inherits: ^TerrainOverlay CustomSelectionSize: CustomBounds: 220,220 + +^Gate: + Inherits: ^Building + Valued: + Cost: 250 + Health: + HP: 350 + Armor: + Type: Heavy + LineBuildNode: + Types: wall, gate + -Building: + -Capturable: + -GivesBuildableArea: + -MustBeDestroyed: + -WithSpriteBody: + WithGateSpriteBody: + Power: + CanPowerDown: + IndicatorPalette: mouse + Tooltip: + Description: Automated barrier that opens for allied units. + Gate: + Adjacent: 4 + BuildSounds: place2.aud + OpeningSound: gateup1.aud + ClosingSound: gatedwn1.aud + TerrainTypes: Clear, Rough, Road, DirtRoad, Green, Sand, Pavement + BlocksProjectilesHeight: 640 + +^Gate_A: + Inherits: ^Gate + Gate: + Dimensions: 3,1 + Footprint: xxx + WithGateSpriteBody: + WallConnections: -1,0, 3,0 + LineBuildNode: + Connections: -1,0, 1,0 + +^Gate_B: + Inherits: ^Gate + Gate: + Dimensions: 1,3 + Footprint: x x x + WithGateSpriteBody: + WallConnections: 0,-1, 0,3 + LineBuildNode: + Connections: 0,-1, 0,1 diff --git a/mods/ts/rules/gdi-support.yaml b/mods/ts/rules/gdi-support.yaml index 334e6eafee..e9f4727b44 100644 --- a/mods/ts/rules/gdi-support.yaml +++ b/mods/ts/rules/gdi-support.yaml @@ -25,6 +25,24 @@ GAWALL: LineBuild: NodeTypes: wall, turret +GAGATE_A: + Inherits: ^Gate_A + Buildable: + Queue: Defense + BuildPaletteOrder: 100 + Prerequisites: gapile, ~structures.gdi + Tooltip: + Name: GDI Gate + +GAGATE_B: + Inherits: ^Gate_B + Buildable: + Queue: Defense + BuildPaletteOrder: 100 + Prerequisites: gapile, ~structures.gdi + Tooltip: + Name: GDI Gate + GACTWR: Inherits: ^Defense -WithSpriteBody: diff --git a/mods/ts/rules/nod-support.yaml b/mods/ts/rules/nod-support.yaml index 05ecd5381d..3aa53567ee 100644 --- a/mods/ts/rules/nod-support.yaml +++ b/mods/ts/rules/nod-support.yaml @@ -25,6 +25,24 @@ NAWALL: LineBuild: NodeTypes: wall, turret +NAGATE_A: + Inherits: ^Gate_A + Buildable: + Queue: Defense + BuildPaletteOrder: 100 + Prerequisites: nahand, ~structures.nod + Tooltip: + Name: Nod Gate + +NAGATE_B: + Inherits: ^Gate_B + Buildable: + Queue: Defense + BuildPaletteOrder: 100 + Prerequisites: nahand, ~structures.nod + Tooltip: + Name: Nod Gate + NALASR: Inherits: ^Defense Valued: diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index 7953c39ac0..fd76b94139 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -602,6 +602,120 @@ gawall: Offset: 0, 0 UseTilesetCode: false +gagate_a: + Defaults: + Offset: -24, -24 + UseTilesetCode: true + idle: + Length: 10 + ShadowStart: 21 + damaged-idle: + Start: 10 + Length: 10 + ShadowStart: 31 + dead: + Start: 20 + Tick: 400 + ShadowStart: 41 + make: + Frames: 9, 8, 7, 6, 5, 4, 3, 2, 1 + Length: 9 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + icon: gateicon + Offset: 0, 0 + UseTilesetCode: false + +gagate_b: + Defaults: + Offset: 24, -24 + UseTilesetCode: true + idle: + Length: 10 + ShadowStart: 21 + damaged-idle: + Start: 10 + Length: 10 + ShadowStart: 31 + dead: + Start: 20 + Tick: 400 + ShadowStart: 41 + make: + Frames: 9, 8, 7, 6, 5, 4, 3, 2, 1 + Length: 9 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + icon: gat2icon + Offset: 0, 0 + UseTilesetCode: false + +nagate_a: + Defaults: + Offset: -24, -24 + UseTilesetCode: true + Tick: 80 + idle: + Length: 7 + ShadowStart: 15 + damaged-idle: + Start: 7 + Length: 7 + ShadowStart: 22 + dead: + Start: 14 + Tick: 400 + ShadowStart: 29 + make: + Frames: 6, 5, 4, 3, 2, 1 + Length: 6 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + icon: ngaticon + Offset: 0, 0 + UseTilesetCode: false + +nagate_b: + Defaults: + Offset: 24, -24 + UseTilesetCode: true + Tick: 80 + idle: + Length: 7 + ShadowStart: 15 + damaged-idle: + Start: 7 + Length: 7 + ShadowStart: 22 + dead: + Start: 14 + Tick: 400 + ShadowStart: 29 + make: + Frames: 6, 5, 4, 3, 2, 1 + Length: 6 + emp-overlay: emp_fx01 + Length: * + Offset: 0, 0 + UseTilesetCode: false + ZOffset: 512 + BlendMode: Additive + icon: nga2icon + Offset: 0, 0 + UseTilesetCode: false + nawall: Defaults: Offset: 0, -12