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-dynlib",
"veloren-common-ecs",
"veloren-rtsim",
]
[[package]]

View File

@ -3,8 +3,10 @@
// `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`.
use rand::{Rng, seq::IteratorRandom};
use serde::{Deserialize, Serialize};
use specs::Component;
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
use crate::comp::dialogue::MoodState;
@ -54,6 +56,121 @@ pub enum MemoryItem {
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)
/// 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
@ -69,6 +186,7 @@ pub struct RtSimController {
/// toward the given location, accounting for obstacles and other
/// high-priority situations like being attacked.
pub travel_to: Option<Vec3<f32>>,
pub personality: Personality,
pub heading_to: Option<String>,
/// Proportion of full speed to move
pub speed_factor: f32,
@ -80,6 +198,7 @@ impl Default for RtSimController {
fn default() -> Self {
Self {
travel_to: None,
personality:Personality::default(),
heading_to: None,
speed_factor: 1.0,
events: Vec::new(),
@ -91,6 +210,7 @@ impl RtSimController {
pub fn with_destination(pos: Vec3<f32>) -> Self {
Self {
travel_to: Some(pos),
personality:Personality::default(),
heading_to: None,
speed_factor: 0.5,
events: Vec::new(),

View File

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

View File

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

View File

@ -9,10 +9,11 @@ use-dyn-lib = ["common-dynlib"]
be-dyn-lib = []
[dependencies]
common = {package = "veloren-common", path = "../../common"}
common = { package = "veloren-common", path = "../../common"}
common-base = { package = "veloren-common-base", path = "../../common/base" }
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"] }
vek = { version = "0.15.8", features = ["serde"] }

View File

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

View File

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

View File

@ -498,7 +498,6 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool {
bdata.controller,
bdata.read_data,
bdata.event_emitter,
will_ambush(/* bdata.rtsim_entity */ None, &bdata.agent_data),
);
} else {
bdata.agent_data.handle_sounds_heard(
@ -747,7 +746,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
controller,
read_data,
event_emitter,
will_ambush(agent_data.rtsim_entity, agent_data),
);
}
@ -775,15 +773,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool {
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(
rtsim_entity: Option<&RtSimEntity>,
read_data: &ReadData,

View File

@ -2,14 +2,14 @@ use common::{
comp::{
agent::{AgentEvent, Target, TimerAction},
compass::{Direction, Distance},
dialogue::{MoodContext, MoodState, Subject},
dialogue::Subject,
inventory::item::{ItemTag, MaterialStatManifest},
invite::{InviteKind, InviteResponse},
tool::AbilityMap,
BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind,
},
event::ServerEvent,
rtsim::{Memory, MemoryItem, RtSimEvent},
rtsim::{Memory, MemoryItem, RtSimEvent, PersonalityTrait},
trade::{TradeAction, TradePhase, TradeResult},
};
use rand::{thread_rng, Rng};
@ -105,172 +105,142 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool {
match subject {
Subject::Regular => {
if let Some(destination_name) = &agent.rtsim_controller.heading_to {
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(
Memory {
item: MemoryItem::CharacterInteraction {
name: tgt_stats.name.clone(),
},
time_to_forget: read_data.time.0 + 600.0,
if let Some(tgt_stats) = read_data.stats.get(target) {
agent.rtsim_controller.events.push(RtSimEvent::AddMemory(
Memory {
item: MemoryItem::CharacterInteraction {
name: tgt_stats.name.clone(),
},
));
if rtsim_entity.brain.remembers_character(&tgt_stats.name) {
if personality.will_ambush {
"Just follow me a bit more, hehe.".to_string()
} else if personality
.personality_traits
.contains(PersonalityTrait::Extroverted)
time_to_forget: read_data.time.0 + 600.0,
},
));
if let Some(destination_name) = &agent.rtsim_controller.heading_to {
let personality = &agent.rtsim_controller.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.is(PersonalityTrait::Extroverted)
{
if personality
.personality_traits
.contains(PersonalityTrait::Extroverted)
format!(
"I'm heading to {}! Want to come along?",
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)
{
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()
if personality.is(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.is(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!(
"Hi again {}! Unfortunately I'm in a \
hurry right now. See you!",
&tgt_stats.name
)
standard_response_msg()
}
} else {
standard_response_msg()
}
};
agent_data.chat_npc(msg, event_emitter);
}
/*else if agent.behavior.can_trade(agent_data.alignment.copied(), by) {
if !agent.behavior.is(BehaviorState::TRADING) {
controller.push_initiate_invite(by, InviteKind::Trade);
agent_data.chat_npc(
"npc-speech-merchant_advertisement",
event_emitter,
);
} else {
standard_response_msg()
};
agent_data.chat_npc(msg, event_emitter);
} else*/
else if agent.behavior.can_trade(agent_data.alignment.copied(), by) {
if !agent.behavior.is(BehaviorState::TRADING) {
controller.push_initiate_invite(by, InviteKind::Trade);
agent_data.chat_npc(
"npc-speech-merchant_advertisement",
event_emitter,
);
} else {
let default_msg = "npc-speech-merchant_busy";
let msg = default_msg/*agent_data.rtsim_entity.map_or(default_msg, |e| {
if e.brain
.personality
.personality_traits
.contains(PersonalityTrait::Disagreeable)
{
let default_msg = "npc-speech-merchant_busy";
let msg = if agent.rtsim_controller.personality.is(PersonalityTrait::Disagreeable) {
"npc-speech-merchant_busy_rude"
} else {
default_msg
}
})*/;
agent_data.chat_npc(msg, event_emitter);
}
} else {
let mut rng = thread_rng();
/*if let Some(extreme_trait) =
agent_data.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"
},
};
agent_data.chat_npc(msg, event_emitter);
} else*/
{
agent_data.chat_npc("npc-speech-villager", event_emitter);
};
agent_data.chat_npc(msg, event_emitter);
}
}*/ else {
let mut rng = thread_rng();
if let Some(extreme_trait) = agent.rtsim_controller.personality.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"
},
};
agent_data.chat_npc(msg, event_emitter);
} else {
agent_data.chat_npc("npc-speech-villager", event_emitter);
}
}
}
},