From f000347cac1a150657088b282f81a6e3677fecc5 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 4 May 2023 11:53:01 +0100 Subject: [PATCH] Make NPCs respond to each other --- assets/voxygen/i18n/en/npc.ftl | 2 +- rtsim/src/rule/npc_ai.rs | 77 ++++++++++++++++----------- server/src/sys/agent/behavior_tree.rs | 8 ++- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/assets/voxygen/i18n/en/npc.ftl b/assets/voxygen/i18n/en/npc.ftl index 5805282a45..00d24ba40e 100644 --- a/assets/voxygen/i18n/en/npc.ftl +++ b/assets/voxygen/i18n/en/npc.ftl @@ -277,7 +277,7 @@ npc-speech-dir_south_east = south-east npc-speech-dir_south = south npc-speech-dir_south_west = south-west npc-speech-dir_west = west -npc-speech-dir_north_west = very far away +npc-speech-dir_north_west = north-west npc-speech-dist_very_far = very far away npc-speech-dist_far = far away diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 2bfba3a1a8..6b7cbeb4f1 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -543,38 +543,55 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time) } -fn talk_to(tgt: Actor, subject: Option) -> impl Action { +fn talk_to(tgt: Actor, _subject: Option) -> impl Action { now(move |ctx| { - // Mention nearby sites - let comment = if ctx.rng.gen_bool(0.3) - && let Some(current_site) = ctx.npc.current_site - && let Some(current_site) = ctx.state.data().sites.get(current_site) - && let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng) - && let Some(mention_site) = ctx.state.data().sites.get(*mention_site) - && let Some(mention_site_name) = mention_site.world_site - .map(|ws| ctx.index.sites.get(ws).name().to_string()) - { - Content::localized_with_args("npc-speech-tell_site", [ - ("site", Content::Plain(mention_site_name)), - ("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()), - ("dist", Distance::from_length(mention_site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), - ]) - // Mention nearby monsters - } else if ctx.rng.gen_bool(0.3) - && let Some(monster) = ctx.state.data().npcs - .values() - .filter(|other| matches!(&other.role, Role::Monster)) - .min_by_key(|other| other.wpos.xy().distance(ctx.npc.wpos.xy()) as i32) - { - Content::localized_with_args("npc-speech-tell_monster", [ - ("body", monster.body.localize()), - ("dir", Direction::from_dir(monster.wpos.xy() - ctx.npc.wpos.xy()).localize_npc()), - ("dist", Distance::from_length(monster.wpos.xy().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), - ]) + if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) { + // Cut off the conversation sometimes to avoid infinite conversations (but only + // if the target is an NPC!) TODO: Don't special case this, have + // some sort of 'bored of conversation' system + idle().l() } else { - ctx.npc.personality.get_generic_comment(&mut ctx.rng) - }; - just(move |ctx| ctx.controller.say(tgt, comment.clone())) + // Mention nearby sites + let comment = if ctx.rng.gen_bool(0.3) + && let Some(current_site) = ctx.npc.current_site + && let Some(current_site) = ctx.state.data().sites.get(current_site) + && let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng) + && let Some(mention_site) = ctx.state.data().sites.get(*mention_site) + && let Some(mention_site_name) = mention_site.world_site + .map(|ws| ctx.index.sites.get(ws).name().to_string()) + { + Content::localized_with_args("npc-speech-tell_site", [ + ("site", Content::Plain(mention_site_name)), + ("dir", Direction::from_dir(mention_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc()), + ("dist", Distance::from_length(mention_site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), + ]) + // Mention nearby monsters + } else if ctx.rng.gen_bool(0.3) + && let Some(monster) = ctx.state.data().npcs + .values() + .filter(|other| matches!(&other.role, Role::Monster)) + .min_by_key(|other| other.wpos.xy().distance(ctx.npc.wpos.xy()) as i32) + { + Content::localized_with_args("npc-speech-tell_monster", [ + ("body", monster.body.localize()), + ("dir", Direction::from_dir(monster.wpos.xy() - ctx.npc.wpos.xy()).localize_npc()), + ("dist", Distance::from_length(monster.wpos.xy().distance(ctx.npc.wpos.xy()) as i32).localize_npc()), + ]) + } else { + ctx.npc.personality.get_generic_comment(&mut ctx.rng) + }; + // TODO: Don't special-case players + let wait = if matches!(tgt, Actor::Character(_)) { + 0.0 + } else { + 1.5 + }; + idle() + .repeat() + .stop_if(timeout(wait)) + .then(just(move |ctx| ctx.controller.say(tgt, comment.clone()))) + .r() + } }) } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 2b4dc59299..1a04733524 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -4,6 +4,7 @@ use common::{ AgentEvent, AwarenessState, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME, }, + dialogue::Subject, Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction, ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind, }, @@ -494,8 +495,13 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { .timer .start(bdata.read_data.time.0, TimerAction::Interact); bdata.controller.push_action(ControlAction::Stand); - } + if let Some(target_uid) = bdata.read_data.uids.get(target) { + bdata + .controller + .push_event(ControlEvent::Interact(*target_uid, Subject::Regular)); + } + } bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.agent_data.chat_npc(msg, bdata.event_emitter); }