]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Melee Executions (#30104)
authorScribbles0 <91828755+Scribbles0@users.noreply.github.com>
Sun, 11 Aug 2024 03:05:54 +0000 (20:05 -0700)
committerGitHub <noreply@github.com>
Sun, 11 Aug 2024 03:05:54 +0000 (13:05 +1000)
* melee executions

* fix damage bug

* cleanup

* address reviews hopefully

* resistance bypass mechanic

* component changes

* self executions (not finished yet)

* self execs part two

* ok i fixed things (still not finished)

* finish everything

* review stuff

* nuke if (kind = special)

* more review stuffs

* Make suicide system much less hardcoded and make much more use of events

* Fix a dumb bug I introduced

* self execution popups

* Integration tests

* Why did they even take 0.5 blunt damage?

* More consistent integration tests

* Destructive equals true

* Allow it to dirty-dispose

* IS THIS WHAT YOU WANT?

* FRESH AND CLEAN

* modifier to multiplier

* don't jinx the integration tests

* no file-scoped namespace

* Move the rest of execution to shared, create SuicideGhostEvent

* handled

* Get rid of unused code and add a comment

* ghost before suicide

* stop cat suicides

* popup fix + small suicide change

* make it a bit better

---------

Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
26 files changed:
Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs [new file with mode: 0644]
Content.Server/Chat/Commands/SuicideCommand.cs
Content.Server/Chat/SuicideSystem.cs
Content.Server/Explosion/EntitySystems/TriggerSystem.Mobstate.cs
Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs
Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
Content.Server/Materials/MaterialReclaimerSystem.cs
Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
Content.Server/Morgue/CrematoriumSystem.cs
Content.Shared/Chat/SharedSuicideSystem.cs [new file with mode: 0644]
Content.Shared/Execution/DoAfterEvent.cs [new file with mode: 0644]
Content.Shared/Execution/ExecutionComponent.cs [new file with mode: 0644]
Content.Shared/Execution/SharedExecutionSystem.cs [new file with mode: 0644]
Content.Shared/Interaction/Events/SuicideEvent.cs
Content.Shared/Mind/SharedMindSystem.cs
Content.Shared/Weapons/Melee/Events/MeleeHitEvent.cs
Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Resources/Locale/en-US/execution/execution.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Materials/crystal_shard.yml
Resources/Prototypes/Entities/Objects/Materials/shards.yml
Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml

diff --git a/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs b/Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
new file mode 100644 (file)
index 0000000..540e86c
--- /dev/null
@@ -0,0 +1,365 @@
+using System.Linq;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Execution;
+using Content.Shared.FixedPoint;
+using Content.Shared.Ghost;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Mind;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Tag;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Commands;
+
+[TestFixture]
+public sealed class SuicideCommandTests
+{
+
+    [TestPrototypes]
+    private const string Prototypes = @"
+- type: entity
+  id: SharpTestObject
+  name: very sharp test object
+  components:
+  - type: Item
+  - type: MeleeWeapon
+    damage:
+      types:
+        Slash: 5
+  - type: Execution
+
+- type: entity
+  id: MixedDamageTestObject
+  name: mixed damage test object
+  components:
+  - type: Item
+  - type: MeleeWeapon
+    damage:
+      types:
+        Slash: 5
+        Blunt: 5
+  - type: Execution
+
+- type: entity
+  id: TestMaterialReclaimer
+  name: test version of the material reclaimer
+  components:
+  - type: MaterialReclaimer";
+
+    /// <summary>
+    /// Run the suicide command in the console
+    /// Should successfully kill the player and ghost them
+    /// </summary>
+    [Test]
+    public async Task TestSuicide()
+    {
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings
+        {
+            Connected = true,
+            Dirty = true,
+            DummyTicker = false
+        });
+        var server = pair.Server;
+        var consoleHost = server.ResolveDependency<IConsoleHost>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var playerMan = server.ResolveDependency<IPlayerManager>();
+        var mindSystem = entManager.System<SharedMindSystem>();
+        var mobStateSystem = entManager.System<MobStateSystem>();
+
+        // We need to know the player and whether they can be hurt, killed, and whether they have a mind
+        var player = playerMan.Sessions.First().AttachedEntity!.Value;
+        var mind = mindSystem.GetMind(player);
+
+        MindComponent mindComponent = default;
+        MobStateComponent mobStateComp = default;
+        await server.WaitPost(() =>
+        {
+            if (mind != null)
+                mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
+
+            mobStateComp = entManager.GetComponent<MobStateComponent>(player);
+        });
+
+
+        // Check that running the suicide command kills the player
+        // and properly ghosts them without them being able to return to their body
+        await server.WaitAssertion(() =>
+        {
+            consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
+            Assert.Multiple(() =>
+            {
+                Assert.That(mobStateSystem.IsDead(player, mobStateComp));
+                Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
+                            !ghostComp.CanReturnToBody);
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Run the suicide command while the player is already injured
+    /// This should only deal as much damage as necessary to get to the dead threshold
+    /// </summary>
+    [Test]
+    public async Task TestSuicideWhileDamaged()
+    {
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings
+        {
+            Connected = true,
+            Dirty = true,
+            DummyTicker = false
+        });
+        var server = pair.Server;
+        var consoleHost = server.ResolveDependency<IConsoleHost>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var playerMan = server.ResolveDependency<IPlayerManager>();
+        var protoMan = server.ResolveDependency<IPrototypeManager>();
+
+        var damageableSystem = entManager.System<DamageableSystem>();
+        var mindSystem = entManager.System<SharedMindSystem>();
+        var mobStateSystem = entManager.System<MobStateSystem>();
+
+        // We need to know the player and whether they can be hurt, killed, and whether they have a mind
+        var player = playerMan.Sessions.First().AttachedEntity!.Value;
+        var mind = mindSystem.GetMind(player);
+
+        MindComponent mindComponent = default;
+        MobStateComponent mobStateComp = default;
+        MobThresholdsComponent mobThresholdsComp = default;
+        DamageableComponent damageableComp = default;
+        await server.WaitPost(() =>
+        {
+            if (mind != null)
+                mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
+
+            mobStateComp = entManager.GetComponent<MobStateComponent>(player);
+            mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
+            damageableComp = entManager.GetComponent<DamageableComponent>(player);
+        });
+
+        if (protoMan.TryIndex<DamageTypePrototype>("Slash", out var slashProto))
+            damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
+
+        // Check that running the suicide command kills the player
+        // and properly ghosts them without them being able to return to their body
+        // and that all the damage is concentrated in the Slash category
+        await server.WaitAssertion(() =>
+        {
+            consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
+            var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(mobStateSystem.IsDead(player, mobStateComp));
+                Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
+                            !ghostComp.CanReturnToBody);
+                Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(lethalDamageThreshold));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+        /// <summary>
+    /// Run the suicide command in the console
+    /// Should only ghost the player but not kill them
+    /// </summary>
+    [Test]
+    public async Task TestSuicideWhenCannotSuicide()
+    {
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings
+        {
+            Connected = true,
+            Dirty = true,
+            DummyTicker = false
+        });
+        var server = pair.Server;
+        var consoleHost = server.ResolveDependency<IConsoleHost>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var playerMan = server.ResolveDependency<IPlayerManager>();
+        var mindSystem = entManager.System<SharedMindSystem>();
+        var mobStateSystem = entManager.System<MobStateSystem>();
+        var tagSystem = entManager.System<TagSystem>();
+
+        // We need to know the player and whether they can be hurt, killed, and whether they have a mind
+        var player = playerMan.Sessions.First().AttachedEntity!.Value;
+        var mind = mindSystem.GetMind(player);
+        MindComponent mindComponent = default;
+        MobStateComponent mobStateComp = default;
+        await server.WaitPost(() =>
+        {
+            if (mind != null)
+                mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
+            mobStateComp = entManager.GetComponent<MobStateComponent>(player);
+        });
+
+        tagSystem.AddTag(player, "CannotSuicide");
+
+        // Check that running the suicide command kills the player
+        // and properly ghosts them without them being able to return to their body
+        await server.WaitAssertion(() =>
+        {
+            consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
+            Assert.Multiple(() =>
+            {
+                Assert.That(mobStateSystem.IsAlive(player, mobStateComp));
+                Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
+                            !ghostComp.CanReturnToBody);
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+
+    /// <summary>
+    /// Run the suicide command while the player is holding an execution-capable weapon
+    /// </summary>
+    [Test]
+    public async Task TestSuicideByHeldItem()
+    {
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings
+        {
+            Connected = true,
+            Dirty = true,
+            DummyTicker = false
+        });
+        var server = pair.Server;
+        var consoleHost = server.ResolveDependency<IConsoleHost>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var playerMan = server.ResolveDependency<IPlayerManager>();
+
+        var handsSystem = entManager.System<SharedHandsSystem>();
+        var mindSystem = entManager.System<SharedMindSystem>();
+        var mobStateSystem = entManager.System<MobStateSystem>();
+        var transformSystem = entManager.System<TransformSystem>();
+
+        // We need to know the player and whether they can be hurt, killed, and whether they have a mind
+        var player = playerMan.Sessions.First().AttachedEntity!.Value;
+        var mind = mindSystem.GetMind(player);
+
+        MindComponent mindComponent = default;
+        MobStateComponent mobStateComp = default;
+        MobThresholdsComponent mobThresholdsComp = default;
+        DamageableComponent damageableComp = default;
+        HandsComponent handsComponent = default;
+        await server.WaitPost(() =>
+        {
+            if (mind != null)
+                mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
+
+            mobStateComp = entManager.GetComponent<MobStateComponent>(player);
+            mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
+            damageableComp = entManager.GetComponent<DamageableComponent>(player);
+            handsComponent = entManager.GetComponent<HandsComponent>(player);
+        });
+
+        // Spawn the weapon of choice and put it in the player's hands
+        await server.WaitPost(() =>
+        {
+            var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
+            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
+            entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
+            Assert.That(executionComponent, Is.Not.EqualTo(null));
+        });
+
+        // Check that running the suicide command kills the player
+        // and properly ghosts them without them being able to return to their body
+        // and that all the damage is concentrated in the Slash category
+        await server.WaitAssertion(() =>
+        {
+            consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
+            var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(mobStateSystem.IsDead(player, mobStateComp));
+                Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
+                            !ghostComp.CanReturnToBody);
+                Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    /// <summary>
+    /// Run the suicide command while the player is holding an execution-capable weapon
+    /// with damage spread between slash and blunt
+    /// </summary>
+    [Test]
+    public async Task TestSuicideByHeldItemSpreadDamage()
+    {
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings
+        {
+            Connected = true,
+            Dirty = true,
+            DummyTicker = false
+        });
+        var server = pair.Server;
+        var consoleHost = server.ResolveDependency<IConsoleHost>();
+        var entManager = server.ResolveDependency<IEntityManager>();
+        var playerMan = server.ResolveDependency<IPlayerManager>();
+
+        var handsSystem = entManager.System<SharedHandsSystem>();
+        var mindSystem = entManager.System<SharedMindSystem>();
+        var mobStateSystem = entManager.System<MobStateSystem>();
+        var transformSystem = entManager.System<TransformSystem>();
+
+        // We need to know the player and whether they can be hurt, killed, and whether they have a mind
+        var player = playerMan.Sessions.First().AttachedEntity!.Value;
+        var mind = mindSystem.GetMind(player);
+
+        MindComponent mindComponent = default;
+        MobStateComponent mobStateComp = default;
+        MobThresholdsComponent mobThresholdsComp = default;
+        DamageableComponent damageableComp = default;
+        HandsComponent handsComponent = default;
+        await server.WaitPost(() =>
+        {
+            if (mind != null)
+                mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
+
+            mobStateComp = entManager.GetComponent<MobStateComponent>(player);
+            mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
+            damageableComp = entManager.GetComponent<DamageableComponent>(player);
+            handsComponent = entManager.GetComponent<HandsComponent>(player);
+        });
+
+        // Spawn the weapon of choice and put it in the player's hands
+        await server.WaitPost(() =>
+        {
+            var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
+            Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
+            entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
+            Assert.That(executionComponent, Is.Not.EqualTo(null));
+        });
+
+        // Check that running the suicide command kills the player
+        // and properly ghosts them without them being able to return to their body
+        // and that slash damage is split in half
+        await server.WaitAssertion(() =>
+        {
+            consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
+            var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
+
+            Assert.Multiple(() =>
+            {
+                Assert.That(mobStateSystem.IsDead(player, mobStateComp));
+                Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
+                            !ghostComp.CanReturnToBody);
+                Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+}
index 0db03fec79b116346c061cfbb75d12e9817cbfd5..ea45158e44bee5513bfd0bedcbf8a739075afbdd 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.GameTicking;
 using Content.Server.Popups;
 using Content.Shared.Administration;
+using Content.Shared.Chat;
 using Content.Shared.Mind;
 using Robust.Shared.Console;
 using Robust.Shared.Enums;
@@ -32,15 +33,13 @@ namespace Content.Server.Chat.Commands
             var minds = _e.System<SharedMindSystem>();
 
             // This check also proves mind not-null for at the end when the mob is ghosted.
-            if (!minds.TryGetMind(player, out var mindId, out var mind) ||
-                mind.OwnedEntity is not { Valid: true } victim)
+            if (!minds.TryGetMind(player, out var mindId, out var mindComp) ||
+                mindComp.OwnedEntity is not { Valid: true } victim)
             {
                 shell.WriteLine(Loc.GetString("suicide-command-no-mind"));
                 return;
             }
 
-
-            var gameTicker = _e.System<GameTicker>();
             var suicideSystem = _e.System<SuicideSystem>();
 
             if (_e.HasComponent<AdminFrozenComponent>(victim))
@@ -53,14 +52,6 @@ namespace Content.Server.Chat.Commands
             }
 
             if (suicideSystem.Suicide(victim))
-            {
-                // Prevent the player from returning to the body.
-                // Note that mind cannot be null because otherwise victim would be null.
-                gameTicker.OnGhostAttempt(mindId, false, mind: mind);
-                return;
-            }
-
-            if (gameTicker.OnGhostAttempt(mindId, true, mind: mind))
                 return;
 
             shell.WriteLine(Loc.GetString("ghost-command-denied"));
index 131d19c5235701c37d12a663e949db55bf7f1106..884292b0fa7320898fa17ba4bf2a66e3fef0162f 100644 (file)
-using Content.Server.Administration.Logs;
-using Content.Server.Popups;
+using Content.Server.GameTicking;
 using Content.Shared.Damage;
-using Content.Shared.Damage.Prototypes;
 using Content.Shared.Database;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Item;
+using Content.Shared.Mind;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Popups;
 using Content.Shared.Tag;
 using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Chat;
+using Content.Shared.Mind.Components;
 
-namespace Content.Server.Chat
+namespace Content.Server.Chat;
+
+public sealed class SuicideSystem : EntitySystem
 {
-    public sealed class SuicideSystem : EntitySystem
+    [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly TagSystem _tagSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    [Dependency] private readonly SharedSuicideSystem _suicide = default!;
+
+    public override void Initialize()
     {
-        [Dependency] private readonly DamageableSystem _damageableSystem = default!;
-        [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-        [Dependency] private readonly TagSystem _tagSystem = default!;
-        [Dependency] private readonly MobStateSystem _mobState = default!;
-        [Dependency] private readonly SharedPopupSystem _popup = default!;
-
-        public bool Suicide(EntityUid victim)
-        {
-            // Checks to see if the CannotSuicide tag exits, ghosts instead.
-            if (_tagSystem.HasTag(victim, "CannotSuicide"))
-                return false;
+        base.Initialize();
 
-            // Checks to see if the player is dead.
-            if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
-                return false;
+        SubscribeLocalEvent<DamageableComponent, SuicideEvent>(OnDamageableSuicide);
+        SubscribeLocalEvent<MobStateComponent, SuicideEvent>(OnEnvironmentalSuicide);
+        SubscribeLocalEvent<MindContainerComponent, SuicideGhostEvent>(OnSuicideGhost);
+    }
 
-            _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
+    /// <summary>
+    /// Calling this function will attempt to kill the user by suiciding on objects in the surrounding area
+    /// or by applying a lethal amount of damage to the user with the default method.
+    /// Used when writing /suicide
+    /// </summary>
+    public bool Suicide(EntityUid victim)
+    {
+        // Can't suicide if we're already dead
+        if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
+            return false;
 
-            var suicideEvent = new SuicideEvent(victim);
+        var suicideGhostEvent = new SuicideGhostEvent(victim);
+        RaiseLocalEvent(victim, suicideGhostEvent);
 
-            //Check to see if there were any systems blocking this suicide
-            if (SuicideAttemptBlocked(victim, suicideEvent))
-                return false;
+        // Suicide is considered a fail if the user wasn't able to ghost
+        // Suiciding with the CannotSuicide tag will ghost the player but not kill the body
+        if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, "CannotSuicide"))
+            return false;
 
-            bool environmentSuicide = false;
-            // If you are critical, you wouldn't be able to use your surroundings to suicide, so you do the default suicide
-            if (!_mobState.IsCritical(victim, mobState))
-            {
-                environmentSuicide = EnvironmentSuicideHandler(victim, suicideEvent);
-            }
+        _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
+        var suicideEvent = new SuicideEvent(victim);
+        RaiseLocalEvent(victim, suicideEvent);
 
-            if (suicideEvent.AttemptBlocked)
-                return false;
+        _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided.");
+        return true;
+    }
 
-            DefaultSuicideHandler(victim, suicideEvent);
+    /// <summary>
+    /// Event subscription created to handle the ghosting aspect relating to suicides
+    /// Mainly useful when you can raise an event in Shared and can't call Suicide() directly
+    /// </summary>
+    private void OnSuicideGhost(Entity<MindContainerComponent> victim, ref SuicideGhostEvent args)
+    {
+        if (args.Handled)
+            return;
 
-            ApplyDeath(victim, suicideEvent.Kind!.Value);
-            _adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided{(environmentSuicide ? " (environment)" : "")}");
-            return true;
-        }
+        if (victim.Comp.Mind == null)
+            return;
 
-        /// <summary>
-        /// If not handled, does the default suicide, which is biting your own tongue
-        /// </summary>
-        private void DefaultSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
-        {
-            if (suicideEvent.Handled)
-                return;
+        if (!TryComp<MindComponent>(victim.Comp.Mind, out var mindComponent))
+            return;
 
-            var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
-            _popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
+        // CannotSuicide tag will allow the user to ghost, but also return to their mind
+        // This is kind of weird, not sure what it applies to?
+        if (_tagSystem.HasTag(victim, "CannotSuicide"))
+            args.CanReturnToBody = true;
 
-            var selfMessage = Loc.GetString("suicide-command-default-text-self");
-            _popup.PopupEntity(selfMessage, victim, victim);
-            suicideEvent.SetHandled(SuicideKind.Bloodloss);
-        }
+        if (_gameTicker.OnGhostAttempt(victim.Comp.Mind.Value, args.CanReturnToBody, mind: mindComponent))
+            args.Handled = true;
+    }
 
-        /// <summary>
-        /// Checks to see if there are any other systems that prevent suicide
-        /// </summary>
-        /// <returns>Returns true if there was a blocked attempt</returns>
-        private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent)
-        {
-            RaiseLocalEvent(victim, suicideEvent, true);
+    /// <summary>
+    /// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
+    /// </summary>
+    private void OnEnvironmentalSuicide(Entity<MobStateComponent> victim, ref SuicideEvent args)
+    {
+        if (args.Handled || _mobState.IsCritical(victim))
+            return;
 
-            if (suicideEvent.AttemptBlocked)
-                return true;
+        var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim);
 
-            return false;
+        // Try to suicide by raising an event on the held item
+        if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
+            && handsComponent.ActiveHandEntity is { } item)
+        {
+            RaiseLocalEvent(item, suicideByEnvironmentEvent);
+            if (suicideByEnvironmentEvent.Handled)
+            {
+                args.Handled = suicideByEnvironmentEvent.Handled;
+                return;
+            }
         }
 
-        /// <summary>
-        /// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
-        /// </summary>
-        private bool EnvironmentSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
+        // Try to suicide by nearby entities, like Microwaves or Crematoriums, by raising an event on it
+        // Returns upon being handled by any entity
+        var itemQuery = GetEntityQuery<ItemComponent>();
+        foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
         {
-            var itemQuery = GetEntityQuery<ItemComponent>();
+            // Skip any nearby items that can be picked up, we already checked the active held item above
+            if (itemQuery.HasComponent(entity))
+                continue;
 
-            // Suicide by held item
-            if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
-                && handsComponent.ActiveHandEntity is { } item)
-            {
-                RaiseLocalEvent(item, suicideEvent, false);
+            RaiseLocalEvent(entity, suicideByEnvironmentEvent);
+            if (!suicideByEnvironmentEvent.Handled)
+                continue;
 
-                if (suicideEvent.Handled)
-                    return true;
-            }
-
-            // Suicide by nearby entity (ex: Microwave)
-            foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
-            {
-                // Skip any nearby items that can be picked up, we already checked the active held item above
-                if (itemQuery.HasComponent(entity))
-                    continue;
+            args.Handled = suicideByEnvironmentEvent.Handled;
+            return;
+        }
+    }
 
-                RaiseLocalEvent(entity, suicideEvent);
+    /// <summary>
+    /// Default suicide behavior for any kind of entity that can take damage
+    /// </summary>
+    private void OnDamageableSuicide(Entity<DamageableComponent> victim, ref SuicideEvent args)
+    {
+        if (args.Handled)
+            return;
 
-                if (suicideEvent.Handled)
-                    return true;
-            }
+        var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
+        _popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
 
-            return false;
-        }
+        var selfMessage = Loc.GetString("suicide-command-default-text-self");
+        _popup.PopupEntity(selfMessage, victim, victim);
 
-        private void ApplyDeath(EntityUid target, SuicideKind kind)
+        if (args.DamageSpecifier != null)
         {
-            if (kind == SuicideKind.Special)
-                return;
-
-            if (!_prototypeManager.TryIndex<DamageTypePrototype>(kind.ToString(), out var damagePrototype))
-            {
-                const SuicideKind fallback = SuicideKind.Blunt;
-                Log.Error($"{nameof(SuicideSystem)} could not find the damage type prototype associated with {kind}. Falling back to {fallback}");
-                damagePrototype = _prototypeManager.Index<DamageTypePrototype>(fallback.ToString());
-            }
-            const int lethalAmountOfDamage = 200; // TODO: Would be nice to get this number from somewhere else
-            _damageableSystem.TryChangeDamage(target, new(damagePrototype, lethalAmountOfDamage), true, origin: target);
+            _suicide.ApplyLethalDamage(victim, args.DamageSpecifier);
+            args.Handled = true;
+            return;
         }
+
+        args.DamageType ??= "Bloodloss";
+        _suicide.ApplyLethalDamage(victim, args.DamageType);
+        args.Handled = true;
     }
 }
index 45198662ec3bd60a0ea1e69f3beb7ecef001da55..ccd2a6e3df06eb3052801b326cbf02d2b86d467a 100644 (file)
@@ -38,16 +38,20 @@ public sealed partial class TriggerSystem
             Trigger(uid);
     }
 
+    /// <summary>
+    /// Checks if the user has any implants that prevent suicide to avoid some cheesy strategies
+    /// Prevents suicide by handling the event without killing the user
+    /// </summary>
     private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component, SuicideEvent args)
     {
         if (args.Handled)
             return;
 
-        if (component.PreventSuicide)
-        {
-            _popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
-            args.BlockSuicideAttempt(component.PreventSuicide);
-        }
+        if (!component.PreventSuicide)
+            return;
+
+        _popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
+        args.Handled = true;
     }
 
     private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args)
index d89af7e94f9c58bf4ba6f867b7abaa146b102587..fec65430c12ca2e7b3da22eadaa8e1aeae7d3ae5 100644 (file)
@@ -2,6 +2,8 @@ using Content.Server.Administration.Logs;
 using Content.Server.Body.Systems;
 using Content.Server.Kitchen.Components;
 using Content.Server.Popups;
+using Content.Shared.Chat;
+using Content.Shared.Damage;
 using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.DragDrop;
@@ -16,7 +18,6 @@ using Content.Shared.Nutrition.Components;
 using Content.Shared.Popups;
 using Content.Shared.Storage;
 using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
@@ -36,6 +37,7 @@ namespace Content.Server.Kitchen.EntitySystems
         [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
         [Dependency] private readonly SharedAudioSystem _audio = default!;
         [Dependency] private readonly MetaDataSystem _metaData = default!;
+        [Dependency] private readonly SharedSuicideSystem _suicide = default!;
 
         public override void Initialize()
         {
@@ -48,31 +50,38 @@ namespace Content.Server.Kitchen.EntitySystems
             //DoAfter
             SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter);
 
-            SubscribeLocalEvent<KitchenSpikeComponent, SuicideEvent>(OnSuicide);
+            SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
 
             SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop);
         }
 
-        private void OnButcherableCanDrop(EntityUid uid, ButcherableComponent component, ref CanDropDraggedEvent args)
+        private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args)
         {
             args.Handled = true;
-            args.CanDrop |= component.Type != ButcheringType.Knife;
+            args.CanDrop |= entity.Comp.Type != ButcheringType.Knife;
         }
 
-        private void OnSuicide(EntityUid uid, KitchenSpikeComponent component, SuicideEvent args)
+        /// <summary>
+        /// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them.
+        /// </summary>
+        private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args)
         {
             if (args.Handled)
                 return;
-            args.SetHandled(SuicideKind.Piercing);
-            var victim = args.Victim;
-            var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", victim));
-            _popupSystem.PopupEntity(othersMessage, victim);
+
+            if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
+                return;
+
+            _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing");
+            var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", ("victim", args.Victim));
+            _popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true);
 
             var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self");
-            _popupSystem.PopupEntity(selfMessage, victim, victim);
+            _popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim);
+            args.Handled = true;
         }
 
-        private void OnDoAfter(EntityUid uid, KitchenSpikeComponent component, DoAfterEvent args)
+        private void OnDoAfter(Entity<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args)
         {
             if (args.Args.Target == null)
                 return;
@@ -82,49 +91,49 @@ namespace Content.Server.Kitchen.EntitySystems
 
             if (args.Cancelled)
             {
-                component.InUse = false;
+                entity.Comp.InUse = false;
                 return;
             }
 
             if (args.Handled)
                 return;
 
-            if (Spikeable(uid, args.Args.User, args.Args.Target.Value, component, butcherable))
-                Spike(uid, args.Args.User, args.Args.Target.Value, component);
+            if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable))
+                Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp);
 
-            component.InUse = false;
+            entity.Comp.InUse = false;
             args.Handled = true;
         }
 
-        private void OnDragDrop(EntityUid uid, KitchenSpikeComponent component, ref DragDropTargetEvent args)
+        private void OnDragDrop(Entity<KitchenSpikeComponent> entity, ref DragDropTargetEvent args)
         {
             if (args.Handled)
                 return;
 
             args.Handled = true;
 
-            if (Spikeable(uid, args.User, args.Dragged, component))
-                TrySpike(uid, args.User, args.Dragged, component);
+            if (Spikeable(entity, args.User, args.Dragged, entity.Comp))
+                TrySpike(entity, args.User, args.Dragged, entity.Comp);
         }
 
-        private void OnInteractHand(EntityUid uid, KitchenSpikeComponent component, InteractHandEvent args)
+        private void OnInteractHand(Entity<KitchenSpikeComponent> entity, ref InteractHandEvent args)
         {
             if (args.Handled)
                 return;
 
-            if (component.PrototypesToSpawn?.Count > 0)
+            if (entity.Comp.PrototypesToSpawn?.Count > 0)
             {
-                _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), uid, args.User);
+                _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User);
                 args.Handled = true;
             }
         }
 
-        private void OnInteractUsing(EntityUid uid, KitchenSpikeComponent component, InteractUsingEvent args)
+        private void OnInteractUsing(Entity<KitchenSpikeComponent> entity, ref InteractUsingEvent args)
         {
             if (args.Handled)
                 return;
 
-            if (TryGetPiece(uid, args.User, args.Used))
+            if (TryGetPiece(entity, args.User, args.Used))
                 args.Handled = true;
         }
 
index 98c875e773525046102ff25206abc096b0a63c65..c05c679f176880da86880a10d41d246e0926b14a 100644 (file)
@@ -39,6 +39,8 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 using Content.Shared.Stacks;
 using Content.Server.Construction.Components;
+using Content.Shared.Chat;
+using Content.Shared.Damage;
 
 namespace Content.Server.Kitchen.EntitySystems
 {
@@ -65,6 +67,7 @@ namespace Content.Server.Kitchen.EntitySystems
         [Dependency] private readonly SharedStackSystem _stack = default!;
         [Dependency] private readonly IPrototypeManager _prototype = default!;
         [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+        [Dependency] private readonly SharedSuicideSystem _suicide = default!;
 
         [ValidatePrototypeId<EntityPrototype>]
         private const string MalfunctionSpark = "Spark";
@@ -83,7 +86,7 @@ namespace Content.Server.Kitchen.EntitySystems
             SubscribeLocalEvent<MicrowaveComponent, BreakageEventArgs>(OnBreak);
             SubscribeLocalEvent<MicrowaveComponent, PowerChangedEvent>(OnPowerChanged);
             SubscribeLocalEvent<MicrowaveComponent, AnchorStateChangedEvent>(OnAnchorChanged);
-            SubscribeLocalEvent<MicrowaveComponent, SuicideEvent>(OnSuicide);
+            SubscribeLocalEvent<MicrowaveComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
 
             SubscribeLocalEvent<MicrowaveComponent, SignalReceivedEvent>(OnSignalReceived);
 
@@ -260,12 +263,22 @@ namespace Content.Server.Kitchen.EntitySystems
             _deviceLink.EnsureSinkPorts(ent, ent.Comp.OnPort);
         }
 
-        private void OnSuicide(Entity<MicrowaveComponent> ent, ref SuicideEvent args)
+        /// <summary>
+        /// Kills the user by microwaving their head
+        /// TODO: Make this not awful, it keeps any items attached to your head still on and you can revive someone and cogni them so you have some dumb headless fuck running around. I've seen it happen.
+        /// </summary>
+        private void OnSuicideByEnvironment(Entity<MicrowaveComponent> ent, ref SuicideByEnvironmentEvent args)
         {
             if (args.Handled)
                 return;
 
-            args.SetHandled(SuicideKind.Heat);
+            // The act of getting your head microwaved doesn't actually kill you
+            if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
+                return;
+
+            // The application of lethal damage is what kills you...
+            _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Heat");
+
             var victim = args.Victim;
             var headCount = 0;
 
@@ -295,6 +308,7 @@ namespace Content.Server.Kitchen.EntitySystems
             ent.Comp.CurrentCookTimerTime = 10;
             Wzhzhzh(ent.Owner, ent.Comp, args.Victim);
             UpdateUserInterfaceState(ent.Owner, ent.Comp);
+            args.Handled = true;
         }
 
         private void OnSolutionChange(Entity<MicrowaveComponent> ent, ref SolutionContainerChangedEvent args)
index 0d6d27777a43bbec7ee4b8a891c56e78c4345259..b962af2b41fd201c180226b6e7b3717cbcb84a0e 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Server.Fluids.EntitySystems;
 using Content.Server.GameTicking;
 using Content.Server.Popups;
@@ -48,7 +48,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
         SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
         SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing,
             before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)});
-        SubscribeLocalEvent<MaterialReclaimerComponent, SuicideEvent>(OnSuicide);
+        SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
         SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged);
     }
     private void OnStartup(Entity<MaterialReclaimerComponent> entity, ref ComponentStartup args)
@@ -86,12 +86,11 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
         args.Handled = TryStartProcessItem(entity.Owner, args.Used, entity.Comp, args.User);
     }
 
-    private void OnSuicide(Entity<MaterialReclaimerComponent> entity, ref SuicideEvent args)
+    private void OnSuicideByEnvironment(Entity<MaterialReclaimerComponent> entity, ref SuicideByEnvironmentEvent args)
     {
         if (args.Handled)
             return;
 
-        args.SetHandled(SuicideKind.Bloodloss);
         var victim = args.Victim;
         if (TryComp(victim, out ActorComponent? actor) &&
             _mind.TryGetMind(actor.PlayerSession, out var mindId, out var mind))
@@ -103,12 +102,15 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
             }
         }
 
-        _popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others", ("victim", Identity.Entity(victim, EntityManager))),
+        _popup.PopupEntity(Loc.GetString("recycler-component-suicide-message-others",
+                ("victim", Identity.Entity(victim, EntityManager))),
             victim,
-            Filter.PvsExcept(victim, entityManager: EntityManager), true);
+            Filter.PvsExcept(victim, entityManager: EntityManager),
+            true);
 
         _body.GibBody(victim, true);
         _appearance.SetData(entity.Owner, RecyclerVisuals.Bloody, true);
+        args.Handled = true;
     }
 
     private void OnActivePowerChanged(Entity<ActiveMaterialReclaimerComponent> entity, ref PowerChangedEvent args)
index 5be93f7fbc4bdbb9dd4bd34f6463e363a261f43e..c5beed718ed7797341ab1893238d4472611ee522 100644 (file)
@@ -106,11 +106,11 @@ namespace Content.Server.Medical.BiomassReclaimer
             SubscribeLocalEvent<BiomassReclaimerComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
             SubscribeLocalEvent<BiomassReclaimerComponent, ClimbedOnEvent>(OnClimbedOn);
             SubscribeLocalEvent<BiomassReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
-            SubscribeLocalEvent<BiomassReclaimerComponent, SuicideEvent>(OnSuicide);
+            SubscribeLocalEvent<BiomassReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
             SubscribeLocalEvent<BiomassReclaimerComponent, ReclaimerDoAfterEvent>(OnDoAfter);
         }
 
-        private void OnSuicide(Entity<BiomassReclaimerComponent> ent, ref SuicideEvent args)
+        private void OnSuicideByEnvironment(Entity<BiomassReclaimerComponent> ent, ref SuicideByEnvironmentEvent args)
         {
             if (args.Handled)
                 return;
@@ -123,7 +123,7 @@ namespace Content.Server.Medical.BiomassReclaimer
 
             _popup.PopupEntity(Loc.GetString("biomass-reclaimer-suicide-others", ("victim", args.Victim)), ent, PopupType.LargeCaution);
             StartProcessing(args.Victim, ent);
-            args.SetHandled(SuicideKind.Blunt);
+            args.Handled = true;
         }
 
         private void OnInit(EntityUid uid, ActiveBiomassReclaimerComponent component, ComponentInit args)
index 54b47cff84adccef09a99bd842a722724ae7a2d3..f6859b610af7dd5b76b03f2873131397867745a1 100644 (file)
@@ -38,7 +38,7 @@ public sealed class CrematoriumSystem : EntitySystem
 
         SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine);
         SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb);
-        SubscribeLocalEvent<CrematoriumComponent, SuicideEvent>(OnSuicide);
+        SubscribeLocalEvent<CrematoriumComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
         SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen);
     }
 
@@ -146,11 +146,10 @@ public sealed class CrematoriumSystem : EntitySystem
         _audio.PlayPvs(component.CremateFinishSound, uid);
     }
 
-    private void OnSuicide(EntityUid uid, CrematoriumComponent component, SuicideEvent args)
+    private void OnSuicideByEnvironment(EntityUid uid, CrematoriumComponent component, SuicideByEnvironmentEvent args)
     {
         if (args.Handled)
             return;
-        args.SetHandled(SuicideKind.Heat);
 
         var victim = args.Victim;
         if (TryComp(victim, out ActorComponent? actor) && _minds.TryGetMind(victim, out var mindId, out var mind))
@@ -179,6 +178,7 @@ public sealed class CrematoriumSystem : EntitySystem
         }
         _entityStorage.CloseStorage(uid);
         Cremate(uid, component);
+        args.Handled = true;
     }
 
     public override void Update(float frameTime)
diff --git a/Content.Shared/Chat/SharedSuicideSystem.cs b/Content.Shared/Chat/SharedSuicideSystem.cs
new file mode 100644 (file)
index 0000000..d341ea8
--- /dev/null
@@ -0,0 +1,67 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Mobs.Components;
+using Robust.Shared.Prototypes;
+using System.Linq;
+
+namespace Content.Shared.Chat;
+
+public sealed class SharedSuicideSystem : EntitySystem
+{
+    [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+    /// <summary>
+    /// Applies lethal damage spread out across the damage types given.
+    /// </summary>
+    public void ApplyLethalDamage(Entity<DamageableComponent> target, DamageSpecifier damageSpecifier)
+    {
+        // Create a new damageSpecifier so that we don't make alterations to the original DamageSpecifier
+        // Failing  to do this will permanently change a weapon's damage making it insta-kill people
+        var appliedDamageSpecifier = new DamageSpecifier(damageSpecifier);
+        if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds))
+            return;
+
+        // Mob thresholds are sorted from alive -> crit -> dead,
+        // grabbing the last key will give us how much damage is needed to kill a target from zero
+        // The exact lethal damage amount is adjusted based on their current damage taken
+        var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage;
+        var totalDamage = appliedDamageSpecifier.GetTotal();
+
+        // Removing structural because it causes issues against entities that cannot take structural damage,
+        // then getting the total to use in calculations for spreading out damage.
+        appliedDamageSpecifier.DamageDict.Remove("Structural");
+
+        // Split the total amount of damage needed to kill the target by every damage type in the DamageSpecifier
+        foreach (var (key, value) in appliedDamageSpecifier.DamageDict)
+        {
+            appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage));
+        }
+
+        _damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target);
+    }
+
+    /// <summary>
+    /// Applies lethal damage in a single type, specified by a single damage type.
+    /// </summary>
+    public void ApplyLethalDamage(Entity<DamageableComponent> target, ProtoId<DamageTypePrototype>? damageType)
+    {
+        if (!TryComp<MobThresholdsComponent>(target, out var mobThresholds))
+            return;
+
+        // Mob thresholds are sorted from alive -> crit -> dead,
+        // grabbing the last key will give us how much damage is needed to kill a target from zero
+        // The exact lethal damage amount is adjusted based on their current damage taken
+        var lethalAmountOfDamage = mobThresholds.Thresholds.Keys.Last() - target.Comp.TotalDamage;
+
+        // We don't want structural damage for the same reasons listed above
+        if (!_prototypeManager.TryIndex(damageType, out var damagePrototype) || damagePrototype.ID == "Structural")
+        {
+            Log.Error($"{nameof(SharedSuicideSystem)} could not find the damage type prototype associated with {damageType}. Falling back to Blunt");
+            damagePrototype = _prototypeManager.Index<DamageTypePrototype>("Blunt");
+        }
+
+        var damage = new DamageSpecifier(damagePrototype, lethalAmountOfDamage);
+        _damageableSystem.TryChangeDamage(target, damage, true, origin: target);
+    }
+}
diff --git a/Content.Shared/Execution/DoAfterEvent.cs b/Content.Shared/Execution/DoAfterEvent.cs
new file mode 100644 (file)
index 0000000..7854974
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Execution;
+
+[Serializable, NetSerializable]
+public sealed partial class ExecutionDoAfterEvent : SimpleDoAfterEvent
+{
+}
diff --git a/Content.Shared/Execution/ExecutionComponent.cs b/Content.Shared/Execution/ExecutionComponent.cs
new file mode 100644 (file)
index 0000000..31477ea
--- /dev/null
@@ -0,0 +1,77 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Execution;
+
+/// <summary>
+/// Added to entities that can be used to execute another target.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ExecutionComponent : Component
+{
+    /// <summary>
+    /// How long the execution duration lasts.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float DoAfterDuration = 5f;
+
+    /// <summary>
+    /// Arbitrarily chosen number to multiply damage by, used to deal reasonable amounts of damage to a victim of an execution.
+    /// /// </summary>
+    [DataField, AutoNetworkedField]
+    public float DamageMultiplier = 9f;
+
+    /// <summary>
+    /// Shown to the person performing the melee execution (attacker) upon starting a melee execution.
+    /// </summary>
+    [DataField]
+    public LocId InternalMeleeExecutionMessage = "execution-popup-melee-initial-internal";
+
+    /// <summary>
+    /// Shown to bystanders and the victim of a melee execution when a melee execution is started.
+    /// </summary>
+    [DataField]
+    public LocId ExternalMeleeExecutionMessage = "execution-popup-melee-initial-external";
+
+    /// <summary>
+    /// Shown to the attacker upon completion of a melee execution.
+    /// </summary>
+    [DataField]
+    public LocId CompleteInternalMeleeExecutionMessage = "execution-popup-melee-complete-internal";
+
+    /// <summary>
+    /// Shown to bystanders and the victim of a melee execution when a melee execution is completed.
+    /// </summary>
+    [DataField]
+    public LocId CompleteExternalMeleeExecutionMessage = "execution-popup-melee-complete-external";
+
+    /// <summary>
+    /// Shown to the person performing the self execution when starting one.
+    /// </summary>
+    [DataField]
+    public LocId InternalSelfExecutionMessage = "execution-popup-self-initial-internal";
+
+    /// <summary>
+    /// Shown to bystanders near a self execution when one is started.
+    /// </summary>
+    [DataField]
+    public LocId ExternalSelfExecutionMessage = "execution-popup-self-initial-external";
+
+    /// <summary>
+    /// Shown to the person performing a self execution upon completion of a do-after or on use of /suicide with a weapon that has the Execution component.
+    /// </summary>
+    [DataField]
+    public LocId CompleteInternalSelfExecutionMessage = "execution-popup-self-complete-internal";
+
+    /// <summary>
+    /// Shown to bystanders when a self execution is completed or a suicide via execution weapon happens nearby.
+    /// </summary>
+    [DataField]
+    public LocId CompleteExternalSelfExecutionMessage = "execution-popup-self-complete-external";
+
+    // Not networked because this is transient inside of a tick.
+    /// <summary>
+    /// True if it is currently executing for handlers.
+    /// </summary>
+    [DataField]
+    public bool Executing = false;
+}
diff --git a/Content.Shared/Execution/SharedExecutionSystem.cs b/Content.Shared/Execution/SharedExecutionSystem.cs
new file mode 100644 (file)
index 0000000..a1105dd
--- /dev/null
@@ -0,0 +1,234 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Chat;
+using Content.Shared.CombatMode;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Mind;
+using Robust.Shared.Player;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Shared.Execution;
+
+/// <summary>
+///     Verb for violently murdering cuffed creatures.
+/// </summary>
+public sealed class SharedExecutionSystem : EntitySystem
+{
+    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedSuicideSystem _suicide = default!;
+    [Dependency] private readonly SharedCombatModeSystem _combat = default!;
+    [Dependency] private readonly SharedExecutionSystem _execution = default!;
+    [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
+    [Dependency] private readonly SharedMindSystem _mind = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ExecutionComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionsVerbs);
+        SubscribeLocalEvent<ExecutionComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
+        SubscribeLocalEvent<ExecutionComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
+        SubscribeLocalEvent<ExecutionComponent, ExecutionDoAfterEvent>(OnExecutionDoAfter);
+    }
+
+    private void OnGetInteractionsVerbs(EntityUid uid, ExecutionComponent comp, GetVerbsEvent<UtilityVerb> args)
+    {
+        if (args.Hands == null || args.Using == null || !args.CanAccess || !args.CanInteract)
+            return;
+
+        var attacker = args.User;
+        var weapon = args.Using.Value;
+        var victim = args.Target;
+
+        if (!CanBeExecuted(victim, attacker))
+            return;
+
+        UtilityVerb verb = new()
+        {
+            Act = () => TryStartExecutionDoAfter(weapon, victim, attacker, comp),
+            Impact = LogImpact.High,
+            Text = Loc.GetString("execution-verb-name"),
+            Message = Loc.GetString("execution-verb-message"),
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void TryStartExecutionDoAfter(EntityUid weapon, EntityUid victim, EntityUid attacker, ExecutionComponent comp)
+    {
+        if (!CanBeExecuted(victim, attacker))
+            return;
+
+        if (attacker == victim)
+        {
+            ShowExecutionInternalPopup(comp.InternalSelfExecutionMessage, attacker, victim, weapon);
+            ShowExecutionExternalPopup(comp.ExternalSelfExecutionMessage, attacker, victim, weapon);
+        }
+        else
+        {
+            ShowExecutionInternalPopup(comp.InternalMeleeExecutionMessage, attacker, victim, weapon);
+            ShowExecutionExternalPopup(comp.ExternalMeleeExecutionMessage, attacker, victim, weapon);
+        }
+
+        var doAfter =
+            new DoAfterArgs(EntityManager, attacker, comp.DoAfterDuration, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon)
+            {
+                BreakOnMove = true,
+                BreakOnDamage = true,
+                NeedHand = true
+            };
+
+        _doAfter.TryStartDoAfter(doAfter);
+
+    }
+
+    public bool CanBeExecuted(EntityUid victim, EntityUid attacker)
+    {
+        // No point executing someone if they can't take damage
+        if (!HasComp<DamageableComponent>(victim))
+            return false;
+
+        // You can't execute something that cannot die
+        if (!TryComp<MobStateComponent>(victim, out var mobState))
+            return false;
+
+        // You're not allowed to execute dead people (no fun allowed)
+        if (_mobState.IsDead(victim, mobState))
+            return false;
+
+        // You must be able to attack people to execute
+        if (!_actionBlocker.CanAttack(attacker, victim))
+            return false;
+
+        // The victim must be incapacitated to be executed
+        if (victim != attacker && _actionBlocker.CanInteract(victim, null))
+            return false;
+
+        // All checks passed
+        return true;
+    }
+
+    private void OnGetMeleeDamage(Entity<ExecutionComponent> entity, ref GetMeleeDamageEvent args)
+    {
+        if (!TryComp<MeleeWeaponComponent>(entity, out var melee) || !entity.Comp.Executing)
+        {
+            return;
+        }
+
+        var bonus = melee.Damage * entity.Comp.DamageMultiplier - melee.Damage;
+        args.Damage += bonus;
+        args.ResistanceBypass = true;
+    }
+
+    private void OnSuicideByEnvironment(Entity<ExecutionComponent> entity, ref SuicideByEnvironmentEvent args)
+    {
+        if (!TryComp<MeleeWeaponComponent>(entity, out var melee))
+            return;
+
+        string? internalMsg = entity.Comp.CompleteInternalSelfExecutionMessage;
+        string? externalMsg = entity.Comp.CompleteExternalSelfExecutionMessage;
+
+        if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
+            return;
+
+        ShowExecutionInternalPopup(internalMsg, args.Victim, args.Victim, entity, false);
+        ShowExecutionExternalPopup(externalMsg, args.Victim, args.Victim, entity);
+        _audio.PlayPredicted(melee.HitSound, args.Victim, args.Victim);
+        _suicide.ApplyLethalDamage((args.Victim, damageableComponent), melee.Damage);
+        args.Handled = true;
+    }
+
+    private void ShowExecutionInternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon, bool predict = true)
+    {
+        if (predict)
+        {
+            _popup.PopupClient(
+               Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
+               attacker,
+               attacker,
+               PopupType.MediumCaution
+               );
+        }
+        else
+        {
+            _popup.PopupEntity(
+               Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
+               attacker,
+               attacker,
+               PopupType.MediumCaution
+               );
+        }
+    }
+
+    private void ShowExecutionExternalPopup(string locString, EntityUid attacker, EntityUid victim, EntityUid weapon)
+    {
+        _popup.PopupEntity(
+            Loc.GetString(locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
+            attacker,
+            Filter.PvsExcept(attacker),
+            true,
+            PopupType.MediumCaution
+            );
+    }
+
+    private void OnExecutionDoAfter(Entity<ExecutionComponent> entity, ref ExecutionDoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
+            return;
+
+        if (!TryComp<MeleeWeaponComponent>(entity, out var meleeWeaponComp))
+            return;
+
+        var attacker = args.User;
+        var victim = args.Target.Value;
+        var weapon = args.Used.Value;
+
+        if (!_execution.CanBeExecuted(victim, attacker))
+            return;
+
+        // This is needed so the melee system does not stop it.
+        var prev = _combat.IsInCombatMode(attacker);
+        _combat.SetInCombatMode(attacker, true);
+        entity.Comp.Executing = true;
+
+        var internalMsg = entity.Comp.CompleteInternalMeleeExecutionMessage;
+        var externalMsg = entity.Comp.CompleteExternalMeleeExecutionMessage;
+
+        if (attacker == victim)
+        {
+            var suicideEvent = new SuicideEvent(victim);
+            RaiseLocalEvent(victim, suicideEvent);
+
+            var suicideGhostEvent = new SuicideGhostEvent(victim);
+            RaiseLocalEvent(victim, suicideGhostEvent);
+        }
+        else
+        {
+            _melee.AttemptLightAttack(attacker, weapon, meleeWeaponComp, victim);
+        }
+
+        _combat.SetInCombatMode(attacker, prev);
+        entity.Comp.Executing = false;
+        args.Handled = true;
+
+        if (attacker != victim)
+        {
+            _execution.ShowExecutionInternalPopup(internalMsg, attacker, victim, entity);
+            _execution.ShowExecutionExternalPopup(externalMsg, attacker, victim, entity);
+        }
+    }
+}
index 7b9c1efe0dd533f07f475ade3154e850a668622f..bcd5df67abf357572c82d9547c3a5741055dbc34 100644 (file)
@@ -1,48 +1,41 @@
-namespace Content.Shared.Interaction.Events
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Interaction.Events;
+
+/// <summary>
+///     Raised Directed at an entity to check whether they will handle the suicide.
+/// </summary>
+public sealed class SuicideEvent : HandledEntityEventArgs
 {
-    /// <summary>
-    ///     Raised Directed at an entity to check whether they will handle the suicide.
-    /// </summary>
-    public sealed class SuicideEvent : EntityEventArgs
+    public SuicideEvent(EntityUid victim)
     {
-        public SuicideEvent(EntityUid victim)
-        {
-            Victim = victim;
-        }
-        public void SetHandled(SuicideKind kind)
-        {
-            if (Handled)
-                throw new InvalidOperationException("Suicide was already handled");
-
-            Kind = kind;
-        }
+        Victim = victim;
+    }
 
-        public void BlockSuicideAttempt(bool suicideAttempt)
-        {
-            if (suicideAttempt)
-                AttemptBlocked = suicideAttempt;
-        }
+    public DamageSpecifier? DamageSpecifier;
+    public ProtoId<DamageTypePrototype>? DamageType;
+    public EntityUid Victim { get; private set; }
+}
 
-        public SuicideKind? Kind { get; private set; }
-        public EntityUid Victim { get; private set; }
-        public bool AttemptBlocked { get; private set; }
-        public bool Handled => Kind != null;
+public sealed class SuicideByEnvironmentEvent : HandledEntityEventArgs
+{
+    public SuicideByEnvironmentEvent(EntityUid victim)
+    {
+        Victim = victim;
     }
 
-    public enum SuicideKind
-    {
-        Special, //Doesn't damage the mob, used for "weird" suicides like gibbing
+    public EntityUid Victim { get; set; }
+}
 
-        //Damage type suicides
-        Blunt,
-        Slash,
-        Piercing,
-        Heat,
-        Shock,
-        Cold,
-        Poison,
-        Radiation,
-        Asphyxiation,
-        Bloodloss
+public sealed class SuicideGhostEvent : HandledEntityEventArgs
+{
+    public SuicideGhostEvent(EntityUid victim)
+    {
+        Victim = victim;
     }
+
+    public EntityUid Victim { get; set; }
+    public bool CanReturnToBody;
 }
index ba365daf15cfe60b3055676e4a3dcd1b1825c8a1..24b47b641205d28b0919579d14f17a60dec9c912 100644 (file)
@@ -168,15 +168,17 @@ public abstract class SharedMindSystem : EntitySystem
             args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]");
     }
 
+    /// <summary>
+    /// Checks to see if the user's mind prevents them from suicide
+    /// Handles the suicide event without killing the user if true
+    /// </summary>
     private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args)
     {
         if (args.Handled)
             return;
 
         if (TryComp(component.Mind, out MindComponent? mind) && mind.PreventSuicide)
-        {
-            args.BlockSuicideAttempt(true);
-        }
+            args.Handled = true;
     }
 
     public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null)
index 55c01c1d6f4c6dbd269dc1970449c8488551dcae..75c85790deb215ea4f7aebdea437be563b082491 100644 (file)
@@ -80,7 +80,7 @@ public sealed class MeleeHitEvent : HandledEntityEventArgs
 /// Raised on a melee weapon to calculate potential damage bonuses or decreases.
 /// </summary>
 [ByRefEvent]
-public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User);
+public record struct GetMeleeDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid User, bool ResistanceBypass = false);
 
 /// <summary>
 /// Raised on a melee weapon to calculate the attack rate.
index fa5e0b3a905b1d85101fc55a6cff6c8c2033b981..212c03475cf11b9eea3fbd99c7c8f50a17acebf8 100644 (file)
@@ -67,6 +67,12 @@ public sealed partial class MeleeWeaponComponent : Component
     [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
     public bool AutoAttack;
 
+    /// <summary>
+    /// If true, attacks will bypass armor resistances.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public bool ResistanceBypass = false;
+    
     /// <summary>
     /// Base damage for this weapon. Can be modified via heavy damage or other means.
     /// </summary>
index ae0ebfe74caeb5e6787069d5ec12f8c31adf610d..bc19235cd3927e00900fce0a70818ac826c58068 100644 (file)
@@ -216,7 +216,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         if (!Resolve(uid, ref component, false))
             return new DamageSpecifier();
 
-        var ev = new GetMeleeDamageEvent(uid, new (component.Damage), new(), user);
+        var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass);
         RaiseLocalEvent(uid, ref ev);
 
         return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers);
@@ -244,6 +244,17 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         return ev.DamageModifier * ev.Multipliers;
     }
 
+    public bool GetResistanceBypass(EntityUid uid, EntityUid user, MeleeWeaponComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+            return false;
+
+        var ev = new GetMeleeDamageEvent(uid, new(component.Damage), new(), user, component.ResistanceBypass);
+        RaiseLocalEvent(uid, ref ev);
+
+        return ev.ResistanceBypass;
+    }
+
     public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee)
     {
         weaponUid = default;
@@ -441,6 +452,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         // If I do not come back later to fix Light Attacks being Heavy Attacks you can throw me in the spider pit -Errant
         var damage = GetDamage(meleeUid, user, component) * GetHeavyDamageModifier(meleeUid, user, component);
         var target = GetEntity(ev.Target);
+        var resistanceBypass = GetResistanceBypass(meleeUid, user, component);
 
         // For consistency with wide attacks stuff needs damageable.
         if (Deleted(target) ||
@@ -497,7 +509,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         RaiseLocalEvent(target.Value, attackedEvent);
 
         var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
-        var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user);
+        var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, origin:user, ignoreResistances:resistanceBypass);
 
         if (damageResult is {Empty: false})
         {
diff --git a/Resources/Locale/en-US/execution/execution.ftl b/Resources/Locale/en-US/execution/execution.ftl
new file mode 100644 (file)
index 0000000..08f9a06
--- /dev/null
@@ -0,0 +1,17 @@
+execution-verb-name = Execute
+execution-verb-message = Use your weapon to execute someone.
+
+# All the below localisation strings have access to the following variables
+# attacker (the person committing the execution)
+# victim (the person being executed)
+# weapon (the weapon used for the execution)
+
+execution-popup-melee-initial-internal = You ready {THE($weapon)} against {$victim}'s throat.
+execution-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against the throat of {$victim}.
+execution-popup-melee-complete-internal = You slit the throat of {$victim}!
+execution-popup-melee-complete-external = {$attacker} slits the throat of {$victim}!
+
+execution-popup-self-initial-internal = You ready {THE($weapon)} against your own throat.
+execution-popup-self-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against their own throat.
+execution-popup-self-complete-internal = You slit your own throat!
+execution-popup-self-complete-external = {$attacker} slits their own throat!
\ No newline at end of file
index 8f522abce4225383010da4ab3f3fd283adeb16bf..47828ed8f58810b2ff16521c5338ba6d65e5ed22 100644 (file)
@@ -6,6 +6,8 @@
   description:  A small piece of crystal.
   components:
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: Sprite
     layers:
       - sprite: Objects/Materials/Shards/crystal.rsi
index fa6937dac341f599faae18ccd32bf49fe35f2a05..5668661e088c7c8d6c21fb6b67a02f8a11c6427f 100644 (file)
@@ -5,6 +5,8 @@
   description: It's a shard of some unknown material.
   components:
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: Sprite
     layers:
       - sprite: Objects/Materials/Shards/shard.rsi
index 9d3ef6c42427cee2a01c73c537ea4960b8223bed..b458f0ae215648f518f2d50dae388d6a8383a90e 100644 (file)
@@ -5,6 +5,8 @@
   description: In Space Glasgow this is called a conversation starter.
   components:
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: MeleeWeapon
     attackRate: 1.5
     damage:
index 49b22c000d595a7b363df0b9bd7f372e88d91933..398c04aee6e6ae35e42c8e7c467a6de1733c64a5 100644 (file)
@@ -5,6 +5,8 @@
   description: A grotesque blade made out of bone and flesh that cleaves through people as a hot knife through butter.
   components:
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: Sprite
     sprite: Objects/Weapons/Melee/armblade.rsi
     state: icon
index 93765ec40c361a2866927f8e3fa5b05858823281..f6a47496549ebb763762976f122b1bce5edbe34a 100644 (file)
@@ -8,6 +8,8 @@
     tags:
     - FireAxe
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: Sprite
     sprite: Objects/Weapons/Melee/fireaxe.rsi
     state: icon
index 39a04120e0d9ec872c27232c7f4ec271f340d8c2..8270a50bd6aa5b5a2a4f23f7ddb6169211ab0631 100644 (file)
@@ -7,6 +7,8 @@
     tags:
     - Knife
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: Utensil
     types:
       - Knife
index 838cde619faf1b3d1c2d2a3ff534199ecf15fade..c2449a6bcbd563acf1878d8f6530313e7bb150e0 100644 (file)
@@ -4,6 +4,8 @@
   abstract: true
   components:
   - type: Sharp
+  - type: Execution
+    doAfterDuration: 4.0
   - type: MeleeWeapon
     wideAnimationRotation: -135
   - type: Sprite