824 lines
37 KiB
C#
824 lines
37 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Barotrauma.MapCreatures.Behavior;
|
|
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
/// <summary>
|
|
/// Explosions are area of effect attacks that can damage characters, items and structures.
|
|
/// </summary>
|
|
/// <doc>
|
|
/// <Field Identifier="showEffects" Type="bool" DefaultValue="true">
|
|
/// Used to enable all particle effects without having to specify them one by one.
|
|
/// </Field>
|
|
/// </doc>
|
|
partial class Explosion
|
|
{
|
|
public readonly Attack Attack;
|
|
|
|
/// <summary>
|
|
/// How much force the explosion applies to the characters.
|
|
/// </summary>
|
|
private readonly float force;
|
|
|
|
/// <summary>
|
|
/// Intensity of the screen shake effect.
|
|
/// </summary>
|
|
/// <doc>
|
|
/// <override type="DefaultValue">
|
|
/// 10% of the range if showEffects is true, 0 otherwise.
|
|
/// </override>
|
|
/// </doc>
|
|
public float CameraShake { get; set; }
|
|
|
|
/// <summary>
|
|
/// How far away does the camera shake effect reach.
|
|
/// </summary>
|
|
/// <doc>
|
|
/// <override type="DefaultValue">
|
|
/// Same as attack range if showEffects is true, 0 otherwise.
|
|
/// </override>
|
|
/// </doc>
|
|
public float CameraShakeRange { get; set; }
|
|
|
|
/// <summary>
|
|
/// Color tint to apply to the player's screen when in range of the explosion.
|
|
/// </summary>
|
|
private readonly Color screenColor;
|
|
|
|
/// <summary>
|
|
/// How far away can the screen color effect be seen.
|
|
/// </summary>
|
|
/// <doc>
|
|
/// <override type="DefaultValue">
|
|
/// 10% of the range if showEffects is true, 0 otherwise.
|
|
/// </override>
|
|
/// </doc>
|
|
private readonly float screenColorRange;
|
|
|
|
/// <summary>
|
|
/// How long the screen color effect lasts.
|
|
/// </summary>
|
|
private readonly float screenColorDuration;
|
|
|
|
/// <summary>
|
|
/// Whether a spark particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool sparks;
|
|
|
|
/// <summary>
|
|
/// Whether a shockwave particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool shockwave;
|
|
|
|
/// <summary>
|
|
/// Whether a flame particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool flames;
|
|
|
|
/// <summary>
|
|
/// Whether a smoke particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool smoke;
|
|
|
|
/// <summary>
|
|
/// Whether a flash effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool flash;
|
|
|
|
/// <summary>
|
|
/// Whether a debris particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool debris;
|
|
|
|
/// <summary>
|
|
/// Whether a underwater bubble particle effect is created when the explosion happens.
|
|
/// </summary>
|
|
private bool underwaterBubble;
|
|
|
|
/// <summary>
|
|
/// Color of the light source created by the explosion.
|
|
/// </summary>
|
|
private readonly Color flashColor;
|
|
|
|
/// <summary>
|
|
/// Whether the explosion plays a tinnitus sound to players who get hit by it.
|
|
/// </summary>
|
|
private readonly bool playTinnitus;
|
|
|
|
/// <summary>
|
|
/// Whether the explosion executes 'OnFire' status effects on the items it hits.
|
|
/// </summary>
|
|
/// <doc>
|
|
/// <override type="DefaultValue">
|
|
/// true if showEffects is true and flames haven't been explicitly set to false, false otherwise.
|
|
/// </override>
|
|
/// </doc>
|
|
private readonly bool applyFireEffects;
|
|
|
|
/// <summary>
|
|
/// List of item tags that the explosion ignores when applying fire effects.
|
|
/// </summary>
|
|
private readonly Identifier[] ignoreFireEffectsForTags;
|
|
|
|
/// <summary>
|
|
/// When set to true, the explosion don't deal less damage when the target is behind a solid object.
|
|
/// </summary>
|
|
public bool IgnoreCover { get; set; }
|
|
|
|
/// <summary>
|
|
/// Does the damage from the explosion decrease with distance from the origin of the explosion?
|
|
/// </summary>
|
|
public bool DistanceFalloff { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Structures that don't count as "cover" that reduces damage from the explosion. Only relevant if IgnoreCover is set to false.
|
|
/// </summary>
|
|
public IEnumerable<Structure> IgnoredCover;
|
|
|
|
/// <summary>
|
|
/// How long the light source created by the explosion lasts.
|
|
/// </summary>
|
|
private readonly float flashDuration;
|
|
|
|
/// <summary>
|
|
/// How large the light source created by the explosion is.
|
|
/// </summary>
|
|
private readonly float? flashRange;
|
|
|
|
/// <summary>
|
|
/// Identifier of the decal the explosion creates on the background structure it explodes over.
|
|
/// Set to empty string to disable.
|
|
/// </summary>
|
|
private readonly string decal;
|
|
|
|
/// <summary>
|
|
/// Relative size of the decal created by the explosion.
|
|
/// </summary>
|
|
private readonly float decalSize;
|
|
|
|
/// <summary>
|
|
/// Whether the explosion only affects characters inside a submarine.
|
|
/// </summary>
|
|
public bool OnlyInside;
|
|
|
|
/// <summary>
|
|
/// Whether the explosion only affects characters outside a submarine.
|
|
/// </summary>
|
|
public bool OnlyOutside;
|
|
|
|
/// <summary>
|
|
/// Should the normal damage sounds be played when the explosion damages something. Usually disabled.
|
|
/// </summary>
|
|
public bool PlayDamageSounds;
|
|
|
|
/// <summary>
|
|
/// How much the explosion repairs items.
|
|
/// </summary>
|
|
private readonly float itemRepairStrength;
|
|
|
|
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
|
|
|
|
public readonly HashSet<Character> IgnoredCharacters = new HashSet<Character>();
|
|
|
|
/// <summary>
|
|
/// Strength of the EMP effect created by the explosion.
|
|
/// </summary>
|
|
public float EmpStrength { get; set; }
|
|
|
|
/// <summary>
|
|
/// How much damage the explosion does to ballast flora.
|
|
/// </summary>
|
|
public float BallastFloraDamage { get; set; }
|
|
|
|
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
|
|
{
|
|
Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, Math.Min(range, 1000000))
|
|
{
|
|
SeverLimbsProbability = 1.0f
|
|
};
|
|
this.force = force;
|
|
this.EmpStrength = empStrength;
|
|
BallastFloraDamage = ballastFloraStrength;
|
|
sparks = true;
|
|
debris = true;
|
|
shockwave = true;
|
|
smoke = true;
|
|
flames = true;
|
|
underwaterBubble = true;
|
|
ignoreFireEffectsForTags = Array.Empty<Identifier>();
|
|
}
|
|
|
|
public Explosion(ContentXElement element, string parentDebugName)
|
|
{
|
|
Attack = new Attack(element, parentDebugName + ", Explosion");
|
|
|
|
force = element.GetAttributeFloat("force", 0.0f);
|
|
|
|
//the "abilityexplosion" field is kept for backwards compatibility (basically the opposite of "showeffects")
|
|
bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true);
|
|
sparks = element.GetAttributeBool("sparks", showEffects);
|
|
shockwave = element.GetAttributeBool("shockwave", showEffects);
|
|
flames = element.GetAttributeBool("flames", showEffects);
|
|
underwaterBubble = element.GetAttributeBool("underwaterbubble", showEffects);
|
|
smoke = element.GetAttributeBool("smoke", showEffects);
|
|
debris = element.GetAttributeBool("debris", false);
|
|
|
|
playTinnitus = element.GetAttributeBool("playtinnitus", showEffects);
|
|
|
|
applyFireEffects = element.GetAttributeBool("applyfireeffects", flames && showEffects);
|
|
ignoreFireEffectsForTags = element.GetAttributeIdentifierArray("ignorefireeffectsfortags", Array.Empty<Identifier>());
|
|
|
|
IgnoreCover = element.GetAttributeBool("ignorecover", false);
|
|
OnlyInside = element.GetAttributeBool("onlyinside", false);
|
|
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
|
|
|
|
DistanceFalloff = element.GetAttributeBool(nameof(DistanceFalloff), true);
|
|
|
|
flash = element.GetAttributeBool("flash", showEffects);
|
|
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
|
|
if (element.GetAttribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); }
|
|
flashColor = element.GetAttributeColor("flashcolor", Color.LightYellow);
|
|
|
|
PlayDamageSounds = element.GetAttributeBool(nameof(PlayDamageSounds), false);
|
|
|
|
EmpStrength = element.GetAttributeFloat("empstrength", 0.0f);
|
|
BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f);
|
|
|
|
itemRepairStrength = element.GetAttributeFloat("itemrepairstrength", 0.0f);
|
|
|
|
decal = element.GetAttributeString("decal", "");
|
|
decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize");
|
|
|
|
CameraShake = element.GetAttributeFloat("camerashake", showEffects ? Attack.Range * 0.1f : 0f);
|
|
CameraShakeRange = element.GetAttributeFloat("camerashakerange", showEffects ? Attack.Range : 0f);
|
|
|
|
screenColorRange = element.GetAttributeFloat("screencolorrange", showEffects ? Attack.Range * 0.1f : 0f);
|
|
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
|
|
screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f);
|
|
|
|
}
|
|
|
|
public void DisableParticles()
|
|
{
|
|
sparks = false;
|
|
shockwave = false;
|
|
smoke = false;
|
|
flash = false;
|
|
debris = false;
|
|
flames = false;
|
|
underwaterBubble = false;
|
|
}
|
|
|
|
public void Explode(Vector2 worldPosition, Entity damageSource, Character attacker = null)
|
|
{
|
|
Hull hull = Hull.FindHull(worldPosition);
|
|
ExplodeProjSpecific(worldPosition, hull);
|
|
|
|
if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f)
|
|
{
|
|
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
|
|
}
|
|
|
|
Attack.DamageMultiplier = 1.0f;
|
|
float displayRange = Attack.Range;
|
|
if (damageSource is Item sourceItem)
|
|
{
|
|
var launcher = sourceItem.GetComponent<Projectile>()?.Launcher;
|
|
displayRange *=
|
|
1.0f
|
|
+ sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius)
|
|
+ (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0);
|
|
Attack.DamageMultiplier *=
|
|
1.0f
|
|
+ sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage)
|
|
+ (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0);
|
|
Attack.SourceItem ??= sourceItem;
|
|
}
|
|
|
|
if (attacker is not null)
|
|
{
|
|
displayRange *= 1f + attacker.GetStatValue(StatTypes.ExplosionRadiusMultiplier);
|
|
Attack.DamageMultiplier *= 1f + attacker.GetStatValue(StatTypes.ExplosionDamageMultiplier);
|
|
}
|
|
|
|
Vector2 cameraPos = GameMain.GameScreen.Cam.Position;
|
|
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
|
|
GameMain.GameScreen.Cam.Shake = CameraShake * Math.Max((CameraShakeRange - cameraDist) / CameraShakeRange, 0.0f);
|
|
#if CLIENT
|
|
if (screenColor != Color.Transparent)
|
|
{
|
|
Color flashColor = Color.Lerp(Color.Transparent, screenColor, Math.Max((screenColorRange - cameraDist) / screenColorRange, 0.0f));
|
|
Screen.Selected.ColorFade(flashColor, Color.Transparent, screenColorDuration);
|
|
}
|
|
foreach (Sonar sonar in Sonar.SonarList)
|
|
{
|
|
sonar.RegisterExplosion(this, worldPosition);
|
|
}
|
|
#endif
|
|
|
|
if (displayRange < 0.1f) { return; }
|
|
|
|
if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f))
|
|
{
|
|
RangedStructureDamage(worldPosition, displayRange,
|
|
Attack.GetStructureDamage(1.0f),
|
|
Attack.GetLevelWallDamage(1.0f),
|
|
attacker, IgnoredSubmarines,
|
|
Attack.EmitStructureDamageParticles,
|
|
Attack.CreateWallDamageProjectiles,
|
|
DistanceFalloff);
|
|
}
|
|
|
|
if (BallastFloraDamage > 0.0f)
|
|
{
|
|
RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker, DistanceFalloff);
|
|
}
|
|
|
|
if (EmpStrength > 0.0f)
|
|
{
|
|
float displayRangeSqr = displayRange * displayRange;
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
|
|
if (distSqr > displayRangeSqr) { continue; }
|
|
float distFactor = DistanceFalloff ? CalculateDistanceFactor(distSqr, displayRange) : 1.0f;
|
|
|
|
//damage repairable power-consuming items
|
|
var powered = item.GetComponent<Powered>();
|
|
if (powered == null || !powered.VulnerableToEMP) { continue; }
|
|
if (item.Repairables.Any())
|
|
{
|
|
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
|
|
}
|
|
|
|
var lightComponent = item.GetComponent<LightComponent>();
|
|
if (lightComponent != null)
|
|
{
|
|
//multiply by 10 to make the effect more noticeable
|
|
//(a strength of 1 is already enough to kill power and shut down the lights, but we want weaker EMPs to make the lights flicker noticeably)
|
|
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor * 10.0f, 10.0f);
|
|
}
|
|
|
|
//discharge batteries
|
|
var powerContainer = item.GetComponent<PowerContainer>();
|
|
if (powerContainer != null)
|
|
{
|
|
powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor;
|
|
}
|
|
}
|
|
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - MathF.Sqrt(distSqr) / displayRange;
|
|
}
|
|
|
|
if (itemRepairStrength > 0.0f)
|
|
{
|
|
float displayRangeSqr = displayRange * displayRange;
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
|
|
if (distSqr > displayRangeSqr) { continue; }
|
|
|
|
float distFactor =
|
|
DistanceFalloff ?
|
|
1.0f - (float)Math.Sqrt(distSqr) / displayRange :
|
|
1.0f;
|
|
//repair repairable items
|
|
if (item.Repairables.Any())
|
|
{
|
|
item.Condition += itemRepairStrength * distFactor;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Attack.Afflictions.None() &&
|
|
MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) &&
|
|
MathUtils.NearlyEqual(Attack.ItemDamage, 0.0f) &&
|
|
MathUtils.NearlyEqual(Attack.StructureDamage, 0.0f))
|
|
{
|
|
return;
|
|
}
|
|
|
|
DamageCharacters(worldPosition, Attack, force, damageSource, attacker, displayRange);
|
|
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
if (item.Condition <= 0.0f) { continue; }
|
|
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
|
|
float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
|
|
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius));
|
|
if (dist > displayRange) { continue; }
|
|
|
|
if (dist < displayRange * 0.5f && applyFireEffects && !item.FireProof && ignoreFireEffectsForTags.None(t => item.HasTag(t)))
|
|
{
|
|
//don't apply OnFire effects if the item is inside a fireproof container
|
|
//(or if it's inside a container that's inside a fireproof container, etc)
|
|
Item container = item.Container;
|
|
bool fireProof = false;
|
|
while (container != null)
|
|
{
|
|
if (container.FireProof)
|
|
{
|
|
fireProof = true;
|
|
break;
|
|
}
|
|
container = container.Container;
|
|
}
|
|
if (!fireProof)
|
|
{
|
|
item.ApplyStatusEffects(ActionType.OnFire, 1.0f);
|
|
if (item.Condition <= 0.0f && GameMain.NetworkMember is { IsServer: true })
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFire));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!item.Indestructible)
|
|
{
|
|
if (item.Prefab.DamagedByExplosions ||
|
|
(item.Prefab.DamagedByContainedItemExplosions && item.ContainedItems.Contains(damageSource)))
|
|
{
|
|
float distFactor =
|
|
DistanceFalloff ?
|
|
1.0f - dist / displayRange :
|
|
1.0f;
|
|
float damageAmount = Attack.GetItemDamage(1.0f, item.Prefab.ExplosionDamageMultiplier);
|
|
|
|
Vector2 explosionPos = worldPosition;
|
|
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
|
|
|
|
damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition, IgnoredCover);
|
|
item.Condition -= damageAmount * distFactor;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
|
|
|
|
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, float range)
|
|
{
|
|
if (range <= 0.0f) { return; }
|
|
|
|
//long range for the broad distance check, because large characters may still be in range even if their collider isn't
|
|
float broadRange = Math.Max(range * 10.0f, 10000.0f);
|
|
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (attack.OnlyHumans && !c.IsHuman) { continue; }
|
|
if (IgnoredCharacters.Contains(c)) { continue; }
|
|
|
|
if (!c.Enabled ||
|
|
Math.Abs(c.WorldPosition.X - worldPosition.X) > broadRange ||
|
|
Math.Abs(c.WorldPosition.Y - worldPosition.Y) > broadRange)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (OnlyInside && c.Submarine == null)
|
|
{
|
|
continue;
|
|
}
|
|
else if (OnlyOutside && c.Submarine != null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector2 explosionPos = worldPosition;
|
|
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
|
|
|
|
Hull hull = Hull.FindHull(explosionPos, null, false);
|
|
bool underWater = hull == null || explosionPos.Y < hull.Surface;
|
|
|
|
explosionPos = ConvertUnits.ToSimUnits(explosionPos);
|
|
|
|
Dictionary<Limb, float> distFactors = new Dictionary<Limb, float>();
|
|
Dictionary<Limb, float> damages = new Dictionary<Limb, float>();
|
|
List<Affliction> modifiedAfflictions = new List<Affliction>();
|
|
|
|
Limb closestLimb = null;
|
|
float closestDistFactor = 0;
|
|
foreach (Limb limb in c.AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; }
|
|
|
|
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
|
|
|
|
//calculate distance from the "outer surface" of the physics body
|
|
//doesn't take the rotation of the limb into account, but should be accurate enough for this purpose
|
|
float limbRadius = limb.body.GetMaxExtent();
|
|
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius));
|
|
|
|
if (dist > range) { continue; }
|
|
|
|
float distFactor =
|
|
DistanceFalloff ?
|
|
1.0f - dist / attack.Range :
|
|
1.0f;
|
|
|
|
//solid obstacles between the explosion and the limb reduce the effect of the explosion
|
|
if (!IgnoreCover)
|
|
{
|
|
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition, IgnoredCover);
|
|
}
|
|
if (distFactor > 0)
|
|
{
|
|
distFactors.Add(limb, distFactor);
|
|
if (distFactor > closestDistFactor)
|
|
{
|
|
closestLimb = limb;
|
|
closestDistFactor = distFactor;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (Limb limb in distFactors.Keys)
|
|
{
|
|
if (!distFactors.TryGetValue(limb, out float distFactor)) { continue; }
|
|
modifiedAfflictions.Clear();
|
|
foreach (Affliction affliction in attack.Afflictions.Keys)
|
|
{
|
|
float dmgMultiplier = distFactor;
|
|
if (affliction.DivideByLimbCount)
|
|
{
|
|
float limbCountFactor = distFactors.Count;
|
|
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == AfflictionPrefab.DamageType)
|
|
{
|
|
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
|
|
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
|
|
limbCountFactor = Math.Min(distFactors.Count, 15);
|
|
}
|
|
dmgMultiplier /= limbCountFactor;
|
|
}
|
|
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction));
|
|
}
|
|
c.LastDamageSource = damageSource;
|
|
if (attacker == null)
|
|
{
|
|
if (damageSource is Item item)
|
|
{
|
|
attacker = item.GetComponent<Projectile>()?.User;
|
|
attacker ??= item.GetComponent<MeleeWeapon>()?.User;
|
|
}
|
|
}
|
|
|
|
if (attack.Afflictions.Any() || attack.Stun > 0.0f)
|
|
{
|
|
if (!attack.OnlyHumans || c.IsHuman)
|
|
{
|
|
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
|
|
if (attackData.Afflictions != null)
|
|
{
|
|
modifiedAfflictions.AddRange(attackData.Afflictions);
|
|
}
|
|
|
|
//use a position slightly from the limb's position towards the explosion
|
|
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
|
|
Vector2 dir = worldPosition - limb.WorldPosition;
|
|
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
|
|
|
|
//only play the damage sound on the closest limb (playing it on all just sounds like a mess)
|
|
bool playSound = PlayDamageSounds && limb == closestLimb;
|
|
|
|
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, playSound: playSound, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
|
|
damages.Add(limb, attackResult.Damage);
|
|
}
|
|
}
|
|
|
|
if (attack.StatusEffects != null && attack.StatusEffects.Any())
|
|
{
|
|
attack.SetUser(attacker);
|
|
var statusEffectTargets = new List<ISerializableEntity>();
|
|
foreach (StatusEffect statusEffect in attack.StatusEffects)
|
|
{
|
|
statusEffectTargets.Clear();
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { statusEffectTargets.Add(c); }
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) { statusEffectTargets.Add(limb); }
|
|
statusEffect.Apply(ActionType.OnUse, 1.0f, damageSource, statusEffectTargets);
|
|
statusEffect.Apply(ActionType.Always, 1.0f, damageSource, statusEffectTargets);
|
|
statusEffect.Apply(underWater ? ActionType.InWater : ActionType.NotInWater, 1.0f, damageSource, statusEffectTargets);
|
|
}
|
|
}
|
|
|
|
if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f))
|
|
{
|
|
Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition);
|
|
if (!MathUtils.IsValid(limbDiff)) { limbDiff = Rand.Vector(1.0f); }
|
|
Vector2 impulse = limbDiff * distFactor * force;
|
|
Vector2 impulsePoint = limb.SimPosition - limbDiff * limb.body.GetMaxExtent();
|
|
limb.body.ApplyLinearImpulse(impulse, impulsePoint, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.2f);
|
|
}
|
|
}
|
|
|
|
if (c == Character.Controlled && !c.IsDead && playTinnitus)
|
|
{
|
|
Limb head = c.AnimController.GetLimb(LimbType.Head);
|
|
if (head != null && damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
|
|
{
|
|
PlayTinnitusProjSpecific(headFactor);
|
|
}
|
|
}
|
|
|
|
//sever joints
|
|
if (attack.SeverLimbsProbability > 0.0f)
|
|
{
|
|
foreach (Limb limb in c.AnimController.Limbs)
|
|
{
|
|
if (limb.character.Removed || limb.Removed) { continue; }
|
|
if (limb.IsSevered) { continue; }
|
|
if (!c.IsDead && !limb.CanBeSeveredAlive) { continue; }
|
|
if (distFactors.TryGetValue(limb, out float distFactor))
|
|
{
|
|
if (damages.TryGetValue(limb, out float damage))
|
|
{
|
|
c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true, attacker: attacker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ThreadLocal for thread-safe structure damage tracking
|
|
private static readonly ThreadLocal<Dictionary<Structure, float>> damagedStructuresLocal =
|
|
new ThreadLocal<Dictionary<Structure, float>>(() => new Dictionary<Structure, float>());
|
|
private static Dictionary<Structure, float> damagedStructures => damagedStructuresLocal.Value;
|
|
|
|
/// <summary>
|
|
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
|
|
/// </summary>
|
|
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable<Submarine> ignoredSubmarines = null,
|
|
bool emitWallDamageParticles = true,
|
|
bool createWallDamageProjectiles = false,
|
|
bool distanceFalloff = true)
|
|
{
|
|
float dist = 600.0f;
|
|
damagedStructures.Clear();
|
|
foreach (Structure structure in Structure.WallList)
|
|
{
|
|
if (ignoredSubmarines != null && structure.Submarine != null && ignoredSubmarines.Contains(structure.Submarine)) { continue; }
|
|
|
|
if (structure.HasBody &&
|
|
!structure.IsPlatform &&
|
|
Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f)
|
|
{
|
|
for (int i = 0; i < structure.SectionCount; i++)
|
|
{
|
|
float distFactor =
|
|
distanceFalloff ?
|
|
1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange) :
|
|
1.0f;
|
|
if (distFactor <= 0.0f) { continue; }
|
|
|
|
structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles, createWallDamageProjectiles);
|
|
|
|
if (damagedStructures.ContainsKey(structure))
|
|
{
|
|
damagedStructures[structure] += damage * distFactor;
|
|
}
|
|
else
|
|
{
|
|
damagedStructures.Add(structure, damage * distFactor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f))
|
|
{
|
|
if (Level.Loaded?.LevelObjectManager != null)
|
|
{
|
|
foreach (var levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(worldPosition, worldRange))
|
|
{
|
|
if (levelObject.Prefab.TakeLevelWallDamage)
|
|
{
|
|
float distFactor = 1.0f - (Vector2.Distance(levelObject.WorldPosition, worldPosition) / worldRange);
|
|
if (distFactor <= 0.0f) { continue; }
|
|
levelObject.AddDamage(levelWallDamage * distFactor, 1.0f, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
|
|
{
|
|
if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; }
|
|
|
|
bool inRange = false;
|
|
foreach (var cell in destructibleWall.Cells)
|
|
{
|
|
if (cell.IsPointInside(worldPosition))
|
|
{
|
|
inRange = true;
|
|
break;
|
|
}
|
|
foreach (var edge in cell.Edges)
|
|
{
|
|
if (MathUtils.LineSegmentToPointDistanceSquared((edge.Point1 + cell.Translation).ToPoint(), (edge.Point2 + cell.Translation).ToPoint(), worldPosition.ToPoint()) < worldRange * worldRange)
|
|
{
|
|
inRange = true;
|
|
break;
|
|
}
|
|
}
|
|
if (inRange) { break; }
|
|
}
|
|
if (inRange)
|
|
{
|
|
destructibleWall.AddDamage(levelWallDamage, worldPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
return damagedStructures;
|
|
}
|
|
|
|
public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null, bool distanceFalloff = true)
|
|
{
|
|
List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>();
|
|
|
|
foreach (Hull hull in Hull.HullList)
|
|
{
|
|
if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); }
|
|
}
|
|
|
|
foreach (BallastFloraBehavior ballastFlora in ballastFlorae)
|
|
{
|
|
float resistanceMuliplier = ballastFlora.HasBrokenThrough ? 1f : 1f - ballastFlora.ExplosionResistance;
|
|
ballastFlora.Branches.ForEachMod(branch =>
|
|
{
|
|
Vector2 branchWorldPos = ballastFlora.GetWorldPosition() + branch.Position;
|
|
float branchDist = Vector2.Distance(branchWorldPos, worldPosition);
|
|
if (branchDist < worldRange)
|
|
{
|
|
float distFactor =
|
|
distanceFalloff ?
|
|
1.0f - (branchDist / worldRange) :
|
|
1.0f;
|
|
if (distFactor <= 0.0f) { return; }
|
|
|
|
Vector2 explosionPos = worldPosition;
|
|
Vector2 branchPos = branchWorldPos;
|
|
if (ballastFlora.Parent?.Submarine != null)
|
|
{
|
|
explosionPos -= ballastFlora.Parent.Submarine.Position;
|
|
branchPos -= ballastFlora.Parent.Submarine.Position;
|
|
}
|
|
distFactor *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, ConvertUnits.ToSimUnits(branchPos));
|
|
ballastFlora.DamageBranch(branch, damage * distFactor * resistanceMuliplier, BallastFloraBehavior.AttackType.Explosives, attacker);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos, IEnumerable<Structure> ignoredCover = null)
|
|
{
|
|
float damageMultiplier = 1.0f;
|
|
var obstacles = Submarine.PickBodies(targetSimPos, explosionSimPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall);
|
|
foreach (var body in obstacles)
|
|
{
|
|
if (body.UserData is Item item)
|
|
{
|
|
var door = item.GetComponent<Door>();
|
|
if (door != null && !door.IsOpen && !door.IsBroken) { damageMultiplier *= 0.01f; }
|
|
}
|
|
else if (body.UserData is Structure structure)
|
|
{
|
|
if (ignoredCover != null)
|
|
{
|
|
if (ignoredCover.Contains(structure)) { continue; }
|
|
}
|
|
int sectionIndex = structure.FindSectionIndex(explosionWorldPos, world: true, clamp: true);
|
|
if (structure.SectionBodyDisabled(sectionIndex))
|
|
{
|
|
continue;
|
|
}
|
|
else if (structure.SectionIsLeaking(sectionIndex))
|
|
{
|
|
damageMultiplier *= 0.1f;
|
|
}
|
|
else
|
|
{
|
|
damageMultiplier *= 0.01f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
damageMultiplier *= 0.1f;
|
|
}
|
|
}
|
|
return damageMultiplier;
|
|
}
|
|
|
|
static partial void PlayTinnitusProjSpecific(float volume);
|
|
}
|
|
}
|