diff --git a/OpenRA.Game/GameRules/VoiceInfo.cs b/OpenRA.Game/GameRules/VoiceInfo.cs index a8ae1fe158..efca5c0114 100644 --- a/OpenRA.Game/GameRules/VoiceInfo.cs +++ b/OpenRA.Game/GameRules/VoiceInfo.cs @@ -41,6 +41,9 @@ namespace OpenRA.GameRules if (!Voices.ContainsKey("Attack")) Voices.Add("Attack", Voices["Move"]); + + if (!Voices.ContainsKey("AttackMove")) + Voices.Add("AttackMove", Voices["Move"]); Pools = Lazy.New(() => Voices.ToDictionary( a => a.Key, a => new VoicePool(a.Value) )); } diff --git a/OpenRA.Game/Orders/GenericSelectTarget.cs b/OpenRA.Game/Orders/GenericSelectTarget.cs index 89e8bbb662..d03984617f 100644 --- a/OpenRA.Game/Orders/GenericSelectTarget.cs +++ b/OpenRA.Game/Orders/GenericSelectTarget.cs @@ -11,18 +11,26 @@ using System.Collections.Generic; using System.Linq; using OpenRA.Graphics; +using OpenRA.Traits; namespace OpenRA.Orders { public class GenericSelectTarget : IOrderGenerator { - readonly Actor subject; + readonly IEnumerable subjects; readonly string order; readonly string cursor; + public GenericSelectTarget(IEnumerable subjects, string order, string cursor) + { + this.subjects = subjects; + this.order = order; + this.cursor = cursor; + } + public GenericSelectTarget(Actor subject, string order, string cursor) { - this.subject = subject; + this.subjects = new Actor[] { subject }; this.order = order; this.cursor = cursor; } @@ -36,16 +44,35 @@ namespace OpenRA.Orders IEnumerable OrderInner(World world, int2 xy, MouseInput mi) { - if( mi.Button == MouseButton.Left && world.Map.IsInMap( xy ) ) + if (mi.Button == MouseButton.Left && world.Map.IsInMap(xy)) { world.CancelInputMode(); - yield return new Order( order, subject, xy ); + foreach (var subject in subjects) + yield return new Order(order, subject, xy); } } public virtual void Tick(World world) { } - public void RenderAfterWorld(WorldRenderer wr, World world) { } - public void RenderBeforeWorld(WorldRenderer wr, World world) { } + + public void RenderBeforeWorld(WorldRenderer wr, World world) + { + foreach (var a in world.Selection.Actors) + if (!a.Destroyed) + foreach (var t in a.TraitsImplementing()) + t.RenderBeforeWorld(wr, a); + + Game.Renderer.Flush(); + } + + public void RenderAfterWorld(WorldRenderer wr, World world) + { + foreach (var a in world.Selection.Actors) + if (!a.Destroyed) + foreach (var t in a.TraitsImplementing()) + t.RenderAfterWorld(wr, a); + + Game.Renderer.Flush(); + } public string GetCursor(World world, int2 xy, MouseInput mi) { return world.Map.IsInMap(xy) ? cursor : "generic-blocked"; } } @@ -67,5 +94,4 @@ namespace OpenRA.Orders world.CancelInputMode(); } } - } diff --git a/OpenRA.Game/Traits/Mobile.cs b/OpenRA.Game/Traits/Mobile.cs index 83b613fbe9..ebc462ccf8 100644 --- a/OpenRA.Game/Traits/Mobile.cs +++ b/OpenRA.Game/Traits/Mobile.cs @@ -179,8 +179,9 @@ namespace OpenRA.Traits return; if( !order.Queued ) self.CancelActivity(); + self.QueueActivity(new Activities.Move(currentLocation, 8)); - + if (self.Owner == self.World.LocalPlayer) self.World.AddFrameEndTask(w => { diff --git a/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs b/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs index 99a841610f..b19ef3d6e1 100644 --- a/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs +++ b/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs @@ -46,6 +46,7 @@ namespace OpenRA.Widgets } float2 dragStart, dragEnd; + public override bool HandleInputInner(MouseInput mi) { var xy = Game.viewport.ViewToWorld(mi); @@ -139,8 +140,20 @@ namespace OpenRA.Widgets GotoNextBase(); return true; } + + if (e.KeyChar == 'a') + { + StartAttackMoveOrder(); + return true; + } } return false; + } + + public void StartAttackMoveOrder() + { + if (world.Selection.Actors.Count() > 0) + world.OrderGenerator = new GenericSelectTarget(world.Selection.Actors, "AttackMove", "attackmove"); } public void GotoNextBase() diff --git a/OpenRA.Mods.RA/Activities/AttackMove.cs b/OpenRA.Mods.RA/Activities/AttackMove.cs new file mode 100644 index 0000000000..003b2f9b0c --- /dev/null +++ b/OpenRA.Mods.RA/Activities/AttackMove.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenRA.Traits; +using OpenRA.Traits.Activities; + +namespace OpenRA.Mods.RA.Activities +{ + public class AttackMove : Move + { + public AttackMove(int2 destination) : base(destination) { } + public AttackMove(int2 destination, int nearEnough) : base(destination, nearEnough) { } + public AttackMove(int2 destination, Actor ignoreBuilding) : base(destination, ignoreBuilding) { } + public AttackMove(Actor target, int range) : base(target, range) { } + public AttackMove(Target target, int range) : base(target, range) { } + public AttackMove(Func> getPath) : base(getPath) { } + } +} diff --git a/OpenRA.Mods.RA/AttackBase.cs b/OpenRA.Mods.RA/AttackBase.cs index 380db77f40..b185ab4fdd 100644 --- a/OpenRA.Mods.RA/AttackBase.cs +++ b/OpenRA.Mods.RA/AttackBase.cs @@ -36,11 +36,17 @@ namespace OpenRA.Mods.RA public readonly bool AlignIdleTurrets = false; public readonly bool CanAttackGround = true; + public readonly float ScanTimeAverage = 2f; + public readonly float ScanTimeSpread = .5f; + public virtual object Create(ActorInitializer init) { return new AttackBase(init.self); } } public class AttackBase : IIssueOrder, IResolveOrder, ITick, IExplodeModifier, IOrderVoice { + [Sync] + int nextScanTime = 0; + public bool IsAttacking { get; internal set; } public Target target; @@ -228,6 +234,7 @@ namespace OpenRA.Mods.RA if (self.HasTrait() && self.Info.Traits.Get().AlignIdleTurrets) self.Trait().desiredFacing = null; } + } public string VoicePhraseForOrder(Actor self, Order order) @@ -251,6 +258,48 @@ namespace OpenRA.Mods.RA public Weapon ChooseWeaponForTarget(Target t) { return Weapons.FirstOrDefault(w => w.IsValidAgainst(self.World, t)); } + public void AttackTarget(Actor self, Actor target, bool allowMovement) + { + var attack = self.Trait(); + if (target != null) + { + if (allowMovement) + attack.ResolveOrder(self, new Order("Attack", self, target)); + else + attack.target = Target.FromActor(target); // for turreted things on rails. + } + } + + public void ScanAndAttack(Actor self, bool allowMovement) + { + if (--nextScanTime <= 0) + { + var attack = self.Trait(); + var range = attack.GetMaximumRange(); + + if (!attack.target.IsValid || + (Util.CellContaining(attack.target.CenterLocation) - self.Location).LengthSquared > range * range) + AttackTarget(self, ChooseTarget(self, range), allowMovement); + + var info = self.Info.Traits.Get(); + nextScanTime = (int)(25 * (info.ScanTimeAverage + + (self.World.SharedRandom.NextDouble() * 2 - 1) * info.ScanTimeSpread)); + } + } + + Actor ChooseTarget(Actor self, float range) + { + var inRange = self.World.FindUnitsInCircle(self.CenterLocation, Game.CellSize * range); + var attack = self.Trait(); + + return inRange + .Where(a => a.Owner != null && self.Owner.Stances[a.Owner] == Stance.Enemy) + .Where(a => attack.HasAnyValidWeapons(Target.FromActor(a))) + .Where(a => !a.HasTrait() || a.Trait().IsVisible(a, self.Owner)) + .OrderBy(a => (a.Location - self.Location).LengthSquared) + .FirstOrDefault(); + } + class AttackOrderTargeter : IOrderTargeter { readonly bool isHeal; @@ -292,6 +341,6 @@ namespace OpenRA.Mods.RA return false; } - } + } } } diff --git a/OpenRA.Mods.RA/AttackMoveTrait.cs b/OpenRA.Mods.RA/AttackMoveTrait.cs new file mode 100644 index 0000000000..83594ea174 --- /dev/null +++ b/OpenRA.Mods.RA/AttackMoveTrait.cs @@ -0,0 +1,74 @@ +using System; +using System.Drawing; +using OpenRA.Traits; +using OpenRA.Effects; + +namespace OpenRA.Mods.RA +{ + class AttackMoveInfo : TraitInfo + { + //public object Create(ActorInitializer init) { return new AttackMove(init.self); } + public readonly bool JustMove = false; + } + + class AttackMove : ITick, IResolveOrder, IOrderVoice + { + public bool AttackMoving { get; set; } + + public string VoicePhraseForOrder(Actor self, Order order) + { + if (order.OrderString == "AttackMove") + { + return "AttackMove"; + } + return null; + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "AttackMove") + { + self.CancelActivity(); + //if we are just moving, we don't turn on attackmove and this becomes a regular move order + if (!self.Info.Traits.Get().JustMove) + { + AttackMoving = true; + } + Order newOrder = new Order("Move", order.Subject, order.TargetLocation); + self.Trait().ResolveOrder(self, newOrder); + + if (self.Owner == self.World.LocalPlayer) + self.World.AddFrameEndTask(w => + { + if (order.TargetActor != null) + w.Add(new FlashTarget(order.TargetActor)); + + var line = self.TraitOrDefault(); + if (line != null) + if (order.TargetActor != null) line.SetTarget(self, Target.FromOrder(order), Color.Red); + else line.SetTarget(self, Target.FromOrder(order), Color.Red); + }); + + if (self.Owner == self.Owner.World.LocalPlayer) + self.World.CancelInputMode(); + } + else + { + AttackMoving = false; //cancel attack move state for other orders + } + } + + public void Tick(Actor self) + { + if (self.Info.Traits.Get().JustMove) return; + if (!self.HasTrait()) + { + Game.Debug("AttackMove: {0} has no AttackBase trait".F(self.ToString())); + return; + } + if (!self.IsIdle && (self.HasTrait() && !(self.Trait().AttackMoving))) return; + + self.Trait().ScanAndAttack(self, true); + } + } +} diff --git a/OpenRA.Mods.RA/AutoTarget.cs b/OpenRA.Mods.RA/AutoTarget.cs index d825fbaf40..0818611334 100644 --- a/OpenRA.Mods.RA/AutoTarget.cs +++ b/OpenRA.Mods.RA/AutoTarget.cs @@ -8,65 +8,22 @@ */ #endregion -using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.RA { class AutoTargetInfo : TraitInfo { - public readonly float ScanTimeAverage = 2f; - public readonly float ScanTimeSpread = .5f; public readonly bool AllowMovement = true; } class AutoTarget : ITick, INotifyDamage { - [Sync] - int nextScanTime = 0; - - void AttackTarget(Actor self, Actor target) - { - var attack = self.Trait(); - if (target != null) - { - if (self.Info.Traits.Get().AllowMovement) - attack.ResolveOrder(self, new Order("Attack", self, target)); - else - attack.target = Target.FromActor(target); // for turreted things on rails. - } - } - public void Tick(Actor self) { if (!self.IsIdle && self.Info.Traits.Get().AllowMovement) return; - if (--nextScanTime <= 0) - { - var attack = self.Trait(); - var range = attack.GetMaximumRange(); - - if (!attack.target.IsValid || - (Util.CellContaining(attack.target.CenterLocation) - self.Location).LengthSquared > range * range) - AttackTarget(self, ChooseTarget(self, range)); - - var info = self.Info.Traits.Get(); - nextScanTime = (int)(25 * (info.ScanTimeAverage + - (self.World.SharedRandom.NextDouble() * 2 - 1) * info.ScanTimeSpread)); - } - } - - Actor ChooseTarget(Actor self, float range) - { - var inRange = self.World.FindUnitsInCircle(self.CenterLocation, Game.CellSize * range); - var attack = self.Trait(); - - return inRange - .Where(a => a.Owner != null && self.Owner.Stances[ a.Owner ] == Stance.Enemy) - .Where(a => attack.HasAnyValidWeapons(Target.FromActor(a))) - .Where(a => !a.HasTrait() || a.Trait().IsVisible(a,self.Owner)) - .OrderBy(a => (a.Location - self.Location).LengthSquared) - .FirstOrDefault(); + self.Trait().ScanAndAttack(self, self.Info.Traits.Get().AllowMovement); } public void Damaged(Actor self, AttackInfo e) @@ -83,7 +40,7 @@ namespace OpenRA.Mods.RA if (e.Damage < 0) return; // don't retaliate against healers - AttackTarget(self, e.Attacker); + self.Trait().AttackTarget(self, e.Attacker, self.Info.Traits.Get().AllowMovement); } } } diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 78c8a21e3f..d988efa8db 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -77,6 +77,7 @@ + diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 4ebb78981e..8ca620eded 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -26,6 +26,7 @@ DrawLineToTarget: ActorLostNotification: Notification: unitlost.aud + AttackMove: ^Tank: AppearsOnRadar: @@ -55,6 +56,7 @@ DrawLineToTarget: ActorLostNotification: Notification: unitlost.aud + AttackMove: ^Helicopter: AppearsOnRadar: @@ -100,6 +102,7 @@ Queue: Infantry RenderInfantry: AutoTarget: + AttackMove: Passenger: CargoType: Infantry HiddenUnderFog: @@ -164,6 +167,7 @@ DrawLineToTarget: ActorLostNotification: Notification: unitlost.aud + AttackMove: ^Building: AppearsOnRadar: diff --git a/mods/cnc/rules/infantry.yaml b/mods/cnc/rules/infantry.yaml index d5c0cafa98..b6eebefb3d 100644 --- a/mods/cnc/rules/infantry.yaml +++ b/mods/cnc/rules/infantry.yaml @@ -153,6 +153,8 @@ E6: EngineerRepair: EngineerCapture: -AutoTarget: + AttackMove: + JustMove: true IdleAnimation: Animations: idle1,idle2 diff --git a/mods/cnc/rules/vehicles.yaml b/mods/cnc/rules/vehicles.yaml index 99e4a76f41..fad082dd57 100644 --- a/mods/cnc/rules/vehicles.yaml +++ b/mods/cnc/rules/vehicles.yaml @@ -28,6 +28,7 @@ MCV: NoTransformSounds: deploy1.aud RenderUnit: MustBeDestroyed: + -AttackMove: HARV: Inherits: ^Tank @@ -58,6 +59,7 @@ HARV: RevealsShroud: Range: 4 RenderUnit: + -AttackMove: APC: Inherits: ^Tank @@ -650,6 +652,8 @@ LST: Cargo: Types: Infantry, Vehicle Passengers: 5 + AttackMove: + JustMove: true LTNK.Husk: Inherits: ^Husk diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index 7641e300f7..94fc8ea07d 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -18,6 +18,7 @@ Passenger: CargoType: Vehicle IronCurtainable: + AttackMove: HiddenUnderFog: GainsExperience: GivesExperience: @@ -44,7 +45,8 @@ Chronoshiftable: Passenger: CargoType: Vehicle - IronCurtainable: + IronCurtainable: + AttackMove: HiddenUnderFog: GainsExperience: GivesExperience: @@ -73,6 +75,7 @@ TargetTypes: Ground RenderInfantry: AutoTarget: + AttackMove: Passenger: CargoType: Infantry HiddenUnderFog: @@ -96,6 +99,7 @@ DetectCloaked: Range: 3 HiddenUnderFog: + AttackMove: GainsExperience: GivesExperience: DrawLineToTarget: diff --git a/mods/ra/rules/infantry.yaml b/mods/ra/rules/infantry.yaml index 825862c91f..1b4d7e0bd2 100644 --- a/mods/ra/rules/infantry.yaml +++ b/mods/ra/rules/infantry.yaml @@ -158,6 +158,8 @@ E6: EngineerCapture: TakeCover: -AutoTarget: + AttackMove: + JustMove: true IdleAnimation: Animations: idle1,idle2 @@ -190,6 +192,8 @@ SPY: Spy: -RenderInfantry: -AutoTarget: + AttackMove: + JustMove: true IdleAnimation: Animations: idle1,idle2 @@ -253,6 +257,8 @@ MEDI: PrimaryWeapon: Heal TakeCover: -AutoTarget: + AttackMove: + JustMove: true IdleAnimation: Animations: idle1,idle2 diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml index 0766ad9e2f..3355c37346 100755 --- a/mods/ra/rules/structures.yaml +++ b/mods/ra/rules/structures.yaml @@ -39,7 +39,6 @@ GAP: # Prerequisites: atek # Owner: allies # Cost: 500 -# Hotkey: g Building: Power: -60 Footprint: _ x @@ -268,7 +267,7 @@ AGUN: BuildPaletteOrder: 50 Prerequisites: dome Owner: allies - Hotkey: a + Hotkey: g Valued: Cost: 600 Tooltip: @@ -679,7 +678,7 @@ AFLD: BuildPaletteOrder: 50 Prerequisites: dome Owner: soviet - Hotkey: a + Hotkey: g Valued: Cost: 1100 Tooltip: diff --git a/mods/ra/rules/vehicles.yaml b/mods/ra/rules/vehicles.yaml index 2385234170..fd7ba20c72 100644 --- a/mods/ra/rules/vehicles.yaml +++ b/mods/ra/rules/vehicles.yaml @@ -270,6 +270,7 @@ HARV: RevealsShroud: Range: 4 RenderUnit: + -AttackMove: MCV: Inherits: ^Vehicle @@ -303,6 +304,7 @@ MCV: RenderUnit: MustBeDestroyed: BaseBuilding: + -AttackMove: JEEP: Inherits: ^Vehicle @@ -394,6 +396,8 @@ MNLY.AP: MineImmune: LimitedAmmo: Ammo: 5 + AttackMove: + JustMove: true MNLY.AT: Inherits: ^Tank @@ -423,6 +427,8 @@ MNLY.AT: MineImmune: LimitedAmmo: Ammo: 5 + AttackMove: + JustMove: true TRUK: Inherits: ^Vehicle @@ -625,6 +631,8 @@ LST: Passengers: 5 IronCurtainable: RepairableNear: + AttackMove: + JustMove: true PT: Inherits: ^Ship