Give NPCs random Big Five personalities.

This commit is contained in:
Tormod G. Hellen 2022-03-08 00:03:18 +01:00
parent 73d6d96499
commit cb88648cca
7 changed files with 372 additions and 68 deletions

View File

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Furniture and waypoints in site2 towns
- text input for trading
- Themed Site CliffTown, hoodoo/arabic inspired stone structures inhabited by mountaineer NPCs.
- NPCs now have rudimentary personalities
### Changed

1
Cargo.lock generated
View File

@ -6703,6 +6703,7 @@ dependencies = [
"chrono",
"chrono-tz",
"crossbeam-channel",
"enumset",
"futures-util",
"hashbrown 0.11.2",
"humantime",

View File

@ -7,40 +7,88 @@
vector_map: {
"npc.speech.villager": [
"Isn't it such a lovely day?",
"How are you today?",
"Top of the morning to you!",
"I love cheese.",
],
"npc.speech.villager_open": [
"I wonder what the Catoblepas thinks when it eats grass.",
"What do you think about this weather?",
"Thinking about those dungeons makes me scared. I hope someone will clear them out.",
"I'd like to go spelunking in a cave when I'm stronger.",
"Have you seen my cat?",
"What do you suppose makes Glowing Remains glow?",
"Have you ever heard of the ferocious Land Sharks? I hear they live in deserts.",
"I wonder what is on the other side of the mountains.",
"I left some cheese with my sibling. Now I don't know if it exists or not. I call it Schrödinger's cheese.",
"Have you ever caught a firefly?",
"They say shiny gems of all kinds can be found in caves.",
"I'm just crackers about cheese!",
"Won't you come in? We were just about to have some cheese!",
"I just can't understand where those Sauroks keep coming from.",
],
"npc.speech.villager_adventurous": [
"I hope to make my own glider someday.",
"I'd like to go spelunking in a cave when I'm stronger.",
],
"npc.speech.villager_closed": [
"You're not from around here are you?",
"Don't you think our village is the best?",
"They say mushrooms are good for your health. Never eat them myself.",
"To be, or not to be? I think I'll be a farmer.",
],
"npc.speech.villager_conscientious": [
"I keep busy. There's always something to do.",
"I hope it rains soon. Would be good for the crops.",
],
"npc.speech.villager_busybody": [
"People should talk less and work more.",
],
"npc.speech.villager_unconscientious": [
"I think it's time for second breakfast!",
"I wish was my house wasn't such a mess. But then I'd have to tidy up! Haha!",
"Now where did I leave that thing...",
],
"npc.speech.villager_extroverted": [
"You won't believe what I did this weekend!",
"Top of the morning to you!",
"What do you think about this weather?",
"I'm just crackers about cheese!",
"Don't forget the crackers!",
"I simply adore dwarven cheese. I wish I could make it.",
"I wonder what is on the other side of the mountains.",
"I hope to make my own glider someday.",
"Would you like to see my garden? Okay, maybe some other time.",
"Lovely day for a stroll in the woods!",
"To be, or not to be? I think I'll be a farmer.",
"Don't you think our village is the best?",
"What do you suppose makes Glowing Remains glow?",
"I think it's time for second breakfast!",
"Have you ever caught a firefly?",
"I just can't understand where those Sauroks keep coming from.",
"I wish someone would keep the wolves away from the village.",
"I had a wonderful dream about cheese last night. What does it mean?",
"I left some cheese with my brother. Now I don't know if it exists or not. I call it Schrödinger's cheese.",
"I left some cheese with my sister. Now I don't know if it exists or not. I call it Schrödinger's cheese.",
"Someone should do something about those cultists. Preferably not me.",
"I hope it rains soon. Would be good for the crops.",
"I love honey! And I hate bees.",
],
"npc.speech.villager_sociable": [
"Won't you come in? We were just about to have some cheese!",
"Would you like to see my garden? Okay, maybe some other time.",
],
"npc.speech.villager_introverted": [
"Hi.",
"Oh me? I'm nothing special.",
],
"npc.speech.villager_agreeable": [
"How are you today?",
"Just tell me if you need anything.",
"Have you seen my cat?",
],
"npc.speech.villager_worried": [
"Be careful, alright? There are so many dangers out there.",
],
"npc.speech.villager_disagreeable": [
"I say it like it is. If people don't like that, too bad.",
"People are too easily offended.",
],
"npc.speech.villager_neurotic": [
"Thinking about those dungeons makes me scared. I hope someone will clear them out.",
"Someone should do something about those cultists. Preferably not me.",
"I have the feeling something bad will happen.",
"I wish someone would keep the wolves away from the village.",
],
"npc.speech.villager_sad_loner": [
"I'm so lonely.",
"... Sorry about this awkward silence. I'm not so good with people.",
],
"npc.speech.villager_seeker": [
"I want to see the world one day. There's got to be more to life than this village.",
],
"npc.speech.villager_stable": [
"Isn't it such a lovely day?",
"Life's not too bad.",
"Lovely day for a stroll in the woods!",
],
"npc.speech.villager_decline_trade": [
"Sorry, I don't have anything to trade.",
"Trade? Like I got anything that may interest you.",
@ -52,13 +100,15 @@
"I have plenty of goods, Do you want to take a look?"
],
"npc.speech.merchant_busy": [
"Hey, wait your turn.",
"Please wait, I'm only one person.",
"Do you see the other person in front of you?",
"Just a moment, let me finish.",
"No cutting in line.",
"I'm busy, come back later."
],
"npc.speech.merchant_busy_rude": [
"Hey, wait your turn.",
"Do you see the other person in front of you?",
"No cutting in line.",
],
"npc.speech.merchant_trade_successful": [
"Thank you for trading with me!",
"Thank you!",

View File

@ -57,6 +57,7 @@ portpicker = { git = "https://github.com/xMAC94x/portpicker-rs", rev = "df6b3787
authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253f8f2f92760ef44d2679c9a" }
slab = "0.4"
rand_distr = "0.4.0"
enumset = "1.0.8"
rusqlite = { version = "0.24.2", features = ["array", "vtab", "bundled", "trace"] }
refinery = { git = "https://gitlab.com/veloren/refinery.git", rev = "8ecf4b4772d791e6c8c0a3f9b66a7530fad1af3e", features = ["rusqlite"] }

View File

@ -6,6 +6,7 @@ use common::{
terrain::TerrainGrid,
trade, LoadoutBuilder,
};
use enumset::*;
use rand_distr::{Distribution, Normal};
use std::f32::consts::PI;
use tracing::warn;
@ -647,7 +648,7 @@ impl Entity {
}
#[derive(Clone, Debug)]
enum Travel {
pub enum Travel {
// The initial state all entities start in, and a fallback for when a state has stopped making
// sense. Non humanoids will always revert to this state after reaching their goal since the
// current site they are in doesn't change their behavior.
@ -689,31 +690,157 @@ enum Travel {
Idle,
}
impl Default for Travel {
fn default() -> Self { Self::Lost }
// Based on https://en.wikipedia.org/wiki/Big_Five_personality_traits
pub struct PersonalityBase {
openness: u8,
conscientiousness: u8,
extraversion: u8,
agreeableness: u8,
neuroticism: u8,
}
impl PersonalityBase {
/* All thresholds here are arbitrary "seems right" values. The goal is for
* most NPCs to have some kind of distinguishing trait - something
* interesting about them. We want to avoid Joe Averages. But we also
* don't want everyone to be completely weird.
*/
pub fn to_personality(&self) -> Personality {
let mut chat_traits: EnumSet<PersonalityTrait> = EnumSet::new();
if self.openness > Personality::HIGH_THRESHOLD {
chat_traits.insert(PersonalityTrait::Open);
if self.neuroticism < Personality::MID {
chat_traits.insert(PersonalityTrait::Adventurous);
}
} else if self.openness < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Closed);
}
if self.conscientiousness > Personality::HIGH_THRESHOLD {
chat_traits.insert(PersonalityTrait::Conscientious);
if self.agreeableness < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Busybody);
}
} else if self.conscientiousness < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Unconscientious);
}
if self.extraversion > Personality::HIGH_THRESHOLD {
chat_traits.insert(PersonalityTrait::Extroverted);
} else if self.extraversion < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Introverted);
}
if self.agreeableness > Personality::HIGH_THRESHOLD {
chat_traits.insert(PersonalityTrait::Agreeable);
if self.extraversion > Personality::MID {
chat_traits.insert(PersonalityTrait::Sociable);
}
} else if self.agreeableness < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Disagreeable);
}
if self.neuroticism > Personality::HIGH_THRESHOLD {
chat_traits.insert(PersonalityTrait::Neurotic);
if self.openness > Personality::LITTLE_HIGH {
chat_traits.insert(PersonalityTrait::Seeker);
}
if self.agreeableness > Personality::LITTLE_HIGH {
chat_traits.insert(PersonalityTrait::Worried);
}
if self.extraversion < Personality::LITTLE_LOW {
chat_traits.insert(PersonalityTrait::SadLoner);
}
} else if self.neuroticism < Personality::LOW_THRESHOLD {
chat_traits.insert(PersonalityTrait::Stable);
}
Personality {
personality_traits: chat_traits,
}
}
}
pub struct Personality {
pub personality_traits: EnumSet<PersonalityTrait>,
}
#[derive(EnumSetType)]
pub enum PersonalityTrait {
Open,
Adventurous,
Closed,
Conscientious,
Busybody,
Unconscientious,
Extroverted,
Introverted,
Agreeable,
Sociable,
Disagreeable,
Neurotic,
Seeker,
Worried,
SadLoner,
Stable,
}
impl Personality {
pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD;
pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20;
pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20;
pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN;
const MAX: u8 = 100;
pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
const MIN: u8 = 0;
pub fn random_chat_trait(&self, rng: &mut impl Rng) -> Option<PersonalityTrait> {
self.personality_traits.into_iter().choose(rng)
}
pub fn random_trait_value_bounded(rng: &mut impl Rng, min: u8, max: u8) -> u8 {
let max_third = max / 3;
let min_third = min / 3;
rng.gen_range(min_third..=max_third)
+ rng.gen_range(min_third..=max_third)
+ rng.gen_range((min - 2 * min_third)..=(max - 2 * max_third))
}
pub fn random_trait_value(rng: &mut impl Rng) -> u8 {
Self::random_trait_value_bounded(rng, Self::MIN, Self::MAX)
}
pub fn random(rng: &mut impl Rng) -> Personality {
let mut random_value =
|| rng.gen_range(0..=33) + rng.gen_range(0..=34) + rng.gen_range(0..=33);
let base = PersonalityBase {
openness: random_value(),
conscientiousness: random_value(),
extraversion: random_value(),
agreeableness: random_value(),
neuroticism: random_value(),
};
base.to_personality()
}
}
#[derive(Default)]
pub struct Brain {
begin: Option<Id<Site>>,
tgt: Option<Id<Site>>,
route: Travel,
last_visited: Option<Id<Site>>,
memories: Vec<Memory>,
pub begin: Option<Id<Site>>,
pub tgt: Option<Id<Site>>,
pub route: Travel,
pub last_visited: Option<Id<Site>>,
pub memories: Vec<Memory>,
pub personality: Personality,
}
impl Brain {
pub fn idle() -> Self {
pub fn idle(rng: &mut impl Rng) -> Self {
Self {
begin: None,
tgt: None,
route: Travel::Idle,
last_visited: None,
memories: Vec::new(),
personality: Personality::random(rng),
}
}
pub fn raid(home_id: Id<Site>, target_id: Id<Site>) -> Self {
pub fn raid(home_id: Id<Site>, target_id: Id<Site>, rng: &mut impl Rng) -> Self {
Self {
begin: None,
tgt: None,
@ -725,36 +852,54 @@ impl Brain {
},
last_visited: None,
memories: Vec::new(),
personality: Personality::random(rng),
}
}
pub fn villager(home_id: Id<Site>) -> Self {
pub fn villager(home_id: Id<Site>, rng: &mut impl Rng) -> Self {
Self {
begin: Some(home_id),
tgt: None,
route: Travel::Idle,
last_visited: None,
memories: Vec::new(),
personality: Personality::random(rng),
}
}
pub fn merchant(home_id: Id<Site>) -> Self {
pub fn merchant(home_id: Id<Site>, rng: &mut impl Rng) -> Self {
// Merchants are generally extraverted and agreeable
let extraversion_bias = (Personality::MAX - Personality::MIN) / 10 * 3;
let extraversion =
Personality::random_trait_value_bounded(rng, extraversion_bias, Personality::MAX);
let agreeableness_bias = extraversion_bias / 2;
let agreeableness =
Personality::random_trait_value_bounded(rng, agreeableness_bias, Personality::MAX);
let personality_base = PersonalityBase {
openness: Personality::random_trait_value(rng),
conscientiousness: Personality::random_trait_value(rng),
extraversion,
agreeableness,
neuroticism: Personality::random_trait_value(rng),
};
Self {
begin: Some(home_id),
tgt: None,
route: Travel::Idle,
last_visited: None,
memories: Vec::new(),
personality: personality_base.to_personality(),
}
}
pub fn town_guard(home_id: Id<Site>) -> Self {
pub fn town_guard(home_id: Id<Site>, rng: &mut impl Rng) -> Self {
Self {
begin: Some(home_id),
tgt: None,
route: Travel::Idle,
last_visited: None,
memories: Vec::new(),
personality: Personality::random(rng),
}
}

View File

@ -1,11 +1,13 @@
#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out
mod chunks;
mod entity;
pub(crate) mod entity;
mod load_chunks;
mod tick;
mod unload_chunks;
use crate::rtsim::entity::{Personality, Travel};
use self::chunks::Chunks;
use common::{
comp,
@ -134,7 +136,14 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::Wanderer,
brain: Default::default(),
brain: Brain {
begin: None,
tgt: None,
route: Travel::Lost,
last_visited: None,
memories: Vec::new(),
personality: Personality::random(&mut thread_rng()),
},
});
}
for (site_id, site) in world
@ -190,7 +199,7 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::Cultist,
brain: Brain::raid(site_id, nearest_village),
brain: Brain::raid(site_id, nearest_village, &mut thread_rng()),
});
}
}
@ -214,7 +223,7 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::Villager,
brain: Brain::villager(site_id),
brain: Brain::villager(site_id, &mut thread_rng()),
});
}
@ -238,7 +247,7 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::TownGuard,
brain: Brain::town_guard(site_id),
brain: Brain::town_guard(site_id, &mut thread_rng()),
});
}
@ -262,7 +271,7 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::Merchant,
brain: Brain::merchant(site_id),
brain: Brain::merchant(site_id, &mut thread_rng()),
});
}
},
@ -287,7 +296,7 @@ pub fn init(
controller: RtSimController::default(),
last_time_ticked: 0.0,
kind: RtSimEntityKind::Merchant,
brain: Brain::merchant(site_id),
brain: Brain::merchant(site_id, &mut thread_rng()),
});
}
},

View File

@ -4,7 +4,7 @@ pub mod data;
pub mod util;
use crate::{
rtsim::RtSim,
rtsim::{entity::PersonalityTrait, RtSim},
sys::agent::{
consts::{
AVG_FOLLOW_DIST, DAMAGE_MEMORY_DURATION, DEFAULT_ATTACK_RANGE, FLEE_DURATION,
@ -1010,6 +1010,25 @@ impl<'a> AgentData<'a> {
Some(rtsim_entity),
) = (&agent.rtsim_controller.travel_to, &self.rtsim_entity)
{
let personality = &rtsim_entity.brain.personality;
let standard_response_msg = || -> String {
if personality
.personality_traits
.contains(PersonalityTrait::Extroverted)
{
format!(
"I'm heading to {}! Want to come along?",
destination_name
)
} else if personality
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{
"Hrm.".to_string()
} else {
"Hello!".to_string()
}
};
let msg =
if let Some(tgt_stats) = read_data.stats.get(target) {
agent.rtsim_controller.events.push(
@ -1024,23 +1043,33 @@ impl<'a> AgentData<'a> {
.brain
.remembers_character(&tgt_stats.name)
{
format!(
"Greetings fair {}! It has been far too \
long since last I saw you. I'm going to \
{} right now.",
&tgt_stats.name, destination_name
)
if personality
.personality_traits
.contains(PersonalityTrait::Extroverted)
{
format!(
"Greetings fair {}! It has been far \
too long since last I saw you. I'm \
going to {} right now.",
&tgt_stats.name, destination_name
)
} else if personality
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{
"Oh. It's you again.".to_string()
} else {
format!(
"Hi again {}! Unfortunately I'm in a \
hurry right now. See you!",
&tgt_stats.name
)
}
} else {
format!(
"I'm heading to {}! Want to come along?",
destination_name
)
standard_response_msg()
}
} else {
format!(
"I'm heading to {}! Want to come along?",
destination_name
)
standard_response_msg()
};
self.chat_npc(msg, event_emitter);
} else if agent.behavior.can_trade() {
@ -1051,13 +1080,81 @@ impl<'a> AgentData<'a> {
event_emitter,
);
} else {
self.chat_npc(
"npc.speech.merchant_busy",
event_emitter,
);
let default_msg = "npc.speech.merchant_busy";
let msg = self.rtsim_entity.map_or(default_msg, |e| {
if e.brain
.personality
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{
"npc.speech.merchant_busy_rude"
} else {
default_msg
}
});
self.chat_npc(msg, event_emitter);
}
} else {
self.chat_npc("npc.speech.villager", event_emitter);
let mut rng = rand::thread_rng();
if let Some(extreme_trait) =
self.rtsim_entity.and_then(|e| {
e.brain.personality.random_chat_trait(&mut rng)
})
{
let msg = match extreme_trait {
PersonalityTrait::Open => {
"npc.speech.villager_open"
},
PersonalityTrait::Adventurous => {
"npc.speech.villager_adventurous"
},
PersonalityTrait::Closed => {
"npc.speech.villager_closed"
},
PersonalityTrait::Conscientious => {
"npc.speech.villager_conscientious"
},
PersonalityTrait::Busybody => {
"npc.speech.villager_busybody"
},
PersonalityTrait::Unconscientious => {
"npc.speech.villager_unconscientious"
},
PersonalityTrait::Extroverted => {
"npc.speech.villager_extroverted"
},
PersonalityTrait::Introverted => {
"npc.speech.villager_introverted"
},
PersonalityTrait::Agreeable => {
"npc.speech.villager_agreeable"
},
PersonalityTrait::Sociable => {
"npc.speech.villager_sociable"
},
PersonalityTrait::Disagreeable => {
"npc.speech.villager_disagreeable"
},
PersonalityTrait::Neurotic => {
"npc.speech.villager_neurotic"
},
PersonalityTrait::Seeker => {
"npc.speech.villager_seeker"
},
PersonalityTrait::SadLoner => {
"npc.speech.villager_sad_loner"
},
PersonalityTrait::Worried => {
"npc.speech.villager_worried"
},
PersonalityTrait::Stable => {
"npc.speech.villager_stable"
},
};
self.chat_npc(msg, event_emitter);
} else {
self.chat_npc("npc.speech.villager", event_emitter);
}
}
},
Subject::Trade => {