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/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/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/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..e226f3667c --- /dev/null +++ b/OpenRA.Mods.Common/Traits/Render/WithGateSpriteBody.cs @@ -0,0 +1,93 @@ +#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, 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); + } + } + + 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 aat in adjacentActorTraits) + aat.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/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