]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Executions (#24150)
authorVeritius <veritiusgaming@gmail.com>
Wed, 17 Jan 2024 08:45:45 +0000 (19:45 +1100)
committerGitHub <noreply@github.com>
Wed, 17 Jan 2024 08:45:45 +0000 (01:45 -0700)
* Execution (you monster)
not done

* woops

* more stuff

* Melee executions

* Prevent executing those who can interact

* Better checks for if you can execute

* Scale the execution time of a knife with its attack speed

* Translations for fucking up an execution

* rename some functions

* Properly scale execution speed of melee weapons

* Fix checks in CanExecuteWithAny

* Allow executing yourself (funny)

* More versatile localisation

* Suicide with guns

* Popups for successful gun executions

* whoops

* Stop flare guns crashing the game on executions

* Various tweaks

* Remove some old usings

* Pacifists can no longer execute

* Remove unnecessary check

* Use CanShoot in gunsystem

* Capitalisation in ftl string

* Fix melee executions not playing a sound

* localisation tweaks

Content.Server/Execution/ExecutionSystem.cs [new file with mode: 0644]
Content.Shared/Execution/DoafterEvent.cs [new file with mode: 0644]
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Resources/Locale/en-US/execution/execution.ftl [new file with mode: 0644]

diff --git a/Content.Server/Execution/ExecutionSystem.cs b/Content.Server/Execution/ExecutionSystem.cs
new file mode 100644 (file)
index 0000000..4354608
--- /dev/null
@@ -0,0 +1,397 @@
+using Content.Server.Interaction;
+using Content.Server.Kitchen.Components;
+using Content.Server.Weapons.Ranged.Systems;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Execution;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Projectiles;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Ranged;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Execution;
+
+/// <summary>
+///     Verb for violently murdering cuffed creatures.
+/// </summary>
+public sealed class ExecutionSystem : EntitySystem
+{
+    [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+    [Dependency] private readonly InteractionSystem _interactionSystem = default!;
+    [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
+    [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IComponentFactory _componentFactory = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+    [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+    [Dependency] private readonly GunSystem _gunSystem = default!;
+
+    private const float MeleeExecutionTimeModifier = 5.0f;
+    private const float GunExecutionTime = 6.0f;
+    private const float DamageModifier = 9.0f;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+        
+        SubscribeLocalEvent<SharpComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionVerbsMelee);
+        SubscribeLocalEvent<GunComponent, GetVerbsEvent<UtilityVerb>>(OnGetInteractionVerbsGun);
+        
+        SubscribeLocalEvent<SharpComponent, ExecutionDoAfterEvent>(OnDoafterMelee);
+        SubscribeLocalEvent<GunComponent, ExecutionDoAfterEvent>(OnDoafterGun);
+    }
+
+    private void OnGetInteractionVerbsMelee(
+        EntityUid uid, 
+        SharpComponent component,
+        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 (!CanExecuteWithMelee(weapon, victim, attacker))
+            return;
+        
+        UtilityVerb verb = new()
+        {
+            Act = () =>
+            {
+                TryStartMeleeExecutionDoafter(weapon, victim, attacker);
+            },
+            Impact = LogImpact.High,
+            Text = Loc.GetString("execution-verb-name"),
+            Message = Loc.GetString("execution-verb-message"),
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void OnGetInteractionVerbsGun(
+        EntityUid uid, 
+        GunComponent component,
+        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 (!CanExecuteWithGun(weapon, victim, attacker))
+            return;
+        
+        UtilityVerb verb = new()
+        {
+            Act = () =>
+            {
+                TryStartGunExecutionDoafter(weapon, victim, attacker);
+            },
+            Impact = LogImpact.High,
+            Text = Loc.GetString("execution-verb-name"),
+            Message = Loc.GetString("execution-verb-message"),
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private bool CanExecuteWithAny(EntityUid weapon, EntityUid victim, EntityUid attacker)
+    {
+        // No point executing someone if they can't take damage
+        if (!TryComp<DamageableComponent>(victim, out var damage))
+            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 (_mobStateSystem.IsDead(victim, mobState))
+            return false;
+        
+        // You must be able to attack people to execute
+        if (!_actionBlockerSystem.CanAttack(attacker, victim))
+            return false;
+
+        // The victim must be incapacitated to be executed
+        if (victim != attacker && _actionBlockerSystem.CanInteract(victim, null))
+            return false;
+
+        // All checks passed
+        return true;
+    }
+
+    private bool CanExecuteWithMelee(EntityUid weapon, EntityUid victim, EntityUid user)
+    {
+        if (!CanExecuteWithAny(weapon, victim, user)) return false;
+        
+        // We must be able to actually hurt people with the weapon
+        if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f)
+            return false;
+
+        return true;
+    }
+    
+    private bool CanExecuteWithGun(EntityUid weapon, EntityUid victim, EntityUid user)
+    {
+        if (!CanExecuteWithAny(weapon, victim, user)) return false;
+        
+        // We must be able to actually fire the gun
+        if (!TryComp<GunComponent>(weapon, out var gun) && _gunSystem.CanShoot(gun!))
+            return false;
+
+        return true;
+    }
+    
+    private void TryStartMeleeExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker)
+    {
+        if (!CanExecuteWithMelee(weapon, victim, attacker))
+            return;
+
+        var executionTime = (1.0f / Comp<MeleeWeaponComponent>(weapon).AttackRate) * MeleeExecutionTimeModifier;
+
+        if (attacker == victim)
+        {
+            ShowExecutionPopup("suicide-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("suicide-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+        else
+        {
+            ShowExecutionPopup("execution-popup-melee-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("execution-popup-melee-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+        
+        var doAfter =
+            new DoAfterArgs(EntityManager, attacker, executionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon)
+            {
+                BreakOnTargetMove = true,
+                BreakOnUserMove = true,
+                BreakOnDamage = true,
+                NeedHand = true
+            };
+
+        _doAfterSystem.TryStartDoAfter(doAfter);
+    }
+    
+    private void TryStartGunExecutionDoafter(EntityUid weapon, EntityUid victim, EntityUid attacker)
+    {
+        if (!CanExecuteWithGun(weapon, victim, attacker))
+            return;
+        
+        if (attacker == victim)
+        {
+            ShowExecutionPopup("suicide-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("suicide-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+        else
+        {
+            ShowExecutionPopup("execution-popup-gun-initial-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("execution-popup-gun-initial-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+
+        var doAfter =
+            new DoAfterArgs(EntityManager, attacker, GunExecutionTime, new ExecutionDoAfterEvent(), weapon, target: victim, used: weapon)
+            {
+                BreakOnTargetMove = true,
+                BreakOnUserMove = true,
+                BreakOnDamage = true,
+                NeedHand = true
+            };
+
+        _doAfterSystem.TryStartDoAfter(doAfter);
+    }
+
+    private bool OnDoafterChecks(EntityUid uid, DoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
+            return false;
+        
+        if (!CanExecuteWithAny(args.Used.Value, args.Target.Value, uid))
+            return false;
+        
+        // All checks passed
+        return true;
+    }
+
+    private void OnDoafterMelee(EntityUid uid, SharpComponent component, DoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
+            return;
+        
+        var attacker = args.User;
+        var victim = args.Target!.Value;
+        var weapon = args.Used!.Value;
+
+        if (!CanExecuteWithMelee(weapon, victim, attacker)) return;
+
+        if (!TryComp<MeleeWeaponComponent>(weapon, out var melee) && melee!.Damage.GetTotal() > 0.0f)
+            return;
+        
+        _damageableSystem.TryChangeDamage(victim, melee.Damage * DamageModifier, true);
+        _audioSystem.PlayEntity(melee.HitSound, Filter.Pvs(weapon), weapon, true, AudioParams.Default);
+
+        if (attacker == victim)
+        {
+            ShowExecutionPopup("suicide-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("suicide-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+        else
+        {
+            ShowExecutionPopup("execution-popup-melee-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("execution-popup-melee-complete-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+        }
+    }
+    
+    // TODO: This repeats a lot of the code of the serverside GunSystem, make it not do that
+    private void OnDoafterGun(EntityUid uid, GunComponent component, DoAfterEvent args)
+    {
+        if (args.Handled || args.Cancelled || args.Used == null || args.Target == null)
+            return;
+        
+        var attacker = args.User;
+        var weapon = args.Used!.Value;
+        var victim = args.Target!.Value;
+
+        if (!CanExecuteWithGun(weapon, victim, attacker)) return;
+        
+        // Check if any systems want to block our shot
+        var prevention = new ShotAttemptedEvent
+        {
+            User = attacker,
+            Used = weapon
+        };
+        
+        RaiseLocalEvent(weapon, ref prevention);
+        if (prevention.Cancelled)
+            return;
+
+        RaiseLocalEvent(attacker, ref prevention);
+        if (prevention.Cancelled)
+            return;
+        
+        // Not sure what this is for but gunsystem uses it so ehhh
+        var attemptEv = new AttemptShootEvent(attacker, null);
+        RaiseLocalEvent(weapon, ref attemptEv);
+
+        if (attemptEv.Cancelled)
+        {
+            if (attemptEv.Message != null)
+            {
+                _popupSystem.PopupClient(attemptEv.Message, weapon, attacker);
+                return;
+            }
+        }
+        
+        // Take some ammunition for the shot (one bullet)
+        var fromCoordinates = Transform(attacker).Coordinates;
+        var ev = new TakeAmmoEvent(1, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, attacker);
+        RaiseLocalEvent(weapon, ev);
+
+        // Check if there's any ammo left
+        if (ev.Ammo.Count <= 0)
+        {
+            _audioSystem.PlayEntity(component.SoundEmpty, Filter.Pvs(weapon), weapon, true, AudioParams.Default);
+            ShowExecutionPopup("execution-popup-gun-empty", Filter.Pvs(weapon), PopupType.Medium, attacker, victim, weapon);
+            return;
+        }
+        
+        // Information about the ammo like damage
+        DamageSpecifier damage = new DamageSpecifier();
+
+        // Get some information from IShootable
+        var ammoUid = ev.Ammo[0].Entity;
+        switch (ev.Ammo[0].Shootable)
+        {
+            case CartridgeAmmoComponent cartridge:
+                // Get the damage value
+                var prototype = _prototypeManager.Index<EntityPrototype>(cartridge.Prototype);
+                prototype.TryGetComponent<ProjectileComponent>(out var projectileA, _componentFactory); // sloth forgive me
+                if (projectileA != null)
+                {
+                    damage = projectileA.Damage * cartridge.Count;
+                }
+
+                // Expend the cartridge
+                cartridge.Spent = true;
+                _appearanceSystem.SetData(ammoUid!.Value, AmmoVisuals.Spent, true);
+                Dirty(ammoUid.Value, cartridge);
+                
+                break;
+            
+            case AmmoComponent newAmmo:
+                TryComp<ProjectileComponent>(ammoUid, out var projectileB);
+                if (projectileB != null)
+                {
+                    damage = projectileB.Damage;
+                }
+                Del(ammoUid);
+                break;
+            
+            case HitscanPrototype hitscan:
+                damage = hitscan.Damage!;
+                break;
+            
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+
+        // Clumsy people have a chance to shoot themselves
+        if (TryComp<ClumsyComponent>(attacker, out var clumsy) && component.ClumsyProof == false)
+        {
+            if (_interactionSystem.TryRollClumsy(attacker, 0.33333333f, clumsy))
+            {
+                ShowExecutionPopup("execution-popup-gun-clumsy-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+                ShowExecutionPopup("execution-popup-gun-clumsy-external", Filter.PvsExcept(attacker), PopupType.MediumCaution, attacker, victim, weapon);
+
+                // You shoot yourself with the gun (no damage multiplier)
+                _damageableSystem.TryChangeDamage(attacker, damage, origin: attacker);
+                _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, true, AudioParams.Default);
+                return;
+            }
+        }
+        
+        // Gun successfully fired, deal damage
+        _damageableSystem.TryChangeDamage(victim, damage * DamageModifier, true);
+        _audioSystem.PlayEntity(component.SoundGunshot, Filter.Pvs(weapon), weapon, false, AudioParams.Default);
+        
+        // Popups
+        if (attacker != victim)
+        {
+            ShowExecutionPopup("execution-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.Medium, attacker, victim, weapon);
+            ShowExecutionPopup("execution-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon);
+        }
+        else
+        {
+            ShowExecutionPopup("suicide-popup-gun-complete-internal", Filter.Entities(attacker), PopupType.LargeCaution, attacker, victim, weapon);
+            ShowExecutionPopup("suicide-popup-gun-complete-external", Filter.PvsExcept(attacker), PopupType.LargeCaution, attacker, victim, weapon);
+        }
+    }
+
+    private void ShowExecutionPopup(string locString, Filter filter, PopupType type,
+        EntityUid attacker, EntityUid victim, EntityUid weapon)
+    {
+        _popupSystem.PopupEntity(Loc.GetString(
+                locString, ("attacker", attacker), ("victim", victim), ("weapon", weapon)),
+            attacker, filter, true, type);
+    }
+}
\ No newline at end of file
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
+{
+}
index c6adf00113247bb9d2c07cce4656a44fa8a6246c..6fa461860f6e3fdc7d0ba4ce7f293e5f068a16ed 100644 (file)
@@ -736,7 +736,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
         return true;
     }
 
-    private void PlayHitSound(EntityUid target, EntityUid? user, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound)
+    public void PlayHitSound(EntityUid target, EntityUid? user, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound)
     {
         var playedSound = 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..8bdf326
--- /dev/null
@@ -0,0 +1,30 @@
+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-gun-initial-internal = You ready the muzzle of {THE($weapon)} against {$victim}'s head.
+execution-popup-gun-initial-external = {$attacker} readies the muzzle of {THE($weapon)} against {$victim}'s head.
+execution-popup-gun-complete-internal = You blast {$victim} in the head!
+execution-popup-gun-complete-external = {$attacker} blasts {$victim} in the head!
+execution-popup-gun-clumsy-internal = You miss {$victim}'s head and shoot your foot instead!
+execution-popup-gun-clumsy-external = {$attacker} misses {$victim} and shoots {POSS-ADJ($attacker)} foot instead!
+execution-popup-gun-empty = {CAPITALIZE(THE($weapon))} clicks.
+
+suicide-popup-gun-initial-internal = You place the muzzle of {THE($weapon)} in your mouth.
+suicide-popup-gun-initial-external = {$attacker} places the muzzle of {THE($weapon)} in {POSS-ADJ($attacker)} mouth.
+suicide-popup-gun-complete-internal = You shoot yourself in the head!
+suicide-popup-gun-complete-external = {$attacker} shoots {REFLEXIVE($attacker)} in the head!
+
+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}!
+
+suicide-popup-melee-initial-internal = You ready {THE($weapon)} against your throat.
+suicide-popup-melee-initial-external = {$attacker} readies {POSS-ADJ($attacker)} {$weapon} against {POSS-ADJ($attacker)} throat.
+suicide-popup-melee-complete-internal = You slit your throat with {THE($weapon)}!
+suicide-popup-melee-complete-external = {$attacker} slits {POSS-ADJ($attacker)} throat with {THE($weapon)}!
\ No newline at end of file