diff --git a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj index c04a430690..2067200577 100644 --- a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj +++ b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj @@ -154,6 +154,8 @@ + + diff --git a/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs b/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs new file mode 100644 index 0000000000..ed4ef6abb2 --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/Attack/AttackTDGunboatTurreted.cs @@ -0,0 +1,73 @@ +#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.Linq; +using OpenRA.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits +{ + [Desc("Actor has a visual turret used to attack.")] + public class AttackTDGunboatTurretedInfo : AttackTurretedInfo, Requires + { + public override object Create(ActorInitializer init) { return new AttackTDGunboatTurreted(init.Self, this); } + } + + public class AttackTDGunboatTurreted : AttackTurreted + { + public AttackTDGunboatTurreted(Actor self, AttackTDGunboatTurretedInfo info) + : base(self, info) { } + + public override Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove, bool forceAttack) + { + return new AttackTDGunboatTurretedActivity(self, newTarget, allowMove, forceAttack); + } + + class AttackTDGunboatTurretedActivity : Activity + { + readonly AttackTDGunboatTurreted attack; + readonly Target target; + readonly bool forceAttack; + bool hasTicked; + + public AttackTDGunboatTurretedActivity(Actor self, Target target, bool allowMove, bool forceAttack) + { + attack = self.Trait(); + this.target = target; + this.forceAttack = forceAttack; + } + + public override Activity Tick(Actor self) + { + if (IsCanceled || !target.IsValidFor(self)) + return NextActivity; + + if (attack.IsTraitDisabled) + return this; + + var weapon = attack.ChooseArmamentsForTarget(target, forceAttack).FirstOrDefault(); + if (weapon != null) + { + // Check that AttackTDGunboatTurreted hasn't cancelled the target by modifying attack.Target + // Having both this and AttackTDGunboatTurreted modify that field is a horrible hack. + if (hasTicked && attack.Target.Type == TargetType.Invalid) + return NextActivity; + + attack.Target = target; + hasTicked = true; + } + + return NextActivity; + } + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/TDGunboat.cs b/OpenRA.Mods.Cnc/Traits/TDGunboat.cs new file mode 100644 index 0000000000..36a5637f9c --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/TDGunboat.cs @@ -0,0 +1,211 @@ +#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.Activities; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits +{ + public class TDGunboatInfo : ITraitInfo, IPositionableInfo, IFacingInfo, IMoveInfo, + UsesInit, UsesInit, IActorPreviewInitInfo + { + public readonly int Speed = 28; + + [Desc("Facing to use when actor spawns. Only 64 and 192 supported.")] + public readonly int InitialFacing = 64; + + [Desc("Facing to use for actor previews (map editor, color picker, etc). Only 64 and 192 supported.")] + public readonly int PreviewFacing = 64; + + public virtual object Create(ActorInitializer init) { return new TDGunboat(init, this); } + + public int GetInitialFacing() { return InitialFacing; } + + IEnumerable IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type) + { + yield return new FacingInit(PreviewFacing); + } + + public IReadOnlyDictionary OccupiedCells(ActorInfo info, CPos location, SubCell subCell = SubCell.Any) + { + var occupied = new Dictionary() { { location, SubCell.FullCell } }; + return new ReadOnlyDictionary(occupied); + } + + bool IOccupySpaceInfo.SharesCell { get { return false; } } + + // Used to determine if actor can spawn + public bool CanEnterCell(World world, Actor self, CPos cell, Actor ignoreActor = null, bool checkTransientActors = false) + { + if (!world.Map.Contains(cell)) + return false; + + return true; + } + } + + public class TDGunboat : ITick, ISync, IFacing, IPositionable, IMove, IDeathActorInitModifier, + INotifyCreated, INotifyAddedToWorld, INotifyRemovedFromWorld, IActorPreviewInitModifier + { + public readonly TDGunboatInfo Info; + readonly Actor self; + + IEnumerable speedModifiers; + + [Sync] public int Facing { get; set; } + [Sync] public WPos CenterPosition { get; private set; } + public CPos TopLeft { get { return self.World.Map.CellContaining(CenterPosition); } } + + // Isn't used anyway + public int TurnSpeed { get { return 255; } } + + CPos cachedLocation; + + public TDGunboat(ActorInitializer init, TDGunboatInfo info) + { + Info = info; + self = init.Self; + + if (init.Contains()) + SetPosition(self, init.Get()); + + if (init.Contains()) + SetPosition(self, init.Get()); + + Facing = init.Contains() ? init.Get() : Info.GetInitialFacing(); + + // Prevent mappers from setting bogus facings + if (Facing != 64 && Facing != 192) + Facing = Facing > 127 ? 192 : 64; + } + + void INotifyCreated.Created(Actor self) + { + speedModifiers = self.TraitsImplementing().ToArray().Select(sm => sm.GetSpeedModifier()); + cachedLocation = self.Location; + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + self.World.AddToMaps(self, this); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + self.World.RemoveFromMaps(self, this); + } + + void ITick.Tick(Actor self) + { + if (cachedLocation != self.Location) + { + // If the actor just left the map, switch facing + if (!self.World.Map.Contains(self.Location)) + Turn(); + } + + cachedLocation = self.Location; + + SetVisualPosition(self, self.CenterPosition + MoveStep(Facing)); + } + + void Turn() + { + if (Facing == 64) + Facing = 192; + else + Facing = 64; + } + + int MovementSpeed + { + get { return Util.ApplyPercentageModifiers(Info.Speed, speedModifiers); } + } + + public IEnumerable> OccupiedCells() { return new[] { Pair.New(TopLeft, SubCell.FullCell) }; } + + WVec MoveStep(int facing) + { + return MoveStep(MovementSpeed, facing); + } + + WVec MoveStep(int speed, int facing) + { + var dir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(facing)); + return speed * dir / 1024; + } + + void IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init) + { + init.Add(new FacingInit(Facing)); + } + + public bool IsLeavingCell(CPos location, SubCell subCell = SubCell.Any) { return false; } + public bool CanEnterCell(CPos cell, Actor ignoreActor = null, bool checkTransientActors = false) { return true; } + public SubCell GetValidSubCell(SubCell preferred) { return SubCell.Invalid; } + public SubCell GetAvailableSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, Actor ignoreActor = null, bool checkTransientActors = true) + { + // Does not use any subcell + return SubCell.Invalid; + } + + public void SetVisualPosition(Actor self, WPos pos) { SetPosition(self, pos); } + + public void SetPosition(Actor self, CPos cell, SubCell subCell = SubCell.Any) + { + SetPosition(self, self.World.Map.CenterOfCell(cell)); + } + + public void SetPosition(Actor self, WPos pos) + { + CenterPosition = pos; + + if (!self.IsInWorld) + return; + + self.World.UpdateMaps(self, this); + } + + public Activity MoveTo(CPos cell, int nearEnough) { return null; } + public Activity MoveTo(CPos cell, Actor ignoreActor) { return null; } + public Activity MoveWithinRange(Target target, WDist range) { return null; } + public Activity MoveWithinRange(Target target, WDist minRange, WDist maxRange) { return null; } + public Activity MoveFollow(Actor self, Target target, WDist minRange, WDist maxRange) { return null; } + public Activity MoveIntoWorld(Actor self, CPos cell, SubCell subCell = SubCell.Any) { return null; } + public Activity MoveToTarget(Actor self, Target target) { return null; } + public Activity MoveIntoTarget(Actor self, Target target) { return null; } + public Activity VisualMove(Actor self, WPos fromPos, WPos toPos) { return null; } + + public CPos NearestMoveableCell(CPos cell) { return cell; } + + // Actors with TDGunboat always move + public bool IsMoving { get { return true; } set { } } + + public bool IsMovingVertically { get { return false; } set { } } + + public bool CanEnterTargetNow(Actor self, Target target) + { + return false; + } + + void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits) + { + if (!inits.Contains() && !inits.Contains()) + inits.Add(new DynamicFacingInit(() => Facing)); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs index ee3bb48dee..06f67bacc9 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs @@ -63,7 +63,6 @@ namespace OpenRA.Mods.Common.Traits readonly IMove move; readonly Target target; readonly bool forceAttack; - readonly bool onRailsHack; bool hasTicked; public AttackActivity(Actor self, Target target, bool allowMove, bool forceAttack) @@ -71,14 +70,6 @@ namespace OpenRA.Mods.Common.Traits attack = self.Trait(); move = allowMove ? self.TraitOrDefault() : null; - // HACK: Mobile.OnRails is horrible. Blergh. - var mobile = move as Mobile; - if (mobile != null && mobile.Info.OnRails) - { - move = null; - onRailsHack = true; - } - this.target = target; this.forceAttack = forceAttack; } @@ -112,14 +103,12 @@ namespace OpenRA.Mods.Common.Traits if (move != null) return ActivityUtils.SequenceActivities(move.MoveFollow(self, target, weapon.Weapon.MinRange, maxRange), this); - if (!onRailsHack && - target.IsInRange(self.CenterPosition, weapon.MaxRange()) && + if (target.IsInRange(self.CenterPosition, weapon.MaxRange()) && !target.IsInRange(self.CenterPosition, weapon.Weapon.MinRange)) return this; } - if (!onRailsHack) - attack.Target = Target.Invalid; + attack.Target = Target.Invalid; return NextActivity; } diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index d09e49367b..a9f24b70cf 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -70,8 +70,6 @@ namespace OpenRA.Mods.Common.Traits public readonly int Speed = 1; - public readonly bool OnRails = false; - [Desc("Allow multiple (infantry) units in one cell.")] public readonly bool SharesCell = false; @@ -583,12 +581,7 @@ namespace OpenRA.Mods.Common.Traits public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) { if (order is MoveOrderTargeter) - { - if (Info.OnRails) - return null; - return new Order("Move", self, queued) { TargetLocation = self.World.Map.CellContaining(target.CenterPosition) }; - } return null; } diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index 42bab8569b..ab93a20f72 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -845,6 +845,38 @@ namespace OpenRA.Mods.Common.UtilityCommands } } + // Replace Mobile.OnRails hack with dedicated TDGunboat traits in Mods.Cnc + if (engineVersion < 20170715) + { + var mobile = node.Value.Nodes.FirstOrDefault(n => n.Key == "Mobile"); + if (mobile != null) + { + var onRailsNode = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "OnRails"); + var onRails = onRailsNode != null ? FieldLoader.GetValue("OnRails", onRailsNode.Value.Value) : false; + if (onRails) + { + var speed = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "Speed"); + var initFacing = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "InitialFacing"); + var previewFacing = mobile.Value.Nodes.FirstOrDefault(n => n.Key == "PreviewFacing"); + var tdGunboat = new MiniYamlNode("TDGunboat", ""); + if (speed != null) + tdGunboat.Value.Nodes.Add(speed); + if (initFacing != null) + tdGunboat.Value.Nodes.Add(initFacing); + if (previewFacing != null) + tdGunboat.Value.Nodes.Add(previewFacing); + + node.Value.Nodes.Add(tdGunboat); + + var attackTurreted = node.Value.Nodes.FirstOrDefault(n => n.Key.StartsWith("AttackTurreted", StringComparison.Ordinal)); + if (attackTurreted != null) + RenameNodeKey(attackTurreted, "AttackTDGunboatTurreted"); + + node.Value.Nodes.Remove(mobile); + } + } + } + UpgradeActorRules(modData, engineVersion, ref node.Value.Nodes, node, depth + 1); } diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 7846ead0ef..2bd9e6b420 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -602,10 +602,6 @@ Inherits@1: ^ExistsInWorld Inherits@3: ^SpriteActor Huntable: - Mobile: - Crushes: crate - TerrainSpeeds: - Water: 100 SelectionDecorations: WithSpriteControlGroupDecoration: Selectable: diff --git a/mods/cnc/rules/ships.yaml b/mods/cnc/rules/ships.yaml index 6e7c815505..5ddb9dee2e 100644 --- a/mods/cnc/rules/ships.yaml +++ b/mods/cnc/rules/ships.yaml @@ -10,20 +10,18 @@ BOAT: HP: 700 Armor: Type: Heavy - Mobile: - InitialFacing: 64 - TurnSpeed: 32 + TDGunboat: Speed: 28 - OnRails: true RevealsShroud: Range: 7c0 + Type: CenterPosition Turreted: TurnSpeed: 5 Offset: 0,896,171 Armament: Weapon: BoatMissile LocalOffset: 85,-85,0, 85,85,0 - AttackTurreted: + AttackTDGunboatTurreted: -QuantizeFacingsFromSequence: BodyOrientation: QuantizedFacings: 2