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