Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Flinch", a mechanic for making speed penalties less deadly and annoying #73050

Open
I-am-Erk opened this issue Apr 16, 2024 · 6 comments
Open
Labels
(P3 - Medium) Medium (normal) priority <Suggestion / Discussion> Talk it out before implementing

Comments

@I-am-Erk
Copy link
Member

I-am-Erk commented Apr 16, 2024

Is your feature request related to a problem? Please describe.

Speed penalties are a necessity in the game. Lots of things, like pain and breathlessness and muscle strain, should lower your speed when doing things like swinging weapons... There's no easy way around this because there are only so many ways to alter character abilities as their resources change. However, speed penalties are disproportionately punishing and frequently lead to hard-to-understand failure cycles where you're getting hit by enemies more and more for each attack you make. This is not just frustrating but generally unfun, as it requires a lot of foreknowledge of the game engine to allow players to make logical assessments about their avatar's capabilities. One turn they seem to be on top of things, then they make a too-slow attack because of slight misjudgment and are now not one turn later, but two or three.

Solution you would like.

Let's stop penalizing attack speed. Instead, anything that penalizes speed only affects movement speed (if it should even do that). Instead of penalizing attack speed, we get a new mechanic, Flinch.

Anything that could be susceptible to a speed penalty, such as melee attacks or smashing objects, makes a call to Flinch() instead. When your character Flinches, they delay for some short amount of time (10-50 ticks) and get a message about it. This means they now can make another decision based on knowing they are slowed down. It also means that faster attack weapons don't have as strong an advantage over slower, the flinch mechanic is the same no matter what.

Some ground rules.

  1. You can only flinch once every two seconds (generally once per two turns). Whenever a Flinch() check returns >0, we record the "last flinched" time in a variable, and check it compared to now. This stops us from getting into an endless flinch cycle.
  2. Whenever Flinch() is called, it returns either 0 or an int from 10-50 for number of ticks flinched. Remember, a tick is 1/100 of a second. We're not talking about long periods of the character picking their nose and watching the enemy, but enough time to matter. At its maximum you're flinching for 50 ticks out of every 200, which is very significant but not a stunlock.
  3. If you flinch while aiming, you also lose a small amount of your aim meter as well as the time. You don't cancel aiming!
  4. Flinching is not random. There's a flinch_chance score that builds up right before each flinch() check, and if it passes flinch_threshold, we get a flinch. The duration of the flinch is based on how high flinch_chance is.

The algorithm

flinch() to start would increment when attempting a melee attack, by an amount equal to:

  • (current_pain - 10) * pain_flinch_mod (pain_flinch_mod is a global JSON setting, default 0.6)
  • and: 100 - 100 * current_stamina / stamina_max * stamina_flinch_mod (stamina_flinch_mod is a global JSON setting, default 1.0)
  • flinch_threshold is another global JSON setting, default 300.
  • When your flinch_chance reaches threshold, you flinch for a duration equal to flinch_chance_gained_this_turn / 5 ticks, max 50, min 10. Then flinch_chance resets to 0.
  • if time since last flinch > 10 seconds, reset flinch_chance to 0.

When I finish adding strain I'll redo these numbers to work it in but this should work for now.

In this model, you start flinching every two turns pretty often, but the duration only caps out if you're both tired and in pain. To get to the full 50 ticks of flinch per turn, you have to be accruing 250+ points of flinch_chance, which most of the time requires you to be in both a lot of pain and also out of breath.

The message given for flinching should probably suggest the things that are causing the flinch, eg:

you flinch for an instant as you push past the pain.

you take a fraction of a second to gather your energy.

Probably if you're both breathless and in pain, we should just randomly select an appropriate message? That way we can have alternative texts too. Or we could implement logic based on which of these is contributing the most to flinch_chance.

Describe alternatives you have considered.

I am not sure about the 'every other turn' limit, but I don't want to keep flooding the player with flinch messages as they get more common. This keeps the spam down

Speed penalties are the other obvious alternative, and I hate them.

Additional context

The effect should be subtle, but give the player a lot more control over their avatar's actions.

I think it's fairly easy to implement, but my time is limited again so I'm sort of hoping someone will get to it before I have a chance; It seems like it might help me to get the first pass of strain working, after testing it out

@I-am-Erk I-am-Erk added <Suggestion / Discussion> Talk it out before implementing (P3 - Medium) Medium (normal) priority labels Apr 16, 2024
@PatrikLundell
Copy link
Contributor

It looks like

When your flinch_chance reaches threshold, you flinch for a duration equal to flinch_threshold / 10 ticks, max 50, min 10. Then flinch_chance resets to 0.
is incorrect based on what was said above, and it should be flinch_chance / 10 instead. Otherwise the flinch would be exactly the same every time, regardless of how far you overshoot the threshold.

It can also be noted that the higher flinch_threshold is set, the harder each flinch hits as it's a higher value being divided, although it occurs with a lower frequency. It seems to me that you'd want to control the threshold for when to trigger it separately from the severity.
I'd suggest the threshold only controls the trigger, while a new parameter would be a flinch_adjustment that would be added to flinch_chance before the division by 10. This value would default to 0, but could be both positive and negative.

@I-am-Erk
Copy link
Member Author

I-am-Erk commented Apr 16, 2024

That was supposed to say chance, yes. However, running the numbers, I realized what I actually want is for it to be based on how much your flinch_chance rose this turn. Probably the best way to do it is to have flinch length be affected by the number of turns since last flinch, so that if your symptoms are mild your flinches are shorter too. The only trick there is that the math should ensure that we don't get too geometric in the rising rate. I have some ideas for how to do that, just gotta open desmos.

Alternatively we might just make flinches always 50 ticks and only play with the frequency. This makes them feel more serious at minor pain but means you can always predict exactly how bad they are.


edit: added those to the OP. For now I kept the simplest version, setting flinch duration to be based on the amount of flinch you gained this turn. I like doing it that way instead of as an average because if you're flinching but your symptoms are improving fast, it shortens the duration.

This was referenced Apr 16, 2024
@IdleSol
Copy link
Contributor

IdleSol commented Apr 16, 2024

I'm not sure I got that right. So take it as an alternative.

graph TB
text1 --> text2 --> text3 --> |Calculating flinch|text4 -->|Comparing| text5

 text5 --> |Yes| text6.1 --> text6.2 --> text6.3 --> text8 --> |go to|text9
 text5 --> |No| text7.1 ----> text8

text1("flinch = 0")
text2((*))
text3([Start of turn]):::c_green
text4("flinch_pain = (current_pain - 10) * pain_flinch_mod

flinch_stamina = 100 - 100 * current_stamina / stamina_max * stamina_flinch_mod

flinch = flinch + flinch_pain + flinch_stamina")
text5{"flinch >= flinch_threshold"}

text6.1["flinch_power = flinch_pain / 5 + flinch_stamina / 5

note: You can make different contributions to the power.
And, if necessary, limit to a maximum and minimum value. "]

text6.2["Display message"]
text6.3["flinch = 0"]

text7.1["flinch = flinch - flinch_threshold / 10
flinch = max(flinch,0)"]

text8([end of turn]):::c_green
text9((*))

classDef c_green fill:#00FF00
Loading

What's going on here.

  1. flinch_pain and flinch_stamina

These are separate values depending on pain and stamina. Calculated for convenience. In case you need to use them separately.

  1. flinch = flinch + flinch_pain + flinch_stamina

The total value, is the sum of these two parameters and the value for the last turn.

  1. flinch >= flinch_threshold. Yes

A flinch event occurs. For which the power flinch_power is calculated.
flinch_power = flinch_pain / 5 + flinch_stamina / 5
It depends on the current value of pain and stamina. Therefore, flinch_pain and flinch_stamina values are used here.

And also a little bit of calculus magic.
If pain = 110 and current_stamina = 0 then
flinch_pain = (110 - 10) * 0.6 = 60
flinch_stamina = 100 - 100 * 0 / stamina_max * 1 = 100
flinch_power = 60 / 5 + 100 / 5 = 32 or 32 ticks

As I said, you can play with coefficients so that stamina and pain affect with different power. And choose them so that at "limit" values they give 50 ticks. For example, 2 and 5. (I'm oriented on pain = 110).

Or just introduce restrictions min(10) & max(50). I'm not sure, maybe it's necessary to operate directly with pain and stamina.

  1. flinch >= flinch_threshold. No

If the value flinch did not exceed flinch_threshold. Then we reduce this value by 10% of the flinch_threshold value. This gives a gradual decrease in value. My assumption is that even the maximum flinch value should decrease to zero in 10 turns. Assuming, of course, that pain is gone and stamina is restored.

I should note that this is a moot point. The alternative is flinch = 0.9*flinch. That is, we should focus on the current flinch value. The higher it is, the higher the number by which it is reduced.

  1. Summary

We do not need to remember when the "flinch" event occurred. The frequency of this event directly depends on the current value of pain and stamina. It cannot be more than once per turn. With the current values, you need to get

pain = (flinch_threshold - flinch_stamina) / 0.6 + 10 = (300 - 100) / 0.6 +10 = 343.

I'm not sure if this is possible in a normal situation. Because that's equivalent to 686 HP damage taken at a time. Or 114 for each body part.

Anyway, you can change the flinch_threshold. And so adjust the maximum frequency.

The power of flinch also depends on pain and stamina.

In general, the higher the pain and lower the stamina, the more often and more powerful you flinch.

P.S. I used Deepl. And I'm sure I made a lot of mistakes.

@zachary-kaelan
Copy link
Contributor

Isn't this what the stumble function does?

@kevingranade
Copy link
Member

Stumble is move loss when missing with a melee attack that scales with weapon size.

Flinch would be move loss that kicks in when you are at high pain and exert yourself, mostly with melee attacks, which would replace speed penalties from accumulating pain.

A flinch would trigger independently of hit or miss.

@zachary-kaelan
Copy link
Contributor

I was thinking of opportunities for code reuse. But true, stumbling is based on physics and flinching is based on physiology. I was going to nitpick what body parts are flinching but I see that's been discussed in #72851 and is waiting on the wounds system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
(P3 - Medium) Medium (normal) priority <Suggestion / Discussion> Talk it out before implementing
Projects
None yet
Development

No branches or pull requests

5 participants