diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index 4550c8b319..8f792eaee6 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -301,11 +301,17 @@ namespace OpenRA.Traits public interface IFacingInfo : ITraitInfoInterface { int GetInitialFacing(); } + [RequireExplicitImplementation] public interface ICrushable { - void OnCrush(Actor crusher); - void WarnCrush(Actor crusher); - bool CrushableBy(HashSet crushClasses, Player owner); + bool CrushableBy(Actor self, Actor crusher, HashSet crushClasses); + } + + [RequireExplicitImplementation] + public interface INotifyCrushed + { + void OnCrush(Actor self, Actor crusher, HashSet crushClasses); + void WarnCrush(Actor self, Actor crusher, HashSet crushClasses); } public interface ITraitInfoInterface { } diff --git a/OpenRA.Mods.Common/Traits/Crates/Crate.cs b/OpenRA.Mods.Common/Traits/Crates/Crate.cs index 58c62d43cb..6b175539e0 100644 --- a/OpenRA.Mods.Common/Traits/Crates/Crate.cs +++ b/OpenRA.Mods.Common/Traits/Crates/Crate.cs @@ -38,7 +38,8 @@ namespace OpenRA.Mods.Common.Traits bool IOccupySpaceInfo.SharesCell { get { return false; } } } - class Crate : ITick, IPositionable, ICrushable, ISync, INotifyParachuteLanded, INotifyAddedToWorld, INotifyRemovedFromWorld + class Crate : ITick, IPositionable, ICrushable, ISync, + INotifyParachuteLanded, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyCrushed { readonly Actor self; readonly CrateInfo info; @@ -56,9 +57,46 @@ namespace OpenRA.Mods.Common.Traits SetPosition(self, init.Get()); } - public void WarnCrush(Actor crusher) { } + void INotifyCrushed.WarnCrush(Actor self, Actor crusher, HashSet crushClasses) { } - public void OnCrush(Actor crusher) + void INotifyCrushed.OnCrush(Actor self, Actor crusher, HashSet crushClasses) + { + // Crate can only be crushed if it is not in the air. + if (!self.IsAtGroundLevel() || !crushClasses.Contains(info.CrushClass)) + return; + + OnCrushInner(crusher); + } + + public void OnLanded() + { + // Check whether the crate landed on anything + var landedOn = self.World.ActorMap.GetActorsAt(self.Location) + .Where(a => a != self); + + if (!landedOn.Any()) + return; + + var collector = landedOn.FirstOrDefault(a => + { + // Mobile is (currently) the only trait that supports crushing + var mi = a.Info.TraitInfoOrDefault(); + if (mi == null) + return false; + + // Make sure that the actor can collect this crate type + // Crate can only be crushed if it is not in the air. + return self.IsAtGroundLevel() && mi.Crushes.Contains(info.CrushClass); + }); + + // Destroy the crate if none of the units in the cell are valid collectors + if (collector != null) + OnCrushInner(collector); + else + self.Dispose(); + } + + void OnCrushInner(Actor crusher) { if (collected) return; @@ -82,39 +120,12 @@ namespace OpenRA.Mods.Common.Traits s.First.Activate(crusher); return; } - else - n -= s.Second; + + n -= s.Second; } } } - public void OnLanded() - { - // Check whether the crate landed on anything - var landedOn = self.World.ActorMap.GetActorsAt(self.Location) - .Where(a => a != self); - - if (!landedOn.Any()) - return; - - var collector = landedOn.FirstOrDefault(a => - { - // Mobile is (currently) the only trait that supports crushing - var mi = a.Info.TraitInfoOrDefault(); - if (mi == null) - return false; - - // Make sure that the actor can collect this crate type - return CrushableBy(mi.Crushes, a.Owner); - }); - - // Destroy the crate if none of the units in the cell are valid collectors - if (collector != null) - OnCrush(collector); - else - self.Dispose(); - } - public void Tick(Actor self) { if (info.Lifetime != 0 && self.IsInWorld && ++ticks >= info.Lifetime * 25) @@ -178,8 +189,8 @@ namespace OpenRA.Mods.Common.Traits return SubCell.FullCell; return !self.World.ActorMap.GetActorsAt(cell) - .Where(x => x != ignoreActor) - .Any() ? SubCell.FullCell : SubCell.Invalid; + .Any(x => x != ignoreActor) + ? SubCell.FullCell : SubCell.Invalid; } public bool CanEnterCell(CPos a, Actor ignoreActor = null, bool checkTransientActors = true) @@ -187,7 +198,7 @@ namespace OpenRA.Mods.Common.Traits return GetAvailableSubCell(a, SubCell.Any, ignoreActor, checkTransientActors) != SubCell.Invalid; } - public bool CrushableBy(HashSet crushClasses, Player owner) + bool ICrushable.CrushableBy(Actor self, Actor crusher, HashSet crushClasses) { // Crate can only be crushed if it is not in the air. return self.IsAtGroundLevel() && crushClasses.Contains(info.CrushClass); diff --git a/OpenRA.Mods.Common/Traits/Crushable.cs b/OpenRA.Mods.Common/Traits/Crushable.cs index f429ca82f1..5a4dd71c7b 100644 --- a/OpenRA.Mods.Common/Traits/Crushable.cs +++ b/OpenRA.Mods.Common/Traits/Crushable.cs @@ -29,7 +29,7 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new Crushable(init.Self, this); } } - class Crushable : ICrushable + class Crushable : ICrushable, INotifyCrushed { readonly Actor self; readonly CrushableInfo info; @@ -40,31 +40,32 @@ namespace OpenRA.Mods.Common.Traits this.info = info; } - public void WarnCrush(Actor crusher) + void INotifyCrushed.WarnCrush(Actor self, Actor crusher, HashSet crushClasses) { + if (!CrushableInner(crushClasses, crusher.Owner)) + return; + var mobile = self.TraitOrDefault(); if (mobile != null && self.World.SharedRandom.Next(100) <= info.WarnProbability) mobile.Nudge(self, crusher, true); } - public void OnCrush(Actor crusher) + void INotifyCrushed.OnCrush(Actor self, Actor crusher, HashSet crushClasses) { - Game.Sound.Play(info.CrushSound, crusher.CenterPosition); - var wda = self.TraitsImplementing() - .FirstOrDefault(s => s.Info.CrushedSequence != null); - if (wda != null) - { - var palette = wda.Info.CrushedSequencePalette; - if (wda.Info.CrushedPaletteIsPlayerPalette) - palette += self.Owner.InternalName; + if (!CrushableInner(crushClasses, crusher.Owner)) + return; - wda.SpawnDeathAnimation(self, wda.Info.CrushedSequence, palette); - } + Game.Sound.Play(info.CrushSound, crusher.CenterPosition); self.Kill(crusher); } - public bool CrushableBy(HashSet crushClasses, Player crushOwner) + bool ICrushable.CrushableBy(Actor self, Actor crusher, HashSet crushClasses) + { + return CrushableInner(crushClasses, crusher.Owner); + } + + bool CrushableInner(HashSet crushClasses, Player crushOwner) { // Only make actor crushable if it is on the ground. if (!self.IsAtGroundLevel()) diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs index 46df88d34c..11c6aeacf5 100644 --- a/OpenRA.Mods.Common/Traits/Mobile.cs +++ b/OpenRA.Mods.Common/Traits/Mobile.cs @@ -260,7 +260,7 @@ namespace OpenRA.Mods.Common.Traits foreach (var crushable in crushables) { lacksCrushability = false; - if (!crushable.CrushableBy(Crushes, self.Owner)) + if (!crushable.CrushableBy(otherActor, self, Crushes)) return true; } @@ -594,10 +594,13 @@ namespace OpenRA.Mods.Common.Traits if (!self.IsAtGroundLevel()) return; - var crushables = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self) - .SelectMany(a => a.TraitsImplementing().Where(b => b.CrushableBy(Info.Crushes, self.Owner))); - foreach (var crushable in crushables) - crushable.WarnCrush(self); + var actors = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self).ToList(); + if (!AnyCrushables(actors)) + return; + + var notifiers = actors.SelectMany(a => a.TraitsImplementing().Select(t => new TraitPair(a, t))); + foreach (var notifyCrushed in notifiers) + notifyCrushed.Trait.WarnCrush(notifyCrushed.Actor, self, Info.Crushes); } public void FinishedMoving(Actor self) @@ -606,10 +609,26 @@ namespace OpenRA.Mods.Common.Traits if (!self.IsAtGroundLevel()) return; - var crushables = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self) - .SelectMany(a => a.TraitsImplementing().Where(c => c.CrushableBy(Info.Crushes, self.Owner))); - foreach (var crushable in crushables) - crushable.OnCrush(self); + var actors = self.World.ActorMap.GetActorsAt(ToCell).Where(a => a != self).ToList(); + if (!AnyCrushables(actors)) + return; + + var notifiers = actors.SelectMany(a => a.TraitsImplementing().Select(t => new TraitPair(a, t))); + foreach (var notifyCrushed in notifiers) + notifyCrushed.Trait.OnCrush(notifyCrushed.Actor, self, Info.Crushes); + } + + bool AnyCrushables(List actors) + { + var crushables = actors.SelectMany(a => a.TraitsImplementing().Select(t => new TraitPair(a, t))).ToList(); + if (crushables.Count == 0) + return false; + + foreach (var crushes in crushables) + if (!crushes.Trait.CrushableBy(crushes.Actor, self, Info.Crushes)) + return false; + + return true; } public int MovementSpeedForCell(Actor self, CPos cell) diff --git a/OpenRA.Mods.Common/Traits/Render/WithDeathAnimation.cs b/OpenRA.Mods.Common/Traits/Render/WithDeathAnimation.cs index de39214c40..51239d295a 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithDeathAnimation.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithDeathAnimation.cs @@ -45,6 +45,9 @@ namespace OpenRA.Mods.Common.Traits "Is only used if UseDeathTypeSuffix is `True`.")] public readonly Dictionary DeathTypes = new Dictionary(); + [Desc("Sequence to use when the actor is killed by some non-standard means (e.g. suicide).")] + [SequenceReference] public readonly string FallbackSequence = null; + public static object LoadDeathTypes(MiniYaml yaml) { var md = yaml.ToDictionary(); @@ -57,10 +60,11 @@ namespace OpenRA.Mods.Common.Traits public object Create(ActorInitializer init) { return new WithDeathAnimation(init.Self, this); } } - public class WithDeathAnimation : INotifyKilled + public class WithDeathAnimation : INotifyKilled, INotifyCrushed { public readonly WithDeathAnimationInfo Info; readonly RenderSprites rs; + bool crushed; public WithDeathAnimation(Actor self, WithDeathAnimationInfo info) { @@ -70,11 +74,23 @@ namespace OpenRA.Mods.Common.Traits public void Killed(Actor self, AttackInfo e) { - // Killed by some non-standard means. This includes being crushed - // by a vehicle (Actors with Crushable trait will spawn CrushedSequence instead). - if (e.Warhead == null || !(e.Warhead is DamageWarhead)) + // Actors with Crushable trait will spawn CrushedSequence. + if (crushed) return; + var palette = Info.DeathSequencePalette; + if (Info.DeathPaletteIsPlayerPalette) + palette += self.Owner.InternalName; + + // Killed by some non-standard means + if (e.Warhead == null || !(e.Warhead is DamageWarhead)) + { + if (Info.FallbackSequence != null) + SpawnDeathAnimation(self, self.CenterPosition, rs.GetImage(self), Info.FallbackSequence, palette); + + return; + } + var sequence = Info.DeathSequence; if (Info.UseDeathTypeSuffix) { @@ -86,20 +102,28 @@ namespace OpenRA.Mods.Common.Traits sequence += Info.DeathTypes[damageType]; } - var palette = Info.DeathSequencePalette; - if (Info.DeathPaletteIsPlayerPalette) - palette += self.Owner.InternalName; - - SpawnDeathAnimation(self, sequence, palette); + SpawnDeathAnimation(self, self.CenterPosition, rs.GetImage(self), sequence, palette); } - public void SpawnDeathAnimation(Actor self, string sequence, string palette) + public void SpawnDeathAnimation(Actor self, WPos pos, string image, string sequence, string palette) { - self.World.AddFrameEndTask(w => - { - if (!self.Disposed) - w.Add(new Corpse(w, self.CenterPosition, rs.GetImage(self), sequence, palette)); - }); + self.World.AddFrameEndTask(w => w.Add(new Corpse(w, pos, image, sequence, palette))); } + + void INotifyCrushed.OnCrush(Actor self, Actor crusher, HashSet crushClasses) + { + crushed = true; + + if (Info.CrushedSequence == null) + return; + + var crushPalette = Info.CrushedSequencePalette; + if (Info.CrushedPaletteIsPlayerPalette) + crushPalette += self.Owner.InternalName; + + SpawnDeathAnimation(self, self.CenterPosition, rs.GetImage(self), Info.CrushedSequence, crushPalette); + } + + void INotifyCrushed.WarnCrush(Actor self, Actor crusher, HashSet crushClasses) { } } } diff --git a/OpenRA.Mods.RA/Traits/Mine.cs b/OpenRA.Mods.RA/Traits/Mine.cs index caed682f1b..560492e69f 100644 --- a/OpenRA.Mods.RA/Traits/Mine.cs +++ b/OpenRA.Mods.RA/Traits/Mine.cs @@ -20,24 +20,25 @@ namespace OpenRA.Mods.RA.Traits public readonly bool AvoidFriendly = true; public readonly HashSet DetonateClasses = new HashSet(); - public object Create(ActorInitializer init) { return new Mine(init, this); } + public object Create(ActorInitializer init) { return new Mine(this); } } - class Mine : ICrushable + class Mine : ICrushable, INotifyCrushed { - readonly Actor self; readonly MineInfo info; - public Mine(ActorInitializer init, MineInfo info) + public Mine(MineInfo info) { - self = init.Self; this.info = info; } - public void WarnCrush(Actor crusher) { } + void INotifyCrushed.WarnCrush(Actor self, Actor crusher, HashSet crushClasses) { } - public void OnCrush(Actor crusher) + void INotifyCrushed.OnCrush(Actor self, Actor crusher, HashSet crushClasses) { + if (!info.CrushClasses.Overlaps(crushClasses)) + return; + if (crusher.Info.HasTraitInfo() || (self.Owner.Stances[crusher.Owner] == Stance.Ally && info.AvoidFriendly)) return; @@ -48,7 +49,7 @@ namespace OpenRA.Mods.RA.Traits self.Kill(crusher); } - public bool CrushableBy(HashSet crushClasses, Player owner) + bool ICrushable.CrushableBy(Actor self, Actor crusher, HashSet crushClasses) { return info.CrushClasses.Overlaps(crushClasses); }