From: Ilya246 <57039557+Ilya246@users.noreply.github.com> Date: Sat, 17 May 2025 17:11:08 +0000 (+0400) Subject: shuttle impacts port (#37422) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=356dabb9c981389dc61093b118d89dbbfdbe02e2;p=space-station-14.git shuttle impacts port (#37422) * initial * adjust densities and thruster hp * Fix evil hack * Last stuff * review, cleanup * admin RW * minor cleanup --------- Co-authored-by: metalgearsloth --- diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index fd712142af..fc02bf8826 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -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> _noFtls = new(); private EntityQuery _bodyQuery; - private EntityQuery _buckleQuery; private EntityQuery _immuneQuery; - private EntityQuery _physicsQuery; private EntityQuery _statusQuery; - private EntityQuery _xformQuery; private void InitializeFTL() { @@ -85,11 +81,8 @@ public sealed partial class ShuttleSystem SubscribeLocalEvent(OnFtlShutdown); _bodyQuery = GetEntityQuery(); - _buckleQuery = GetEntityQuery(); _immuneQuery = GetEntityQuery(); - _physicsQuery = GetEntityQuery(); _statusQuery = GetEntityQuery(); - _xformQuery = GetEntityQuery(); _cfg.OnValueChanged(CCVars.FTLStartupTime, time => DefaultStartupTime = time, true); _cfg.OnValueChanged(CCVars.FTLTravelTime, time => DefaultTravelTime = time, true); diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs index 436b248407..771103711a 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs @@ -1,60 +1,435 @@ -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 { - /// - /// Minimum velocity difference between 2 bodies for a shuttle "impact" to occur. - /// - 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 _platingId = "Plating"; + private readonly EntProtoId _sparkEffect = "EffectSparks"; + + private EntityQuery _dmgQuery; + private EntityQuery _projQuery; + + private HashSet _countedEnts = new(); + private HashSet _intersecting = new(); + // for _adminLogSpacing + private Dictionary _impactedAt = new(); private void InitializeImpact() { SubscribeLocalEvent(OnShuttleCollide); + + _dmgQuery = GetEntityQuery(); + _projQuery = GetEntityQuery(); + + 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; } + /// + /// Handles collision between two shuttles, applying impact damage and effects. + /// private void OnShuttleCollide(EntityUid uid, ShuttleComponent component, ref StartCollideEvent args) { - if (!HasComp(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 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); + } + + /// + /// Knocks and throws all unbuckled entities on the specified grid. + /// + private void ThrowEntitiesOnGrid(EntityUid gridUid, TransformComponent xform, Vector2 direction) + { + var movedByPressureQuery = GetEntityQuery(); + 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); + /// + /// Structure to hold impact tile processing data for batch processing + /// + private record struct ImpactTileData(Vector2i Tile, float Energy, float DistanceFactor); + + /// + /// Gets the total mass of all entities and tiles (using ContentTileDefinition.Mass) belonging to this grid in a circle + /// + 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; + } + + /// + /// Processes a zone of tiles around the impact point + /// + 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(); + + // 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(); + + 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); + } + } + + /// + /// Process a batch of tiles from the impact zone + /// + private void ProcessTileBatch( + EntityUid uid, + MapGridComponent grid, + List tilesToProcess, + Vector2 throwDirection, + int startIndex, + int endIndex, + List<(Vector2i, Tile)> brokenTiles, + List 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>(); + 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)); + + } + } + + /// + /// Process visual effects and tile breaking after entity processing + /// + private void ProcessBrokenTilesAndSparks( + EntityUid uid, + MapGridComponent grid, + List<(Vector2i, Tile)> brokenTiles, + List 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); + } + } + + /// + /// Check whether this impact should be logged to admins. + /// Used to prevent spamming logs. + /// + private bool CheckShouldLog(EntityUid uid) + { + return !(_impactedAt.ContainsKey(uid) && _gameTiming.CurTime < _impactedAt[uid] + _adminLogSpacing); } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index dadadeb211..96a173d68e 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -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 _buckleQuery; private EntityQuery _gridQuery; + private EntityQuery _physicsQuery; + private EntityQuery _xformQuery; public const float TileMassMultiplier = 0.5f; @@ -71,7 +81,10 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem { base.Initialize(); + _buckleQuery = GetEntityQuery(); _gridQuery = GetEntityQuery(); + _physicsQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); InitializeFTL(); InitializeGridFills(); diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index 0042ba8f72..dc8265bb43 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -467,5 +467,10 @@ public enum LogType /// /// Artifact node got activated. /// - ArtifactNode = 101 + ArtifactNode = 101, + + /// + /// Damaging grid collision has occurred. + /// + ShuttleImpact = 102 } diff --git a/Content.Shared/CCVar/CCVars.Shuttle.cs b/Content.Shared/CCVar/CCVars.Shuttle.cs index 47fc816c05..7ec089d68b 100644 --- a/Content.Shared/CCVar/CCVars.Shuttle.cs +++ b/Content.Shared/CCVar/CCVars.Shuttle.cs @@ -194,4 +194,92 @@ public sealed partial class CCVars /// public static readonly CVarDef GridImpulseMultiplier = CVarDef.Create("shuttle.grid_impulse_multiplier", 0.01f, CVar.SERVERONLY); + + #region impacts + + /// + /// Whether shuttle impacts should do anything beyond produce a sound. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactEnabled = + CVarDef.Create("shuttle.impact.enabled", true, CVar.SERVERONLY); + + /// + /// Minimum impact inertia to trigger special shuttle impact behaviors when impacting slower than MinimumImpactVelocity. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MinimumImpactInertia = + CVarDef.Create("shuttle.impact.minimum_inertia", 5f * 50f, CVar.SERVERONLY); // 100tile grid (cargo shuttle) going at 5 m/s + + /// + /// Minimum velocity difference between 2 bodies for a shuttle impact to be guaranteed to trigger any special behaviors like damage. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MinimumImpactVelocity = + CVarDef.Create("shuttle.impact.minimum_velocity", 15f, CVar.SERVERONLY); // needed so that random space debris can be rammed + + /// + /// Multiplier of Kinetic energy required to dismantle a single tile in relation to its mass + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef TileBreakEnergyMultiplier = + CVarDef.Create("shuttle.impact.tile_break_energy", 3000f, CVar.SERVERONLY); + + /// + /// Multiplier of damage done to entities on colliding areas + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactDamageMultiplier = + CVarDef.Create("shuttle.impact.damage_multiplier", 0.00005f, CVar.SERVERONLY); + + /// + /// Multiplier of additional structural damage to do + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactStructuralDamage = + CVarDef.Create("shuttle.impact.structural_damage", 5f, CVar.SERVERONLY); + + /// + /// Kinetic energy required to spawn sparks + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef SparkEnergy = + CVarDef.Create("shuttle.impact.spark_energy", 2000000f, CVar.SERVERONLY); + + /// + /// Area to consider for impact calculations + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactRadius = + CVarDef.Create("shuttle.impact.radius", 4f, CVar.SERVERONLY); + + /// + /// Affects slowdown on impact + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactSlowdown = + CVarDef.Create("shuttle.impact.slowdown", 0.8f, CVar.SERVERONLY); + + /// + /// Minimum velocity change from impact for special throw effects (e.g. stuns, beakers breaking) to occur + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef 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 + + /// + /// Affects how much damage reduction to give to grids with higher mass + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactMassBias = + CVarDef.Create("shuttle.impact.mass_bias", 0.65f, CVar.SERVERONLY); + + /// + /// How much should total grid inertia affect our collision damage + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef ImpactInertiaScaling = + CVarDef.Create("shuttle.impact.inertia_scaling", 0.5f, CVar.SERVERONLY); + + #endregion } diff --git a/Content.Shared/Maps/ContentTileDefinition.cs b/Content.Shared/Maps/ContentTileDefinition.cs index edccd1729a..9fc4bee481 100644 --- a/Content.Shared/Maps/ContentTileDefinition.cs +++ b/Content.Shared/Maps/ContentTileDefinition.cs @@ -47,6 +47,12 @@ namespace Content.Shared.Maps [DataField] public PrototypeFlags DeconstructTools { get; set; } = new(); + /// + /// Effective mass of this tile for grid impacts. + /// + [DataField] + public float Mass = 800f; + /// /// Legacy AF but nice to have. /// diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml index cbc556cb12..27ec0fe92d 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml @@ -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"] @@ -72,13 +72,13 @@ 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"] diff --git a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml index 34e84d39f6..ec9ae49ffc 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml @@ -48,6 +48,17 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Walls/walls.yml b/Resources/Prototypes/Entities/Structures/Walls/walls.yml index b690769fd8..7edefca9f9 100644 --- a/Resources/Prototypes/Entities/Structures/Walls/walls.yml +++ b/Resources/Prototypes/Entities/Structures/Walls/walls.yml @@ -183,6 +183,17 @@ - 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 @@ -343,6 +354,17 @@ 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 @@ -472,6 +494,17 @@ - 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 @@ -605,6 +638,17 @@ 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 @@ -804,6 +848,26 @@ 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 @@ -896,6 +960,17 @@ 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 @@ -1048,6 +1123,17 @@ 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 diff --git a/Resources/Prototypes/Entities/Structures/Windows/plastitanium.yml b/Resources/Prototypes/Entities/Structures/Windows/plastitanium.yml index d544db9b80..a319d06224 100644 --- a/Resources/Prototypes/Entities/Structures/Windows/plastitanium.yml +++ b/Resources/Prototypes/Entities/Structures/Windows/plastitanium.yml @@ -34,6 +34,17 @@ - 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 @@ -144,6 +155,7 @@ - FullTileMask layer: - GlassLayer + density: 4000 - type: Airtight noAirWhenFullyAirBlocked: false airBlockedDirection: diff --git a/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml b/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml index 358943c58e..58e6078ff3 100644 --- a/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml +++ b/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml @@ -55,6 +55,17 @@ 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 @@ -148,6 +159,7 @@ - FullTileMask layer: - GlassLayer + density: 2000 - type: Airtight noAirWhenFullyAirBlocked: false airBlockedDirection: diff --git a/Resources/Prototypes/Tiles/plating.yml b/Resources/Prototypes/Tiles/plating.yml index e3f4c89d94..2879272a59 100644 --- a/Resources/Prototypes/Tiles/plating.yml +++ b/Resources/Prototypes/Tiles/plating.yml @@ -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