veloren_rtsim/data/sentiment.rs
1use common::{
2 character::CharacterId,
3 rtsim::{Actor, FactionId, NpcId},
4};
5use hashbrown::HashMap;
6use rand::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::collections::BinaryHeap;
9
10// Factions have a larger 'social memory' than individual NPCs and so we allow
11// them to have more sentiments
12pub const FACTION_MAX_SENTIMENTS: usize = 1024;
13pub const NPC_MAX_SENTIMENTS: usize = 128;
14
15/// Magic factor used to control sentiment decay speed (note: higher = slower
16/// decay, for implementation reasons).
17const DECAY_TIME_FACTOR: f32 = 2500.0;
18
19/// The target that a sentiment is felt toward.
20// NOTE: More could be added to this! For example:
21// - Animal species (dislikes spiders?)
22// - Kind of food (likes meat?)
23// - Occupations (hatred of hunters or chefs?)
24// - Ideologies (dislikes democracy, likes monarchy?)
25// - etc.
26#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
27pub enum Target {
28 Character(CharacterId),
29 Npc(NpcId),
30 Faction(FactionId),
31}
32
33impl From<NpcId> for Target {
34 fn from(npc: NpcId) -> Self { Self::Npc(npc) }
35}
36impl From<FactionId> for Target {
37 fn from(faction: FactionId) -> Self { Self::Faction(faction) }
38}
39impl From<CharacterId> for Target {
40 fn from(character: CharacterId) -> Self { Self::Character(character) }
41}
42impl From<Actor> for Target {
43 fn from(actor: Actor) -> Self {
44 match actor {
45 Actor::Character(character) => Self::Character(character),
46 Actor::Npc(npc) => Self::Npc(npc),
47 }
48 }
49}
50
51#[derive(Clone, Default, Serialize, Deserialize)]
52pub struct Sentiments {
53 #[serde(rename = "m")]
54 map: HashMap<Target, Sentiment>,
55}
56
57impl Sentiments {
58 /// Return the sentiment that is felt toward the given target.
59 pub fn toward(&self, target: impl Into<Target>) -> &Sentiment {
60 self.map.get(&target.into()).unwrap_or(&Sentiment::DEFAULT)
61 }
62
63 /// Return the sentiment that is felt toward the given target.
64 pub fn toward_mut(&mut self, target: impl Into<Target>) -> &mut Sentiment {
65 self.map.entry(target.into()).or_default()
66 }
67
68 /// Progressively decay the sentiment back to a neutral sentiment.
69 ///
70 /// Note that sentiment get decay gets slower the harsher the sentiment is.
71 /// You can calculate the **average** number of seconds required for a
72 /// sentiment to neutral decay with the following rough formula:
73 ///
74 /// ```ignore
75 /// seconds_until_neutrality = (sentiment_value^2 * 24 + 1) / 25 * DECAY_TIME_FACTOR * sentiment_value * 128
76 /// ```
77 ///
78 /// Some 'common' sentiment decay times are as follows:
79 ///
80 /// - `POSITIVE`/`NEGATIVE`: ~26 minutes
81 /// - `ALLY`/`RIVAL`: ~3.4 hours
82 /// - `FRIEND`/`ENEMY`: ~21 hours
83 /// - `HERO`/`VILLAIN`: ~47 hours
84 pub fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
85 self.map.retain(|_, sentiment| {
86 sentiment.decay(rng, dt);
87 // We can eliminate redundant sentiments that don't need remembering
88 !sentiment.is_redundant()
89 });
90 }
91
92 /// Clean up sentiments to avoid them growing too large
93 pub fn cleanup(&mut self, max_sentiments: usize) {
94 if self.map.len() > max_sentiments {
95 let mut sentiments = self.map
96 .iter()
97 // For each sentiment, calculate how valuable it is for us to remember.
98 // For now, we just use the absolute value of the sentiment but later on we might want to favour
99 // sentiments toward factions and other 'larger' groups over, say, sentiments toward players/other NPCs
100 .map(|(tgt, sentiment)| (sentiment.positivity.unsigned_abs(), *tgt))
101 .collect::<BinaryHeap<_>>();
102
103 // Remove the superfluous sentiments
104 for (_, tgt) in sentiments
105 .drain_sorted()
106 .take(self.map.len() - max_sentiments)
107 {
108 self.map.remove(&tgt);
109 }
110 }
111 }
112}
113
114#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)]
115pub struct Sentiment {
116 /// How positive the sentiment is.
117 ///
118 /// Using i8 to reduce on-disk memory footprint.
119 /// Semantically, this value is -1 <= x <= 1.
120 #[serde(rename = "p")]
121 positivity: i8,
122}
123
124impl Sentiment {
125 /// Substantial positive sentiments: NPC may go out of their way to help
126 /// actors associated with the target, greet them, etc.
127 pub const ALLY: f32 = 0.3;
128 const DEFAULT: Self = Self { positivity: 0 };
129 /// Very negative sentiments: NPC may confront the actor, get aggressive
130 /// with them, or even use force against them.
131 pub const ENEMY: f32 = -0.6;
132 /// Very positive sentiments: NPC may join the actor as a companion,
133 /// encourage them to join their faction, etc.
134 pub const FRIEND: f32 = 0.6;
135 /// Extremely positive sentiments: NPC may switch sides to join the actor's
136 /// faction, protect them at all costs, turn against friends for them,
137 /// etc. Verging on cult-like behaviour.
138 pub const HERO: f32 = 0.8;
139 /// Minor negative sentiments: NPC might be less willing to provide
140 /// information, give worse trade deals, etc.
141 pub const NEGATIVE: f32 = -0.1;
142 /// Minor positive sentiments: NPC might be more willing to provide
143 /// information, give better trade deals, etc.
144 pub const POSITIVE: f32 = 0.1;
145 /// Substantial negative sentiments: NPC may reject attempts to trade or
146 /// avoid actors associated with the target, insult them, but will not
147 /// use physical force.
148 pub const RIVAL: f32 = -0.3;
149 /// Extremely negative sentiments: NPC may aggressively persue or hunt down
150 /// the actor, organise others around them to do the same, and will
151 /// generally try to harm the actor in any way they can.
152 pub const VILLAIN: f32 = -0.8;
153
154 fn value(&self) -> f32 { self.positivity as f32 * (1.0 / 126.0) }
155
156 /// Change the sentiment toward the given target by the given amount,
157 /// capping out at the given value.
158 pub fn change_by(&mut self, change: f32, cap: f32) {
159 // There's a bit of ceremony here for two reasons:
160 // 1) Very small changes should not be rounded to 0
161 // 2) Sentiment should never (over/under)flow
162 if change != 0.0 {
163 let abs = (change * 126.0).abs().clamp(1.0, 126.0) as i8;
164 let cap = (cap.abs().min(1.0) * 126.0) as i8;
165 self.positivity = if change > 0.0 {
166 self.positivity.saturating_add(abs).min(cap)
167 } else {
168 self.positivity.saturating_sub(abs).max(-cap)
169 };
170 }
171 }
172
173 /// Limit the sentiment to the given value, either positive or negative. The
174 /// resulting sentiment is guaranteed to be less than the cap (at least,
175 /// as judged by [`Sentiment::is`]).
176 pub fn limit_below(&mut self, cap: f32) {
177 if cap > 0.0 {
178 self.positivity = self
179 .positivity
180 .min(((cap.min(1.0) * 126.0) as i8 - 1).max(0));
181 } else {
182 self.positivity = self
183 .positivity
184 .max(((-cap.max(-1.0) * 126.0) as i8 + 1).min(0));
185 }
186 }
187
188 fn decay(&mut self, rng: &mut impl Rng, dt: f32) {
189 if self.positivity != 0 {
190 // TODO: Find a slightly nicer way to have sentiment decay, perhaps even by
191 // remembering the last interaction instead of constant updates.
192 let chance = (1.0
193 / ((self.value().powi(2) * 0.24 + 1.0) * (1.0 / 25.0) * DECAY_TIME_FACTOR * dt))
194 .min(1.0) as f64;
195
196 // For some reason, RNG doesn't work with small chances (possibly due to impl
197 // limits), so use two bools
198 if rng.gen_bool(chance.sqrt()) && rng.gen_bool(chance.sqrt()) {
199 self.positivity -= self.positivity.signum();
200 }
201 }
202 }
203
204 /// Return `true` if the sentiment can be forgotten without changing
205 /// anything (i.e: is entirely neutral, the default stance).
206 fn is_redundant(&self) -> bool { self.positivity == 0 }
207
208 /// Returns `true` if the sentiment has reached the given threshold.
209 pub fn is(&self, val: f32) -> bool {
210 if val > 0.0 {
211 self.value() >= val
212 } else {
213 self.value() <= val
214 }
215 }
216}