]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Weapon Reflection Movement Mechanic (#27219)
authorHannah Giovanna Dawson <karakkaraz@gmail.com>
Tue, 7 May 2024 18:14:58 +0000 (19:14 +0100)
committerGitHub <noreply@github.com>
Tue, 7 May 2024 18:14:58 +0000 (20:14 +0200)
* Weapon Reflection Movement Mechanic

Adds a movement mechanic to deflection.
Standing still gives you your best chance of deflecting a shot.
Moving lowers this to 2/3rds. Sprinting to 1/3rd.

This allows for robust players to express better and provides
counterplay to someone finding a goober-strong deflection
weapon, giving more design space.

As part of this PR I've also touched the numbers of a few swords,
shields, etc. and modified some descriptions to make them read
better. The balance numbers are not remotely final, but as intent:

1. All the sidearm swords (katana, cutlass, captain's sabre) have the same damage. There's no good reason the "ceremonial" blade the captain has doing more damage than a katana.
2. The Captain's Sabre has a 30% reflect chance, dropping to 20% when moving and 10% when sprinting. This one is controversial due to the recent nerf, I suspect: This could easily be 15->10->5?
3. The Energy Katana has a flat 30% reflect chance.
4. The meme Throngler has a 30% reflect chance, dropping to 20% when moving and 10% when sprinting.
5. The E-Sword has a 30% reflect chance, dropping to 20% when moving and 10% when sprinting.
6. The Double E-Sword has a mighty 75% reflect chance, dropping to 50% and then 25%.
7. Both reflective shields - Mirror and Energy - have a 95% deflect chance, dropping to 63% then 31%.

* Resolve PR comments.

* Weh?

* Reign in double esword a tad

* Shield nerfs no longer real

* Improve Mirror Cult desc

* Simple alert for deflection! No art yet.

* Added a new icon for deflecting

13 files changed:
Content.Shared/Alert/AlertType.cs
Content.Shared/Weapons/Reflect/ReflectComponent.cs
Content.Shared/Weapons/Reflect/ReflectSystem.cs
Resources/Locale/en-US/alerts/alerts.ftl
Resources/Prototypes/Alerts/alerts.yml
Resources/Prototypes/Anomaly/behaviours.yml
Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml
Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml
Resources/Prototypes/Entities/Objects/Shields/shields.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml
Resources/Textures/Interface/Alerts/deflecting.rsi/deflecting0.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json [new file with mode: 0644]

index b917dd692d7e55c8c83b8ca3500b3eb3da755b5d..b989b8d4b6f5643ffb711e95c0748358c20f4e6e 100644 (file)
@@ -52,7 +52,8 @@ namespace Content.Shared.Alert
         SuitPower,
         BorgHealth,
         BorgCrit,
-        BorgDead
+        BorgDead,
+        Deflecting
     }
 
 }
index 8e7b8975d9d1b128ac21a7213c351ee55863e880..5d8432ac776c3cc0002571b0dff3581716e4a113 100644 (file)
@@ -21,17 +21,42 @@ public sealed partial class ReflectComponent : Component
     [ViewVariables(VVAccess.ReadWrite), DataField("reflects")]
     public ReflectType Reflects = ReflectType.Energy | ReflectType.NonEnergy;
 
+    [DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public Angle Spread = Angle.FromDegrees(45);
+
+    [DataField("soundOnReflect")]
+    public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
+
     /// <summary>
-    /// Probability for a projectile to be reflected.
+    /// Is the deflection an innate power or something actively maintained? If true, this component grants a flat
+    /// deflection chance rather than a chance that degrades when moving/weightless/stunned/etc.
+    /// </summary>
+    [DataField]
+    public bool Innate = false;
+
+    /// <summary>
+    /// Maximum probability for a projectile to be reflected.
     /// </summary>
     [DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
     public float ReflectProb = 0.25f;
 
-    [DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-    public Angle Spread = Angle.FromDegrees(45);
+    /// <summary>
+    /// The maximum velocity a wielder can move at before losing effectiveness.
+    /// </summary>
+    [DataField]
+    public float VelocityBeforeNotMaxProb = 2.5f; // Walking speed for a human. Suitable for a weightless deflector like an e-sword.
 
-    [DataField("soundOnReflect")]
-    public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
+    /// <summary>
+    /// The velocity a wielder has to be moving at to use the minimum effectiveness value.
+    /// </summary>
+    [DataField]
+    public float VelocityBeforeMinProb = 4.5f; // Sprinting speed for a human. Suitable for a weightless deflector like an e-sword.
+
+    /// <summary>
+    /// Minimum probability for a projectile to be reflected.
+    /// </summary>
+    [DataField]
+    public float MinReflectProb = 0.1f;
 }
 
 [Flags]
index 014b3cfe1ff37b9bae10f0db28c3092463630d07..36dbedb4cb16079fdf63b17c62c4abd6cf7f3783 100644 (file)
@@ -1,17 +1,20 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Alert;
 using Content.Shared.Audio;
+using Content.Shared.Damage.Components;
 using Content.Shared.Database;
+using Content.Shared.Gravity;
 using Content.Shared.Hands;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item.ItemToggle.Components;
 using Content.Shared.Popups;
 using Content.Shared.Projectiles;
+using Content.Shared.Standing;
 using Content.Shared.Weapons.Ranged.Components;
 using Content.Shared.Weapons.Ranged.Events;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Network;
 using Robust.Shared.Physics.Components;
@@ -35,6 +38,9 @@ public sealed class ReflectSystem : EntitySystem
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
+    [Dependency] private readonly SharedGravitySystem _gravity = default!;
+    [Dependency] private readonly StandingStateSystem _standing = default!;
+    [Dependency] private readonly AlertsSystem _alerts = default!;
 
     public override void Initialize()
     {
@@ -91,15 +97,20 @@ public sealed class ReflectSystem : EntitySystem
 
     private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null)
     {
-        if (!Resolve(reflector, ref reflect, false) ||
+        // Do we have the components needed to try a reflect at all?
+        if (
+            !Resolve(reflector, ref reflect, false) ||
             !reflect.Enabled ||
             !TryComp<ReflectiveComponent>(projectile, out var reflective) ||
             (reflect.Reflects & reflective.Reflective) == 0x0 ||
-            !_random.Prob(reflect.ReflectProb) ||
-            !TryComp<PhysicsComponent>(projectile, out var physics))
-        {
+            !TryComp<PhysicsComponent>(projectile, out var physics) ||
+            TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical ||
+            _standing.IsDown(reflector)
+        )
+            return false;
+
+        if (!_random.Prob(CalcReflectChance(reflector, reflect)))
             return false;
-        }
 
         var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
         var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics);
@@ -137,6 +148,34 @@ public sealed class ReflectSystem : EntitySystem
         return true;
     }
 
+    private float CalcReflectChance(EntityUid reflector, ReflectComponent reflect)
+    {
+        /*
+         *  The rules of deflection are as follows:
+         *  If you innately reflect things via magic, biology etc., you always have a full chance.
+         *  If you are standing up and standing still, you're prepared to deflect and have full chance.
+         *  If you have velocity, your deflection chance depends on your velocity, clamped.
+         *  If you are floating, your chance is the minimum value possible.
+         *  You cannot deflect if you are knocked down or stunned.
+         */
+
+        if (reflect.Innate)
+            return reflect.ReflectProb;
+
+        if (_gravity.IsWeightless(reflector))
+            return reflect.MinReflectProb;
+
+        if (!TryComp<PhysicsComponent>(reflector, out var reflectorPhysics))
+            return reflect.ReflectProb;
+
+        return MathHelper.Lerp(
+            reflect.MinReflectProb,
+            reflect.ReflectProb,
+            // Inverse progression between velocities fed in as progression between probabilities. We go high -> low so the output here needs to be _inverted_.
+            1 - Math.Clamp((reflectorPhysics.LinearVelocity.Length() - reflect.VelocityBeforeNotMaxProb) / (reflect.VelocityBeforeMinProb - reflect.VelocityBeforeNotMaxProb), 0, 1)
+        );
+    }
+
     private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args)
     {
         if (args.Reflected ||
@@ -162,7 +201,14 @@ public sealed class ReflectSystem : EntitySystem
     {
         if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
             !reflect.Enabled ||
-            !_random.Prob(reflect.ReflectProb))
+            TryComp<StaminaComponent>(reflector, out var staminaComponent) && staminaComponent.Critical ||
+            _standing.IsDown(reflector))
+        {
+            newDirection = null;
+            return false;
+        }
+
+        if (!_random.Prob(CalcReflectChance(reflector, reflect)))
         {
             newDirection = null;
             return false;
@@ -191,6 +237,9 @@ public sealed class ReflectSystem : EntitySystem
             return;
 
         EnsureComp<ReflectUserComponent>(args.Equipee);
+
+        if (component.Enabled)
+            EnableAlert(args.Equipee);
     }
 
     private void OnReflectUnequipped(EntityUid uid, ReflectComponent comp, GotUnequippedEvent args)
@@ -204,6 +253,9 @@ public sealed class ReflectSystem : EntitySystem
             return;
 
         EnsureComp<ReflectUserComponent>(args.User);
+
+        if (component.Enabled)
+            EnableAlert(args.User);
     }
 
     private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, GotUnequippedHandEvent args)
@@ -215,6 +267,11 @@ public sealed class ReflectSystem : EntitySystem
     {
         comp.Enabled = args.Activated;
         Dirty(uid, comp);
+
+        if (comp.Enabled)
+            EnableAlert(uid);
+        else
+            DisableAlert(uid);
     }
 
     /// <summary>
@@ -228,9 +285,22 @@ public sealed class ReflectSystem : EntitySystem
                 continue;
 
             EnsureComp<ReflectUserComponent>(user);
+            EnableAlert(user);
+
             return;
         }
 
         RemCompDeferred<ReflectUserComponent>(user);
+        DisableAlert(user);
+    }
+
+    private void EnableAlert(EntityUid alertee)
+    {
+        _alerts.ShowAlert(alertee, AlertType.Deflecting);
+    }
+
+    private void DisableAlert(EntityUid alertee)
+    {
+        _alerts.ClearAlert(alertee, AlertType.Deflecting);
     }
 }
index 319809da40a28435ee8461bf585d19e4109ebae3..24bc60cbf1530e2e2b52ceebfd0d057e6d5de414 100644 (file)
@@ -107,3 +107,6 @@ alerts-revenant-essence-desc = The power of souls. It sustains you and is used f
 
 alerts-revenant-corporeal-name = Corporeal
 alerts-revenant-corporeal-desc = You have manifested physically. People around you can see and hurt you.
+
+alerts-deflecting-name = Deflecting
+alerts-deflecting-desc = You have a chance to deflect incoming projectiles. Standing still or moving slowly will increase this chance.
index e9a7f9c95848843d18a9d47bdd86ef7fe7503892..7881cddd4aab9e7c5b6e4b3c452d54a6ddff4428 100644 (file)
@@ -24,6 +24,7 @@
     - category: Thirst
     - alertType: Magboots
     - alertType: Pacified
+    - alertType: Deflecting
 
 - type: entity
   id: AlertSpriteView
     state: critical
   name: Debug6
   description: Debug
+
+- type: alert
+  id: Deflecting
+  icons:
+  - sprite: /Textures/Interface/Alerts/deflecting.rsi
+    state: deflecting0
+  name: alerts-deflecting-name
+  description: alerts-deflecting-desc
index dea1ddb69c36ff114dbac5f0bdc89793845cbb87..e39933c365c959c40eded82fb447e4272abdbe0c 100644 (file)
@@ -84,6 +84,7 @@
   description: anomaly-behavior-reflect
   components:
   - type: Reflect
+    innate: true
     reflectProb: 0.5
     reflects:
       - Energy
index ecc4156affaa3261dc39b9e4276181ebf5bc721a..6da428ee5f67ec77ac2e98a2cdbf33f327e13614 100644 (file)
@@ -96,6 +96,7 @@
         Heat: 0.4 # this technically means it protects against fires pretty well? -heat is just for lasers and stuff, not atmos temperature
   - type: Reflect
     reflectProb: 1
+    innate: true # armor grants a passive shield that does not require concentration to maintain
     reflects:
       - Energy
 
index 26fbe4e0734150c43dbe29ea197d860ef721642a..74658f0a2db83a2be81d46fae1d4f05fcc43c7e4 100644 (file)
@@ -55,6 +55,7 @@
   - type: Perishable
   - type: Reflect
     reflectProb: 0.7
+    innate: true
     reflects:
       - Energy
   - type: Fixtures
index b794e42ff7d33c98e4a73117c662ef334bdf401c..e7ebb1b98d45ce28df0a3075221709904de2561f 100644 (file)
   name: mirror shield
   parent: BaseShield
   id: MirrorShield
-  description: Eerily glows red... you hear the geometer whispering
+  description: Glows an eerie red. You hear the Geometer whispering...
   components:
     - type: Sprite
       state: mirror-icon
       heldPrefix: mirror
     - type: Reflect
       reflectProb: 0.95
+      innate: true
       reflects:
         - Energy
     - type: Blocking #Mirror shield reflects heat/laser, but is relatively weak to everything else.
     - type: Reflect
       enabled: false
       reflectProb: 0.95
+      innate: true
       reflects:
         - Energy
     - type: Blocking
index 13c8b9cb25c508a3d5f60831018092cf150bee00..7f593353bb51bbdfb5acb47bc5764f63d654388c 100644 (file)
@@ -78,6 +78,8 @@
     malus: 0
   - type: Reflect
     enabled: false
+    reflectProb: 0.5
+    minReflectProb: 0.25
   - type: IgnitionSource
     temperature: 700
 
   name: double-bladed energy sword
   parent: EnergySword
   id: EnergySwordDouble
-  description: Syndicate Command Interns thought that having one blade on the energy sword was not enough. This can be stored in pockets.
+  description: Syndicate Command's intern thought that having only one blade on energy swords was not cool enough. This can be stored in pockets.
   components:
   - type: EnergySword
   - type: ItemToggle
     size: Small
     sprite: Objects/Weapons/Melee/e_sword_double-inhands.rsi
   - type: Reflect
-    reflectProb: .75
+    reflectProb: .80
+    minReflectProb: .65
     spread: 75
   - type: UseDelay
     delay: 1
index 7cc33b715501cf4907fb7d6a2cf4221dbb65f453..11e7f983e054956c979ea3cee3b8170cd49b4580 100644 (file)
     attackRate: 1.5
     damage:
       types:
-        Slash: 17 #cmon, it has to be at least BETTER than the rest.
+        Slash: 15
     soundHit:
         path: /Audio/Weapons/bladeslice.ogg
   - type: Reflect
     enabled: true
-    reflectProb: .1
+    # Design intent: a robust captain or tot can sacrifice movement to make the most of this weapon, but they have to
+    # really restrict themselves to walking speed or less.
+    reflectProb: 0.5
+    velocityBeforeNotMaxProb: 1.0
+    velocityBeforeMinProb: 3.0
+    minReflectProb: 0.1
     spread: 90
   - type: Item
     size: Normal
@@ -83,6 +88,9 @@
     - Back
     - Belt
   - type: Reflect
+    reflectProb: 0.3
+    velocityBeforeNotMaxProb: 6.0 # don't punish ninjas for being ninjas
+    velocityBeforeMinProb: 10.0
 
 - type: entity
   name: machete
     wideAnimationRotation: -135
     damage:
       types:
-        Slash: 16
+        Slash: 15
     soundHit:
         path: /Audio/Weapons/bladeslice.ogg
   - type: Item
   name: The Throngler
   parent: BaseItem
   id: Throngler
-  description: Why would you make this?
+  description: Why would someone make this?
   components:
     - type: Sharp
     - type: Sprite
         path: /Audio/Effects/explosion_small1.ogg
     - type: Reflect
       enabled: true
-      reflectProb: .25
+      reflectProb: 0.5 # In robust hands, deflects as well as an e-sword
+      velocityBeforeNotMaxProb: 1.0
+      velocityBeforeMinProb: 3.0
+      minReflectProb: 0.1
       spread: 90
     - type: Item
       size: Ginormous
diff --git a/Resources/Textures/Interface/Alerts/deflecting.rsi/deflecting0.png b/Resources/Textures/Interface/Alerts/deflecting.rsi/deflecting0.png
new file mode 100644 (file)
index 0000000..37404e7
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/deflecting.rsi/deflecting0.png differ
diff --git a/Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json b/Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json
new file mode 100644 (file)
index 0000000..f5d94c8
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Deflecting icon by Ubaser",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "deflecting0"
+        }
+    ]
+}