use core::time; use std::sync::Arc; #[double] use crate::db::DBTrans; use crate::{ language::{self, indefinite_article}, message_handler::{ user_commands::{user_error, UResult, UserError, VerbContext}, ListenerSession, }, models::{ consent::ConsentType, item::{ ActiveConversation, ConversationIntensity, ConversationTopic, ConversationalInterestType, ConversationalStyle, Item, ItemFlag, SkillType, }, task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{TaskHandler, TaskRunContext}, DResult, }; use ansi::ansi; use async_trait::async_trait; use chrono::Utc; use itertools::Itertools; use mockall_double::double; use nom::{ branch::alt, bytes::complete::tag, character::complete::space1, combinator::{cut, map, success}, error::{context, VerboseError, VerboseErrorKind}, sequence::{preceded, tuple}, }; use rand::{prelude::SliceRandom, Rng}; use super::{comms::broadcast_to_room, display::bar_n_of_m, skills::skill_check_and_grind}; struct ConversationResult { pub my_total_interest: u64, pub their_total_interest: u64, pub my_direct_interest: u64, pub my_skill_level: f64, pub my_transition_time: u64, pub their_skill_level: f64, pub their_transition_time: u64, } #[derive(Clone)] pub struct ShareTaskHandler; #[async_trait] impl TaskHandler for ShareTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let (p1_type, p1_code) = ctx .task .meta .task_code .split_once("/") .ok_or("Bad share task code")?; let p1 = match ctx.trans.find_item_by_type_code(p1_type, p1_code).await? { None => return Ok(None), Some(v) => v, }; let p1_conv = match p1.active_conversation.as_ref() { None => return Ok(None), Some(v) => v, }; let (p2_type, p2_code) = p1_conv .partner_ref .split_once("/") .ok_or("Bad share partner")?; let p2 = match ctx.trans.find_item_by_type_code(p2_type, p2_code).await? { None => return Ok(None), Some(v) => v, }; let p2_conv = match p2.active_conversation.as_ref() { None => return Ok(None), Some(v) => v, }; let intensity_word = match p1_conv.current_intensity { ConversationIntensity::Fast => "quickly ", ConversationIntensity::Slow => "slowly ", ConversationIntensity::Normal => "", }; let (pa, pb) = if rand::thread_rng().gen::() < 0.5 { (&p1, &p2) } else { (&p2, &p1) }; let msg = match p1_conv.current_topic { ConversationTopic::ParodyKingsOffice => { let act = (&vec![ "mocks the existence of a self-styled king".to_owned(), format!("declares {} king in a mocking tone", &pa.pronouns.intensive), format!("puts on {} best Queens English and asks if {} would like some wasteland tea from {} finest rusty mug", &pa.pronouns.possessive, &pb.display_for_sentence(1, false), &pa.pronouns.possessive), format!("utters, with no hint of fear, a phrase that would have {} up for les majeste if the king had any real power", &pa.pronouns.object) ]).choose(&mut rand::thread_rng()).unwrap().clone(); let reaction = (&vec![ format!("can barely contain {} laughter", &pb.pronouns.possessive), "thinks it is hilarious".to_owned(), "finds it rather funny".to_owned(), "is quite amused".to_owned(), "snorts to suppress a laugh".to_owned(), ]) .choose(&mut rand::thread_rng()) .unwrap() .clone(); format!( "{} {}{} and {} {}", pa.display_for_sentence(1, true), intensity_word, &act, &pb.display_for_sentence(1, false), &reaction, ) } ConversationTopic::PlayFight => { let act = *(&vec![ "pretends to throw a punch", "engages in a playful wrestling match", "swings an imaginary sword", "throws a fake kick", ]) .choose(&mut rand::thread_rng()) .unwrap(); let reaction = *(&vec![ "laughs it off", "plays along with a grin", "dodges the imaginary attack", "feigns a dramatic injury", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} {}", pa.display_for_sentence(1, true), intensity_word, &act, &pb.display_for_sentence(1, false), &reaction, ) } ConversationTopic::ThoughtsOnSunTzu => { let thought = *(&vec![ "reflects on Sun Tzu's strategic brilliance", "ponders Sun Tzu's timeless wisdom", "recalls a favorite Sun Tzu quote", "shares a strategic insight inspired by Sun Tzu", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} listens attentively", pa.display_for_sentence(1, true), intensity_word, &thought, &pb.display_for_sentence(1, false), ) } ConversationTopic::ThoughtsOnMachiavelli => { let thought = *(&vec![ "discusses Machiavelli's political theories", "evaluates the relevance of Machiavellian principles", "analyzes Machiavelli's views on power", "ponders the ethics of Machiavellian strategies", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} nods in agreement", pa.display_for_sentence(1, true), intensity_word, &thought, &pb.display_for_sentence(1, false), ) } ConversationTopic::ExploringRuins => { let exploration = *(&vec![ "describes the eerie atmosphere of ruined buildings", "shares interesting findings from recent explorations", "reminisces about a thrilling encounter in a ruin", "suggests the best ruins for scavenging", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} listens intently", pa.display_for_sentence(1, true), intensity_word, &exploration, &pb.display_for_sentence(1, false), ) } ConversationTopic::RoamingEnemies => { let enemy_tales = *(&vec![ "recounts a close encounter with a formidable enemy", "discusses strategies for dealing with roaming threats", "shares tips on identifying dangerous foes", "warns about the most treacherous areas for enemies", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} nods with newfound caution", pa.display_for_sentence(1, true), intensity_word, &enemy_tales, &pb.display_for_sentence(1, false), ) } ConversationTopic::FishingSpots => { let fishing_info = *(&vec![ "reveals secret fishing spots with the best catches", "discusses the ideal bait for different fishing spots", "shares amusing anecdotes from fishing adventures", "boasts about the biggest fish caught in a particular spot", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} expresses interest", pa.display_for_sentence(1, true), intensity_word, &fishing_info, &pb.display_for_sentence(1, false), ) } ConversationTopic::GoodAmbushSpots => { let ambush_strategy = *(&vec![ "reveals the best spots for surprise ambushes", "discusses tactics for setting up successful ambushes", "describes the terrain for effective surprise attacks", "shares personal experiences with ambush strategies", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} listens with a thoughtful expression", pa.display_for_sentence(1, true), intensity_word, &ambush_strategy, &pb.display_for_sentence(1, false), ) } ConversationTopic::SurvivingWeather => { let weather_survival = *(&vec![ "shares tips on surviving harsh weather conditions", "discusses the best clothing for different climates", "recounts a daring adventure in extreme weather", "offers advice on weather-related challenges", ]) .choose(&mut rand::thread_rng()) .unwrap(); format!( "{} {}{} and {} nods appreciatively", pa.display_for_sentence(1, true), intensity_word, &weather_survival, &pb.display_for_sentence(1, false), ) } }; let msg = format!(ansi!("{}\n"), &msg); broadcast_to_room(&ctx.trans, &p1.location, None, &msg).await?; let mut p1_mut = (*p1).clone(); let mut p2_mut = (*p2).clone(); for (interest, mut growth) in topic_to_interest_growth(&p1_conv.current_topic) { growth *= 100; if growth > 0 { match p1_conv.current_intensity { ConversationIntensity::Fast => { growth *= 6; growth /= 2; } ConversationIntensity::Slow => { growth *= 4; growth /= 3; } ConversationIntensity::Normal => { growth *= 2; } } } else { growth *= 2; } p1_mut.active_conversation.as_mut().map(|ac| { ac.interest_levels .entry(interest.clone()) .and_modify(|v| (*v) = ((*v) as i64 + growth).max(0).min(10000) as u64) .or_insert(growth.max(0) as u64) }); p2_mut.active_conversation.as_mut().map(|ac| { ac.interest_levels .entry(interest.clone()) .and_modify(|v| (*v) = ((*v) as i64 + growth).max(0).min(10000) as u64) .or_insert(growth.max(0) as u64) }); } let res = compute_conversation_result(&p1, &p1_conv, &p2, &p2_conv); p1_mut.active_conversation.as_mut().map(|ac| { ac.peak_total_interest = res.my_total_interest.max(ac.peak_total_interest); }); p2_mut.active_conversation.as_mut().map(|ac| { ac.peak_total_interest = res.their_total_interest.max(ac.peak_total_interest); }); let peak_reached_interest: Option = p1_mut.active_conversation.as_ref().and_then(|ac| { ac.interest_levels .iter() .find(|(_, lev)| **lev >= 10000) .map(|(it, _)| it.clone()) }); if let Some(peak_reached_interest) = peak_reached_interest { ctx.trans.save_item_model(&p2_mut).await?; stop_conversation_mut( &ctx.trans, &mut p1_mut, &format!( "has had {} fill of talk about {} so puts a stop to the conversation with", &p1.pronouns.possessive, peak_reached_interest.display() ), ) .await?; ctx.trans.save_item_model(&p1_mut).await?; return Ok(None); } let p1_ago = (Utc::now() - p1_conv.last_change).num_seconds(); if p1_ago >= (res.my_transition_time as i64) && p1_ago - 5 < (res.my_transition_time as i64) && p1.item_type == "player" { if let Some((sess, _)) = ctx.trans.find_session_for_player(&p1.item_code).await? { inform_player_convo_change_ready(&ctx.trans, &p1_conv, &sess).await?; } } let p2_ago = (Utc::now() - p2_conv.last_change).num_seconds(); if p2_ago >= (res.their_transition_time as i64) && p2_ago - 5 < (res.their_transition_time as i64) && p2.item_type == "player" { if let Some((sess, _)) = ctx.trans.find_session_for_player(&p2.item_code).await? { inform_player_convo_change_ready(&ctx.trans, &p2_conv, &sess).await?; } } ctx.trans.save_item_model(&p1_mut).await?; ctx.trans.save_item_model(&p2_mut).await?; Ok(Some(time::Duration::from_millis(5000))) } } pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &ShareTaskHandler; async fn inform_player_convo_change_ready( trans: &DBTrans, convo: &ActiveConversation, sess: &ListenerSession, ) -> DResult<()> { let intensity_commands = vec![ ConversationIntensity::Slow, ConversationIntensity::Normal, ConversationIntensity::Fast, ] .into_iter() .filter(|ci| ci != &convo.current_intensity) .map(|ci| format!(ansi!("{}"), ci.to_command())) .join(" or "); let topics = style_to_allowed_topics(&convo.style) .into_iter() .filter(|t| t != &convo.current_topic) .map(|t| format!(ansi!("{}"), t.display_command())) .join(" or "); let styles = vec![ ConversationalStyle::Amicable, ConversationalStyle::Joking, ConversationalStyle::Serious, ] .into_iter() .filter(|s| s != &convo.style) .map(|ci| format!(ansi!("{}"), ci.display())) .join(" or "); trans .queue_for_session( sess, Some(&format!(ansi!( "It's been long enough that you feel you'd have a good shot at changing the pace \ (try {}), topic (try {}) or style (try {}) of the conversation. \ [use share status to check interest levels, and help share \ to learn more]\n"), intensity_commands, topics, styles, )), ) .await?; Ok(()) } fn share_skill_to_base_transition_time(skill: f64) -> f64 { ((13.6666666666666666666667 - skill) * 3.0).max(1.0) } fn compute_conversation_result( me: &Item, my_conversation: &ActiveConversation, them: &Item, their_conversation: &ActiveConversation, ) -> ConversationResult { let my_direct_interest = my_conversation .interest_levels .iter() .map(|(_, pl)| pl) .sum(); let their_direct_interest: u64 = their_conversation .interest_levels .iter() .map(|(_, pl)| pl) .sum(); let my_theoretical_skill_level: f64 = *(me.total_skills.get(&SkillType::Share).unwrap_or(&8.0)); let their_theoretical_skill_level: f64 = *(them.total_skills.get(&SkillType::Share).unwrap_or(&8.0)); let my_transition_time = share_skill_to_base_transition_time(my_theoretical_skill_level) as u64; let their_transition_time = share_skill_to_base_transition_time(their_theoretical_skill_level) as u64; let my_total_interest = my_direct_interest * their_transition_time; let their_total_interest = their_direct_interest * my_transition_time; ConversationResult { my_total_interest, their_total_interest, my_direct_interest, my_skill_level: my_theoretical_skill_level, my_transition_time, their_skill_level: their_theoretical_skill_level, their_transition_time, } } fn relevant_interest_types(_character: &Item) -> Vec { // Static for now vec![ ConversationalInterestType::Philosophy, ConversationalInterestType::LocalGeography, ConversationalInterestType::Threats, ConversationalInterestType::Tactics, ConversationalInterestType::Weather, ConversationalInterestType::Politics, ConversationalInterestType::Frivolity, ] } fn append_interest_graph(msg: &mut String, character: &Item, conversation: &ActiveConversation) { for pt in relevant_interest_types(character) { let level = *(conversation.interest_levels.get(&pt).unwrap_or(&0)); msg.push_str(&format!( ansi!("{}:\t{}[{}]\n"), pt.display(), if pt.display().len() >= 15 { "" } else { "\t" }, // 10,000 levels, 25 divisions 10000/25 => 400 per bar. bar_n_of_m(level / 400, 25) )); } } pub async fn display_conversation_status( trans: &DBTrans, to_whom: &Item, with_whom: &Item, ) -> DResult<()> { let conversation = match to_whom.active_conversation.as_ref() { None => return Ok(()), Some(v) => v, }; let partner_conversation = match with_whom.active_conversation.as_ref() { None => return Ok(()), Some(v) => v, }; let result = compute_conversation_result(&to_whom, &conversation, &with_whom, &partner_conversation); let mut msg = format!(ansi!("The current conversational style is {}. Type the name of one of the following alternative styles to switch and open different topics: {}\n"), conversation.style.display(), conversation.style.transitions().iter().map(|v| v.display()).join(" ")); let alt_topics: Vec = style_to_allowed_topics(&conversation.style) .into_iter() .filter(|t| t != &conversation.current_topic) .map(|t| { format!( ansi!("{} (type {})"), t.display_readable(), t.display_command() ) }) .collect(); msg.push_str(&format!("The current topic is {}. Your current style allows switching to the following topics: {}\n", conversation.current_topic.display_readable(), &alt_topics.join(" or "))); msg.push_str("Your current interest levels:\n"); append_interest_graph(&mut msg, to_whom, conversation); msg.push_str(&format!( "Total direct interest: {}\n", (result.my_direct_interest as f64 / 100.0).round() )); let partner_txt = with_whom.display_for_sentence(1, false); if result.their_skill_level < result.my_skill_level { msg.push_str(&format!("Your interest level is increased as you observe how {} is learning despite being a less skilled conversationalist than you.\n", &partner_txt)); } if to_whom.item_type == "player" { if let Some((sess, _)) = trans.find_session_for_player(&to_whom.item_code).await? { trans.queue_for_session(&sess, Some(&msg)).await?; } } Ok(()) } // Designed to be neutral as to whether it is p1 or p2. fn share_event_name(p1: &Item, p2: &Item) -> String { let canon_it = if p1.item_code < p2.item_code || (p1.item_code == p2.item_code && p1.item_type < p2.item_type) { p1 } else { p2 }; canon_it.refstr() } pub async fn start_conversation( trans: &'_ DBTrans, initiator: &'_ Item, acceptor: &'_ Item, ) -> UResult<()> { if acceptor.item_code == initiator.item_code && acceptor.item_type == initiator.item_type { user_error("That's you... and that would make you feel a bit lonely.".to_owned())?; } if acceptor.item_type == "player" { if initiator.item_type != "player" { user_error("Only players can initiate conversation with players".to_owned())?; } let (_other_sess, other_sessdat) = trans .find_session_for_player(&acceptor.item_code) .await? .ok_or_else(|| { UserError(format!("You propose sharing knowledge with {}, but {} doesn't seem interested. [That player is currently not logged in]", acceptor.display_for_sentence(1, false), &acceptor.pronouns.subject )) })?; trans.delete_expired_user_consent().await?; trans .find_user_consent_by_parties_type( &acceptor.item_code, &initiator.item_code, &ConsentType::Share, ) .await? .ok_or_else(|| { UserError(format!( ansi!( "You ask {} to share knowledge, but {} doesn't \ seem interested. [The other player will need to type \ allow share from {} before their \ character will consent to knowledge sharing]" ), &acceptor.display_for_sentence(1, false), &acceptor.pronouns.subject, initiator.display_for_session(&other_sessdat) )) })?; } else { if !acceptor.flags.contains(&ItemFlag::AllowShare) { user_error(format!( "You ask {} to share knowledge with you, but {} doesn't seem interested.", &acceptor.display_for_sentence(1, false), &acceptor.pronouns.subject ))?; } } if !initiator.queue.is_empty() { user_error(ansi!("You're a bit busy right now! [Use the stop command to stop what you are doing].").to_owned())?; } if initiator .active_combat .as_ref() .map(|ac| ac.attacking.is_some()) .unwrap_or(false) { user_error( "You can share knowledge, or you can fight... but both at once seems like too much!" .to_owned(), )?; } if !acceptor.queue.is_empty() || acceptor .active_combat .as_ref() .map(|ac| ac.attacking.is_some()) .unwrap_or(false) { user_error(format!( "{} seems to be a bit busy right now!", acceptor.display_for_sentence(1, true) ))?; } if acceptor.active_conversation.is_some() { user_error(format!( "{} seems to be already deep in conversation!", acceptor.display_for_sentence(1, true) ))?; } broadcast_to_room( trans, &initiator.location, None, &format!( ansi!("{} proposes to share knowledge with {}, and {} accepts!\n"), &initiator.display_for_sentence(1, true), &acceptor.display_for_sentence(1, true), &acceptor.pronouns.subject ), ) .await?; let mut acceptor_mut = (*acceptor).clone(); let mut initiator_mut = (*initiator).clone(); acceptor_mut.active_conversation = Some(ActiveConversation { partner_ref: initiator.refstr(), last_change: Utc::now(), ..Default::default() }); trans.save_item_model(&acceptor_mut).await?; initiator_mut.active_conversation = Some(ActiveConversation { partner_ref: acceptor.refstr(), last_change: Utc::now(), ..Default::default() }); trans.save_item_model(&initiator_mut).await?; trans .upsert_task(&Task { meta: TaskMeta { task_code: share_event_name(&initiator, &acceptor), next_scheduled: Utc::now() + chrono::Duration::milliseconds(5000), ..Default::default() }, details: TaskDetails::ShareTick, }) .await?; Ok(()) } pub async fn stop_conversation_mut( trans: &DBTrans, participant: &mut Item, // Should make sense with actor first and other partner after. e.g. walks away from conversation with leave_description: &str, ) -> DResult<()> { let (partner_type, partner_code) = match participant .active_conversation .as_ref() .and_then(|ac| ac.partner_ref.split_once("/")) { None => return Ok(()), Some(v) => v, }; let partner = match trans .find_item_by_type_code(partner_type, partner_code) .await? { None => return Ok(()), Some(v) => v, }; let mut partner_mut = (*partner).clone(); participant.active_conversation = None; partner_mut.active_conversation = None; trans.save_item_model(&partner_mut).await?; broadcast_to_room( trans, &participant.location, None, &format!( ansi!("{} {} {}.\n"), &participant.display_for_sentence(1, true), leave_description, &partner_mut.display_for_sentence(1, false) ), ) .await?; Ok(()) } pub fn parse_conversation_topic(input: &str) -> Result, &'static str> { let r = alt(( map( tuple(( tag("parody"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try parody kings office"), tuple((tag("kings"), space1, tag("office"))), )), )), |_| Some(ConversationTopic::ParodyKingsOffice), ), map( tuple(( tag("play"), space1::<&str, VerboseError<&str>>, cut(context(ansi!("Try play fight"), tag("fight"))), )), |_| Some(ConversationTopic::PlayFight), ), preceded( tuple((tag("thoughts"), space1::<&str, VerboseError<&str>>)), cut(context( ansi!( "Try thoughts on machiavelli or thoughts on sun tzu" ), preceded( tuple((tag("on"), space1)), alt(( map( preceded( tuple((tag("sun"), space1)), cut(context( ansi!("Try thoughts on sun tzu"), tag("tzu"), )), ), |_| Some(ConversationTopic::ThoughtsOnSunTzu), ), map(tag("machiavelli"), |_| { Some(ConversationTopic::ThoughtsOnMachiavelli) }), )), ), )), ), map( tuple(( tag("exploring"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try exploring ruins"), tag("ruins"), )), )), |_| Some(ConversationTopic::ExploringRuins), ), map( tuple(( tag("roaming"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try roaming enemies"), tag("enemies"), )), )), |_| Some(ConversationTopic::RoamingEnemies), ), map( tuple(( tag("fishing"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try fishing spots"), tag("spots"), )), )), |_| Some(ConversationTopic::FishingSpots), ), map( tuple(( tag("good"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try good ambush spots"), tuple((tag("ambush"), space1, tag("spots"))), )), )), |_| Some(ConversationTopic::GoodAmbushSpots), ), map( tuple(( tag("surviving"), space1::<&str, VerboseError<&str>>, cut(context( ansi!("Try surviving weather"), tag("weather"), )), )), |_| Some(ConversationTopic::SurvivingWeather), ), success(None), ))(input); const CATCHALL_ERROR: &str = "Invalid command"; match r { Ok((_, result)) => Ok(result), Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(e .errors .into_iter() .find_map(|k| match k.1 { VerboseErrorKind::Context(s) => Some(s), _ => None, }) .unwrap_or(CATCHALL_ERROR)), Err(_) => Err(CATCHALL_ERROR), } } async fn check_conversation_change<'l>( ctx: &VerbContext<'_>, player_item: &'l mut Item, ) -> UResult)>> { let my_convo = player_item .active_conversation .as_ref() .ok_or_else(|| UserError("You're not in a conversation".to_owned()))?; let how_long_ago = (Utc::now() - my_convo.last_change).num_seconds(); if how_long_ago < 1 { user_error( "You can't keep up with the rate of change in the conversation [try again later]" .to_owned(), )?; } // Changing topic after 20s is 50/50 at level 7, level needed increases for every 3s less. let level = 13.6666666666666666666667 - (how_long_ago as f64) / 3.0; let result = skill_check_and_grind(ctx.trans, player_item, &SkillType::Share, level).await?; if result < -0.5 { // Mostly to prevent spamming until they succeed, have a chance of resetting timer... if let Some(ac) = player_item.active_conversation.as_mut() { ac.last_change = Utc::now(); } ctx.trans.queue_for_session(&ctx.session, Some(ansi!("Your attempt to change the conversation goes so awkwardly you not only fail, but feel you'll have to wait longer now to have another shot!\n"))).await?; return Ok(None); } else if result < 0.0 { ctx.trans .queue_for_session( &ctx.session, Some(ansi!( "Your attempt to change the conversation doesn't pan out.\n" )), ) .await?; return Ok(None); } // This way lets us pass the borrow checker with the skillcheck. let my_convo = player_item .active_conversation .as_mut() .ok_or_else(|| UserError("You're not in a conversation".to_owned()))?; let player_info = my_convo .partner_ref .split_once("/") .ok_or_else(|| UserError("Can't get partner ref".to_owned()))?; let other_player = ctx .trans .find_item_by_type_code(player_info.0, player_info.1) .await? .ok_or_else(|| UserError("Partner is gone!".to_owned()))?; return Ok(Some((my_convo, other_player))); } fn style_to_initial_topic(style: &ConversationalStyle) -> ConversationTopic { match style { ConversationalStyle::Amicable => ConversationTopic::FishingSpots, ConversationalStyle::Serious => ConversationTopic::RoamingEnemies, ConversationalStyle::Joking => ConversationTopic::ParodyKingsOffice, } } fn style_to_allowed_topics(style: &ConversationalStyle) -> Vec { match style { ConversationalStyle::Amicable => vec![ ConversationTopic::FishingSpots, ConversationTopic::ThoughtsOnSunTzu, ConversationTopic::ExploringRuins, ], ConversationalStyle::Joking => vec![ ConversationTopic::ParodyKingsOffice, ConversationTopic::PlayFight, ConversationTopic::ThoughtsOnMachiavelli, ], ConversationalStyle::Serious => vec![ ConversationTopic::RoamingEnemies, ConversationTopic::SurvivingWeather, ConversationTopic::GoodAmbushSpots, ], } } fn topic_to_allowed_style(topic: &ConversationTopic) -> ConversationalStyle { match topic { ConversationTopic::FishingSpots => ConversationalStyle::Amicable, ConversationTopic::ThoughtsOnSunTzu => ConversationalStyle::Amicable, ConversationTopic::ExploringRuins => ConversationalStyle::Amicable, ConversationTopic::ParodyKingsOffice => ConversationalStyle::Joking, ConversationTopic::PlayFight => ConversationalStyle::Joking, ConversationTopic::ThoughtsOnMachiavelli => ConversationalStyle::Joking, ConversationTopic::RoamingEnemies => ConversationalStyle::Serious, ConversationTopic::SurvivingWeather => ConversationalStyle::Serious, ConversationTopic::GoodAmbushSpots => ConversationalStyle::Serious, } } fn topic_to_interest_growth(style: &ConversationTopic) -> Vec<(ConversationalInterestType, i64)> { use ConversationTopic::*; use ConversationalInterestType::*; match style { FishingSpots => vec![ (Philosophy, 0), (LocalGeography, 2), (Threats, -1), (Tactics, -1), (Weather, 3), (Politics, -1), (Frivolity, 2), ], ThoughtsOnSunTzu => vec![ (Philosophy, 2), (LocalGeography, -1), (Threats, -1), (Tactics, 4), (Weather, -1), (Politics, 3), (Frivolity, 0), ], ExploringRuins => vec![ (Philosophy, 1), (LocalGeography, 4), (Threats, -1), (Tactics, -1), (Weather, 2), (Politics, -1), (Frivolity, 2), ], ParodyKingsOffice => vec![ (Philosophy, 1), (LocalGeography, -1), (Threats, -1), (Tactics, -1), (Weather, -1), (Politics, 4), (Frivolity, 3), ], PlayFight => vec![ (Philosophy, 1), (LocalGeography, -1), (Threats, 2), (Tactics, 4), (Weather, -1), (Politics, -1), (Frivolity, 2), ], ThoughtsOnMachiavelli => vec![ (Philosophy, 4), (LocalGeography, -1), (Threats, -1), (Tactics, 2), (Weather, -1), (Politics, 4), (Frivolity, -1), ], RoamingEnemies => vec![ (Philosophy, -1), (LocalGeography, 1), (Threats, 4), (Tactics, 1), (Weather, -1), (Politics, 0), (Frivolity, 0), ], SurvivingWeather => vec![ (Philosophy, -1), (LocalGeography, 3), (Threats, 2), (Tactics, -1), (Weather, 5), (Politics, -1), (Frivolity, -1), ], GoodAmbushSpots => vec![ (Philosophy, -1), (LocalGeography, 1), (Threats, 3), (Tactics, 3), (Weather, 2), (Politics, -1), (Frivolity, -1), ], } } pub async fn change_conversational_style( ctx: &VerbContext<'_>, player_item: &mut Item, style: ConversationalStyle, ) -> UResult<()> { if player_item .active_conversation .as_ref() .map(|ac| ac.style == style) .unwrap_or(false) { user_error(format!( "You're alreading talking in {} {} style!", indefinite_article(style.display()), style.display() ))?; } let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? { None => return Ok(()), Some(c) => c, }; my_convo.last_change = Utc::now(); my_convo.style = style.clone(); let topic = style_to_initial_topic(&style); my_convo.current_topic = topic.clone(); let mut other_player = (*other_player).clone(); other_player .active_conversation .as_mut() .map(|other_convo| { other_convo.style = style.clone(); other_convo.current_topic = topic.clone(); }); ctx.trans.save_item_model(&other_player).await?; let alt_topics: Vec = style_to_allowed_topics(&style) .into_iter() .filter(|t| t != &topic) .map(|t| { format!( ansi!("{} (type {})"), t.display_readable(), t.display_command() ) }) .collect(); let alt_topics_str: String = language::join_words_or(&alt_topics.iter().map(|t| t.as_str()).collect::>()); let msg = format!(ansi!( "{} changes the style of conversation with {} to be {}. The conversation drifts to {}, but you realise it could shift to {}\n"), player_item.display_for_sentence(1, true), other_player.display_for_sentence(1, false), style.display(), topic.display_readable(), &alt_topics_str ); broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?; Ok(()) } pub async fn change_conversation_topic( ctx: &VerbContext<'_>, player_item: &mut Item, topic: ConversationTopic, ) -> UResult<()> { if player_item .active_conversation .as_ref() .map(|ac| ac.current_topic == topic) .unwrap_or(false) { user_error("That's already the topic!".to_owned())?; } let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? { None => return Ok(()), Some(c) => c, }; let expected_style = topic_to_allowed_style(&topic); if my_convo.style != expected_style { user_error(format!( ansi!( "You need to switch to be talking in the {} style \ before you can switch to {}." ), expected_style.display(), topic.display_readable() ))?; } let mut other_player = (*other_player).clone(); my_convo.current_topic = topic.clone(); my_convo.last_change = Utc::now(); other_player.active_conversation.as_mut().map(|ac| { ac.current_topic = topic.clone(); }); ctx.trans.save_item_model(&other_player).await?; let msg = format!( "{} subtley changes the topic of conversation with {}, and it drifts to {}\n", player_item.display_for_sentence(1, true), other_player.display_for_sentence(1, false), topic.display_readable(), ); broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?; Ok(()) } pub async fn change_conversation_intensity( ctx: &VerbContext<'_>, player_item: &mut Item, intensity: ConversationIntensity, ) -> UResult<()> { if player_item .active_conversation .as_ref() .map(|ac| ac.current_intensity == intensity) .unwrap_or(false) { user_error(format!( "You're already talking {}", intensity.display_readable() ))?; } let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? { None => return Ok(()), Some(c) => c, }; my_convo.current_intensity = intensity.clone(); my_convo.last_change = Utc::now(); let mut other_player = (*other_player).clone(); other_player .active_conversation .as_mut() .map(|ac| ac.current_intensity = intensity.clone()); ctx.trans.save_item_model(&other_player).await?; let msg = format!( "You notice a change in the pace of the conversation started by {}, and soon picked up by {}. The conversation is now proceeding {}.\n", player_item.display_for_sentence(1, true), other_player.display_for_sentence(1, false), intensity.display_readable(), ); broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?; Ok(()) } #[cfg(test)] mod tests { use crate::models::item::ConversationTopic; use super::*; #[test] fn parse_conversation_topic_works() { let cases: Vec<(&str, Result, &str>)> = vec![ ( "parody kings office", Ok(Some(ConversationTopic::ParodyKingsOffice)), ), ( "parody your face", Err(ansi!("Try parody kings office")), ), ("play fight", Ok(Some(ConversationTopic::PlayFight))), ("play fgight", Err(ansi!("Try play fight"))), ( "thoughts on sun tzu", Ok(Some(ConversationTopic::ThoughtsOnSunTzu)), ), ( "thoughts on sun sets", Err(ansi!("Try thoughts on sun tzu")), ), ( "thoughts on machiavelli", Ok(Some(ConversationTopic::ThoughtsOnMachiavelli)), ), ( "thoughts non machiavelli", Err(ansi!( "Try thoughts on machiavelli or thoughts on sun tzu" )), ), ( "exploring ruins", Ok(Some(ConversationTopic::ExploringRuins)), ), ( "exploring roads", Err(ansi!("Try exploring ruins")), ), ( "roaming enemies ", Ok(Some(ConversationTopic::RoamingEnemies)), ), ( "roaming villains", Err(ansi!("Try roaming enemies")), ), ("fishing spots", Ok(Some(ConversationTopic::FishingSpots))), ( "fishing sports", Err(ansi!("Try fishing spots")), ), ( "good ambush spots", Ok(Some(ConversationTopic::GoodAmbushSpots)), ), ( "good ambush places", Err(ansi!("Try good ambush spots")), ), ("say hello world", Ok(None)), ]; for case in cases { assert_eq!(parse_conversation_topic(case.0), case.1); } } #[test] fn style_to_topic_to_style_roundtrips() { use ConversationalStyle::*; for style in vec![Serious, Amicable, Joking] { for topic in style_to_allowed_topics(&style) { assert_eq!(topic_to_allowed_style(&topic), style); } } } }