diff --git a/Cargo.lock b/Cargo.lock index 7b04dd4df1..fa46bcc324 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7032,6 +7032,7 @@ dependencies = [ "veloren-common-base", "veloren-common-dynlib", "veloren-common-ecs", + "veloren-rtsim", ] [[package]] diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 0d32bc48bb..e50dc9b920 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -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::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>, + pub personality: Personality, pub heading_to: Option, /// 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) -> Self { Self { travel_to: Some(pos), + personality:Personality::default(), heading_to: None, speed_factor: 0.5, events: Vec::new(), diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bd676b7dfe..bb68a6c742 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -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, pub home: Option, pub faction: Option, - pub riding: Option, + pub personality: Personality, + // Unpersisted state #[serde(skip_serializing, skip_deserializing)] pub chunk_pos: Option>, @@ -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>) -> Self { self.profession = profession.into(); self diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index a777aa07a3..8bc2677125 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -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), ); } diff --git a/server/agent/Cargo.toml b/server/agent/Cargo.toml index 9b2ee49c14..9009caae63 100644 --- a/server/agent/Cargo.toml +++ b/server/agent/Cargo.toml @@ -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"] } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index e4907eb356..115cc5fb11 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -650,7 +650,6 @@ impl<'a> AgentData<'a> { controller: &mut Controller, read_data: &ReadData, event_emitter: &mut Emitter, - 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) { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 47dd8b60d5..4949c4d959 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -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) => { diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6a8cad8ef6..5c262bf3ef 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -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, diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index e438fe9d86..cb7ef5b431 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -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); + } } } },