]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
shuttle impacts port (#37422)
authorIlya246 <57039557+Ilya246@users.noreply.github.com>
Sat, 17 May 2025 17:11:08 +0000 (21:11 +0400)
committerGitHub <noreply@github.com>
Sat, 17 May 2025 17:11:08 +0000 (03:11 +1000)
* initial

* adjust densities and thruster hp

* Fix evil hack

* Last stuff

* review, cleanup

* admin RW

* minor cleanup

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
12 files changed:
Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs
Content.Server/Shuttles/Systems/ShuttleSystem.cs
Content.Shared.Database/LogType.cs
Content.Shared/CCVar/CCVars.Shuttle.cs
Content.Shared/Maps/ContentTileDefinition.cs
Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml
Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
Resources/Prototypes/Entities/Structures/Walls/walls.yml
Resources/Prototypes/Entities/Structures/Windows/plastitanium.yml
Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
Resources/Prototypes/Tiles/plating.yml

index fd712142af7fd34e2ccbef750e4b705a9a9352f2..fc02bf8826c9b22dd3d5924b70bad2f3f7fa3f95 100644 (file)
@@ -5,7 +5,6 @@ using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Events;
 using Content.Shared.Body.Components;
-using Content.Shared.Buckle.Components;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
 using Content.Shared.Ghost;
@@ -73,11 +72,8 @@ public sealed partial class ShuttleSystem
     private readonly HashSet<Entity<NoFTLComponent>> _noFtls = new();
 
     private EntityQuery<BodyComponent> _bodyQuery;
-    private EntityQuery<BuckleComponent> _buckleQuery;
     private EntityQuery<FTLSmashImmuneComponent> _immuneQuery;
-    private EntityQuery<PhysicsComponent> _physicsQuery;
     private EntityQuery<StatusEffectsComponent> _statusQuery;
-    private EntityQuery<TransformComponent> _xformQuery;
 
     private void InitializeFTL()
     {
@@ -85,11 +81,8 @@ public sealed partial class ShuttleSystem
         SubscribeLocalEvent<FTLComponent, ComponentShutdown>(OnFtlShutdown);
 
         _bodyQuery = GetEntityQuery<BodyComponent>();
-        _buckleQuery = GetEntityQuery<BuckleComponent>();
         _immuneQuery = GetEntityQuery<FTLSmashImmuneComponent>();
-        _physicsQuery = GetEntityQuery<PhysicsComponent>();
         _statusQuery = GetEntityQuery<StatusEffectsComponent>();
-        _xformQuery = GetEntityQuery<TransformComponent>();
 
         _cfg.OnValueChanged(CCVars.FTLStartupTime, time => DefaultStartupTime = time, true);
         _cfg.OnValueChanged(CCVars.FTLTravelTime, time => DefaultTravelTime = time, true);
index 436b24840736da2c13eb8b396e9c4eb716e725d0..771103711a21ab9f6c8af210b9bde1230fa26d81 100644 (file)
-using System.Numerics;
 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.Player;
+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
 {
-    /// <summary>
-    /// Minimum velocity difference between 2 bodies for a shuttle "impact" to occur.
-    /// </summary>
-    private const int MinimumImpactVelocity = 10;
+    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 (!HasComp<ShuttleComponent>(args.OtherEntity))
+        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(uid);
+        var ourXform = Transform(args.OurEntity);
+        var otherXform = Transform(args.OtherEntity);
+        var worldPoints = args.WorldPoints;
 
-        if (ourXform.MapUid == null)
-            return;
+        for (var i = 0; i < worldPoints.Length; i++)
+        {
+            var worldPoint = worldPoints[i];
 
-        var otherXform = Transform(args.OtherEntity);
+            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 jungleDiff = (ourVelocity - otherVelocity).Length();
+
+            // 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}");
 
-        var ourPoint = Vector2.Transform(args.WorldPoint, _transform.GetInvWorldMatrix(ourXform));
-        var otherPoint = Vector2.Transform(args.WorldPoint, _transform.GetInvWorldMatrix(otherXform));
+            // 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 ourVelocity = _physics.GetLinearVelocity(uid, ourPoint, ourBody, ourXform);
-        var otherVelocity = _physics.GetLinearVelocity(args.OtherEntity, otherPoint, otherBody, otherXform);
-        var jungleDiff = (ourVelocity - otherVelocity).Length();
+            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}");
 
-        if (jungleDiff < MinimumImpactVelocity)
+            _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))
         {
-            return;
+            // 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);
+            }
         }
+    }
 
-        var coordinates = new EntityCoordinates(ourXform.MapUid.Value, args.WorldPoint);
-        var volume = MathF.Min(10f, 1f * MathF.Pow(jungleDiff, 0.5f) - 5f);
-        var audioParams = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(volume);
+    /// <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();
 
-        _audio.PlayPvs(_shuttleImpactSound, coordinates, audioParams);
+        foreach (var tileRef in _mapSystem.GetLocalTilesIntersecting(uid, grid, new Circle(centerTile, radius)))
+        {
+            var def = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId];
+            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 = (ContentTileDefinition)_tileDefManager[_mapSystem.GetTileRef(uid, grid, tileData.Tile).Tile.TypeId];
+            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);
     }
 }
index dadadeb21109dca11a42eeffb694df273e59fdcf..96a173d68ed4e492ad4ea55820a07c1ed4de9d81 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Administration.Logs;
 using Content.Server.Body.Systems;
+using Content.Server.Buckle.Systems;
 using Content.Server.Doors.Systems;
 using Content.Server.Parallax;
 using Content.Server.Procedural;
@@ -7,7 +8,10 @@ using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Systems;
 using Content.Server.Stunnable;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Damage;
 using Content.Shared.GameTicking;
+using Content.Shared.Inventory;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Events;
 using Content.Shared.Salvage;
@@ -38,18 +42,21 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly IComponentFactory _factory = default!;
     [Dependency] private readonly IConfigurationManager _cfg = default!;
     [Dependency] private readonly IGameTiming _gameTiming = default!;
-    [Dependency] private readonly MapSystem _mapSystem = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly IPrototypeManager _protoManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
     [Dependency] private readonly BiomeSystem _biomes = default!;
     [Dependency] private readonly BodySystem _bobby = default!;
+    [Dependency] private readonly BuckleSystem _buckle = default!;
+    [Dependency] private readonly DamageableSystem _damageSys = default!;
     [Dependency] private readonly DockingSystem _dockSystem = default!;
     [Dependency] private readonly DungeonSystem _dungeon = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly FixtureSystem _fixtures = default!;
+    [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly MapLoaderSystem _loader = default!;
+    [Dependency] private readonly MapSystem _mapSystem = default!;
     [Dependency] private readonly MetaDataSystem _metadata = default!;
     [Dependency] private readonly PvsOverrideSystem _pvs = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -63,7 +70,10 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly ThrusterSystem _thruster = default!;
     [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
 
+    private EntityQuery<BuckleComponent> _buckleQuery;
     private EntityQuery<MapGridComponent> _gridQuery;
+    private EntityQuery<PhysicsComponent> _physicsQuery;
+    private EntityQuery<TransformComponent> _xformQuery;
 
     public const float TileMassMultiplier = 0.5f;
 
@@ -71,7 +81,10 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     {
         base.Initialize();
 
+        _buckleQuery = GetEntityQuery<BuckleComponent>();
         _gridQuery = GetEntityQuery<MapGridComponent>();
+        _physicsQuery = GetEntityQuery<PhysicsComponent>();
+        _xformQuery = GetEntityQuery<TransformComponent>();
 
         InitializeFTL();
         InitializeGridFills();
index 0042ba8f721b4b906d0279115f6a7d4195dc2e96..dc8265bb434f0b5c20ebca1a591e55ced0b00751 100644 (file)
@@ -467,5 +467,10 @@ public enum LogType
     /// <summary>
     /// Artifact node got activated.
     /// </summary>
-    ArtifactNode = 101
+    ArtifactNode = 101,
+
+    /// <summary>
+    /// Damaging grid collision has occurred.
+    /// </summary>
+    ShuttleImpact = 102
 }
index 47fc816c05956402c4a30a9898451a4e91cff17a..7ec089d68bec31e0d6f59647970508e91fc53534 100644 (file)
@@ -194,4 +194,92 @@ public sealed partial class CCVars
     /// </summary>
     public static readonly CVarDef<float> GridImpulseMultiplier =
         CVarDef.Create("shuttle.grid_impulse_multiplier", 0.01f, CVar.SERVERONLY);
+
+    #region impacts
+
+    /// <summary>
+    /// Whether shuttle impacts should do anything beyond produce a sound.
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<bool> ImpactEnabled =
+        CVarDef.Create("shuttle.impact.enabled", true, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Minimum impact inertia to trigger special shuttle impact behaviors when impacting slower than MinimumImpactVelocity.
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> MinimumImpactInertia =
+        CVarDef.Create("shuttle.impact.minimum_inertia", 5f * 50f, CVar.SERVERONLY); // 100tile grid (cargo shuttle) going at 5 m/s
+
+    /// <summary>
+    /// Minimum velocity difference between 2 bodies for a shuttle impact to be guaranteed to trigger any special behaviors like damage.
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> MinimumImpactVelocity =
+        CVarDef.Create("shuttle.impact.minimum_velocity", 15f, CVar.SERVERONLY); // needed so that random space debris can be rammed
+
+    /// <summary>
+    /// Multiplier of Kinetic energy required to dismantle a single tile in relation to its mass
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> TileBreakEnergyMultiplier =
+        CVarDef.Create("shuttle.impact.tile_break_energy", 3000f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Multiplier of damage done to entities on colliding areas
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactDamageMultiplier =
+        CVarDef.Create("shuttle.impact.damage_multiplier", 0.00005f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Multiplier of additional structural damage to do
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactStructuralDamage =
+        CVarDef.Create("shuttle.impact.structural_damage", 5f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Kinetic energy required to spawn sparks
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> SparkEnergy =
+        CVarDef.Create("shuttle.impact.spark_energy", 2000000f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Area to consider for impact calculations
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactRadius =
+        CVarDef.Create("shuttle.impact.radius", 4f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Affects slowdown on impact
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactSlowdown =
+        CVarDef.Create("shuttle.impact.slowdown", 0.8f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// Minimum velocity change from impact for special throw effects (e.g. stuns, beakers breaking) to occur
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactMinThrowVelocity =
+        CVarDef.Create("shuttle.impact.min_throw_velocity", 1f, CVar.SERVERONLY); // due to how it works this is about 16 m/s for cargo shuttle
+
+    /// <summary>
+    /// Affects how much damage reduction to give to grids with higher mass
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactMassBias =
+        CVarDef.Create("shuttle.impact.mass_bias", 0.65f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// How much should total grid inertia affect our collision damage
+    /// </summary>
+    [CVarControl(AdminFlags.VarEdit)]
+    public static readonly CVarDef<float> ImpactInertiaScaling =
+        CVarDef.Create("shuttle.impact.inertia_scaling", 0.5f, CVar.SERVERONLY);
+
+    #endregion
 }
index edccd1729a5541add3c68b320ec4e396d571c3c4..9fc4bee4813e5c6d019d3921ab29d3687935e7af 100644 (file)
@@ -47,6 +47,12 @@ namespace Content.Shared.Maps
         [DataField]
         public PrototypeFlags<ToolQualityPrototype> DeconstructTools { get; set; } = new();
 
+        /// <summary>
+        /// Effective mass of this tile for grid impacts.
+        /// </summary>
+        [DataField]
+        public float Mass = 800f;
+
         /// <remarks>
         /// Legacy AF but nice to have.
         /// </remarks>
index cbc556cb12e9236a30a6961a02795b88f8299999..27ec0fe92da9d69401ec8dd27d606451f053fde0 100644 (file)
@@ -46,7 +46,7 @@
       thresholds:
       - trigger:
           !type:DamageTrigger
-          damage: 100  # Considering we need a lot of thrusters didn't want to make an individual one too tanky
+          damage: 300  # Changed 100->300 because impact damage is real
         behaviors:
           - !type:DoActsBehavior
             acts: ["Destruction"]
     thresholds:
     - trigger:
         !type:DamageTrigger
-        damage: 300
+        damage: 600
       behaviors:
         - !type:DoActsBehavior
           acts: ["Destruction"]
     - trigger:
         !type:DamageTrigger
-        damage: 100
+        damage: 300
       behaviors:
         - !type:DoActsBehavior
           acts: ["Destruction"]
index 34e84d39f62453027d460dedefa77427150e33ad..ec9ae49ffc1925da76b6ccf93bea9311a071fe37 100644 (file)
           path: /Audio/Effects/break_stone.ogg
           params:
             volume: -6
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 2000
 
 - type: entity
   abstract: true
index b690769fd85dbbdb54b6a4c36afa3ec4796693d7..7edefca9f998ea70104878ef3c128f6f474988b1 100644 (file)
   - type: IconSmooth
     key: walls
     base: clown
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 8000 # really good ramming wall, bananium is rare so it's probably fine
 
 - type: entity
   parent: BaseWall
         node: girder
       - !type:DoActsBehavior
         acts: ["Destruction"]
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 4000
   - type: IconSmooth
     key: walls
     base: gold
     - type: IconSmooth
       key: walls
       base: plastitanium
+    - type: Fixtures
+      fixtures:
+        fix1:
+          shape:
+            !type:PhysShapeAabb
+            bounds: "-0.5,-0.5,0.5,0.5"
+          mask:
+          - FullTileMask
+          layer:
+          - WallLayer
+          density: 4000
     - type: Damageable
       damageContainer: StructuralInorganic
       damageModifierSet: StructuralMetallicStrong
     price: 250
   - type: RadiationBlocker
     resistance: 5
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 2000
 
 - type: entity
   parent: WallReinforced
     state: state0
   - type: Reflect
     reflectProb: 1
+  - type: Pullable
+  - type: Airtight
+    noAirWhenFullyAirBlocked: false
+    airBlockedDirection:
+    - South
+    - East
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PolygonShape
+            vertices:
+            - "-0.5,-0.5"
+            - "0.5,0.5"
+            - "0.5,-0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 2000
   - type: Damageable
     damageContainer: StructuralInorganic
     damageModifierSet: StructuralMetallicStrong
           5: { state: shuttle_construct-5, visible: true}
   - type: Reflect
     reflectProb: 1
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 2000
 
 - type: entity
   parent: BaseWall
         node: girder
       - !type:DoActsBehavior
         acts: ["Destruction"]
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 6000
   - type: IconSmooth
     key: walls
     base: uranium
index d544db9b800cfdfb692c64fc05ffc4fbad6a898c..a319d0622423a578cfd0a36ccdd05ec9dffd9159 100644 (file)
   - type: StaticPrice
     price: 100
   - type: BlockWeather
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 4000
 
 - type: entity
   id: PlastitaniumWindowSquareBase
         - FullTileMask
         layer:
         - GlassLayer
+        density: 4000
   - type: Airtight
     noAirWhenFullyAirBlocked: false
     airBlockedDirection:
index 358943c58e34ecaadca5b513a259d2193ae0bfe4..58e6078ff391afc71b410fd9c7b5c0261210b32c 100644 (file)
       sprite: Structures/Windows/cracks.rsi
   - type: StaticPrice
     price: 132
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.5,-0.5,0.5,0.5"
+        mask:
+        - FullTileMask
+        layer:
+        - WallLayer
+        density: 2000
 
 - type: entity
   id: PlasmaReinforcedWindowDirectional
         - FullTileMask
         layer:
         - GlassLayer
+        density: 2000
   - type: Airtight
     noAirWhenFullyAirBlocked: false
     airBlockedDirection:
index e3f4c89d94b9c4591b759a12a1eaa7a5b2610c5f..2879272a59e949096b9e03996211da8d5754a174 100644 (file)
@@ -72,6 +72,7 @@
   isSpace: true
   itemDrop: PartRodMetal1
   heatCapacity: 10000
+  mass: 200
 
 - type: tile
   id: TrainLattice
@@ -87,3 +88,4 @@
   isSpace: true
   itemDrop: PartRodMetal1
   heatCapacity: 10000
+  mass: 200