CleverRaven / CleverRaven/Cataclysm-DDA

Rebuilding focus and xp gain to a simpler (LOL) system.

Open
#53,372 11 comments 2 reactions 0 assignees View on GitHub
(P3 - Medium) [C++] <Suggestion / Discussion> Game: Balance Mechanics: Effects / Skills / Stats
Dominant language
C++
Stars
13.1k
Forks
4.6k
Avg merge
2d 1h
Merged PRs (30d)
205

Description

## Problem

Focus is meant to discourage you from spending too much time grinding skills, and tie your skill gain to various factors that can slow learning. However in practice, it tends to be either trivial and ignorable, or an enormous pain in the ass, and nothing in between. The numbers need tweaking, and in doing so we can make our already cool learning model incredibly robust.

### Solution

Buckle up, buttercup, we're going to be going into some deep details here. Interestingly, I don't think this will be *that* difficult to implement: a lot of the time current systems that use focus should still plug and play into this fine.

### Plain talk description

Everything that follows is going to be quite technical, and I was quite excited to make it so it may be a little hard to follow. In the interests of clarity, here is my basic write up of what I'm trying to do.

- **Focus** will work similarly to before and be influenced by the same things, but now is slower to recover. However, its impact on your learning should be less all-or-nothing. If your focus is lower you do get less xp, but it shouldn't be totally crippling... instead, when your focus is very low, mainly you gain xp *over a longer time*.
- **XP gains over time**. You gain less XP in the moment you do an action now: at full focus, half your XP is instant and the other half goes to a "pool" that is distributed over time. As your focus drops, more of your XP goes into the pool, and less into your instant gain. This represents you needing to take time to think about what you just learned, because you can't really process it right now. Your actual total XP gain is less directly influenced by focus, now (although current focus still does have a direct role). However, as your pool increases, that does lower the amount of xp you gain. Since lower focus increases your pooled xp faster, focus continues to have a downstream impact.
- **The inevitable downslide of focus**: Learning in the moment costs focus. Distributing your pooled xp costs focus. When you spend focus, your focus can't regenerate, and if you have a lot of pooled xp to distribute that can leave you unable to recover focus for a long time. However, if your equilibrium focus is low, you can become distracted quite quickly.
- **Distraction is temporary**: We now track if your focus loss is from mental exhaustion (xp gain) versus temporary distraction (transitory low mood or pain for example). Distraction focus loss is regained quickly.

### Focus equilibrium

Currently, we have a stat called "focus equilibrium". This is the value your focus gradually trends towards, left to its own devices. This doesn't work too badly right now, I don't want to change it too much, but I think we'll have to change it to a formula calculation anyway.
- Pain, morale, and fatigue should drop focus equilibrium
- Stimulant effects should offer a small flat boost to focus equilibrium
- Alcohol, opioids, and marijuana should slightly drop focus equilibrium

#### Equation

- Max equilibrium before drug effects = 100.
- Min equilibrium before drug effects = 20.

* If morale >-5: `focus_equilibrium = 100 - (pain/5)^2 - fatigue/10 - std::min(fatigue-400,0)/40 + std::max(morale, 0)`
* If morale <= -5: `focus_equilibrium = 100 - (pain/5)^2 - fatigue/10 - (morale/5)^2`

Under this model, negative morale starts having an effect after -5. After that, every 5 points of morale drop lead to a new squared drop, so:
* -10 your morale gives you a -4 to focus.
* -15 morale = -9 to focus.
* -20 = -16 focus.
* -30 = -36 focus.
* -40 = -64 penalty, so at this point you're likely capping off your focus at the minimum equilibrium.

Positive morale is just flat added, so if you're in a great mood you can ignore some pain and fatigue. We could look into some diminishing returns from morale at higher levels, but I think there are enough penalties already, we don't need to nerf the one positive.

Similarly, low pain has minimal effect on focus until it rises:
* mild pain: -4 focus
* moderate pain: -16 focus
* distracting pain: -36 focus
* distressing pain: -64 focus. At this point and beyond, you're probably not going to be doing any substantive learning.

With fatigue:
* "tired" fatigue ~200 gives a -20.
* "dead tired" ~400 gives -40.
* After that, the second fatigue factor starts to kick in and it drops faster, so "exhausted" ~600 gives -75.

Update focus equilibrium every time we change stored focus. See "focus expenditure and recovery" for this.

#### Special: "morning fog"
When you wake up after >4 hours sleep, we should check if you have any remaining fatigue. Add a percent chance equal to `(remaining fatigue/10)` to gain a "morning fog" effect of duration 1 hour. This effect drops your focus equilibrium by 10. Taking a stimulant will cancel this effect.

#### Substances
It's outside this issue, but stimulants should give a flat bonus to equilibrium, and many medications should drop it. In particular, opioids should drop focus equilibrium, but in general this should not be as severe as the pain they treat unless you're using them for minor aches.

### Rewrite practice/xp gain totally.
OK, here's where things are going to get hairy, people. Hang on to your seats because we're going to rewrite everything.

First, let's normalize XP gain to something we check each turn. No more complex focus equations to mitigate it, we're going to use an accumulator. In the process, we are also going to change XP gains to two types: `instant` and `pool`. Whenever you practice, you gain both of those. Instant is applied to your skill immediately, while pool is stored and added to your skill gradually over a few hours. Let's look at how this works before delving into deeper ramifications

#### Basic algorithm.
* Each turn, check if current_focus is higher than focus_equilibrium. If no, run `disctract_me`.
* Then, regardless of the result, check if we are practicing a skill or not. If yes, run `practice_now`
* If we are not practicing a skill, check if there is any `pooled_xp` waiting to be distributed. If yes, run `distribute_xp`
* If there is no xp to be learned, check if our `focus_current` is equal to `focus_equilibrium`. If no, run `focus_normalize`.

**Note: Mind rest**: A few of these factors rely on tracking if the character is sleeping or doing a "mind rest" activity. We could either flag this in an activity, or determine it by checking if the activity requires a skill or not. Any activity that requires no skill is "mind rest" - listening to music, playing video games, waiting, walking idly in the forest.

##### `distract_me`
This routine runs each turn if the `current_focus` is higher than `focus_equilibrium`. It drops focus towards equilibrium. It shouldn't do this too quickly, but in general if your equilibrium is very low you lose focus faster than you'd gain it. We will store some of this focus loss in a temporary buffer, so that if your equilibrium is dropped by a transient effect, you get some of your focus back when the effect is gone.

* `int distract_increment = 0` accumulates and causes your focus to drop. You can never lose more than 1 focus per turn through this process, and that should be extraordinary. Mostly this will happen over many turns, and the increment is there to track how much it is happening.
* `int distract_buffer = 0` stores how much focus you've lost from distraction, and allows you to potentially recover focus a bit faster during `focus_normalize`.
* `distract_increment += std::min(((current_focus - focus_equilibrium)/10)^2,1)`
* If `distract_increment >1000`, reduce focus by 1, increase `distract_buffe`r by 1, and reduce `distract_increment` by 1000.

By this model, if your focus is 100 and equilibrium is very low, like 20, your 1distract_increment` will increase by (focus-equilibrium/10)^2=(80/10)^2= **64** each turn and you'll lose 1 focus roughly every 20 turns as increment reaches 1000. At a lower gap (focus 100, equilibrium 50), you'll lose 1 focus every 40 turns. As the gap gets smaller the time increases, and when your equilibrium is 10 points or fewer below current, you'll only lose 1 focus per 15 minutes or so.

**Explanation**: The goal of this function is to allow immediate distractions to have consequences on your learning and xp distribution, to keep them from being too harshly penalizing in a model where focus comes back much slower otherwise.

##### `practice_now`
This will use a practice_incrementer to determine if we should get instant xp this turn or not. A pool_proportion determines how much of your gained xp goes immediately to practice xp, and how much is saved to gain later. pool_mitigator makes your practice drop based on how much xp you have pooled waiting to distribute from the relevant pool.

`pracice_now` can drop your focus extremely quickly, if you're learning efficiently. Since focus has less immediate effect on learning this is not a disaster.

Note that we will calculate a variable **task_simplicity** here.
* `task_simplicity` is `(your skill level - difficulty of what you're doing )^2`, min 1 - note that this means more difficult tasks than your level also decrease xp, not increase.
* If `task_simplicity` > 9 and your skill level > difficulty of task, cancel `practice_now` and give the "this task is too simple to train your %s" message. Consider the character as undergoing "mind rest" during this action, which means you may regain pooled xp from other sources faster and regain focus faster. This doesn't apply if difficulty is higher, in that case you just gain xp very slowly and are still burning focus.
* For "your skill level" use knowledge if the task is academic (currently, just reading books). Otherwise, default to practical skill level. If practical skill level < difficulty of task, check if avg(knowledge,practical) levels would be more optimal, and if so use that instead.

* `practice_incrementer` and `pool_incrementer` are equal to 100. Whenever a check finds these to be >100, reduce them by 100 and add 1 xp to either the skill itself, or the appropriate pooled xp. We may want these to be global variables that can be adjusted for testing and balance purposes.
* `practice_rate = (focus_factor + int + per ) / task_simplicity * pool_mitigator`

where:
* `focus_factor` is:
* 100 if your `current_focus > 50`.
* `current_focus * 2` if your focus is <=50 and >20
* `current_focus +20 ` if your focus is <=20

and:
* `pool_mitigator = std::min((total_pooled_xp / (50 + int*2 + per*2 + this_skill.knowledge_level * 5 )),1)`
* `total_pooled_xp` is the sum of all pooled xp waiting to distribute from all sources, not just this skill.

So:
* If practice_rate is less than 1, no xp is gained.
* Using task difficulty, this then creates an xp floor, you can't gain xp as the difference between the thing you're doing and your skill level rises too far.

If practice_rate is 1 or more, go on to:
```
pool_gain = practice_rate - (current_focus/2), min 0.
practice_gain = practice_rate - pool_gain
pool_incrementer += pool_gain
practice_incrementer += practice_gain
```
as your focus drops, more of your learning goes to the pool and it takes longer to gain.

* Check if practice_incrementer >100, if so, add +1 to relevant skill xp and reduce incrementer by 100.
* Check if pool_incrementer >100, if so, add +1 to relevant skill pooled xp and reduce incrementer by 100. Note: Update `pool_time_tracker` to the current turn for that pool.
* (note that if practice or pool incrementer wind up way over 100, that's ok, the xp is not lost, but we only ever gain 1 xp per turn).
* If practice_incrementer cycled, reduce `current_focus` by `0.1`. pool_incrementer does not impact focus.
* If either of those incrementers incremented, do not go on to `distribute_xp` nor `focus_normalize`. Even if we didn't gain xp this turn, we were trying to, and the other two don't proc.

*Note - integration of knowledge vs. practical*: I (or whomever does this) should take a good look at how Character::practice and related functions work. We may want to integrate `catchup_modifier` and `knowledge_modifier` from Character::practice directly here, having `catchup_modifier` increase the amount of xp you gain at this stage if you're training a skill for which you have a higher knowledge level. In fact, most of Character::practice and skill train and such may wind up gutted and replaced with this set of functions.

**Expected function**: Focus has some immediate impact on your skill gain, but note that the impact is reduced. There is less difference in effect for the upper half of focus, then it drops sharply, and hits more or less a floor. At 0 focus you're gaining 20% of your usual XP, which is penalizing but I hope not crippling. However, at 0 focus, *all* your XP gains go to pooled xp. You don't gain levels as quickly, and it won't take long for your "pool mitigator" factor to rise up past 1 and start further reducing the xp you gain. This means that at low focus, you can still do a bit of learning in the moment, but it rapidly falls off.

Additionally, because pool_mitigator increases with skill level in a linear fashion, but level xp requirements increase geometrically, at higher levels, your focus becomes more and more important because you are less and less able to pool a meaningful amount of xp.

##### `distribute_xp`
If we didn't do any learning this turn, and there's xp stored in `pooled_xp`, check if we should distribute that pooled xp to the appropriate skill this turn.
`last_distributed` = turn number of the last turn we distributed xp.
`distribute_interval = 15000/(std::min(focus_equilibrium,current_focus) * (int +per) )`
If `current_turn-last_distributed` <= distribute_interval, reduce pooled_xp by 1 and add that xp to the relevant skill. Decrease current_focus by `0.1`, unless player is sleeping or doing some "mind rest" (then no effect on focus). Set last_distributed to this turn.

If there is more than one set of pooled xp, distribute xp from all of the pools at this time. Current focus goes down by `0.1` regardless of how many pools we distributed.

Note that distribute interval uses the more volatile `focus equilibrium` if it is lower than `current focus`. Things happening to you in the moment will have a more dramatic effect on the speed your xp pools distribute, so being in pain *right now* means you are probably not thinking about what you can learn from that sparring match you had this morning. This is important because it allows your present focus in the moment to matter, but in a way that doesn't overly penalize you. You're still going to get the xp, just not *right now*.

Note also that we should never distribute pooled xp on a turn when we also gained instant xp. It's one or the other, sweet cheeks.

##### `focus_normalize`
Your focus should trend towards your equilibrium value. However, the rate at which it does this is going to be much, much slower now. Focus should run on a roughly 24 hour cycle. Short term distractions, measured in `distract_me`, allow it to drop fairly quickly, but unless your equilibrium stays low, it should quickly bounce back and then trend towards an even keel. Tracking back, recall we had a factor called `distract_buffer` in `distract_me` that stores how much focus you've lost from being distracted by minor things.

If you did not run `practice_now` this turn, we can try to normalize focus. `distribute_xp` does not prevent `focus_normalize` from running.

This will use a `focus_incrementer` just as in the processes above , since this will span multiple turns. When `focus_incrementer > 50000`, increase focus by 1. When `focus_incrementer < 50000`, decrease focus by 1. `Focus_incrementer` may also be best as a global variable to allow adjustment of balance.

* If `current_focus = focus_equilibrium`: reduce `distract_buffer` by 1 (min 0) and break.

Otherwise:
* `focus_adjust = std::min((current_focus - focus_equilibrium), 1)`
* if current_focus < focus_equilibrium: `focus_adjust += distract_buffer * 10`
* if current_focus < focus_equilibrium and you are asleep or doing a "mind rest" activity: `focus_adjust *= 2`
* Finally, `focus_incrementer += focus_adjust`
* if distract_buffer >0: `distract_buffer -= 1`

This means that focus_adjust will max out at around 100, before the distract buffer and adjustment for rest is included, causing you to regain around 1 focus per 500 turns (10 minutes or so) at the widest gap. As the gap closes, the recovery rate decreases, and it should take around 20-30 hours to recover all your focus from nothing while otherwise busy, and half that if you're resting your brain somehow. The distract buffer, which represents focus you lost temporarily while distracted, can increase this quite rapidly, but it will dissipate if your focus_equilibrium remains low or if your current_focus is at equilibrium.

### Display / UI / UX
This model adds a lot of moving parts, and I don't think it's reasonable for the player to have to track them as numbers, nor very helpful. However, the player does need to know what's going on!

First, I think we should change focus to a `||\--` style bar display. Another option would be words, but I like the standardization of the bar. This bar should aggregate not just `current_focus` but also `pooled_xp`, so as your pooled xp rises, your focus appears to drop, just to help players understand that this isn't the best time to start learning. I will post a suggested equation shortly.

Second, we do need some way to show that a skill has pooled XP. I think the easiest way is in the `@` menu displaying a `+` next to skill percentages that are still rising, and if they are highlighted, add a line in the detailed description like "You are still thinking about what you've recently learned about this skill".

## Describe alternatives you have considered.

I initially was planning on some similar changes to focus equilibrium, and adding three new focus pools (see additional context below). The idea of pooled xp, which overlaps with but isn't the same as focus, worked better with that concept.

## Additional context

These calculations assume we will treat all pooled xp as the same. However ideally I would like to keep three separate `total_pooled_xp` trackers:
- `total_academic_pooled_xp`: pooled_xp gained from reading books.
- `total_finedetail_pooled_xp`: pooled_xp gained from crafting, construction, mending, and doing practice actions.
- `total_attention_pooled_xp`: pooled_xp gained from combat, driving, and everything else.

These represent three general types of learning, 'academic' ie. studying and thinking, 'fine detail', ie. paying attention to the little things you're doing, and 'broad attention', ie. noticing a lot of disparate details in your environment. By keeping the three separate we will encourage *not* doing a single thing ad infinitum to gain xp but instead, diversifying and doing lots of different tasks to maximize learning. Later on we may wish to fine tune what counts as what type of learning.

I think we probably shouldn't track these separate pools on a first implementation. We already need to track skill gain and knowledge gain, that's enough for a first pass. It should be easily to extend the system this way in a later PR, and it's a straight buff so it should be well accepted.

Contributor guide

Open the contributing guide

Research direction

Start by locating the current focus equilibrium, stored-focus updates, and practice/xp turn handling described in the issue. Trace how focus, instant XP, pooled XP, distraction, and normalization currently interact before assessing the proposed routines. Done means the focus and XP systems follow the stated formulas, timing rules, and mind-rest behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
game-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.