rtsim personalities

This commit is contained in:
Isse 2023-03-22 13:59:55 +01:00 committed by Joshua Barretto
parent 7ac6c6b453
commit 1c0fdf9228
9 changed files with 268 additions and 175 deletions

1
Cargo.lock generated
View File

@ -7032,6 +7032,7 @@ dependencies = [
"veloren-common-base", "veloren-common-base",
"veloren-common-dynlib", "veloren-common-dynlib",
"veloren-common-ecs", "veloren-common-ecs",
"veloren-rtsim",
] ]
[[package]] [[package]]

View File

@ -3,8 +3,10 @@
// `Agent`). When possible, this should be moved to the `rtsim` // `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`. // module in `server`.
use rand::{Rng, seq::IteratorRandom};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::Component; use specs::Component;
use strum::{EnumIter, IntoEnumIterator};
use vek::*; use vek::*;
use crate::comp::dialogue::MoodState; use crate::comp::dialogue::MoodState;
@ -54,6 +56,121 @@ pub enum MemoryItem {
Mood { state: MoodState }, Mood { state: MoodState },
} }
#[derive(EnumIter, Clone, Copy)]
pub enum PersonalityTrait {
Open,
Adventurous,
Closed,
Conscientious,
Busybody,
Unconscientious,
Extroverted,
Introverted,
Agreeable,
Sociable,
Disagreeable,
Neurotic,
Seeker,
Worried,
SadLoner,
Stable,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub struct Personality {
openness: u8,
conscientiousness: u8,
extraversion: u8,
agreeableness: u8,
neuroticism: u8,
}
fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 {
let l = max - min;
min + rng.gen_range(0..=l / 3) + rng.gen_range(0..=l / 3 + l % 3 % 2) + rng.gen_range(0..=l / 3 + l % 3 / 2)
}
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 MIN: u8 = 0;
pub const MID: u8 = (Self::MAX - Self::MIN) / 2;
const MAX: u8 = 255;
fn distributed_value(rng: &mut impl Rng) -> u8 {
distributed(Self::MIN, Self::MAX, rng)
}
pub fn random(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
conscientiousness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
agreeableness: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
}
}
pub fn random_evil(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng),
}
}
pub fn random_good(rng: &mut impl Rng) -> Self {
Self {
openness: Self::distributed_value(rng),
extraversion: Self::distributed_value(rng),
neuroticism: Self::distributed_value(rng),
agreeableness: Self::distributed_value(rng),
conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng),
}
}
pub fn is(&self, trait_: PersonalityTrait) -> bool {
match trait_ {
PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Adventurous => self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID,
PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD,
PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD,
PersonalityTrait::Unconscientious => self.conscientiousness < Personality::LOW_THRESHOLD,
PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD,
PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD,
PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD,
PersonalityTrait::Sociable => self.agreeableness > Personality::HIGH_THRESHOLD && self.extraversion > Personality::MID,
PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD,
PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD,
PersonalityTrait::Seeker => self.neuroticism > Personality::HIGH_THRESHOLD && self.openness > Personality::LITTLE_HIGH,
PersonalityTrait::Worried => self.neuroticism > Personality::HIGH_THRESHOLD && self.agreeableness > Personality::LITTLE_HIGH,
PersonalityTrait::SadLoner => self.neuroticism > Personality::HIGH_THRESHOLD && self.extraversion < Personality::LITTLE_LOW,
PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD,
}
}
pub fn chat_trait(&self, rng: &mut impl Rng) -> Option<PersonalityTrait> {
PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng)
}
pub fn will_ambush(&self) -> bool {
self.agreeableness < Self::LOW_THRESHOLD
&& self.conscientiousness < Self::LOW_THRESHOLD
}
}
impl Default for Personality {
fn default() -> Self {
Self { openness: Personality::MID, conscientiousness: Personality::MID, extraversion: Personality::MID, agreeableness: Personality::MID, neuroticism: Personality::MID }
}
}
/// This type is the map route through which the rtsim (real-time simulation) /// This type is the map route through which the rtsim (real-time simulation)
/// aspect of the game communicates with the rest of the game. It is analagous /// aspect of the game communicates with the rest of the game. It is analagous
/// to `comp::Controller` in that it provides a consistent interface for /// to `comp::Controller` in that it provides a consistent interface for
@ -69,6 +186,7 @@ pub struct RtSimController {
/// toward the given location, accounting for obstacles and other /// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked. /// high-priority situations like being attacked.
pub travel_to: Option<Vec3<f32>>, pub travel_to: Option<Vec3<f32>>,
pub personality: Personality,
pub heading_to: Option<String>, pub heading_to: Option<String>,
/// Proportion of full speed to move /// Proportion of full speed to move
pub speed_factor: f32, pub speed_factor: f32,
@ -80,6 +198,7 @@ impl Default for RtSimController {
fn default() -> Self { fn default() -> Self {
Self { Self {
travel_to: None, travel_to: None,
personality:Personality::default(),
heading_to: None, heading_to: None,
speed_factor: 1.0, speed_factor: 1.0,
events: Vec::new(), events: Vec::new(),
@ -91,6 +210,7 @@ impl RtSimController {
pub fn with_destination(pos: Vec3<f32>) -> Self { pub fn with_destination(pos: Vec3<f32>) -> Self {
Self { Self {
travel_to: Some(pos), travel_to: Some(pos),
personality:Personality::default(),
heading_to: None, heading_to: None,
speed_factor: 0.5, speed_factor: 0.5,
events: Vec::new(), events: Vec::new(),

View File

@ -3,7 +3,7 @@ pub use common::rtsim::{NpcId, Profession};
use common::{ use common::{
comp, comp,
grid::Grid, grid::Grid,
rtsim::{FactionId, SiteId, VehicleId}, rtsim::{FactionId, SiteId, VehicleId, Personality},
store::Id, store::Id,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -80,9 +80,10 @@ pub struct Npc {
pub profession: Option<Profession>, pub profession: Option<Profession>,
pub home: Option<SiteId>, pub home: Option<SiteId>,
pub faction: Option<FactionId>, pub faction: Option<FactionId>,
pub riding: Option<Riding>, pub riding: Option<Riding>,
pub personality: Personality,
// Unpersisted state // Unpersisted state
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub chunk_pos: Option<Vec2<i32>>, pub chunk_pos: Option<Vec2<i32>>,
@ -113,6 +114,7 @@ impl Clone for Npc {
faction: self.faction, faction: self.faction,
riding: self.riding.clone(), riding: self.riding.clone(),
body: self.body, body: self.body,
personality: self.personality,
// Not persisted // Not persisted
chunk_pos: None, chunk_pos: None,
current_site: Default::default(), current_site: Default::default(),
@ -129,6 +131,7 @@ impl Npc {
seed, seed,
wpos, wpos,
body, body,
personality: Personality::default(),
profession: None, profession: None,
home: None, home: None,
faction: None, faction: None,
@ -141,6 +144,11 @@ impl Npc {
} }
} }
pub fn with_personality(mut self, personality: Personality) -> Self {
self.personality = personality;
self
}
pub fn with_profession(mut self, profession: impl Into<Option<Profession>>) -> Self { pub fn with_profession(mut self, profession: impl Into<Option<Profession>>) -> Self {
self.profession = profession.into(); self.profession = profession.into();
self self

View File

@ -11,7 +11,7 @@ use common::{
comp::{self, Body}, comp::{self, Body},
grid::Grid, grid::Grid,
resources::TimeOfDay, resources::TimeOfDay,
rtsim::WorldSettings, rtsim::{WorldSettings, Personality},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
vol::RectVolSize, vol::RectVolSize,
}; };
@ -103,6 +103,7 @@ impl Data {
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_personality(Personality::random(&mut rng))
.with_profession(match rng.gen_range(0..20) { .with_profession(match rng.gen_range(0..20) {
0 => Profession::Hunter, 0 => Profession::Hunter,
1 => Profession::Blacksmith, 1 => Profession::Blacksmith,
@ -119,6 +120,7 @@ impl Data {
for _ in 0..15 { for _ in 0..15 {
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_personality(Personality::random_evil(&mut rng))
.with_faction(site.faction) .with_faction(site.faction)
.with_home(site_id) .with_home(site_id)
.with_profession(match rng.gen_range(0..20) { .with_profession(match rng.gen_range(0..20) {
@ -130,6 +132,7 @@ impl Data {
this.npcs.create_npc( this.npcs.create_npc(
Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng))
.with_home(site_id) .with_home(site_id)
.with_personality(Personality::random_good(&mut rng))
.with_profession(Profession::Merchant), .with_profession(Profession::Merchant),
); );
@ -143,6 +146,7 @@ impl Data {
Npc::new(rng.gen(), wpos, random_humanoid(&mut rng)) Npc::new(rng.gen(), wpos, random_humanoid(&mut rng))
.with_home(site_id) .with_home(site_id)
.with_profession(Profession::Captain) .with_profession(Profession::Captain)
.with_personality(Personality::random_good(&mut rng))
.steering(vehicle_id), .steering(vehicle_id),
); );
} }

View File

@ -9,10 +9,11 @@ use-dyn-lib = ["common-dynlib"]
be-dyn-lib = [] be-dyn-lib = []
[dependencies] [dependencies]
common = {package = "veloren-common", path = "../../common"} common = { package = "veloren-common", path = "../../common"}
common-base = { package = "veloren-common-base", path = "../../common/base" } common-base = { package = "veloren-common-base", path = "../../common/base" }
common-ecs = { package = "veloren-common-ecs", path = "../../common/ecs" } common-ecs = { package = "veloren-common-ecs", path = "../../common/ecs" }
common-dynlib = {package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true} common-dynlib = { package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true}
rtsim = { package = "veloren-rtsim", path = "../../rtsim" }
specs = { version = "0.18", features = ["shred-derive"] } specs = { version = "0.18", features = ["shred-derive"] }
vek = { version = "0.15.8", features = ["serde"] } vek = { version = "0.15.8", features = ["serde"] }

View File

@ -650,7 +650,6 @@ impl<'a> AgentData<'a> {
controller: &mut Controller, controller: &mut Controller,
read_data: &ReadData, read_data: &ReadData,
event_emitter: &mut Emitter<ServerEvent>, event_emitter: &mut Emitter<ServerEvent>,
will_ambush: bool,
) { ) {
enum ActionStateTimers { enum ActionStateTimers {
TimerChooseTarget = 0, TimerChooseTarget = 0,
@ -673,7 +672,7 @@ impl<'a> AgentData<'a> {
.get(entity) .get(entity)
.map_or(false, |eu| eu != self.uid) .map_or(false, |eu| eu != self.uid)
}; };
if will_ambush if agent.rtsim_controller.personality.will_ambush()
&& self_different_from_entity() && self_different_from_entity()
&& !self.passive_towards(entity, read_data) && !self.passive_towards(entity, read_data)
{ {

View File

@ -364,6 +364,7 @@ impl<'a> System<'a> for Sys {
// Update entity state // Update entity state
if let Some(agent) = agent { if let Some(agent) = agent {
agent.rtsim_controller.personality = npc.personality;
if let Some(action) = npc.action { if let Some(action) = npc.action {
match action { match action {
rtsim2::data::npc::NpcAction::Goto(wpos, sf) => { rtsim2::data::npc::NpcAction::Goto(wpos, sf) => {

View File

@ -498,7 +498,6 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
bdata.controller, bdata.controller,
bdata.read_data, bdata.read_data,
bdata.event_emitter, bdata.event_emitter,
will_ambush(/* bdata.rtsim_entity */ None, &bdata.agent_data),
); );
} else { } else {
bdata.agent_data.handle_sounds_heard( bdata.agent_data.handle_sounds_heard(
@ -747,7 +746,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
controller, controller,
read_data, read_data,
event_emitter, event_emitter,
will_ambush(agent_data.rtsim_entity, agent_data),
); );
} }
@ -775,15 +773,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
false false
} }
fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool {
// TODO: implement for rtsim2
// agent_data
// .health
// .map_or(false, |h| h.current() / h.maximum() > 0.7)
// && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush)
false
}
fn remembers_fight_with( fn remembers_fight_with(
rtsim_entity: Option<&RtSimEntity>, rtsim_entity: Option<&RtSimEntity>,
read_data: &ReadData, read_data: &ReadData,

View File

@ -2,14 +2,14 @@ use common::{
comp::{ comp::{
agent::{AgentEvent, Target, TimerAction}, agent::{AgentEvent, Target, TimerAction},
compass::{Direction, Distance}, compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject}, dialogue::Subject,
inventory::item::{ItemTag, MaterialStatManifest}, inventory::item::{ItemTag, MaterialStatManifest},
invite::{InviteKind, InviteResponse}, invite::{InviteKind, InviteResponse},
tool::AbilityMap, tool::AbilityMap,
BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind,
}, },
event::ServerEvent, event::ServerEvent,
rtsim::{Memory, MemoryItem, RtSimEvent}, rtsim::{Memory, MemoryItem, RtSimEvent, PersonalityTrait},
trade::{TradeAction, TradePhase, TradeResult}, trade::{TradeAction, TradePhase, TradeResult},
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -105,44 +105,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
match subject { match subject {
Subject::Regular => { Subject::Regular => {
if let Some(destination_name) = &agent.rtsim_controller.heading_to { if let Some(tgt_stats) = read_data.stats.get(target) {
let msg = format!(
"I'm heading to {}! Want to come along?",
destination_name
);
agent_data.chat_npc(msg, event_emitter);
}
/*if let (
Some((_travel_to, destination_name)),
Some(rtsim_entity),
) = (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity)
{
let personality = &rtsim_entity.brain.personality;
let standard_response_msg = || -> String {
if personality.will_ambush {
format!(
"I'm heading to {}! Want to come along? We'll make \
great travel buddies, hehe.",
destination_name
)
} else 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(RtSimEvent::AddMemory( agent.rtsim_controller.events.push(RtSimEvent::AddMemory(
Memory { Memory {
item: MemoryItem::CharacterInteraction { item: MemoryItem::CharacterInteraction {
@ -151,16 +114,34 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
time_to_forget: read_data.time.0 + 600.0, time_to_forget: read_data.time.0 + 600.0,
}, },
)); ));
if rtsim_entity.brain.remembers_character(&tgt_stats.name) { if let Some(destination_name) = &agent.rtsim_controller.heading_to {
if personality.will_ambush { let personality = &agent.rtsim_controller.personality;
"Just follow me a bit more, hehe.".to_string() let standard_response_msg = || -> String {
} else if personality if personality.will_ambush() {
.personality_traits format!(
.contains(PersonalityTrait::Extroverted) "I'm heading to {}! Want to come along? We'll make \
great travel buddies, hehe.",
destination_name
)
} else if personality.is(PersonalityTrait::Extroverted)
{ {
if personality format!(
.personality_traits "I'm heading to {}! Want to come along?",
.contains(PersonalityTrait::Extroverted) destination_name
)
} else if personality.is(PersonalityTrait::Disagreeable)
{
"Hrm.".to_string()
} else {
"Hello!".to_string()
}
};
let msg = if false /* TODO: Remembers character */ {
if personality.will_ambush() {
"Just follow me a bit more, hehe.".to_string()
} else if personality.is(PersonalityTrait::Extroverted)
{
if personality.is(PersonalityTrait::Extroverted)
{ {
format!( format!(
"Greetings fair {}! It has been far \ "Greetings fair {}! It has been far \
@ -168,9 +149,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
going to {} right now.", going to {} right now.",
&tgt_stats.name, destination_name &tgt_stats.name, destination_name
) )
} else if personality } else if personality.is(PersonalityTrait::Disagreeable)
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{ {
"Oh. It's you again.".to_string() "Oh. It's you again.".to_string()
} else { } else {
@ -187,8 +166,8 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
standard_response_msg() standard_response_msg()
}; };
agent_data.chat_npc(msg, event_emitter); agent_data.chat_npc(msg, event_emitter);
} else*/ }
else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { /*else if agent.behavior.can_trade(agent_data.alignment.copied(), by) {
if !agent.behavior.is(BehaviorState::TRADING) { if !agent.behavior.is(BehaviorState::TRADING) {
controller.push_initiate_invite(by, InviteKind::Trade); controller.push_initiate_invite(by, InviteKind::Trade);
agent_data.chat_npc( agent_data.chat_npc(
@ -197,25 +176,16 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
); );
} else { } else {
let default_msg = "npc-speech-merchant_busy"; let default_msg = "npc-speech-merchant_busy";
let msg = default_msg/*agent_data.rtsim_entity.map_or(default_msg, |e| { let msg = if agent.rtsim_controller.personality.is(PersonalityTrait::Disagreeable) {
if e.brain
.personality
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{
"npc-speech-merchant_busy_rude" "npc-speech-merchant_busy_rude"
} else { } else {
default_msg default_msg
} };
})*/;
agent_data.chat_npc(msg, event_emitter); agent_data.chat_npc(msg, event_emitter);
} }
} else { }*/ else {
let mut rng = thread_rng(); let mut rng = thread_rng();
/*if let Some(extreme_trait) = if let Some(extreme_trait) = agent.rtsim_controller.personality.chat_trait(&mut rng)
agent_data.rtsim_entity.and_then(|e| {
e.brain.personality.random_chat_trait(&mut rng)
})
{ {
let msg = match extreme_trait { let msg = match extreme_trait {
PersonalityTrait::Open => { PersonalityTrait::Open => {
@ -268,11 +238,11 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
}, },
}; };
agent_data.chat_npc(msg, event_emitter); agent_data.chat_npc(msg, event_emitter);
} else*/ } else {
{
agent_data.chat_npc("npc-speech-villager", event_emitter); agent_data.chat_npc("npc-speech-villager", event_emitter);
} }
} }
}
}, },
Subject::Trade => { Subject::Trade => {
if agent.behavior.can_trade(agent_data.alignment.copied(), by) { if agent.behavior.can_trade(agent_data.alignment.copied(), by) {