166 lines
5.3 KiB
C#
166 lines
5.3 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2022 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.GameRules;
|
|
using OpenRA.Primitives;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
public enum ExplosionType { Footprint, CenterPosition }
|
|
|
|
public enum DamageSource { Self, Killer }
|
|
|
|
[Desc("This actor explodes when killed.")]
|
|
public class ExplodesInfo : ConditionalTraitInfo, Requires<IHealthInfo>
|
|
{
|
|
[WeaponReference]
|
|
[FieldLoader.Require]
|
|
[Desc("Default weapon to use for explosion if ammo/payload is loaded.")]
|
|
public readonly string Weapon = null;
|
|
|
|
[WeaponReference]
|
|
[Desc("Fallback weapon to use for explosion if empty (no ammo/payload).")]
|
|
public readonly string EmptyWeapon = "UnitExplode";
|
|
|
|
[Desc("Chance that the explosion will use Weapon instead of EmptyWeapon when exploding, provided the actor has ammo/payload.")]
|
|
public readonly int LoadedChance = 100;
|
|
|
|
[Desc("Chance that this actor will explode at all.")]
|
|
public readonly int Chance = 100;
|
|
|
|
[Desc("Health level at which actor will explode.")]
|
|
public readonly int DamageThreshold = 0;
|
|
|
|
[Desc("DeathType(s) that trigger the explosion. Leave empty to always trigger an explosion.")]
|
|
public readonly BitSet<DamageType> DeathTypes = default(BitSet<DamageType>);
|
|
|
|
[Desc("Who is counted as source of damage for explosion.",
|
|
"Possible values are Self and Killer.")]
|
|
public readonly DamageSource DamageSource = DamageSource.Self;
|
|
|
|
[Desc("Possible values are CenterPosition (explosion at the actors' center) and ",
|
|
"Footprint (explosion on each occupied cell).")]
|
|
public readonly ExplosionType Type = ExplosionType.CenterPosition;
|
|
|
|
[Desc("Offset of the explosion from the center of the exploding actor (or cell).")]
|
|
public readonly WVec Offset = WVec.Zero;
|
|
|
|
public WeaponInfo WeaponInfo { get; private set; }
|
|
public WeaponInfo EmptyWeaponInfo { get; private set; }
|
|
|
|
public override object Create(ActorInitializer init) { return new Explodes(this, init.Self); }
|
|
public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
|
|
{
|
|
if (!string.IsNullOrEmpty(Weapon))
|
|
{
|
|
var weaponToLower = Weapon.ToLowerInvariant();
|
|
if (!rules.Weapons.TryGetValue(weaponToLower, out var weapon))
|
|
throw new YamlException($"Weapons Ruleset does not contain an entry '{weaponToLower}'");
|
|
WeaponInfo = weapon;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(EmptyWeapon))
|
|
{
|
|
var emptyWeaponToLower = EmptyWeapon.ToLowerInvariant();
|
|
if (!rules.Weapons.TryGetValue(emptyWeaponToLower, out var emptyWeapon))
|
|
throw new YamlException($"Weapons Ruleset does not contain an entry '{emptyWeaponToLower}'");
|
|
EmptyWeaponInfo = emptyWeapon;
|
|
}
|
|
|
|
base.RulesetLoaded(rules, ai);
|
|
}
|
|
}
|
|
|
|
public class Explodes : ConditionalTrait<ExplodesInfo>, INotifyKilled, INotifyDamage
|
|
{
|
|
readonly IHealth health;
|
|
BuildingInfo buildingInfo;
|
|
Armament[] armaments;
|
|
|
|
public Explodes(ExplodesInfo info, Actor self)
|
|
: base(info)
|
|
{
|
|
health = self.Trait<IHealth>();
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
buildingInfo = self.Info.TraitInfoOrDefault<BuildingInfo>();
|
|
armaments = self.TraitsImplementing<Armament>().ToArray();
|
|
|
|
base.Created(self);
|
|
}
|
|
|
|
void INotifyKilled.Killed(Actor self, AttackInfo e)
|
|
{
|
|
if (IsTraitDisabled || !self.IsInWorld)
|
|
return;
|
|
|
|
if (self.World.SharedRandom.Next(100) > Info.Chance)
|
|
return;
|
|
|
|
if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes))
|
|
return;
|
|
|
|
var weapon = ChooseWeaponForExplosion(self);
|
|
if (weapon == null)
|
|
return;
|
|
|
|
var source = Info.DamageSource == DamageSource.Self ? self : e.Attacker;
|
|
if (weapon.Report != null && weapon.Report.Length > 0)
|
|
Game.Sound.Play(SoundType.World, weapon.Report, self.World, self.CenterPosition);
|
|
|
|
if (Info.Type == ExplosionType.Footprint && buildingInfo != null)
|
|
{
|
|
var cells = buildingInfo.OccupiedTiles(self.Location);
|
|
foreach (var c in cells)
|
|
weapon.Impact(Target.FromPos(self.World.Map.CenterOfCell(c) + Info.Offset), source);
|
|
|
|
return;
|
|
}
|
|
|
|
// Use .FromPos since this actor is killed. Cannot use Target.FromActor
|
|
weapon.Impact(Target.FromPos(self.CenterPosition + Info.Offset), source);
|
|
}
|
|
|
|
WeaponInfo ChooseWeaponForExplosion(Actor self)
|
|
{
|
|
if (armaments.Length == 0)
|
|
return Info.WeaponInfo;
|
|
else if (self.World.SharedRandom.Next(100) > Info.LoadedChance)
|
|
return Info.EmptyWeaponInfo;
|
|
|
|
// PERF: Avoid LINQ
|
|
foreach (var a in armaments)
|
|
if (!a.IsReloading)
|
|
return Info.WeaponInfo;
|
|
|
|
return Info.EmptyWeaponInfo;
|
|
}
|
|
|
|
void INotifyDamage.Damaged(Actor self, AttackInfo e)
|
|
{
|
|
if (Info.DamageThreshold == 0 || IsTraitDisabled || !self.IsInWorld)
|
|
return;
|
|
|
|
if (!Info.DeathTypes.IsEmpty && !e.Damage.DamageTypes.Overlaps(Info.DeathTypes))
|
|
return;
|
|
|
|
// Cast to long to avoid overflow when multiplying by the health
|
|
var source = Info.DamageSource == DamageSource.Self ? self : e.Attacker;
|
|
if (health.HP * 100L < Info.DamageThreshold * (long)health.MaxHP)
|
|
self.World.AddFrameEndTask(w => self.Kill(source, e.Damage.DamageTypes));
|
|
}
|
|
}
|
|
}
|