Designers can hand-author these, or tooling can suggest initial values based on tags and NPC descriptions.
Action impact weight (I_a)
Separately, give each type of action a magnitude- how big a moral move is it, independent of direction?
Killing might be 0.9. Stealing might be 0.4. Lying, 0.3. Offering aid, 0.2. Minor small talk, 0.1.
When a player acts, multiply the action’s impact by a function of the target’s moral value. A world where “killing evil is good, killing good is evil” might use:
- Killing a villain (M_o = –0.5) with I_kill = 0.9 gives Δm = +0.45 (a positive moral move)
- Killing a saint (M_o = +0.8) with I_kill = 0.9 gives Δm = –0.72 (a serious moral crime)
The specific combining function is a design surface. A pacifist world could penalize any killing heavily regardless of the target. A grim noir setting might be tolerant of violence but harsh on betrayal. The point is that every event now has a composable, well-defined contribution to the player’s state, rather than an ad-hoc script.
This means the very similar play styles can have more possibilities than it’s possible to easily count; making re- plays have more variance.
The specific function g(⋅) is a design surface: a pacifist world could penalize any killing heavily, or a grim setting could be more tolerant of brutality.
The important part is that every AEP carries a well-defined, composable contribution instead of ad-hoc one-off script logic.
Time-weighted moral history: redemption and decay
Here’s a problem that single-slider systems almost always get wrong: your first hour of play shouldn’t define your last. If a player murders their way through Act 1, then spends Acts 2 and 3 being genuinely heroic, the world should be able to notice that shift. Conversely, a seemingly good character who suddenly starts making dark choices should raise flags and not get a free pass because their old score is high.
The fix is to weight events by recency. Instead of just summing all past Δm values equally, we apply a decay function so that older events contribute less than recent ones.
A natural choice is exponential decay:
w(t, t_n) = λ^(t — t_n)
- t is the current moment (could be event count, quest milestones, or in-game days)
- t_n is when event n happened
- λ is a decay rate between 0 and 1
With λ close to 1, the world has a long memory and history matters a lot. With smaller λ, recent actions dominate and the world is quicker to update its opinion of you.
The current moral score is then a weighted average of all past impacts:
M_player(t) = Σ w(t, t_n) · Δm_n / Σ w(t, t_n)
In practice, it’s useful to maintain two windows simultaneously, a long-memory lifetime score and a short-window recent score. Dialogue and systems can then distinguish between who the player has usually been and who they seem to be becoming lately. That distinction is what unlocks credible redemption arcs, sudden heel-turns, and gradual corruption, without any hard-coded special cases.
From a single slider to a state vector
Even a well-tuned, time-weighted morality score is still just one axis. Real people are more complicated than that- and so are interesting characters. A player can be kind but unreliable, loyal but brutal, lawful yet deeply selfish, or chaotic but fiercely protective of people they care about. A single good–evil slider collapses all of that into a single number, which is part of why most morality systems feel shallow.
The solution is to track the player as a vector of behavioral dimensions, not a single scalar:
S(t) = [morality, honor, loyalty, chaos, mercy, faction_X_reputation, …]
Each component tracks a different axis of identity. Each action–object pair now yields a vector contribution ΔS_n- nudging multiple dimensions at once. Stealing from a temple might hurt your morality and your reputation with a religious faction, but possibly increase your chaos score. Defecting from the thieves’ guild might drop your loyalty score sharply while improving your standing with law-abiding factions.
The time-weighted aggregation works exactly the same way, just applied across every dimension:
S(t) = Σ w(t, t_n) · ΔS_n / Σ w(t, t_n)
The practical payoff, as mentioned before will be that no two players who play differently are likely to end up with the same state vector. Their identity fingerprints will diverge- and systems across the game can react to those differences in principled, consistent ways.
Keeping authoring sane: tools, tags, and personas
At this point a reasonable objection is: specifying the full state contribution vector for every action–object combination sounds like an enormous amount of work. Won’t that explode the authoring burden?
It doesn’t have to, if you structure it correctly. The key insight is that most objects and actions can be described with a small set of semantic tags- things like “values loyalty,” “abhors betrayal,” “holy relic,” “minor theft,” “armed civilian.” Rule-based templates or lightweight learned models can propose default state contributions from those tags, which designers then review, tweak, and lock in. You get predictability and editorial control without hand-crafting a full table from scratch.
For the trickiest cases- novel action–object combinations that weren’t anticipated at authoring time- transformer-based text encoders can embed both the action description and the NPC description into a shared space and estimate a reasonable ΔS. Designers can always override these estimates with hard rules (”killing any child is always maximally penalized, regardless of the model’s output”).
Feeding the state into dialogue
Once the state model exists, dialogue becomes the most immediate consumer. Traditional dialogue trees gate branches on simple conditions- “has item,” “finished quest,” “morality above 50.” A richer state allows conditions that actually say something:
- “High mercy, recently spiked in chaos” → an NPC senses the contradiction and calls it out
- “Historically cruel but improving lately” → a reformed mentor gives a cautious second chance
- “Trusted by faction A, distrusted by faction B” → a joint meeting becomes genuinely tense