diff --git a/OpenRA.Mods.RA2/Activities/Infect.cs b/OpenRA.Mods.RA2/Activities/Infect.cs new file mode 100644 index 000000000..2145abc2e --- /dev/null +++ b/OpenRA.Mods.RA2/Activities/Infect.cs @@ -0,0 +1,162 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.RA2.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA2.Activities +{ + class Infect : Enter + { + readonly AttackInfect attackInfect; + readonly Target target; + + bool isPlayingInfectAnimation; + + public Infect(Actor self, Target target, AttackInfect attackInfect, Color? targetLineColor) + : base(self, target, targetLineColor) + { + this.target = target; + this.attackInfect = attackInfect; + } + + protected override void OnFirstRun(Actor self) + { + attackInfect.IsAiming = true; + } + + protected override void OnLastRun(Actor self) + { + attackInfect.IsAiming = false; + } + + protected override void OnEnterComplete(Actor self, Actor targetActor) + { + self.World.AddFrameEndTask(w => + { + if (self.IsDead || attackInfect.IsTraitDisabled) + return; + + if (isPlayingInfectAnimation) + { + attackInfect.RevokeJoustCondition(self); + isPlayingInfectAnimation = false; + } + + attackInfect.DoAttack(self, target); + + var infectable = targetActor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return; + + w.Remove(self); + + infectable.Infector = self; + infectable.AttackInfect = attackInfect; + + infectable.FirepowerMultipliers = self.TraitsImplementing() + .Select(a => a.GetFirepowerModifier()).ToArray(); + + var info = attackInfect.InfectInfo; + infectable.Ticks = info.DamageInterval; + infectable.GrantCondition(targetActor); + infectable.RevokeCondition(targetActor, self); + }); + } + + void CancelInfection(Actor self) + { + if (isPlayingInfectAnimation) + { + attackInfect.RevokeJoustCondition(self); + isPlayingInfectAnimation = false; + } + + if (target.Type != TargetType.Actor) + return; + + if (target.Actor.IsDead) + return; + + var infectable = target.Actor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return; + + infectable.RevokeCondition(target.Actor, self); + } + + bool IsValidInfection(Actor self, Actor targetActor) + { + if (attackInfect.IsTraitDisabled) + return false; + + if (targetActor.IsDead) + return false; + + if (!target.IsValidFor(self) || !attackInfect.HasAnyValidWeapons(target)) + return false; + + var infectable = targetActor.TraitOrDefault(); + if (infectable == null || infectable.IsTraitDisabled || infectable.Infector != null) + return false; + + return true; + } + + bool CanStartInfect(Actor self, Actor targetActor) + { + if (!IsValidInfection(self, targetActor)) + return false; + + // IsValidInfection validated the lookup, no need to check here. + var infectable = targetActor.Trait(); + return infectable.TryStartInfecting(targetActor, self); + } + + protected override bool TryStartEnter(Actor self, Actor targetActor) + { + var canStartInfect = CanStartInfect(self, targetActor); + if (!canStartInfect) + { + CancelInfection(self); + Cancel(self, true); + } + + // Can't leap yet + if (attackInfect.Armaments.All(a => a.IsReloading)) + return false; + + return true; + } + + protected override void TickInner(Actor self, in Target target, bool targetIsDeadOrHiddenActor) + { + if (target.Type != TargetType.Actor || !IsValidInfection(self, target.Actor)) + { + CancelInfection(self); + Cancel(self, true); + return; + } + + var info = attackInfect.InfectInfo; + if (!isPlayingInfectAnimation && !IsCanceling && (self.CenterPosition - target.CenterPosition).Length < info.JumpRange.Length) + { + isPlayingInfectAnimation = true; + attackInfect.GrantJoustCondition(self); + IsInterruptible = false; + } + } + } +} diff --git a/OpenRA.Mods.RA2/Traits/AttackInfect.cs b/OpenRA.Mods.RA2/Traits/AttackInfect.cs new file mode 100644 index 000000000..65835b05a --- /dev/null +++ b/OpenRA.Mods.RA2/Traits/AttackInfect.cs @@ -0,0 +1,87 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.RA2.Activities; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA2.Traits +{ + [Desc("Move onto the target then execute the attack.")] + public class AttackInfectInfo : AttackFrontalInfo, Requires + { + [Desc("Range of the final jump of the infector.")] + public readonly WDist JumpRange = WDist.Zero; + + [Desc("Conditions that last from start of the joust until the attack.")] + [GrantedConditionReference] + public readonly string JumpCondition = "jumping"; + + [FieldLoader.Require] + [Desc("How much damage to deal.")] + public readonly int Damage = 0; + + [FieldLoader.Require] + [Desc("How often to deal the damage.")] + public readonly int DamageInterval = 0; + + [Desc("Damage types for the infection damage.")] + public readonly BitSet DamageTypes = default(BitSet); + + [Desc("Damage types which allows the infector survive when it's host dies.")] + public readonly BitSet SurviveHostDamageTypes = default(BitSet); + + public override object Create(ActorInitializer init) { return new AttackInfect(init.Self, this); } + } + + public class AttackInfect : AttackFrontal + { + public readonly AttackInfectInfo InfectInfo; + + int joustToken = Actor.InvalidConditionToken; + + public AttackInfect(Actor self, AttackInfectInfo info) + : base(self, info) + { + InfectInfo = info; + } + + protected override bool CanAttack(Actor self, in Target target) + { + if (target.Type != TargetType.Actor) + return false; + + if (self.Location == target.Actor.Location && HasAnyValidWeapons(target)) + return true; + + return base.CanAttack(self, target); + } + + public void GrantJoustCondition(Actor self) + { + if (!string.IsNullOrEmpty(InfectInfo.JumpCondition)) + joustToken = self.GrantCondition(InfectInfo.JumpCondition); + } + + public void RevokeJoustCondition(Actor self) + { + if (joustToken != Actor.InvalidConditionToken) + joustToken = self.RevokeCondition(joustToken); + } + + public override Activity GetAttackActivity(Actor self, AttackSource source, in Target newTarget, bool allowMove, bool forceAttack, Color? targetLineColor) + { + return new Infect(self, newTarget, this, targetLineColor); + } + } +} diff --git a/OpenRA.Mods.RA2/Traits/Infectable.cs b/OpenRA.Mods.RA2/Traits/Infectable.cs new file mode 100644 index 000000000..d35f02242 --- /dev/null +++ b/OpenRA.Mods.RA2/Traits/Infectable.cs @@ -0,0 +1,198 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA2.Traits +{ + [Desc("Handle infection by infector units.")] + public class InfectableInfo : ConditionalTraitInfo, Requires + { + [Desc("Damage types that removes the infector.")] + public readonly BitSet RemoveInfectorDamageTypes = default(BitSet); + + [Desc("Damage types that kills the infector.")] + public readonly BitSet KillInfectorDamageTypes = default(BitSet); + + [GrantedConditionReference] + [Desc("The condition to grant to self while infected by any actor.")] + public readonly string InfectedCondition = null; + + [GrantedConditionReference] + [Desc("Condition granted when being infected by another actor.")] + public readonly string BeingInfectedCondition = null; + + [Desc("Conditions to grant when infected by specified actors.", + "A dictionary of [actor id]: [condition].")] + public readonly Dictionary InfectedByConditions = new Dictionary(); + + [GrantedConditionReference] + public IEnumerable LinterConditions { get { return InfectedByConditions.Values; } } + + public override object Create(ActorInitializer init) { return new Infectable(init.Self, this); } + } + + public class Infectable : ConditionalTrait, ISync, ITick, INotifyCreated, INotifyDamage, INotifyKilled, IRemoveInfector + { + readonly Health health; + + public Actor Infector; + public AttackInfect AttackInfect; + + public int[] FirepowerMultipliers = new int[] { }; + + [Sync] + public int Ticks; + + int beingInfectedToken = Actor.InvalidConditionToken; + Actor enteringInfector; + int infectedToken = Actor.InvalidConditionToken; + int infectedByToken = Actor.InvalidConditionToken; + + public Infectable(Actor self, InfectableInfo info) + : base(info) + { + health = self.Trait(); + } + + public bool TryStartInfecting(Actor self, Actor infector) + { + if (infector != null) + { + if (enteringInfector == null) + { + enteringInfector = infector; + + if (beingInfectedToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.BeingInfectedCondition)) + beingInfectedToken = self.GrantCondition(Info.BeingInfectedCondition); + + return true; + } + } + + return false; + } + + public void GrantCondition(Actor self) + { + if (infectedToken == Actor.InvalidConditionToken && !string.IsNullOrEmpty(Info.InfectedCondition)) + infectedToken = self.GrantCondition(Info.InfectedCondition); + + string infectedByCondition; + if (Info.InfectedByConditions.TryGetValue(Infector.Info.Name, out infectedByCondition)) + infectedByToken = self.GrantCondition(infectedByCondition); + } + + public void RevokeCondition(Actor self, Actor infector = null) + { + if (infector != null) + { + if (enteringInfector == infector) + { + enteringInfector = null; + + if (beingInfectedToken != Actor.InvalidConditionToken) + beingInfectedToken = self.RevokeCondition(beingInfectedToken); + } + } + else + { + if (infectedToken != Actor.InvalidConditionToken) + infectedToken = self.RevokeCondition(infectedToken); + + if (infectedByToken != Actor.InvalidConditionToken) + infectedByToken = self.RevokeCondition(infectedByToken); + } + } + + void RemoveInfector(Actor self, bool kill, AttackInfo info) + { + if (Infector != null && !Infector.IsDead) + { + var positionable = Infector.TraitOrDefault(); + if (positionable != null) + positionable.SetPosition(Infector, self.CenterPosition); + + self.World.AddFrameEndTask(w => + { + if (Infector == null || Infector.IsDead) + return; + + w.Add(Infector); + + if (kill) + { + if (info != null) + Infector.Kill(info.Attacker, info.Damage.DamageTypes); + else + Infector.Kill(self); + } + else + { + var mobile = Infector.TraitOrDefault(); + if (mobile != null) + mobile.Nudge(Infector); + } + + RevokeCondition(self); + Infector = null; + FirepowerMultipliers = new int[] { }; + }); + } + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (Infector != null) + { + var info = AttackInfect.InfectInfo; + if (e.Damage.DamageTypes.Overlaps(Info.KillInfectorDamageTypes)) + RemoveInfector(self, true, e); + else if (e.Damage.DamageTypes.Overlaps(Info.RemoveInfectorDamageTypes)) + RemoveInfector(self, false, e); + } + } + + void INotifyKilled.Killed(Actor self, AttackInfo e) + { + if (Infector != null) + { + var info = AttackInfect.InfectInfo; + var kill = !info.SurviveHostDamageTypes.Overlaps(e.Damage.DamageTypes); + RemoveInfector(self, kill, e); + } + } + + void ITick.Tick(Actor self) + { + if (!IsTraitDisabled && Infector != null) + { + if (--Ticks < 0) + { + var info = AttackInfect.InfectInfo; + var damage = Util.ApplyPercentageModifiers(info.Damage, FirepowerMultipliers); + health.InflictDamage(self, Infector, new Damage(damage, info.DamageTypes), false); + + Ticks = info.DamageInterval; + } + } + } + + void IRemoveInfector.RemoveInfector(Actor self, bool kill, AttackInfo e) + { + RemoveInfector(self, kill, e); + } + } +} diff --git a/OpenRA.Mods.RA2/TraitsInterfaces.cs b/OpenRA.Mods.RA2/TraitsInterfaces.cs new file mode 100644 index 000000000..d619acdaf --- /dev/null +++ b/OpenRA.Mods.RA2/TraitsInterfaces.cs @@ -0,0 +1,21 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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 OpenRA.Traits; + +namespace OpenRA.Mods.RA2.Traits +{ + [RequireExplicitImplementation] + public interface IRemoveInfector + { + void RemoveInfector(Actor self, bool kill, AttackInfo e = null); + } +} diff --git a/mods/ra2/rules/allied-structures.yaml b/mods/ra2/rules/allied-structures.yaml index ec99eb905..7a41371a0 100644 --- a/mods/ra2/rules/allied-structures.yaml +++ b/mods/ra2/rules/allied-structures.yaml @@ -618,6 +618,7 @@ gadept: IsPlayerPalette: false RepairsUnits: Interval: 50 + RepairDamageTypes: KillTerrorDrone StartRepairingNotification: Repairing FinishRepairingNotification: UnitRepaired WithIdleOverlay@side: diff --git a/mods/ra2/rules/defaults.yaml b/mods/ra2/rules/defaults.yaml index 2c4685bc2..e69411ea6 100644 --- a/mods/ra2/rules/defaults.yaml +++ b/mods/ra2/rules/defaults.yaml @@ -566,6 +566,7 @@ Inherits@5: ^CrateStatModifiers Inherits@6: ^DamagedByRadiation Inherits@handicaps: ^PlayerHandicaps + Inherits@7: ^AffectedByTerrorDrone Health: OwnerLostAction: Action: Kill @@ -650,6 +651,7 @@ Categories: Infantry ^Parachutable: + Inherits: ^ParachutableAffectedByTerrorDrone WithSpriteBody@Parachute: Name: parachute Sequence: paradrop @@ -683,6 +685,7 @@ Condition: parachute ^ParachutableVehicle: + Inherits: ^ParachutableAffectedByTerrorDrone Parachutable: FallRate: 26 KilledOnImpassableTerrain: true @@ -702,6 +705,15 @@ Targetable@airborne: TargetTypes: Air RequiresCondition: parachute + Targetable@Parasiteable: + TargetTypes: DroneParasiteable + RequiresCondition: !parachute && !infected + Targetable@byTerrorDrone: + TargetTypes: ValidForTerrorDrone + RequiresCondition: !parachute + Targetable@InfectedByTerrorDrone: + TargetTypes: TerrorDroned + RequiresCondition: !parachute && infected ExternalCondition@PARACHUTE: Condition: parachute @@ -824,6 +836,7 @@ Inherits@5: ^CrateStatModifiers Inherits@6: ^ParachutableVehicle Inherits@handicaps: ^PlayerHandicaps + Inherits@7: ^AffectedByTerrorDrone OwnerLostAction: Action: Kill DeathTypes: BulletDeath @@ -1233,3 +1246,32 @@ MapEditorData: Categories: System Interactable: + +^AffectedByTerrorDrone: + Targetable@Parasiteable: + TargetTypes: DroneParasiteable + RequiresCondition: !infected + Targetable@byTerrorDrone: + TargetTypes: ValidForTerrorDrone + Targetable@InfectedByTerrorDrone: + TargetTypes: TerrorDroned + RequiresCondition: infected + Infectable: + RemoveInfectorDamageTypes: DropTerrorDrone + KillInfectorDamageTypes: KillTerrorDrone + BeingInfectedCondition: being-infected + InfectedCondition: infected + SpeedMultiplier@Infected: + Modifier: 0 + RequiresCondition: being-infected + +^ParachutableAffectedByTerrorDrone: + Targetable@Parasiteable: + TargetTypes: DroneParasiteable + RequiresCondition: !parachute && !infected + Targetable@byTerrorDrone: + TargetTypes: ValidForTerrorDrone + RequiresCondition: !parachute + Targetable@InfectedByTerrorDrone: + TargetTypes: TerrorDroned + RequiresCondition: !parachute && infected diff --git a/mods/ra2/rules/soviet-structures.yaml b/mods/ra2/rules/soviet-structures.yaml index 678cba509..1143b6bec 100644 --- a/mods/ra2/rules/soviet-structures.yaml +++ b/mods/ra2/rules/soviet-structures.yaml @@ -569,6 +569,7 @@ nadept: RepairsUnits: Interval: 148 HpPerStep: 20 + RepairDamageTypes: KillTerrorDrone StartRepairingNotification: Repairing FinishRepairingNotification: UnitRepaired WithIdleOverlay@normal: diff --git a/mods/ra2/rules/soviet-vehicles.yaml b/mods/ra2/rules/soviet-vehicles.yaml index e97d08663..0d9a0216d 100644 --- a/mods/ra2/rules/soviet-vehicles.yaml +++ b/mods/ra2/rules/soviet-vehicles.yaml @@ -140,17 +140,24 @@ dron: -RenderVoxels: WithInfantryBody: StandSequences: stand - DefaultAttackSequence: shoot + DefaultAttackSequence: jump + RequiresCondition: !jumping + WithFacingSpriteBody: + Sequence: shoot + RequiresCondition: jumping Armament: Weapon: DroneJump - ReloadingCondition: attack-cooldown - AttackLeap: + AttackInfect: Voice: Attack - PauseOnCondition: attacking || attack-cooldown + Damage: 25 + DamageInterval: 20 + DamageTypes: DefaultDeath, BulletDeath, DroneSurvive + SurviveHostDamageTypes: DroneSurvive + JumpRange: 1c768 AutoTarget: InitialStance: AttackAnything AutoTargetPriority@DEFAULT: - ValidTargets: Infantry + ValidTargets: DroneParasiteable Voiced: VoiceSet: TerrorDroneVoice HitShape: diff --git a/mods/ra2/rules/tech-structures.yaml b/mods/ra2/rules/tech-structures.yaml index 77a098cdf..e90ee3d7d 100644 --- a/mods/ra2/rules/tech-structures.yaml +++ b/mods/ra2/rules/tech-structures.yaml @@ -160,6 +160,7 @@ caoutp: IsPlayerPalette: false RepairsUnits: Interval: 50 + RepairDamageTypes: KillTerrorDrone FinishRepairingNotification: UnitRepaired WithIdleOverlay@tower: Sequence: idle-tower diff --git a/mods/ra2/weapons/melee.yaml b/mods/ra2/weapons/melee.yaml index 54bc3a280..9c841d862 100644 --- a/mods/ra2/weapons/melee.yaml +++ b/mods/ra2/weapons/melee.yaml @@ -23,8 +23,29 @@ DogJaw: DamageTypes: BulletDeath DroneJump: - Inherits: DogJaw + ValidTargets: DroneParasiteable + ReloadDelay: 10 + Range: 3c0 Report: vteratta.wav + Projectile: InstantHit + Warhead@1Dam: SpreadDamage + Spread: 64 + Falloff: 100, 0 + Damage: 10000 + ValidTargets: ValidForTerrorDrone + Versus: + None: 100 + Flak: 100 + Plate: 100 + Light: 0 + Medium: 0 + Heavy: 0 + Wood: 0 + Steel: 0 + Concrete: 0 + Drone: 0 + Rocket: 0 + DamageTypes: AltBulletDeath, DefaultDeath, DroneSurvive AlligatorBite: ReloadDelay: 30 diff --git a/mods/ra2/weapons/misc.yaml b/mods/ra2/weapons/misc.yaml index b889244ac..6cb4708df 100644 --- a/mods/ra2/weapons/misc.yaml +++ b/mods/ra2/weapons/misc.yaml @@ -53,6 +53,7 @@ RepairBullet: Spread: 213 Damage: -50 ValidTargets: Repair + DamageTypes: KillTerrorDrone MindControl: ReloadDelay: 200