Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs
Eero 45312af297 WIP Make static collections thread-safe using ThreadStatic and ThreadLocal
Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety.
2025-12-28 14:14:53 +08:00

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);
}
}