From b90373356e7f4f0eee693732964eac9c9eaa1f02 Mon Sep 17 00:00:00 2001 From: Hannah Giovanna Dawson Date: Tue, 7 May 2024 19:14:58 +0100 Subject: [PATCH] Weapon Reflection Movement Mechanic (#27219) * 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 --- Content.Shared/Alert/AlertType.cs | 3 +- .../Weapons/Reflect/ReflectComponent.cs | 35 ++++++-- .../Weapons/Reflect/ReflectSystem.cs | 84 ++++++++++++++++-- Resources/Locale/en-US/alerts/alerts.ftl | 3 + Resources/Prototypes/Alerts/alerts.yml | 9 ++ Resources/Prototypes/Anomaly/behaviours.yml | 1 + .../Entities/Clothing/OuterClothing/armor.yml | 1 + .../Entities/Mobs/NPCs/hellspawn.yml | 1 + .../Entities/Objects/Shields/shields.yml | 4 +- .../Objects/Weapons/Melee/e_sword.yml | 7 +- .../Entities/Objects/Weapons/Melee/sword.yml | 21 +++-- .../Alerts/deflecting.rsi/deflecting0.png | Bin 0 -> 1761 bytes .../Interface/Alerts/deflecting.rsi/meta.json | 14 +++ 13 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 Resources/Textures/Interface/Alerts/deflecting.rsi/deflecting0.png create mode 100644 Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index b917dd692d..b989b8d4b6 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -52,7 +52,8 @@ namespace Content.Shared.Alert SuitPower, BorgHealth, BorgCrit, - BorgDead + BorgDead, + Deflecting } } diff --git a/Content.Shared/Weapons/Reflect/ReflectComponent.cs b/Content.Shared/Weapons/Reflect/ReflectComponent.cs index 8e7b8975d9..5d8432ac77 100644 --- a/Content.Shared/Weapons/Reflect/ReflectComponent.cs +++ b/Content.Shared/Weapons/Reflect/ReflectComponent.cs @@ -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"); + /// - /// 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. + /// + [DataField] + public bool Innate = false; + + /// + /// Maximum probability for a projectile to be reflected. /// [DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] public float ReflectProb = 0.25f; - [DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] - public Angle Spread = Angle.FromDegrees(45); + /// + /// The maximum velocity a wielder can move at before losing effectiveness. + /// + [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"); + /// + /// The velocity a wielder has to be moving at to use the minimum effectiveness value. + /// + [DataField] + public float VelocityBeforeMinProb = 4.5f; // Sprinting speed for a human. Suitable for a weightless deflector like an e-sword. + + /// + /// Minimum probability for a projectile to be reflected. + /// + [DataField] + public float MinReflectProb = 0.1f; } [Flags] diff --git a/Content.Shared/Weapons/Reflect/ReflectSystem.cs b/Content.Shared/Weapons/Reflect/ReflectSystem.cs index 014b3cfe1f..36dbedb4cb 100644 --- a/Content.Shared/Weapons/Reflect/ReflectSystem.cs +++ b/Content.Shared/Weapons/Reflect/ReflectSystem.cs @@ -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(projectile, out var reflective) || (reflect.Reflects & reflective.Reflective) == 0x0 || - !_random.Prob(reflect.ReflectProb) || - !TryComp(projectile, out var physics)) - { + !TryComp(projectile, out var physics) || + TryComp(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(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(reflector, out var reflect) || !reflect.Enabled || - !_random.Prob(reflect.ReflectProb)) + TryComp(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(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(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); } /// @@ -228,9 +285,22 @@ public sealed class ReflectSystem : EntitySystem continue; EnsureComp(user); + EnableAlert(user); + return; } RemCompDeferred(user); + DisableAlert(user); + } + + private void EnableAlert(EntityUid alertee) + { + _alerts.ShowAlert(alertee, AlertType.Deflecting); + } + + private void DisableAlert(EntityUid alertee) + { + _alerts.ClearAlert(alertee, AlertType.Deflecting); } } diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 319809da40..24bc60cbf1 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -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. diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index e9a7f9c958..7881cddd4a 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -24,6 +24,7 @@ - category: Thirst - alertType: Magboots - alertType: Pacified + - alertType: Deflecting - type: entity id: AlertSpriteView @@ -474,3 +475,11 @@ 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 diff --git a/Resources/Prototypes/Anomaly/behaviours.yml b/Resources/Prototypes/Anomaly/behaviours.yml index dea1ddb69c..e39933c365 100644 --- a/Resources/Prototypes/Anomaly/behaviours.yml +++ b/Resources/Prototypes/Anomaly/behaviours.yml @@ -84,6 +84,7 @@ description: anomaly-behavior-reflect components: - type: Reflect + innate: true reflectProb: 0.5 reflects: - Energy diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml index ecc4156aff..6da428ee5f 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml b/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml index 26fbe4e073..74658f0a2d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml @@ -55,6 +55,7 @@ - type: Perishable - type: Reflect reflectProb: 0.7 + innate: true reflects: - Energy - type: Fixtures diff --git a/Resources/Prototypes/Entities/Objects/Shields/shields.yml b/Resources/Prototypes/Entities/Objects/Shields/shields.yml index b794e42ff7..e7ebb1b98d 100644 --- a/Resources/Prototypes/Entities/Objects/Shields/shields.yml +++ b/Resources/Prototypes/Entities/Objects/Shields/shields.yml @@ -313,7 +313,7 @@ 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 @@ -321,6 +321,7 @@ 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. @@ -408,6 +409,7 @@ - type: Reflect enabled: false reflectProb: 0.95 + innate: true reflects: - Energy - type: Blocking diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml index 13c8b9cb25..7f593353bb 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml @@ -78,6 +78,8 @@ malus: 0 - type: Reflect enabled: false + reflectProb: 0.5 + minReflectProb: 0.25 - type: IgnitionSource temperature: 700 @@ -218,7 +220,7 @@ 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 @@ -269,7 +271,8 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 7cc33b7155..11e7f983e0 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -13,12 +13,17 @@ 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 @@ -152,7 +160,7 @@ wideAnimationRotation: -135 damage: types: - Slash: 16 + Slash: 15 soundHit: path: /Audio/Weapons/bladeslice.ogg - type: Item @@ -164,7 +172,7 @@ name: The Throngler parent: BaseItem id: Throngler - description: Why would you make this? + description: Why would someone make this? components: - type: Sharp - type: Sprite @@ -185,7 +193,10 @@ 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 index 0000000000000000000000000000000000000000..37404e77f76444946eaa481b8f4ec861f85736c6 GIT binary patch literal 1761 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}e5nzhX}-P; zT0k}j5QD&_;K@Lev%n*=n1MmW7law7oa)tQU|_Dx42dX-@b$4u&d=3LOvz75)vL%Y z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3 znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8 zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Uod0?Yb6da36%JYk|ZS*0kQB8q}q8e_akHsA} zAm3X>2Bj9~=ahoN-_Fq3$OarHD58j%far+8ssmXRT}MDhen~zsWff&6d*+p-78Mi$ zQyJJsn0>fapqquTJTxz}#13WvnlO?sq*$_o23!a@MlgdDr&6eKkf23Mv5@Qljs{?U zu;a4PhvzLjuD|Ez{a|2VdgSTi7!twxHf(>ll%q)9>g)3aH%&4L?C@0f=sKY3^-_5x?PXMg=&dwlOg7bU|>CT~x9 zb_T6CWpZ@TUF_5_Lklimk-JuS7}Ko>mE&-XwT%@ zz~AnCsWjGS_c!$k_qyy%=bv{x)ZwDkSM>CArNyI_f|-FzCbQ0}DK0pe;IJ{`jA%$K z$3xdA@?HlRJyy6KS>*1mHaTQ%m}f)%-=FRGo-dZ+d604WWl4U)jbbK=2$gdI!WH>j zqh6HV`q?dPS6*4`o*(~j>3RD+3qv0!n~cdU8!k@z*R#?f>E!~ag&Ur}|8vr7UvkaQ z=;b+Pr=RmN9(OaAd3jBxQDi~KqKg_A7l)U0m1^@YVz=nh@z^ISVL63sg*fk&6Deo1 z6F1+ypsT#ltU+0H_0+W;yW~=II3G+hT(|GUqiho?6S@AK`V$0rA1qp_DOl|Id#y;u z%5t^N%geXT-<=&@XLj20rd8Jjj+DHd=yt)~@iJ*|HCHywVB%a6_`e{Ld3DQ-g`9e2 zJHC5NO!Nqp7KGu&fThp?^U)%zxfj8D>H6YSo*o)!HwtY_Bk?~ z3617rwSO<0-|6{uH|pctX{S|7cgLQ8@i3*mA^1zdvC9PwTy;L%{TO><_x)_sd|sTk z@bjThlf;icpI+^l)7}u_&|l5Ce9_&!$2uEl8U5J%o_$~c7pCKr88jzx3*G2gzbKlU ziBEnW%e&>y6Ao;Tv%SAUaD|Y_o#_q1Q<_}#rY|nA(CNRW_~66)T7N6U4bP6eznPTy lZ%sw4nOvUSG&zw4^;={Tb>7Fkn+K|1JYD@<);T3K0RY#fl2ZTx literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json b/Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json new file mode 100644 index 0000000000..f5d94c891a --- /dev/null +++ b/Resources/Textures/Interface/Alerts/deflecting.rsi/meta.json @@ -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" + } + ] +} -- 2.52.0