]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Gibbing refactor (Per-part gibbing and giblet throwing!) (#24989)
authorJezithyr <jezithyr@gmail.com>
Sat, 10 Feb 2024 23:37:06 +0000 (15:37 -0800)
committerGitHub <noreply@github.com>
Sat, 10 Feb 2024 23:37:06 +0000 (15:37 -0800)
* Moving Gibbing rework out from medrefactor into it's own PR

* Re-enabled warning for missing gibbable on TryGibEntity

* Implemented better logic for gibbing failover and better logging

* Allowing audio params and drop scattering customization per component. Created UnGibbable organ base types and made brains ungibbable.
Removed delete brain from gibBody function. Artifact crusher does not destroy brains anymore. It only destroyed brains before not other organs which was wierd.

* Update Content.Shared/Body/Systems/SharedBodySystem.Body.cs

Fixing space for multiplication

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
* Added event raised when attempting to gib contained entities to allow modification of allowed and excluded container ids

* removing audioParams var from component (sound specifier includes it)

* Fixing signature

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
12 files changed:
Content.Server/Body/Systems/BodySystem.cs
Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs
Content.Shared/Body/Systems/SharedBodySystem.Body.cs
Content.Shared/Gibbing/Components/GibbableComponent.cs [new file with mode: 0644]
Content.Shared/Gibbing/Events/GibbingEvents.cs [new file with mode: 0644]
Content.Shared/Gibbing/Systems/GibbingSystem.cs [new file with mode: 0644]
Resources/Prototypes/Body/Organs/Animal/animal.yml
Resources/Prototypes/Body/Organs/human.yml
Resources/Prototypes/Body/Parts/animal.yml
Resources/Prototypes/Body/Parts/base.yml
Resources/Prototypes/Body/Parts/skeleton.yml
Resources/Prototypes/Body/Parts/terminator.yml

index 1630d4cb10dbc070a99165763dc058d3719bdcd3..0e9b295f718d044b8854cab7e4b7f3105cbc2d64 100644 (file)
@@ -15,6 +15,7 @@ using Robust.Shared.Player;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
 using System.Numerics;
+using Content.Shared.Gibbing.Components;
 using Content.Shared.Movement.Systems;
 using Robust.Shared.Audio.Systems;
 
@@ -106,7 +107,17 @@ public sealed class BodySystem : SharedBodySystem
         _humanoidSystem.SetLayersVisibility(bodyUid, layers, false, true, humanoid);
     }
 
-    public override HashSet<EntityUid> GibBody(EntityUid bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false, bool deleteBrain = false)
+    public override HashSet<EntityUid> GibBody(
+        EntityUid bodyId,
+        bool gibOrgans = false,
+        BodyComponent? body = null ,
+        bool deleteItems = false,
+        bool launchGibs = true,
+        Vector2? splatDirection = null,
+        float splatModifier = 1,
+        Angle splatCone = default,
+        SoundSpecifier? gibSoundOverride = null
+    )
     {
         if (!Resolve(bodyId, ref body, false))
             return new HashSet<EntityUid>();
@@ -118,28 +129,8 @@ public sealed class BodySystem : SharedBodySystem
         if (xform.MapUid == null)
             return new HashSet<EntityUid>();
 
-        var gibs = base.GibBody(bodyId, gibOrgans, body, deleteItems, deleteBrain);
-
-        var coordinates = xform.Coordinates;
-        var filter = Filter.Pvs(bodyId, entityManager: EntityManager);
-        var audio = AudioParams.Default.WithVariation(0.025f);
-
-        _audio.PlayStatic(body.GibSound, filter, coordinates, true, audio);
-
-        foreach (var entity in gibs)
-        {
-            if (deleteItems)
-            {
-                if (!HasComp<BrainComponent>(entity) || deleteBrain)
-                {
-                    QueueDel(entity);
-                }
-            }
-            else
-            {
-                SharedTransform.SetCoordinates(entity, coordinates.Offset(_random.NextVector2(.3f)));
-            }
-        }
+        var gibs = base.GibBody(bodyId, gibOrgans, body, deleteItems, launchGibs: launchGibs,
+            splatDirection: splatDirection, splatModifier: splatModifier, splatCone:splatCone);
         RaiseLocalEvent(bodyId, new BeingGibbedEvent(gibs));
         QueueDel(bodyId);
 
index 09fdc260d7cd4ee0c1784b9f5b9b94bddd3f68d0..6606f284327eb9dec5ea15f3f172c9314e28ffcf 100644 (file)
@@ -106,7 +106,7 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
             if (!TryComp<BodyComponent>(contained, out var body))
                 Del(contained);
 
-            var gibs = _body.GibBody(contained, body: body, gibOrgans: true, deleteBrain: true);
+            var gibs = _body.GibBody(contained, body: body, gibOrgans: true);
             foreach (var gib in gibs)
             {
                 ContainerSystem.Insert((gib, null, null, null), crusher.OutputContainer);
index e5e4edddb07079f0e5c7af89e46db7531eaf80f2..85b6758d78a1aa2068c2c4e3f3fce90d1e58625a 100644 (file)
@@ -5,8 +5,13 @@ using Content.Shared.Body.Organ;
 using Content.Shared.Body.Part;
 using Content.Shared.Body.Prototypes;
 using Content.Shared.DragDrop;
+using Content.Shared.Gibbing.Components;
+using Content.Shared.Gibbing.Events;
+using Content.Shared.Gibbing.Systems;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
 using Robust.Shared.Containers;
 using Robust.Shared.Map;
 using Robust.Shared.Utility;
@@ -23,7 +28,10 @@ public partial class SharedBodySystem
      */
 
     [Dependency] private readonly InventorySystem _inventory = default!;
-
+    [Dependency] private readonly GibbingSystem _gibbingSystem = default!;
+    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+    private const float GibletLaunchImpulse = 8;
+    private const float GibletLaunchImpulseVariance = 3;
     private void InitializeBody()
     {
         // Body here to handle root body parts.
@@ -263,29 +271,45 @@ public partial class SharedBodySystem
         }
     }
 
-    public virtual HashSet<EntityUid> GibBody(EntityUid bodyId, bool gibOrgans = false,
-        BodyComponent? body = null, bool deleteItems = false, bool deleteBrain = false)
+    public virtual HashSet<EntityUid> GibBody(
+        EntityUid bodyId,
+        bool gibOrgans = false,
+        BodyComponent? body = null ,
+        bool deleteItems = false,
+        bool launchGibs = true,
+        Vector2? splatDirection = null,
+        float splatModifier = 1,
+        Angle splatCone = default,
+        SoundSpecifier? gibSoundOverride = null
+        )
     {
         var gibs = new HashSet<EntityUid>();
 
         if (!Resolve(bodyId, ref body, false))
             return gibs;
 
+        var root = GetRootPartOrNull(bodyId, body);
+        if (root != null && TryComp(root.Value.Entity, out GibbableComponent? gibbable))
+        {
+            gibSoundOverride ??= gibbable.GibSound;
+        }
         var parts = GetBodyChildren(bodyId, body).ToArray();
         gibs.EnsureCapacity(parts.Length);
-
         foreach (var part in parts)
         {
-            SharedTransform.AttachToGridOrMap(part.Id);
-            gibs.Add(part.Id);
+
+            _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs,
+                playAudio: false, launchGibs:true, launchDirection:splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier,
+                launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
 
             if (!gibOrgans)
                 continue;
 
             foreach (var organ in GetPartOrgans(part.Id, part.Component))
             {
-                SharedTransform.AttachToGridOrMap(organ.Id);
-                gibs.Add(organ.Id);
+                _gibbingSystem.TryGibEntityWithRef(bodyId, organ.Id, GibType.Drop, GibContentsOption.Skip,
+                    ref gibs, playAudio: false, launchImpulse: GibletLaunchImpulse * splatModifier,
+                    launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
             }
         }
         if(TryComp<InventoryComponent>(bodyId, out var inventory))
@@ -296,6 +320,7 @@ public partial class SharedBodySystem
                 gibs.Add(item);
             }
         }
+        _audioSystem.PlayPredicted(gibSoundOverride, Transform(bodyId).Coordinates, null);
         return gibs;
     }
 }
diff --git a/Content.Shared/Gibbing/Components/GibbableComponent.cs b/Content.Shared/Gibbing/Components/GibbableComponent.cs
new file mode 100644 (file)
index 0000000..9c7501f
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Gibbing.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Gibbing.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(GibbingSystem))]
+public sealed partial class GibbableComponent : Component
+{
+    /// <summary>
+    /// Giblet entity prototypes to randomly select from when spawning additional giblets
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public List<EntProtoId> GibPrototypes = new();
+
+    /// <summary>
+    /// Number of giblet entities to spawn in addition to entity contents
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public int GibCount;
+
+    /// <summary>
+    /// Sound to be played when this entity is gibbed, only played when playsound is true on the gibbing function
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? GibSound = new SoundCollectionSpecifier("gib", AudioParams.Default.WithVariation(0.025f));
+
+    /// <summary>
+    /// Max distance giblets can be dropped from an entity when NOT using physics-based scattering
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float GibScatterRange = 0.3f;
+}
diff --git a/Content.Shared/Gibbing/Events/GibbingEvents.cs b/Content.Shared/Gibbing/Events/GibbingEvents.cs
new file mode 100644 (file)
index 0000000..949b10e
--- /dev/null
@@ -0,0 +1,50 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Gibbing.Events;
+
+
+
+/// <summary>
+/// Called just before we actually gib the target entity
+/// </summary>
+/// <param name="Target">The entity being gibed</param>
+/// <param name="GibType">What type of gibbing is occuring</param>
+/// <param name="AllowedContainers">Containers we are allow to gib</param>
+/// <param name="ExcludedContainers">Containers we are allow not allowed to gib</param>
+[ByRefEvent] public record struct AttemptEntityContentsGibEvent(
+    EntityUid Target,
+    GibContentsOption GibType,
+    List<string>? AllowedContainers,
+    List<string>? ExcludedContainers
+    );
+
+
+/// <summary>
+/// Called just before we actually gib the target entity
+/// </summary>
+/// <param name="Target">The entity being gibed</param>
+/// <param name="GibletCount">how many giblets to spawn</param>
+/// <param name="GibType">What type of gibbing is occuring</param>
+[ByRefEvent] public record struct AttemptEntityGibEvent(EntityUid Target, int GibletCount, GibType GibType);
+
+/// <summary>
+/// Called immediately after we gib the target entity
+/// </summary>
+/// <param name="Target">The entity being gibbed</param>
+/// <param name="DroppedEntities">Any entities that are spilled out (if any)</param>
+[ByRefEvent] public record struct EntityGibbedEvent(EntityUid Target, List<EntityUid> DroppedEntities);
+
+[Serializable, NetSerializable]
+public enum GibType : byte
+{
+    Skip,
+    Drop,
+    Gib,
+}
+
+public enum GibContentsOption : byte
+{
+    Skip,
+    Drop,
+    Gib
+}
diff --git a/Content.Shared/Gibbing/Systems/GibbingSystem.cs b/Content.Shared/Gibbing/Systems/GibbingSystem.cs
new file mode 100644 (file)
index 0000000..5d00b2a
--- /dev/null
@@ -0,0 +1,341 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Content.Shared.Gibbing.Components;
+using Content.Shared.Gibbing.Events;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.Gibbing.Systems;
+
+public sealed class GibbingSystem : EntitySystem
+{
+    [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    //TODO: (future optimization) implement a system that "caps" giblet entities by deleting the oldest ones once we reach a certain limit, customizable via CVAR
+
+    /// <summary>
+    /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only
+    /// work on the target and any entities it contains (depending on gibContentsOption)
+    /// </summary>
+    /// <param name="outerEntity">The outermost entity we care about, used to place the dropped items</param>
+    /// <param name="gibbable">Target entity/comp we wish to gib</param>
+    /// <param name="gibType">What type of gibing are we performing</param>
+    /// <param name="gibContentsOption">What type of gibing do we perform on any container contents?</param>
+    /// <param name="droppedEntities">a hashset containing all the entities that have been dropped/created</param>
+    /// <param name="randomSpreadMod">How much to multiply the random spread on dropped giblets(if we are dropping them!)</param>
+    /// <param name="playAudio">Should we play audio</param>
+    /// <param name="allowedContainers">A list of containerIds on the target that permit gibing</param>
+    /// <param name="excludedContainers">A list of containerIds on the target that DO NOT permit gibing</param>
+    /// <param name="launchCone">The cone we are launching giblets in (if we are launching them!)</param>
+    /// <param name="launchGibs">Should we launch giblets or just drop them</param>
+    /// <param name="launchDirection">The direction to launch giblets (if we are launching them!)</param>
+    /// <param name="launchImpulse">The impluse to launch giblets at(if we are launching them!)</param>
+    /// /// <param name="logMissingGibable">Should we log if we are missing a gibbableComp when we call this function</param>
+    /// <param name="launchImpulseVariance">The variation in giblet launch impulse (if we are launching them!)</param>
+    /// <returns>True if successful, false if not</returns>
+    public bool TryGibEntity(EntityUid outerEntity, Entity<GibbableComponent?> gibbable, GibType gibType,
+        GibContentsOption gibContentsOption,
+        out HashSet<EntityUid> droppedEntities, bool launchGibs = true,
+        Vector2 launchDirection = default, float launchImpulse = 0f, float launchImpulseVariance = 0f,
+        Angle launchCone = default,
+        float randomSpreadMod = 1.0f, bool playAudio = true, List<string>? allowedContainers = null,
+        List<string>? excludedContainers = null, bool logMissingGibable = false)
+    {
+        droppedEntities = new();
+        return TryGibEntityWithRef(outerEntity, gibbable, gibType, gibContentsOption, ref droppedEntities,
+            launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone, randomSpreadMod, playAudio,
+            allowedContainers, excludedContainers, logMissingGibable);
+    }
+
+
+    /// <summary>
+    /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only
+    /// work on the target and any entities it contains (depending on gibContentsOption)
+    /// </summary>
+    /// <param name="outerEntity">The outermost entity we care about, used to place the dropped items</param>
+    /// <param name="gibbable">Target entity/comp we wish to gib</param>
+    /// <param name="gibType">What type of gibing are we performing</param>
+    /// <param name="gibContentsOption">What type of gibing do we perform on any container contents?</param>
+    /// <param name="droppedEntities">a hashset containing all the entities that have been dropped/created</param>
+    /// <param name="randomSpreadMod">How much to multiply the random spread on dropped giblets(if we are dropping them!)</param>
+    /// <param name="playAudio">Should we play audio</param>
+    /// <param name="allowedContainers">A list of containerIds on the target that permit gibing</param>
+    /// <param name="excludedContainers">A list of containerIds on the target that DO NOT permit gibing</param>
+    /// <param name="launchCone">The cone we are launching giblets in (if we are launching them!)</param>
+    /// <param name="launchGibs">Should we launch giblets or just drop them</param>
+    /// <param name="launchDirection">The direction to launch giblets (if we are launching them!)</param>
+    /// <param name="launchImpulse">The impluse to launch giblets at(if we are launching them!)</param>
+    /// <param name="launchImpulseVariance">The variation in giblet launch impulse (if we are launching them!)</param>
+    /// <param name="logMissingGibable">Should we log if we are missing a gibbableComp when we call this function</param>
+    /// <returns>True if successful, false if not</returns>
+    public bool TryGibEntityWithRef(
+        EntityUid outerEntity,
+        Entity<GibbableComponent?> gibbable,
+        GibType gibType,
+        GibContentsOption gibContentsOption,
+        ref HashSet<EntityUid> droppedEntities,
+        bool launchGibs = true,
+        Vector2? launchDirection = null,
+        float launchImpulse = 0f,
+        float launchImpulseVariance = 0f,
+        Angle launchCone = default,
+        float randomSpreadMod = 1.0f,
+        bool playAudio = true,
+        List<string>? allowedContainers = null,
+        List<string>? excludedContainers = null,
+        bool logMissingGibable = false)
+    {
+        if (!Resolve(gibbable, ref gibbable.Comp, logMissing: false))
+        {
+            DropEntity(gibbable, Transform(outerEntity), randomSpreadMod, ref droppedEntities,
+                launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone);
+            if (logMissingGibable)
+            {
+                Log.Warning($"{ToPrettyString(gibbable)} does not have a GibbableComponent! " +
+                            $"This is not required but may cause issues contained items to not be dropped.");
+            }
+
+            return false;
+        }
+
+        if (gibType == GibType.Skip && gibContentsOption == GibContentsOption.Skip)
+            return true;
+        if (launchGibs)
+        {
+            randomSpreadMod = 0;
+        }
+
+        var parentXform = Transform(outerEntity);
+        HashSet<BaseContainer> validContainers = new();
+        var gibContentsAttempt =
+            new AttemptEntityContentsGibEvent(gibbable, gibContentsOption, allowedContainers, excludedContainers);
+        RaiseLocalEvent(gibbable, ref gibContentsAttempt);
+
+        foreach (var container in _containerSystem.GetAllContainers(gibbable))
+        {
+            var valid = true;
+            if (allowedContainers != null)
+                valid = allowedContainers.Contains(container.ID);
+            if (excludedContainers != null)
+                valid = valid && !excludedContainers.Contains(container.ID);
+            if (valid)
+                validContainers.Add(container);
+        }
+
+        switch (gibContentsOption)
+        {
+            case GibContentsOption.Skip:
+                break;
+            case GibContentsOption.Drop:
+            {
+                foreach (var container in validContainers)
+                {
+                    foreach (var ent in container.ContainedEntities)
+                    {
+                        DropEntity(new Entity<GibbableComponent?>(ent, null), parentXform, randomSpreadMod,
+                            ref droppedEntities, launchGibs,
+                            launchDirection, launchImpulse, launchImpulseVariance, launchCone);
+                    }
+                }
+
+                break;
+            }
+            case GibContentsOption.Gib:
+            {
+                foreach (var container in _containerSystem.GetAllContainers(gibbable))
+                {
+                    foreach (var ent in container.ContainedEntities)
+                    {
+                        GibEntity(new Entity<GibbableComponent?>(ent, null), parentXform, randomSpreadMod,
+                            ref droppedEntities, launchGibs,
+                            launchDirection, launchImpulse, launchImpulseVariance, launchCone);
+                    }
+                }
+
+                break;
+            }
+        }
+
+        switch (gibType)
+        {
+            case GibType.Skip:
+                break;
+            case GibType.Drop:
+            {
+                DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs,
+                    launchDirection, launchImpulse, launchImpulseVariance, launchCone);
+                break;
+            }
+            case GibType.Gib:
+            {
+                GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs,
+                    launchDirection, launchImpulse, launchImpulseVariance, launchCone);
+                break;
+            }
+        }
+
+        if (playAudio)
+        {
+            _audioSystem.PlayPredicted(gibbable.Comp.GibSound, parentXform.Coordinates, null);
+        }
+
+        if (gibType == GibType.Gib)
+            QueueDel(gibbable);
+        return true;
+    }
+
+    private void DropEntity(Entity<GibbableComponent?> gibbable, TransformComponent parentXform, float randomSpreadMod,
+        ref HashSet<EntityUid> droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse,
+        float scatterImpulseVariance, Angle scatterCone)
+    {
+        var gibCount = 0;
+        if (Resolve(gibbable, ref gibbable.Comp, logMissing: false))
+        {
+            gibCount = gibbable.Comp.GibCount;
+        }
+
+        var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop);
+        RaiseLocalEvent(gibbable, ref gibAttemptEvent);
+        switch (gibAttemptEvent.GibType)
+        {
+            case GibType.Skip:
+                return;
+            case GibType.Gib:
+                GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity, scatterDirection,
+                    scatterImpulse, scatterImpulseVariance, scatterCone, deleteTarget: false);
+                return;
+        }
+
+        _transformSystem.AttachToGridOrMap(gibbable);
+        _transformSystem.SetCoordinates(gibbable, parentXform.Coordinates);
+        _transformSystem.SetWorldRotation(gibbable, _random.NextAngle());
+        droppedEntities.Add(gibbable);
+        if (flingEntity)
+        {
+            FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
+        }
+
+        var gibbedEvent = new EntityGibbedEvent(gibbable, new List<EntityUid> {gibbable});
+        RaiseLocalEvent(gibbable, ref gibbedEvent);
+    }
+
+    private List<EntityUid> GibEntity(Entity<GibbableComponent?> gibbable, TransformComponent parentXform,
+        float randomSpreadMod,
+        ref HashSet<EntityUid> droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse,
+        float scatterImpulseVariance, Angle scatterCone, bool deleteTarget = true)
+    {
+        var localGibs = new List<EntityUid>();
+        var gibCount = 0;
+        var gibProtoCount = 0;
+        if (Resolve(gibbable, ref gibbable.Comp, logMissing: false))
+        {
+            gibCount = gibbable.Comp.GibCount;
+            gibProtoCount = gibbable.Comp.GibPrototypes.Count;
+        }
+
+        var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop);
+        RaiseLocalEvent(gibbable, ref gibAttemptEvent);
+        switch (gibAttemptEvent.GibType)
+        {
+            case GibType.Skip:
+                return localGibs;
+            case GibType.Drop:
+                DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity,
+                    scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
+                localGibs.Add(gibbable);
+                return localGibs;
+        }
+
+        if (gibbable.Comp != null && gibProtoCount > 0)
+        {
+            if (flingEntity)
+            {
+                for (var i = 0; i < gibAttemptEvent.GibletCount; i++)
+                {
+                    if (!TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet,
+                            randomSpreadMod))
+                        continue;
+                    FlingDroppedEntity(giblet.Value, scatterDirection, scatterImpulse, scatterImpulseVariance,
+                        scatterCone);
+                    droppedEntities.Add(giblet.Value);
+                }
+            }
+            else
+            {
+                for (var i = 0; i < gibAttemptEvent.GibletCount; i++)
+                {
+                    if (TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet,
+                            randomSpreadMod))
+                        droppedEntities.Add(giblet.Value);
+                }
+            }
+        }
+
+        _transformSystem.AttachToGridOrMap(gibbable, Transform(gibbable));
+        if (flingEntity)
+        {
+            FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone);
+        }
+
+        var gibbedEvent = new EntityGibbedEvent(gibbable, localGibs);
+        RaiseLocalEvent(gibbable, ref gibbedEvent);
+        if (deleteTarget)
+            QueueDel(gibbable);
+        return localGibs;
+    }
+
+
+    public bool TryCreateRandomGiblet(Entity<GibbableComponent?> gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity,
+        float randomSpreadModifier = 1.0f, bool playSound = true)
+    {
+        gibletEntity = null;
+        return Resolve(gibbable, ref gibbable.Comp) && TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates,
+            playSound, out gibletEntity, randomSpreadModifier);
+    }
+
+    public bool TryCreateAndFlingRandomGiblet(Entity<GibbableComponent?> gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity,
+        Vector2 scatterDirection, float force, float scatterImpulseVariance, Angle scatterCone = default,
+        bool playSound = true)
+    {
+        gibletEntity = null;
+        if (!Resolve(gibbable, ref gibbable.Comp) ||
+            !TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates, playSound, out gibletEntity))
+            return false;
+        FlingDroppedEntity(gibletEntity.Value, scatterDirection, force, scatterImpulseVariance, scatterCone);
+        return true;
+    }
+
+    private void FlingDroppedEntity(EntityUid target, Vector2? direction, float impulse, float impulseVariance,
+        Angle scatterConeAngle)
+    {
+        var scatterAngle = direction?.ToAngle() ?? _random.NextAngle();
+        var scatterVector = _random.NextAngle(scatterAngle - scatterConeAngle / 2, scatterAngle + scatterConeAngle / 2)
+            .ToVec() * (impulse + _random.NextFloat(impulseVariance));
+        _physicsSystem.ApplyLinearImpulse(target, scatterVector);
+    }
+
+    private bool TryCreateRandomGiblet(GibbableComponent gibbable, EntityCoordinates coords,
+        bool playSound, [NotNullWhen(true)] out EntityUid? gibletEntity, float? randomSpreadModifier = null)
+    {
+        gibletEntity = null;
+        if (gibbable.GibPrototypes.Count == 0)
+            return false;
+        gibletEntity = Spawn(gibbable.GibPrototypes[_random.Next(0, gibbable.GibPrototypes.Count)],
+            randomSpreadModifier == null
+                ? coords
+                : coords.Offset(_random.NextVector2(gibbable.GibScatterRange * randomSpreadModifier.Value)));
+        if (playSound)
+            _audioSystem.PlayPredicted(gibbable.GibSound, coords, null);
+        _transformSystem.SetWorldRotation(gibletEntity.Value, _random.NextAngle());
+        return true;
+    }
+}
index 358fc74bca601febea2bf5a9e2518ecc6e1affbf..2f50821df353a36c1e99e5e91cf6f7610f2b1099 100644 (file)
@@ -1,5 +1,5 @@
 - type: entity
-  id: BaseAnimalOrgan
+  id: BaseAnimalOrganUnGibbable
   parent: BaseItem
   abstract: true
   components:
     tags:
       - Meat
 
+- type: entity
+  id: BaseAnimalOrgan
+  parent: BaseAnimalOrganUnGibbable
+  abstract: true
+  components:
+  - type: Gibbable
 
 - type: entity
   id: OrganAnimalLungs
index 6abf168a9e75ba4594b210b16b0300502123e12d..fe33c11029f00c9d9160054fe085366396ffab0d 100644 (file)
@@ -1,5 +1,5 @@
 - type: entity
-  id: BaseHumanOrgan
+  id: BaseHumanOrganUnGibbable
   parent: BaseItem
   abstract: true
   components:
     tags:
       - Meat
 
+- type: entity
+  id: BaseHumanOrgan
+  parent: BaseHumanOrganUnGibbable
+  abstract: true
+  components:
+  - type: Gibbable
+
 - type: entity
   id: OrganHumanBrain
-  parent: BaseHumanOrgan
+  parent: BaseHumanOrganUnGibbable
   name: brain
   description: "The source of incredible, unending intelligence. Honk."
   components:
index cf65fd21360dc0be39eca52d314f1491fc6f9151..4db026b40fbc55af661b1abd139bc6da9fc53b40 100644 (file)
@@ -23,6 +23,7 @@
   - type: Tag
     tags:
       - Trash
+  - type: Gibbable
   - type: Extractable
     juiceSolution:
       reagents:
index a27059f59f309a29078c10f03e6138f856614626..836d0f140afe89028006b206f506fe403bfa4794 100644 (file)
@@ -9,6 +9,7 @@
   - type: Damageable
     damageContainer: Biological
   - type: BodyPart
+  - type: Gibbable
   - type: ContainerContainer
     containers:
       bodypart: !type:Container
index 3f41f88ee549649922cfbb23b09f327e399265d1..1b378c62332fd08b13999d2be356b60470bc93eb 100644 (file)
@@ -14,6 +14,7 @@
         ents: []
   - type: StaticPrice
     price: 20
+  - type: Gibbable
   - type: Tag
     tags:
       - Trash
index dec0c99f7c20a91c49395e95cc3cabeed81638ba..58530da959c640a5f93578c5315a56df70c20a8e 100644 (file)
@@ -16,6 +16,7 @@
     containers:
       bodypart: !type:Container
         ents: []
+  - type: Gibbable
   - type: StaticPrice
     price: 200