Files
Delta-v/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
T
29e55b9464 Upstream Merge (June / July) attempt 2 electric boogaloo (#4607)
* Fix the sensor monitoring console forcing a GC every 3 seconds (#38146)

* Optimize sensor monitoring window graph drawing

* Add shared static Vector2 pool for all GraphView instances

* Address requested changes

* remove lock

* Update submodule to 264.0.0 (#38629)

* Toy/Plushie Inhands and Wearables (#38514)

* Fixed dependency injection and some other issues in a few places.

* More issue fixes

* patchfor broken NetworkConfiguratorLinkMenu (#38632)

fix

* Fix solutions flickering when transferring contents (#34838)

* Use Solution clones when applying SolutionComponent states

* Revert "Use Solution clones when applying SolutionComponent states"

This reverts commit 013fd111cf92b22562e00f98a7aaa49bc4b4ed62.

* Make Solution implement ICloneable and rename Clone method.

* Copy CanReact value when cloning a Solution

* Convert to IRobustCloneable

* Fix typos in guidebook: Buisness → Business (#38636)

* Fix typo in `MinorAntagonists.xml`

Buisness → Business

* Fix typo in `YourFirstCharacter.xml`

Buisness → Business

* Various Headphones Fixes and Tweaks (#38479)

* sprites, wearables

* neck

* icon-on sprite

* Cleanup prototype instantiation in `DamageTest` (#38639)

Cleanup prototype instantiation in DamageTest

* Cleanup prototype instantiation in `ExplosionSystem` (#38642)

Cleanup prototype instantiation in ExplosionSystem

* AddBodyPartCommand localization. (#38612)

commit

* fix ItemSlotsSystem debug assert (#38655)

* Allow the Command & Super door remotes to use the access of their user. (Re-creation of PR due to changes to game balance) (#35536)

* Added directional beacons (#38284)

* Added directional beacons

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Fixed names

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Power stat and nuke codes commands get some LEC love. (#38585)

* commit

* requested changes.

* Dsay Dirty and Follow commands converted to LEC and localized. (#38666)

* commit

* whoopwhoopwhoop

* Retro laser sprite fix (#38676)

* Fixed everything except the icon

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* fixed icon

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* fix water coolers (#38681)

* Monochromacy typo fix (#38686)

* fixes the typo

* Fixed cloning looking for the trait, not the component, RE https://github.com/space-wizards/space-station-14/pull/38686#issuecomment-3025093504

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Validate `CloningSettingsPrototype`s (#38688)

* Validate CloningSettingsPrototypes

* Update Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Check EventComponents too

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Add test of objective-related console commands (#36400)

* Add test of objective add/list/remove commands

* Not sure why we're validating test prototypes, but sure

* We don't need a map

* Improvements and fixups for New Status Effect API (#38660)

* Predict healing and bloodstream (#38690)

* initial commit

* reapply 38126

* fix rootable

* someone missed an important minus sign here

* try this

* fix

* fix

* reenable crit hits

* cleanup

* fix status time dirtying

* fix

* camelCase

* Healing and bloodstream prediction fixes.

* Resolving Wizard casting recall on nuke disk making it impossible to disarm (#38661)

* Resolving Wizard Recall on Nuke disk making it impossible to disarm
- Adding a DisarmBomb case to nuke status update loop
- Changing a few methods and parameters to properly follow formatting standards
- Updating some names to follow camelCase

* Updating missed tag

* Reverting DataField change

Should prevent this preventative bugfix being a breaking change.

* Switch HSV to the default colorspace for character customization (#38434)

* Made HSV default for character editor

* Adds/fixes comments to HSV defaulting

* Added dropbox fix, potentially cursed

* Revert "Added dropbox fix, potentially cursed"

This reverts commit a709883366fbee813e839742125e70844672af29.

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Fix: Don't deploy foldables when clicking on items inside containers (#38709)

* Fix

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Pressure Relief Valve (#36708)

* initial system (this math is probably WRONG)

* General code cleanup and OnExamined support
(holy moly this code sucks)

* UICode and related events foundation
TODO:
- Actually write the XAML UI and the underlying system
- Un-shitcode the entire thing
- Actually test everything...

* Working UI code
TODO: Make predicted, as this certainly isn't predicted. Even though I said it was. It isn't.

* Remove one TODO for unshitcoding the examine code

* Add reminder
yea

* Make predicted (defenitely isn't)
(also defenitely isn't a copypaste from pressure pump code)

* It's predicted!
TODO:
- Give it snazzy predicted visuals!
- Have a different field for pressure entry, lest it gets bulldozed every UI update.

* Improve gas pressure relief valve UI
TODO: Reminder to reduce amount of dirties using deltafields

* Implement DirtyField prediction

* Entity<T> cleanup
A lot of Entity<T> conversions and lukewarm cleanup.

Also got caught copy pasting code in 4K UHD but it's not like you couldn't tell.

* More cleanup and comments

* Remove TODO comment on bulldozing window title

* """refactoring"""
- Move appearance out of shared and finally fix it. Pointless to predict appearance in this instance.
- More Entity<T> conversions because I like them.
- Move UI creation handling over entirely to the ActivatableUI system.
- Fix a hardcoded locale string (why????).

* Add visuals

* Revert debugging variable replacememt
yea

* Revert skissue

* Remove unused using directives and remove TODO

* Localize, cleanup, document

* Fix adminlogging discrepancy

* Add ability to construct, add guidebook entry

* Clear up comment

* Add guidebook tooltip to valve

* Convert GasPressureReliefValveBoundUserInterface declaration into primary constructor

* Adds more input handling and adds autofill on open

* Un-deepfry input validator shitcode
Genuinely what was I smoking

* improve visuals logic

* Refactor again
- Update math to the correct implementation
- Moved code that could be re-used in the future into a helper method under AtmosphereSystem.Gases.cs

* I'm sorry but I hate warnings

* Remove unused using directive in AtmosphereSystem.Gases.cs

* Review and cleanup

* Lukewarm UI glossup

* Maintainer for the upstream project btw

* Remove redundant state sets and messy logic

* Unduplicate valve updater code

* Redo UI (im sorry Slarti)

* run tests

* Test refactored UI messaging

* Second round of UI improvements
- God please find a way to improve this system. Feels bad.

* Update loop implementation

* Further predict UI

* Clear up SetToCurrentThreshold

* cleanup

* Update to master + pipe layers and bug fixes
want to run tests

* fixes

* Deploy rename pipebomb

* Documentation and requested changes

* Rename the method that wiggled away

* Undo rounding changes

* Fix comment

* Rename and cleanup

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Fix TextLinkTag (#32203)

* Adjust uplink buy button to be under item icon (#38596)

* Adjusted uplink buy button to be under item icon

* Put the discount subtext under the icon

* Indent fixes, added margin

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Scurrets - Audio Improvements (#38482)

* Scurret audio tuning

* Add new sfx

* Update sneezing sfx

* YAML support

* Rename a folder

* make telesci wreck easier (#37569)

rel

* Validate `ShaderPrototype` IDs (#38728)

* Convert all shader prototype string literals to protoids in overlays

* Convert more shader prototype literal strings to protoids

* Convert ValidatePrototypeId to ProtoId

* Later

* [BUGFIX] Fixed revenant malfunction ability not working properly only MediBots and Stasis bed (#38664)

* fixed

* clean up

* orks fix smart

* review fix 1

* more requested changes

* less cursed

* more descriptive description

* better wording

* Tiny Tiny Cleanup of the EyeClosingSystem. (#38734)

Update EyeClosingSystem.cs

* Fix human skin tone distribution (#38701)

* fix: Use PredictedQueueDel for gib spell (#38729)

* fix: don't default to uncharged sprite state for cells (#38730)

* Fix Hristov description - remove inaccurate technical specs (#38746)

- Removes inaccurate 'armor piercing 14.5mm shells' reference
- Replaces it with a more funny description, matching the style of the other snipers and guns
- Keeps ammunition type '.60 anti-materiel ammo' specification
- Fixes issue #38590

Co-authored-by: Arthur Fiorese de Andrade <aandrade@cmcxs.gov.br>

* feat: allow mopping evaporating puddles (#38743)

* Validate remaining `ProtoId` strings (#38747)

Validate remaining ProtoId strings

* Validate `ProtoId`s in tests (#38745)

* Convert string literals to protoids in Content.Tests

* Convert string literals to protoids or consts in Content.IntegrationTests

* Fix linter failures
Tricksy static using misled me

* Cleanup warning in StomachSystem (#38748)

you did not see this

* Vox scars (#38592)

* Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl

* Revert "Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl"

This reverts commit c73da55ba3b39ddf93b493aecd85604c54dd8a15.

* locale key fix

* Changed top surgery scar names to be more generalized

* Adjusted face scars

* Formatting fixes

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* UnlockNode command to LEC. (#38751)

* commit

* Update UnlockNodeCommand.cs

* commit

* move command locale to its own file.

* Update Content.Server/Xenoarchaeology/Artifact/UnlockNodeCommand.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Typofixes for figurine dialogue (#38737)

* Typofixes for figurine dialogue

* Forgot two

* Janitor Tool: Wire Brush (#38667)

* Wow! It's -brush-

* spacing.

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* fixed changes

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Minor escape menu UX improvements (#38650)

* fix: spellbooks can have infinite charges (#38376)

* fix: spellbooks can have infinite charges

* refactor: indicate infinite spellbook charges with null

Not sure if I like this much better...
2025-11-06 18:22:39 +00:00

444 lines
20 KiB
C#

using Content.Server.Shuttles.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
using Content.Shared.Slippery;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Numerics;
namespace Content.Server.Shuttles.Systems;
// shuttle impact damage ported from Goobstation (AGPLv3) with agreement of all coders involved
public sealed partial class ShuttleSystem
{
private bool _enabled;
private float _minimumImpactInertia;
private float _minimumImpactVelocity;
private float _tileBreakEnergyMultiplier;
private float _damageMultiplier;
private float _structuralDamage;
private float _sparkEnergy;
private float _impactRadius;
private float _impactSlowdown;
private float _minThrowVelocity;
private float _massBias;
private float _inertiaScaling;
// this doesn't update if plating mass is changed but edgecase
private float _platingMass;
private const float _sparkChance = 0.2f;
// shuttle mass to consider the neutral point for inertia scaling
private const float _baseShuttleMass = 50f;
// exists primarily for optimisation so not a cvar
private const float _minImpulseVelocity = 0.07f;
// high-speed collisions tend to be a series of increasingly smaller collisions so don't spam admin logs
private readonly TimeSpan _adminLogSpacing = TimeSpan.FromSeconds(3);
private readonly SoundCollectionSpecifier _shuttleImpactSound = new("ShuttleImpactSound");
private readonly ProtoId<ContentTileDefinition> _platingId = "Plating";
private readonly EntProtoId _sparkEffect = "EffectSparks";
private EntityQuery<DamageableComponent> _dmgQuery;
private EntityQuery<ProjectileComponent> _projQuery;
private HashSet<EntityUid> _countedEnts = new();
private HashSet<EntityUid> _intersecting = new();
// for _adminLogSpacing
private Dictionary<EntityUid, TimeSpan> _impactedAt = new();
private void InitializeImpact()
{
SubscribeLocalEvent<ShuttleComponent, StartCollideEvent>(OnShuttleCollide);
_dmgQuery = GetEntityQuery<DamageableComponent>();
_projQuery = GetEntityQuery<ProjectileComponent>();
Subs.CVar(_cfg, CCVars.ImpactEnabled, value => _enabled = value, true);
Subs.CVar(_cfg, CCVars.MinimumImpactInertia, value => _minimumImpactInertia = value, true);
Subs.CVar(_cfg, CCVars.MinimumImpactInertia, value => _minimumImpactInertia = value, true);
Subs.CVar(_cfg, CCVars.MinimumImpactVelocity, value => _minimumImpactVelocity = value, true);
Subs.CVar(_cfg, CCVars.TileBreakEnergyMultiplier, value => _tileBreakEnergyMultiplier = value, true);
Subs.CVar(_cfg, CCVars.ImpactDamageMultiplier, value => _damageMultiplier = value, true);
Subs.CVar(_cfg, CCVars.ImpactStructuralDamage, value => _structuralDamage = value, true);
Subs.CVar(_cfg, CCVars.SparkEnergy, value => _sparkEnergy = value, true);
Subs.CVar(_cfg, CCVars.ImpactRadius, value => _impactRadius = value, true);
Subs.CVar(_cfg, CCVars.ImpactSlowdown, value => _impactSlowdown = value, true);
Subs.CVar(_cfg, CCVars.ImpactMinThrowVelocity, value => _minThrowVelocity = value, true);
Subs.CVar(_cfg, CCVars.ImpactMassBias, value => _massBias = value, true);
Subs.CVar(_cfg, CCVars.ImpactInertiaScaling, value => _inertiaScaling = value, true);
_platingMass = _protoManager.Index(_platingId).Mass;
}
/// <summary>
/// Handles collision between two shuttles, applying impact damage and effects.
/// </summary>
private void OnShuttleCollide(EntityUid uid, ShuttleComponent component, ref StartCollideEvent args)
{
if (TerminatingOrDeleted(uid) || EntityManager.IsQueuedForDeletion(uid)
|| TerminatingOrDeleted(args.OtherEntity) || EntityManager.IsQueuedForDeletion(args.OtherEntity)
)
return;
if (!_gridQuery.TryComp(args.OurEntity, out var ourGrid) ||
!_gridQuery.TryComp(args.OtherEntity, out var otherGrid)
)
return;
var ourBody = args.OurBody;
var otherBody = args.OtherBody;
// TODO: Would also be nice to have a continuous sound for scraping.
var ourXform = Transform(args.OurEntity);
var otherXform = Transform(args.OtherEntity);
var worldPoints = args.WorldPoints;
var worldNormal = args.WorldNormal;
for (var i = 0; i < worldPoints.Length; i++)
{
var worldPoint = worldPoints[i];
var ourPoint = _transform.ToCoordinates((args.OurEntity, ourXform), new MapCoordinates(worldPoint, ourXform.MapID));
var otherPoint = _transform.ToCoordinates((args.OtherEntity, otherXform), new MapCoordinates(worldPoint, otherXform.MapID));
var ourVelocity = _physics.GetLinearVelocity(args.OurEntity, ourPoint.Position, ourBody, ourXform);
var otherVelocity = _physics.GetLinearVelocity(args.OtherEntity, otherPoint.Position, otherBody, otherXform);
var topDiff = (ourVelocity - otherVelocity);
var jungleDiff = topDiff.Length();
// Get the velocity in relation to the contact normal
// If this still causes issues see https://box2d.org/posts/2020/06/ghost-collisions/
// This should only be a potential problem on chunk seams.
var dotProduct = MathF.Abs(Vector2.Dot(topDiff.Normalized(), worldNormal.Normalized()));
jungleDiff *= dotProduct;
// this is cursed but makes it so that collisions of small grid with large grid count the inertia as being approximately the small grid's
var effectiveInertiaMult = (ourBody.FixturesMass * otherBody.FixturesMass) / (ourBody.FixturesMass + otherBody.FixturesMass);
var effectiveInertia = jungleDiff * effectiveInertiaMult;
// TODO: squish damage so that a tiny splinter grid can't stop 2 big grids by being in the way
if (jungleDiff < _minimumImpactVelocity && effectiveInertia < _minimumImpactInertia
|| ourXform.MapUid == null
|| float.IsNaN(jungleDiff))
{
continue;
}
// Play impact sound
var coordinates = new EntityCoordinates(ourXform.MapUid.Value, worldPoint);
var volume = MathF.Min(10f, MathF.Pow(jungleDiff, 0.5f) - 5f);
var audioParams = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(volume);
_audio.PlayPvs(_shuttleImpactSound, coordinates, audioParams);
// if we're not enabled, stop after playing sound
if (!_enabled)
continue;
// Convert the collision point directly to tile indices
var ourTile = new Vector2i((int)Math.Floor(ourPoint.X / ourGrid.TileSize), (int)Math.Floor(ourPoint.Y / ourGrid.TileSize));
var otherTile = new Vector2i((int)Math.Floor(otherPoint.X / otherGrid.TileSize), (int)Math.Floor(otherPoint.Y / otherGrid.TileSize));
var ourMass = GetRegionMass(args.OurEntity, ourGrid, ourTile, _impactRadius, out var ourTiles);
var otherMass = GetRegionMass(args.OtherEntity, otherGrid, otherTile, _impactRadius, out var otherTiles);
// just in case
if (ourTiles == 0 || otherTiles == 0)
continue;
Log.Info($"Shuttle impact of {ToPrettyString(args.OurEntity)} with {ToPrettyString(args.OtherEntity)}; our mass: {ourMass}, other: {otherMass}, velocity {jungleDiff}, impact point {worldPoint}");
// E = MV^2/2
var energyMult = MathF.Pow(jungleDiff, 2) / 2;
// mass-based damage reduction to grid with more mass so that plastitanium block rammer doesn't die to lattice
var ourMassDR = MathF.Max(otherMass / ourMass, 1f);
var otherMassDR = MathF.Max(ourMass / otherMass, 1f);
// multiplier to make large grids not just bonk against each other
var inertiaMult = MathF.Pow(effectiveInertiaMult / _baseShuttleMass, _inertiaScaling);
var toUsEnergy = otherMass * energyMult * inertiaMult * ourMassDR;
var toOtherEnergy = ourMass * energyMult * inertiaMult * otherMassDR;
var impact = LogImpact.High;
// if impact isn't tiny, log it as extreme
if (toUsEnergy + toOtherEnergy > 2f * _tileBreakEnergyMultiplier * _platingMass)
impact = LogImpact.Extreme;
// TODO: would be nice for it to also log who is piloting the grid(s)
if (CheckShouldLog(args.OurEntity) && CheckShouldLog(args.OtherEntity))
_logger.Add(LogType.ShuttleImpact, impact, $"Shuttle impact of {ToPrettyString(args.OurEntity)} with {ToPrettyString(args.OtherEntity)} at {worldPoint}");
_impactedAt[args.OurEntity] = _gameTiming.CurTime;
_impactedAt[args.OtherEntity] = _gameTiming.CurTime;
// uses local region mass for slowdown calculation so lattice doesn't have same slowdown as wall block
var totalInertia = ourVelocity * ourMass + otherVelocity * otherMass;
var inelasticVel = totalInertia / (ourMass + otherMass);
DoGridImpact((args.OurEntity, ourGrid, ourXform, ourBody), args.OurFixture, inelasticVel, ourVelocity, ourTile, ourTiles, toUsEnergy);
DoGridImpact((args.OtherEntity, otherGrid, otherXform, otherBody), args.OtherFixture, inelasticVel, otherVelocity, otherTile, otherTiles, toOtherEnergy);
}
}
private void DoGridImpact(Entity<MapGridComponent, TransformComponent, PhysicsComponent> ent,
Fixture fix,
Vector2 inelasticVelocity,
Vector2 velocity,
Vector2i tile,
int tiles,
float energy)
{
// for readability to not have .Comp1 .Comp2 for everything
var (_, grid, xform, body) = ent;
// radius in which to actually do things so we don't hurt person 4 tiles away on slow bump
var radius = Math.Min(_impactRadius, MathF.Sqrt(energy / _tileBreakEnergyMultiplier / _platingMass));
// slow us down since destroying impacting grid tiles prevents the collision
// without this impacts which destroy tiles just make grids slice straight through each other
var postImpactVelocity = Vector2.Lerp(velocity, inelasticVelocity, MathF.Min(1f, _impactSlowdown * tiles * fix.Density / body.FixturesMass));
var deltaV = -velocity + postImpactVelocity;
_physics.ApplyLinearImpulse(ent, deltaV * body.FixturesMass, body: body);
// process tile and entity damage
ProcessImpactZone(ent, grid, tile, energy, deltaV.Normalized(), radius);
// throw every entity on grid if the impulse is not negligible
if (deltaV.Length() > _minImpulseVelocity)
ThrowEntitiesOnGrid(ent, xform, -deltaV);
}
/// <summary>
/// Knocks and throws all unbuckled entities on the specified grid.
/// </summary>
private void ThrowEntitiesOnGrid(EntityUid gridUid, TransformComponent xform, Vector2 direction)
{
var movedByPressureQuery = GetEntityQuery<MovedByPressureComponent>();
var knockdownTime = TimeSpan.FromSeconds(5);
var minsq = _minThrowVelocity * _minThrowVelocity;
// iterate all entities on the grid
// TODO: only iterate non-static entities
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var uid))
{
// don't throw static bodies
if (!_physicsQuery.TryGetComponent(uid, out var physics) || (physics.BodyType & BodyType.Static) != 0)
continue;
// don't throw if buckled
if (_buckle.IsBuckled(uid, _buckleQuery.CompOrNull(uid)))
continue;
// don't throw them if they have magboots
if (movedByPressureQuery.TryComp(uid, out var moved) && !moved.Enabled)
continue;
if (direction.LengthSquared() > minsq)
{
_stuns.TryKnockdown(uid, knockdownTime, true);
_throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
}
else
{
_physics.ApplyLinearImpulse(uid, direction * physics.Mass, body: physics);
}
}
}
/// <summary>
/// Structure to hold impact tile processing data for batch processing
/// </summary>
private record struct ImpactTileData(Vector2i Tile, float Energy, float DistanceFactor);
/// <summary>
/// Gets the total mass of all entities and tiles (using ContentTileDefinition.Mass) belonging to this grid in a circle
/// </summary>
private float GetRegionMass(EntityUid uid, MapGridComponent grid, Vector2i centerTile, float radius, out int tileCount)
{
tileCount = 0;
var mass = 0f;
_countedEnts.Clear();
foreach (var tileRef in _mapSystem.GetLocalTilesIntersecting(uid, grid, new Circle(centerTile, radius)))
{
var def = _turf.GetContentTileDefinition(tileRef);
mass += def.Mass;
tileCount++;
_intersecting.Clear();
_lookup.GetLocalEntitiesIntersecting(uid, tileRef.GridIndices, _intersecting, gridComp: grid);
foreach (var localUid in _intersecting)
{
if (!_countedEnts.Add(localUid))
continue;
if (_physicsQuery.TryComp(localUid, out var physics))
mass += physics.FixturesMass;
}
}
return mass;
}
/// <summary>
/// Processes a zone of tiles around the impact point
/// </summary>
private void ProcessImpactZone(EntityUid uid, MapGridComponent grid, Vector2i centerTile, float energy, Vector2 dir, float radius)
{
// Create a list of all tiles to process
var tilesToProcess = new List<ImpactTileData>();
// Pre-calculate all tiles that need processing
foreach (var tileRef in _mapSystem.GetLocalTilesIntersecting(uid, grid, new Circle(centerTile, radius)))
{
var distance = centerTile - tileRef.GridIndices;
// Calculate distance-based energy falloff
float distanceFactor = 1.0f - distance.Length / (radius + 1);
float tileEnergy = energy * distanceFactor;
tilesToProcess.Add(new ImpactTileData(tileRef.GridIndices, tileEnergy, distanceFactor));
}
// Process tiles sequentially for safety
var brokenTiles = new List<(Vector2i, Tile)>();
var sparkTiles = new List<Vector2i>();
ProcessTileBatch(uid, grid, tilesToProcess, dir, 0, tilesToProcess.Count, brokenTiles, sparkTiles);
// Only proceed with visual effects if the entity still exists
if (Exists(uid))
{
ProcessBrokenTilesAndSparks(uid, grid, brokenTiles, sparkTiles);
}
}
/// <summary>
/// Process a batch of tiles from the impact zone
/// </summary>
private void ProcessTileBatch(
EntityUid uid,
MapGridComponent grid,
List<ImpactTileData> tilesToProcess,
Vector2 throwDirection,
int startIndex,
int endIndex,
List<(Vector2i, Tile)> brokenTiles,
List<Vector2i> sparkTiles)
{
// here so we don't have to `new` it every iteration
var damageSpec = new DamageSpecifier()
{
DamageDict = { ["Blunt"] = 0, ["Structural"] = 0 }
};
var entitiesOnTile = new HashSet<Entity<TransformComponent>>();
var tileCenter = new Vector2(grid.TileSize / 2f, grid.TileSize / 2f);
for (var i = startIndex; i < endIndex; i++)
{
var tileData = tilesToProcess[i];
bool canBreakTile = true;
// Process entities on this tile
entitiesOnTile.Clear();
_lookup.GetLocalEntitiesIntersecting(uid, tileData.Tile, entitiesOnTile, gridComp: grid);
// this loop is a hotspot so tell if you know how to optimise it
foreach (var localEnt in entitiesOnTile)
{
// the query can ocassionally return entities barely touching this tile so check for that
var toCenter = tileData.Tile + tileCenter - localEnt.Comp.Coordinates.Position;
if (MathF.Abs(toCenter.X) > 0.5f || MathF.Abs(toCenter.Y) > 0.5f)
continue;
if (_dmgQuery.TryComp(localEnt, out var damageable))
{
// Apply damage scaled by distance but capped to prevent gibbing
var scaledDamage = tileData.Energy * _damageMultiplier;
damageSpec.DamageDict["Blunt"] = scaledDamage;
damageSpec.DamageDict["Structural"] = scaledDamage * _structuralDamage;
_damageSys.TryChangeDamage(localEnt, damageSpec, damageable: damageable);
}
// might've been destroyed
if (TerminatingOrDeleted(localEnt) || EntityManager.IsQueuedForDeletion(localEnt))
continue;
if (!_physicsQuery.TryComp(localEnt, out var physics))
continue;
// no breaking tiles under walls that haven't been destroyed
if ((physics.BodyType & BodyType.Static) != 0
&& (physics.CollisionLayer & (int)CollisionGroup.Impassable) != 0)
{
canBreakTile = false;
}
else
{
var direction = throwDirection * tileData.DistanceFactor;
_throwing.TryThrow(localEnt, direction, physics, localEnt.Comp, _projQuery, direction.Length(), playSound: false);
}
}
// Mark tiles for spark effects
if (tileData.Energy > _sparkEnergy && tileData.DistanceFactor > 0.7f && _random.Prob(_sparkChance))
sparkTiles.Add(tileData.Tile);
if (!canBreakTile)
continue;
// Mark tiles for breaking/effects
var def = _turf.GetContentTileDefinition(_mapSystem.GetTileRef(uid, grid, tileData.Tile));
if (tileData.Energy > def.Mass * _tileBreakEnergyMultiplier)
brokenTiles.Add((tileData.Tile, Tile.Empty));
}
}
/// <summary>
/// Process visual effects and tile breaking after entity processing
/// </summary>
private void ProcessBrokenTilesAndSparks(
EntityUid uid,
MapGridComponent grid,
List<(Vector2i, Tile)> brokenTiles,
List<Vector2i> sparkTiles)
{
// Break tiles
_mapSystem.SetTiles(uid, grid, brokenTiles);
if (TerminatingOrDeleted(uid))
return;
// Spawn spark effects
foreach (var tile in sparkTiles)
{
var coords = _mapSystem.GridTileToLocal(uid, grid, tile);
Spawn(_sparkEffect, coords);
}
}
/// <summary>
/// Check whether this impact should be logged to admins.
/// Used to prevent spamming logs.
/// </summary>
private bool CheckShouldLog(EntityUid uid)
{
return !(_impactedAt.ContainsKey(uid) && _gameTiming.CurTime < _impactedAt[uid] + _adminLogSpacing);
}
}