1259 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			1259 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
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<Option<time::Duration>> {
 | 
						|
        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::<f32>() < 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!("<magenta>{}<reset>\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<ConversationalInterestType> =
 | 
						|
            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!("<bold>{}<reset>"), 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!("<bold>{}<reset>"), 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!("<bold>{}<reset>"), 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 <bold>share status<reset> to check interest levels, and <bold>help share<reset> \
 | 
						|
                 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<ConversationalInterestType> {
 | 
						|
    // 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{}<bold>[<reset><magenta><bgblack>{}<reset><bold>]<reset>\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: <bold>{}<reset>\n"),
 | 
						|
                          conversation.style.display(),
 | 
						|
                          conversation.style.transitions().iter().map(|v| v.display()).join(" "));
 | 
						|
    let alt_topics: Vec<String> = style_to_allowed_topics(&conversation.style)
 | 
						|
        .into_iter()
 | 
						|
        .filter(|t| t != &conversation.current_topic)
 | 
						|
        .map(|t| {
 | 
						|
            format!(
 | 
						|
                ansi!("{} (type <bold>{}<reset>)"),
 | 
						|
                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 \
 | 
						|
                                             <bold>allow share from {}<reset> 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 <bold>stop<reset> 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!("<magenta>{} proposes to share knowledge with {}, and {} accepts!<reset>\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!("<magenta>{} {} {}.<reset>\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<Option<ConversationTopic>, &'static str> {
 | 
						|
    let r = alt((
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("parody"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>parody kings office<reset>"),
 | 
						|
                    tuple((tag("kings"), space1, tag("office"))),
 | 
						|
                )),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::ParodyKingsOffice),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("play"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(ansi!("Try <bold>play fight<reset>"), tag("fight"))),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::PlayFight),
 | 
						|
        ),
 | 
						|
        preceded(
 | 
						|
            tuple((tag("thoughts"), space1::<&str, VerboseError<&str>>)),
 | 
						|
            cut(context(
 | 
						|
                ansi!(
 | 
						|
                    "Try <bold>thoughts on machiavelli<reset> or <bold>thoughts on sun tzu<reset>"
 | 
						|
                ),
 | 
						|
                preceded(
 | 
						|
                    tuple((tag("on"), space1)),
 | 
						|
                    alt((
 | 
						|
                        map(
 | 
						|
                            preceded(
 | 
						|
                                tuple((tag("sun"), space1)),
 | 
						|
                                cut(context(
 | 
						|
                                    ansi!("Try <bold>thoughts on sun tzu<reset>"),
 | 
						|
                                    tag("tzu"),
 | 
						|
                                )),
 | 
						|
                            ),
 | 
						|
                            |_| Some(ConversationTopic::ThoughtsOnSunTzu),
 | 
						|
                        ),
 | 
						|
                        map(tag("machiavelli"), |_| {
 | 
						|
                            Some(ConversationTopic::ThoughtsOnMachiavelli)
 | 
						|
                        }),
 | 
						|
                    )),
 | 
						|
                ),
 | 
						|
            )),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("exploring"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>exploring ruins<reset>"),
 | 
						|
                    tag("ruins"),
 | 
						|
                )),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::ExploringRuins),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("roaming"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>roaming enemies<reset>"),
 | 
						|
                    tag("enemies"),
 | 
						|
                )),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::RoamingEnemies),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("fishing"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>fishing spots<reset>"),
 | 
						|
                    tag("spots"),
 | 
						|
                )),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::FishingSpots),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("good"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>good ambush spots<reset>"),
 | 
						|
                    tuple((tag("ambush"), space1, tag("spots"))),
 | 
						|
                )),
 | 
						|
            )),
 | 
						|
            |_| Some(ConversationTopic::GoodAmbushSpots),
 | 
						|
        ),
 | 
						|
        map(
 | 
						|
            tuple((
 | 
						|
                tag("surviving"),
 | 
						|
                space1::<&str, VerboseError<&str>>,
 | 
						|
                cut(context(
 | 
						|
                    ansi!("Try <bold>surviving weather<reset>"),
 | 
						|
                    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<Option<(&'l mut ActiveConversation, Arc<Item>)>> {
 | 
						|
    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!("<red>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!<reset>\n"))).await?;
 | 
						|
        return Ok(None);
 | 
						|
    } else if result < 0.0 {
 | 
						|
        ctx.trans
 | 
						|
            .queue_for_session(
 | 
						|
                &ctx.session,
 | 
						|
                Some(ansi!(
 | 
						|
                    "<magenta>Your attempt to change the conversation doesn't pan out.<reset>\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<ConversationTopic> {
 | 
						|
    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<String> = style_to_allowed_topics(&style)
 | 
						|
        .into_iter()
 | 
						|
        .filter(|t| t != &topic)
 | 
						|
        .map(|t| {
 | 
						|
            format!(
 | 
						|
                ansi!("{} (type <bold>{}<reset>)"),
 | 
						|
                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::<Vec<&str>>());
 | 
						|
 | 
						|
    let msg = format!(ansi!(
 | 
						|
        "{} changes the style of conversation with {} to be {}. The conversation drifts to <bold>{}<reset>, 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 <bold>{}<reset> 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<Option<ConversationTopic>, &str>)> = vec![
 | 
						|
            (
 | 
						|
                "parody  kings   office",
 | 
						|
                Ok(Some(ConversationTopic::ParodyKingsOffice)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "parody your face",
 | 
						|
                Err(ansi!("Try <bold>parody kings office<reset>")),
 | 
						|
            ),
 | 
						|
            ("play fight", Ok(Some(ConversationTopic::PlayFight))),
 | 
						|
            ("play fgight", Err(ansi!("Try <bold>play fight<reset>"))),
 | 
						|
            (
 | 
						|
                "thoughts on sun 	tzu",
 | 
						|
                Ok(Some(ConversationTopic::ThoughtsOnSunTzu)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "thoughts on sun sets",
 | 
						|
                Err(ansi!("Try <bold>thoughts on sun tzu<reset>")),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "thoughts  on machiavelli",
 | 
						|
                Ok(Some(ConversationTopic::ThoughtsOnMachiavelli)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "thoughts non machiavelli",
 | 
						|
                Err(ansi!(
 | 
						|
                    "Try <bold>thoughts on machiavelli<reset> or <bold>thoughts on sun tzu<reset>"
 | 
						|
                )),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "exploring ruins",
 | 
						|
                Ok(Some(ConversationTopic::ExploringRuins)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "exploring roads",
 | 
						|
                Err(ansi!("Try <bold>exploring ruins<reset>")),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "roaming enemies ",
 | 
						|
                Ok(Some(ConversationTopic::RoamingEnemies)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "roaming villains",
 | 
						|
                Err(ansi!("Try <bold>roaming enemies<reset>")),
 | 
						|
            ),
 | 
						|
            ("fishing spots", Ok(Some(ConversationTopic::FishingSpots))),
 | 
						|
            (
 | 
						|
                "fishing sports",
 | 
						|
                Err(ansi!("Try <bold>fishing spots<reset>")),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "good  ambush spots",
 | 
						|
                Ok(Some(ConversationTopic::GoodAmbushSpots)),
 | 
						|
            ),
 | 
						|
            (
 | 
						|
                "good ambush places",
 | 
						|
                Err(ansi!("Try <bold>good ambush spots<reset>")),
 | 
						|
            ),
 | 
						|
            ("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);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |