use super::{ get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user, user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext, }; use crate::{ language::{self, indefinite_article}, models::item::Item, regular_tasks::queued_command::{ queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, }, services::{ comms::broadcast_to_room, destroy_container, skills::{crit_fail_penalty_for_skill, skill_check_and_grind}, urges::change_stress_considering_cool, }, static_content::possession_type::{ improv_by_ingredient, improv_by_output, possession_data, possession_type_names, PossessionData, PossessionType, }, }; use ansi::ansi; use async_trait::async_trait; use rand::seq::IteratorRandom; use rand::seq::SliceRandom; use std::collections::BTreeSet; use std::sync::Arc; use std::time; pub struct WithQueueHandler; #[async_trait] impl QueueCommandHandler for WithQueueHandler { async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } let item_id = match ctx.command { QueueCommand::ImprovWith { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; if ctx.item.urges.as_ref().map(|u| u.stress.value).unwrap_or(0) > 7000 { user_error( ansi!( "You are too tired and stressed to consider crafts. Maybe try to \ sit or recline for a bit!" ) .to_owned(), )?; } let item = match ctx .trans .find_item_by_type_code("possession", &item_id) .await? { None => user_error("Item not found".to_owned())?, Some(it) => it, }; if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have it.".to_owned())?; } broadcast_to_room( &ctx.trans, &ctx.item.location, None, &format!( "{} tries to work out what {} can make from {}.\n", &ctx.item.display_for_sentence(1, true), &ctx.item.pronouns.subject, &item.display_for_sentence(1, false), ), ) .await?; Ok(time::Duration::from_secs(1)) } #[allow(unreachable_patterns)] async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } let item_id = match ctx.command { QueueCommand::ImprovWith { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; let item = match ctx .trans .find_item_by_type_code("possession", &item_id) .await? { None => user_error("Item not found".to_owned())?, Some(it) => it, }; if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have it.".to_owned())?; } let session = if ctx.item.item_type == "player" { ctx.trans .find_session_for_player(&ctx.item.item_code) .await? } else { None }; let opts: Vec<&'static PossessionData> = improv_by_ingredient() .get( item.possession_type .as_ref() .ok_or_else(|| UserError("You can't improvise with that!".to_owned()))?, ) .ok_or_else(|| { UserError(format!( "You can't think of anything you could make with {}", item.display_for_sentence(1, false) )) })? .iter() .filter_map(|it| possession_data().get(&it.output).map(|v| *v)) .collect(); let result_data = opts .as_slice() .choose(&mut rand::thread_rng()) .ok_or_else(|| { UserError(format!( "You can't think of anything you could make with {}", item.display_for_sentence(1, false) )) })?; if let Some((sess, _)) = session { ctx.trans .queue_for_session( &sess, Some(&format!( "You think you could make {} {} from {}\n", indefinite_article(result_data.display), result_data.display, item.display_for_sentence(1, false) )), ) .await?; } Ok(()) } } pub struct FromQueueHandler; #[async_trait] impl QueueCommandHandler for FromQueueHandler { async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } let (already_used, item_ids) = match ctx.command { QueueCommand::ImprovFrom { possession_ids, already_used, .. } => (already_used, possession_ids), _ => user_error("Unexpected command".to_owned())?, }; if !already_used.is_empty() { return Ok(time::Duration::from_secs(1)); } for item_id in item_ids { let item = match ctx .trans .find_item_by_type_code("possession", &item_id) .await? { None => user_error("Item not found".to_owned())?, Some(it) => it, }; if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have the things you'd planned to use." .to_owned())?; } } Ok(time::Duration::from_secs(1)) } #[allow(unreachable_patterns)] async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } let (output, possession_ids, already_used) = match ctx.command { QueueCommand::ImprovFrom { output, possession_ids, already_used, } => (output, possession_ids, already_used), _ => user_error("Unexpected command".to_owned())?, }; let craft_data = improv_by_output().get(&output).ok_or_else(|| { UserError("You don't think it is possible to improvise that.".to_owned()) })?; let mut ingredients_left: Vec = craft_data.inputs.clone(); let mut to_destroy_if_success: Vec> = Vec::new(); for item_id in already_used { let item = ctx .trans .find_item_by_type_code("possession", &item_id) .await? .ok_or_else(|| UserError("Item used in crafting not found.".to_owned()))?; to_destroy_if_success.push(item.clone()); let possession_type = item .possession_type .as_ref() .ok_or_else(|| UserError("Item used in crafting not a possession.".to_owned()))?; if let Some(match_pos) = ingredients_left.iter().position(|pt| pt == possession_type) { ingredients_left.remove(match_pos); } } let mut possession_id_iter = possession_ids.iter(); let session = if ctx.item.item_type == "player" { ctx.trans .find_session_for_player(&ctx.item.item_code) .await? } else { None }; match possession_id_iter.next() { None => { let choice = ingredients_left .iter() .choose(&mut rand::thread_rng()) .clone(); match choice { // Nothing left to add, and nothing needed - success! None => { for item in to_destroy_if_success { destroy_container(&ctx.trans, &item).await?; } let mut new_item: Item = craft_data.output.clone().into(); new_item.item_code = ctx.trans.alloc_item_code().await?.to_string(); new_item.location = ctx.item.refstr(); ctx.trans.create_item(&new_item).await?; broadcast_to_room( &ctx.trans, &ctx.item.location, None, &format!( "{} proudly holds up the {} {} just made.\n", &ctx.item.display_for_sentence(1, true), &new_item.display_for_sentence(1, false), &ctx.item.pronouns.subject ), ) .await?; } // Nothing left to add, but recipe incomplete. Some(missing_type) => { let possession_data = possession_data().get(missing_type).ok_or_else(|| { UserError( "It looks like it's no longer possible to improvise that." .to_owned(), ) })?; user_error(format!( "You realise you'll also need {} {} to craft that.", language::indefinite_article(possession_data.display), possession_data.display ))?; } } } Some(possession_id) => { let item = ctx .trans .find_item_by_type_code("possession", &possession_id) .await? .ok_or_else(|| { UserError( "An item you planned to use for crafting seems to be gone.".to_owned(), ) })?; if !ingredients_left.contains( item.possession_type .as_ref() .ok_or_else(|| UserError("Uncraftable item used.".to_owned()))?, ) { user_error(format!( "You try adding {}, but it doesn't really seem to fit right.", &item.display_for_sentence(1, false) ))?; } let skill_result = skill_check_and_grind( &ctx.trans, ctx.item, &craft_data.skill, craft_data.difficulty, ) .await?; if skill_result <= -0.5 { change_stress_considering_cool(&ctx.trans, &mut ctx.item, 1000).await?; crit_fail_penalty_for_skill(&ctx.trans, ctx.item, &craft_data.skill).await?; ctx.trans .delete_item(&item.item_type, &item.item_code) .await?; if let Some((sess, _)) = session { ctx.trans .queue_for_session( &sess, Some(&format!( "You try adding {}, but it goes badly and you waste it.\n", &item.display_for_sentence(1, false) )), ) .await?; } } else if skill_result <= 0.0 { change_stress_considering_cool(&ctx.trans, &mut ctx.item, 500).await?; if let Some((sess, _)) = session { ctx.trans .queue_for_session( &sess, Some(&format!( "You try and fail at adding {}.\n", &item.display_for_sentence(1, false) )), ) .await?; } } else { if let Some((sess, _)) = session { ctx.trans .queue_for_session( &sess, Some(&format!( "You try adding {}.\n", &item.display_for_sentence(1, false), )), ) .await?; } let mut new_possession_ids = possession_ids.clone(); new_possession_ids.remove(possession_id); let mut new_already_used = already_used.clone(); new_already_used.insert(possession_id.clone()); ctx.item.queue.push_front(QueueCommand::ImprovFrom { output: output.clone(), possession_ids: new_possession_ids, already_used: new_already_used, }); } } } Ok(()) } } async fn improv_query( ctx: &mut VerbContext<'_>, player_item: &Item, with_what: &str, ) -> UResult<()> { let item = search_item_for_user( ctx, &ItemSearchParams { include_contents: true, ..ItemSearchParams::base(player_item, with_what) }, ) .await?; if item.item_type != "possession" { user_error("You can't improvise with that!".to_owned())? } queue_command_and_save( ctx, player_item, &QueueCommand::ImprovWith { possession_id: item.item_code.clone(), }, ) .await } pub struct Verb; #[async_trait] impl UserVerb for Verb { async fn handle( self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str, ) -> UResult<()> { let rtrim = remaining.trim(); let player_item = get_player_item_or_fail(ctx).await?; if player_item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } if rtrim.starts_with("with ") { return improv_query(ctx, &player_item, rtrim["with ".len()..].trim_start()).await; } let (output, inputs_str) = rtrim.split_once(" from ").ok_or_else( || UserError(ansi!("Try improvise with item or improvise item from item, item, ...").to_owned()))?; let output_type: PossessionType = match possession_type_names() .get(&output.trim().to_lowercase()) .map(|x| x.as_slice()) .unwrap_or_else(|| &[]) { [] => user_error("I don't recognise the thing you want to make.".to_owned())?, [t] => t.clone(), _ => user_error( "You'll have to be more specific about what you want to make.".to_owned(), )?, }; let inputs = inputs_str.split(",").map(|v| v.trim()); let mut input_ids: BTreeSet = BTreeSet::new(); for mut input in inputs { let mut use_limit = Some(1); if input == "all" || input.starts_with("all ") { input = input[3..].trim(); use_limit = None; } else if let (Some(n), remaining2) = parse_count(input) { use_limit = Some(n); input = remaining2; } let items = search_items_for_user( ctx, &ItemSearchParams { include_contents: true, limit: use_limit.unwrap_or(100), ..ItemSearchParams::base(&player_item, input) }, ) .await?; for item in items { if item.item_type != "possession" { user_error(format!( "You can't improvise with {}!", &item.display_for_session(&ctx.session_dat) ))? } input_ids.insert(item.item_code.to_owned()); } } queue_command_and_save( ctx, &player_item, &QueueCommand::ImprovFrom { output: output_type, possession_ids: input_ids, already_used: BTreeSet::new(), }, ) .await } } static VERB_INT: Verb = Verb; pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;