]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Pacifism rework (#23037)
authorKara <lunarautomaton6@gmail.com>
Wed, 27 Dec 2023 09:55:48 +0000 (02:55 -0700)
committerGitHub <noreply@github.com>
Wed, 27 Dec 2023 09:55:48 +0000 (02:55 -0700)
* Pacifism rework

* grammar

12 files changed:
Content.Shared/ActionBlocker/ActionBlockerSystem.cs
Content.Shared/CombatMode/Pacification/PacificationSystem.cs
Content.Shared/CombatMode/Pacification/PacifiedComponent.cs
Content.Shared/CombatMode/Pacification/PacifismDangerousAttackComponent.cs [new file with mode: 0644]
Content.Shared/Interaction/Events/AttackAttemptEvent.cs
Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Resources/Locale/en-US/alerts/alerts.ftl
Resources/Locale/en-US/pacified/pacified.ftl
Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml
Resources/Prototypes/Entities/Structures/Power/substation.yml
Resources/Prototypes/Entities/Structures/Storage/Tanks/tanks.yml

index d2b12a4b2925f59f28ee9ee6b9f0e7bf7d361d0b..a3cd83042e69f5784b42993c2d146bc27a3cd7c3 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Events;
 using Content.Shared.Speech;
 using Content.Shared.Throwing;
+using Content.Shared.Weapons.Melee;
 using JetBrains.Annotations;
 using Robust.Shared.Containers;
 
@@ -145,7 +146,7 @@ namespace Content.Shared.ActionBlocker
             return !ev.Cancelled;
         }
 
-        public bool CanAttack(EntityUid uid, EntityUid? target = null)
+        public bool CanAttack(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
         {
             _container.TryGetOuterContainer(uid, Transform(uid), out var outerContainer);
             if (target != null &&  target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
@@ -155,7 +156,7 @@ namespace Content.Shared.ActionBlocker
                 return containerEv.CanAttack;
             }
 
-            var ev = new AttackAttemptEvent(uid, target);
+            var ev = new AttackAttemptEvent(uid, target, weapon, disarm);
             RaiseLocalEvent(uid, ev);
 
             if (ev.Cancelled)
index a1332fec76f6a8eef5ce493e0032102ff9fe11b9..0c5e12e6f0f6cf1de596c0fbbec4f0ef7162254f 100644 (file)
@@ -1,50 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Actions;
 using Content.Shared.Alert;
+using Content.Shared.FixedPoint;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Popups;
 using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.CombatMode.Pacification;
 
-/// <summary>
-/// Raised when a Pacified entity attempts to throw something.
-/// The throw is only permitted if this event is not cancelled.
-/// </summary>
-[ByRefEvent]
-public struct AttemptPacifiedThrowEvent
-{
-    public EntityUid ItemUid;
-    public EntityUid PlayerUid;
-
-    public AttemptPacifiedThrowEvent(EntityUid itemUid,  EntityUid playerUid)
-    {
-        ItemUid = itemUid;
-        PlayerUid = playerUid;
-    }
-
-    public bool Cancelled { get; private set; } = false;
-    public string? CancelReasonMessageId { get; private set; }
-
-    /// <param name="reasonMessageId">
-    /// Localization string ID for the reason this event has been cancelled.
-    /// If null, a generic message will be shown to the player.
-    /// Note that any supplied localization string MUST accept a '$projectile'
-    /// parameter specifying the name of the thrown entity.
-    /// </param>
-    public void Cancel(string? reasonMessageId = null)
-    {
-        Cancelled = true;
-        CancelReasonMessageId = reasonMessageId;
-    }
-}
-
 public sealed class PacificationSystem : EntitySystem
 {
     [Dependency] private readonly AlertsSystem _alertsSystem = default!;
     [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
     [Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
 
     public override void Initialize()
     {
@@ -53,10 +27,78 @@ public sealed class PacificationSystem : EntitySystem
         SubscribeLocalEvent<PacifiedComponent, ComponentShutdown>(OnShutdown);
         SubscribeLocalEvent<PacifiedComponent, BeforeThrowEvent>(OnBeforeThrow);
         SubscribeLocalEvent<PacifiedComponent, AttackAttemptEvent>(OnAttackAttempt);
+        SubscribeLocalEvent<PacifiedComponent, ShotAttemptedEvent>(OnShootAttempt);
+        SubscribeLocalEvent<PacifiedComponent, EntityUnpausedEvent>(OnUnpaused);
+        SubscribeLocalEvent<PacifismDangerousAttackComponent, AttemptPacifiedAttackEvent>(OnPacifiedDangerousAttack);
+    }
+
+    private void OnUnpaused(Entity<PacifiedComponent> ent, ref EntityUnpausedEvent args)
+    {
+        if (ent.Comp.NextPopupTime != null)
+            ent.Comp.NextPopupTime = ent.Comp.NextPopupTime.Value + args.PausedTime;
+    }
+
+    private bool PacifiedCanAttack(EntityUid user, EntityUid target, [NotNullWhen(false)] out string? reason)
+    {
+        var ev = new AttemptPacifiedAttackEvent(user);
+
+        RaiseLocalEvent(target, ref ev);
+
+        if (ev.Cancelled)
+        {
+            reason = ev.Reason;
+            return false;
+        }
+
+        reason = null;
+        return true;
+    }
+
+    private void ShowPopup(Entity<PacifiedComponent> user, EntityUid target, string reason)
+    {
+        // Popup logic.
+        // Cooldown is needed because the input events for melee/shooting etc. will fire continuously
+        if (target == user.Comp.LastAttackedEntity
+            && !(_timing.CurTime > user.Comp.NextPopupTime))
+            return;
+
+        _popup.PopupClient(Loc.GetString(reason, ("entity", target)), user, user);
+        user.Comp.NextPopupTime = _timing.CurTime + user.Comp.PopupCooldown;
+        user.Comp.LastAttackedEntity = target;
+    }
+
+    private void OnShootAttempt(Entity<PacifiedComponent> ent, ref ShotAttemptedEvent args)
+    {
+        // Disallow firing guns in all cases.
+        ShowPopup(ent, args.Used, "pacified-cannot-fire-gun");
+        args.Cancel();
     }
 
     private void OnAttackAttempt(EntityUid uid, PacifiedComponent component, AttackAttemptEvent args)
     {
+        if (component.DisallowAllCombat || args.Disarm && component.DisallowDisarm)
+        {
+            args.Cancel();
+            return;
+        }
+
+        // If it's a disarm, let it go through (unless we disallow them, which is handled earlier)
+        if (args.Disarm)
+            return;
+
+        // Allow attacking with no target. This should be fine.
+        // If it's a wide swing, that will be handled with a later AttackAttemptEvent raise.
+        if (args.Target == null)
+            return;
+
+        // If we would do zero damage, it should be fine.
+        if (args.Weapon != null && args.Weapon.Value.Comp.Damage.GetTotal() == FixedPoint2.Zero)
+            return;
+
+        if (PacifiedCanAttack(uid, args.Target.Value, out var reason))
+            return;
+
+        ShowPopup((uid, component), args.Target.Value, reason);
         args.Cancel();
     }
 
@@ -65,11 +107,15 @@ public sealed class PacificationSystem : EntitySystem
         if (!TryComp<CombatModeComponent>(uid, out var combatMode))
             return;
 
-        if (combatMode.CanDisarm != null)
+        if (component.DisallowDisarm && combatMode.CanDisarm != null)
             _combatSystem.SetCanDisarm(uid, false, combatMode);
 
-        _combatSystem.SetInCombatMode(uid, false, combatMode);
-        _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
+        if (component.DisallowAllCombat)
+        {
+            _combatSystem.SetInCombatMode(uid, false, combatMode);
+            _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
+        }
+
         _alertsSystem.ShowAlert(uid, AlertType.Pacified);
     }
 
@@ -103,4 +149,51 @@ public sealed class PacificationSystem : EntitySystem
         var cannotThrowMessage = ev.CancelReasonMessageId ?? "pacified-cannot-throw";
         _popup.PopupEntity(Loc.GetString(cannotThrowMessage, ("projectile", itemName)), ent, ent);
     }
+
+    private void OnPacifiedDangerousAttack(Entity<PacifismDangerousAttackComponent> ent, ref AttemptPacifiedAttackEvent args)
+    {
+        args.Cancelled = true;
+        args.Reason = "pacified-cannot-harm-indirect";
+    }
 }
+
+
+/// <summary>
+/// Raised when a Pacified entity attempts to throw something.
+/// The throw is only permitted if this event is not cancelled.
+/// </summary>
+[ByRefEvent]
+public struct AttemptPacifiedThrowEvent
+{
+    public EntityUid ItemUid;
+    public EntityUid PlayerUid;
+
+    public AttemptPacifiedThrowEvent(EntityUid itemUid,  EntityUid playerUid)
+    {
+        ItemUid = itemUid;
+        PlayerUid = playerUid;
+    }
+
+    public bool Cancelled { get; private set; } = false;
+    public string? CancelReasonMessageId { get; private set; }
+
+    /// <param name="reasonMessageId">
+    /// Localization string ID for the reason this event has been cancelled.
+    /// If null, a generic message will be shown to the player.
+    /// Note that any supplied localization string MUST accept a '$projectile'
+    /// parameter specifying the name of the thrown entity.
+    /// </param>
+    public void Cancel(string? reasonMessageId = null)
+    {
+        Cancelled = true;
+        CancelReasonMessageId = reasonMessageId;
+    }
+}
+
+/// <summary>
+///     Raised ref directed on an entity when a pacified user is attempting to attack it.
+///     If <see cref="Cancelled"/> is true, don't allow attacking.
+///     <see cref="Reason"/> should be a loc string, if there needs to be special text for why the user isn't able to attack this.
+/// </summary>
+[ByRefEvent]
+public record struct AttemptPacifiedAttackEvent(EntityUid User, bool Cancelled = false, string Reason = "pacified-cannot-harm-directly");
index 4b6dff76a2a51a725fe493a8da98ebb90dd4dec4..e271628fcb9448d4706d5ece3ab5b906bcb5f28d 100644 (file)
@@ -3,11 +3,42 @@ using Robust.Shared.GameStates;
 namespace Content.Shared.CombatMode.Pacification;
 
 /// <summary>
-/// Status effect that disables combat mode and restricts aggressive actions.
+/// Status effect that disallows harming living things and restricts aggressive actions.
+///
+/// There is a caveat with pacifism. It's not intended to be wholly encompassing: there are ways of harming people
+/// while pacified--plenty of them, even! The goal is to restrict the obvious ones to make gameplay more interesting
+/// while not overly limiting.
+///
+/// If you want full-pacifism (no combat mode at all), you can simply set <see cref="DisallowAllCombat"/> before adding.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 [Access(typeof(PacificationSystem))]
 public sealed partial class PacifiedComponent : Component
 {
+    [DataField]
+    public bool DisallowDisarm = false;
+
+    /// <summary>
+    ///  If true, this will disable combat entirely instead of only disallowing attacking living creatures and harmful things.
+    /// </summary>
+    [DataField]
+    public bool DisallowAllCombat = false;
+
+
+    /// <summary>
+    ///     When attempting attack against the same entity multiple times,
+    ///     don't spam popups every frame and instead have a cooldown.
+    /// </summary>
+    [DataField]
+    public TimeSpan PopupCooldown = TimeSpan.FromSeconds(3.0);
+
+    [DataField]
+    public TimeSpan? NextPopupTime = null;
+
+    /// <summary>
+    ///     The last entity attacked, used for popup purposes (avoid spam)
+    /// </summary>
+    [DataField]
+    public EntityUid? LastAttackedEntity = null;
 
 }
diff --git a/Content.Shared/CombatMode/Pacification/PacifismDangerousAttackComponent.cs b/Content.Shared/CombatMode/Pacification/PacifismDangerousAttackComponent.cs
new file mode 100644 (file)
index 0000000..df79d46
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.CombatMode.Pacification;
+
+/// <summary>
+/// This is used for marking entities which could cause serious harm if attacked and should not be able to be harmed by
+/// pacifists.
+/// TODO ideally destructible is shared + converted to components so we can just check for a harmful damage trigger instead of this.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PacifismDangerousAttackComponent : Component
+{
+}
index 8fd2f75d895b899868313aac1429f5891068799b..a71dccdf913cb35c504f716badcd5e6a50770ef9 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.Weapons.Melee;
+
 namespace Content.Shared.Interaction.Events
 {
     /// <summary>
@@ -12,10 +14,19 @@ namespace Content.Shared.Interaction.Events
         public EntityUid Uid { get; }
         public EntityUid? Target { get; }
 
-        public AttackAttemptEvent(EntityUid uid, EntityUid? target = null)
+        public Entity<MeleeWeaponComponent>? Weapon { get; }
+
+        /// <summary>
+        ///     If this attempt is a disarm as opposed to an actual attack, for things that care about the difference.
+        /// </summary>
+        public bool Disarm { get; }
+
+        public AttackAttemptEvent(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
         {
             Uid = uid;
             Target = target;
+            Weapon = weapon;
+            Disarm = disarm;
         }
     }
 
index 15999322465ed4b0ea236e8a3e95774bd61c3f42..8ce12db51838cb4486823be5f81e5e9b226dd9f4 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Bed.Sleep;
+using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Damage.ForceSay;
 using Content.Shared.Emoting;
 using Content.Shared.Hands;
@@ -39,6 +40,7 @@ public partial class MobStateSystem
         SubscribeLocalEvent<MobStateComponent, StandAttemptEvent>(CheckAct);
         SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
         SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
+        SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
     }
 
     private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
@@ -166,5 +168,10 @@ public partial class MobStateSystem
             args.Cancelled = true;
     }
 
+    private void OnAttemptPacifiedAttack(Entity<MobStateComponent> ent, ref AttemptPacifiedAttackEvent args)
+    {
+        args.Cancelled = true;
+    }
+
     #endregion
 }
index 3505149564f18ebd0c1a98bb68fe87ba7e2b3269..68f625d9494730ca684bd2498d0141eec6fa5bc2 100644 (file)
@@ -28,6 +28,7 @@ using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
+using Robust.Shared.Toolshed.Syntax;
 
 namespace Content.Shared.Weapons.Melee;
 
@@ -350,7 +351,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
             case LightAttackEvent light:
                 var lightTarget = GetEntity(light.Target);
 
-                if (!Blocker.CanAttack(user, lightTarget))
+                if (!Blocker.CanAttack(user, lightTarget, (weaponUid, weapon)))
                     return false;
 
                 // Can't self-attack if you're the weapon
@@ -361,11 +362,11 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
             case DisarmAttackEvent disarm:
                 var disarmTarget = GetEntity(disarm.Target);
 
-                if (!Blocker.CanAttack(user, disarmTarget))
+                if (!Blocker.CanAttack(user, disarmTarget, (weaponUid, weapon), true))
                     return false;
                 break;
             default:
-                if (!Blocker.CanAttack(user))
+                if (!Blocker.CanAttack(user, weapon: (weaponUid, weapon)))
                     return false;
                 break;
         }
@@ -642,20 +643,27 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
 
         foreach (var entity in targets)
         {
+            // We raise an attack attempt here as well,
+            // primarily because this was an untargeted wideswing: if a subscriber to that event cared about
+            // the potential target (such as for pacifism), they need to be made aware of the target here.
+            // In that case, just continue.
+            if (!Blocker.CanAttack(user, entity, (weapon, component)))
+                continue;
+
             var attackedEvent = new AttackedEvent(meleeUid, user, GetCoordinates(ev.Coordinates));
             RaiseLocalEvent(entity, attackedEvent);
             var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
 
             var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, origin:user);
 
-            if (damageResult != null && damageResult.Total > FixedPoint2.Zero)
+            if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
             {
                 appliedDamage += damageResult;
 
                 if (meleeUid == user)
                 {
                     AdminLogger.Add(LogType.MeleeHit, LogImpact.Medium,
-                        $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.Total:damage} damage");
+                        $"{ToPrettyString(user):actor} melee attacked (heavy) {ToPrettyString(entity):subject} using their hands and dealt {damageResult.GetTotal():damage} damage");
                 }
                 else
                 {
@@ -667,7 +675,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
 
         if (entities.Count != 0)
         {
-            if (appliedDamage.Total > FixedPoint2.Zero)
+            if (appliedDamage.GetTotal() > FixedPoint2.Zero)
             {
                 var target = entities.First();
                 PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component.HitSound);
@@ -685,7 +693,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
             }
         }
 
-        if (appliedDamage.Total > FixedPoint2.Zero)
+        if (appliedDamage.GetTotal() > FixedPoint2.Zero)
         {
             DoDamageEffect(targets, user, Transform(targets[0]));
         }
index bb3ca510376f44e2d1c224a682bf76e0774ad168..1e9b8a2017437dec0019b10d0d8ac8ad6569f767 100644 (file)
@@ -94,7 +94,7 @@ alerts-bleed-name = [color=red]Bleed[/color]
 alerts-bleed-desc = You're [color=red]bleeding[/color].
 
 alerts-pacified-name = [color=green]Pacified[/color]
-alerts-pacified-desc = You're pacified; you won't be able to attack anyone directly.
+alerts-pacified-desc = You're pacified; you won't be able to harm living creatures.
 
 alerts-suit-power-name = Suit Power
 alerts-suit-power-desc = How much power your space ninja suit has.
index 4d45f13bd3cd3e512b5af9434a11ad1e6d25ca96..e0a0e9d1c3d0b71085a1008ac0f3a017e3b8a85c 100644 (file)
@@ -2,10 +2,14 @@
 ## Messages shown to Pacified players when they try to do violence:
 
 # With projectiles:
-pacified-cannot-throw = You can't bring yourself to throw { THE($projectile) }, that could hurt someone!
+pacified-cannot-throw = I can't bring myself to throw { THE($projectile) }, that could hurt someone!
 # With embedding projectiles:
-pacified-cannot-throw-embed = No way you can throw { THE($projectile) }, that could get lodged inside someone!
+pacified-cannot-throw-embed = No way I could throw { THE($projectile) }, that could get lodged inside someone!
 # With liquid-spilling projectiles:
-pacified-cannot-throw-spill = You can't possibly throw { THE($projectile) }, that could spill nasty stuff on someone!
+pacified-cannot-throw-spill = I can't possibly throw { THE($projectile) }, that could spill nasty stuff on someone!
 # With bolas and snares:
-pacified-cannot-throw-snare = You can't throw { THE($projectile) }, what if someone trips?!
+pacified-cannot-throw-snare = I can't throw { THE($projectile) }, what if someone trips?!
+
+pacified-cannot-harm-directly = I can't bring myself to hurt { THE($entity) }!
+pacified-cannot-harm-indirect = I can't damage { THE($entity) }, it could hurt someone!
+pacified-cannot-fire-gun = I can't fire { THE($entity) }, it could hurt someone!
index ad227956a7d85b857301f2ab0c2d5a74d822cb2c..2bcd65533f6ff81b60928095ab37ce45fde68f47 100644 (file)
@@ -54,6 +54,7 @@
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: PacifismDangerousAttack
   - type: Destructible
     thresholds:
     - trigger:
index fe6936d4113dad1da69329675eb71cac766242ab..2155baa6adedbd3d3ecedbb7ec2ff9b61717ca27 100644 (file)
@@ -59,6 +59,7 @@
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: StrongMetallic
+  - type: PacifismDangerousAttack
   - type: Destructible
     thresholds:
     - trigger:
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: PacifismDangerousAttack
   - type: Destructible
     thresholds:
     - trigger:
index 15504fe0d7898c5e610c78de6a0f5381ebb90afa..b7e41d9e9ca661fd9dbc8774618b8645cb7b3670 100644 (file)
@@ -28,6 +28,7 @@
     weldingDamage:
       types:
         Heat: 10
+  - type: PacifismDangerousAttack
   - type: Explosive
     explosionType: Default
     totalIntensity: 120 # ~ 5 tile radius