use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail, search_item_for_user}; use async_trait::async_trait; use ansi::{ansi, flow_around, word_wrap}; use crate::{ db::ItemSearchParams, models::{item::{ Item, LocationActionType, Subattack, ItemFlag, ItemSpecialData }}, static_content::{ room::{self, Direction, GridCoords}, dynzone::self, possession_type::possession_data, }, language, services::combat::max_health, }; use itertools::Itertools; use std::sync::Arc; use mockall_double::double; #[double] use crate::db::DBTrans; pub fn render_map(room: &room::Room, width: usize, height: usize) -> String { let mut buf = String::new(); let my_loc = &room.grid_coords; let min_x = my_loc.x - (width as i64) / 2; let max_x = min_x + (width as i64); let min_y = my_loc.y - (height as i64) / 2; let max_y = min_y + (height as i64); for y in min_y..max_y { for x in min_x..max_x { if my_loc.x == x && my_loc.y == y { buf.push_str(ansi!("()")) } else { buf.push_str(room::room_map_by_zloc() .get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z })) .map(|r| if room.zone == r.zone { r.short } else { r.secondary_zones.iter() .find(|sz| sz.zone == room.zone) .map(|sz| sz.short) .expect("Secondary zone missing") }) .unwrap_or(" ")); } } buf.push('\n'); } buf } pub fn render_map_dyn(dynzone: &dynzone::Dynzone, dynroom: &dynzone::Dynroom, width: usize, height: usize) -> String { let mut buf = String::new(); let my_loc = &dynroom.grid_coords; let min_x = my_loc.x - (width as i64) / 2; let max_x = min_x + (width as i64); let min_y = my_loc.y - (height as i64) / 2; let max_y = min_y + (height as i64); let main_exit: Option = dynzone.dyn_rooms .iter() .flat_map(|(_, dr)| dr.exits.iter() .filter(|ex| match ex.target { dynzone::ExitTarget::ExitZone => true, _ => false }) .map(|ex| dr.grid_coords.apply(&ex.direction)) ).next(); for y in min_y..max_y { for x in min_x..max_x { if my_loc.x == x && my_loc.y == y { buf.push_str(ansi!("()")) } else { buf.push_str(dynzone.dyn_rooms.iter() .find( |(_, dr)| dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z) .map(|(_, r)| r.short) .or_else(|| main_exit.as_ref().and_then( |ex_pos| if ex_pos.x == x && ex_pos.y == y && ex_pos.z == my_loc.z { Some("<<") } else { None })) .unwrap_or(" ")); } } buf.push('\n'); } buf } pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> { let mut contents_desc = String::new(); let mut items = ctx.trans.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code)).await?; items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display)); let all_groups: Vec>> = items .iter() .group_by(|i| (i.display_for_sentence(true, 1, false), &i.action_type)) .into_iter() .map(|(_, g)|g.collect::>>()) .collect::>>>(); if all_groups.len() > 0 { contents_desc.push_str(&(language::caps_first(&item.pronouns.subject))); if item.item_type == "player" || item.item_type == "npc" { contents_desc.push_str("'s carrying "); } else { contents_desc.push_str(" contains "); } let mut phrases = Vec::::new(); for group_items in all_groups { let head = &group_items[0]; let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode, group_items.len(), false); if head.action_type == LocationActionType::Wielded { details.push_str(" (wielded)"); } phrases.push(details); } let phrases_str: Vec<&str> = phrases.iter().map(|p| p.as_str()).collect(); contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n")); } let health_max = max_health(&item); if health_max > 0 { let health_ratio = (item.health as f64) / (health_max as f64); if item.item_type == "player" || item.item_type == "npc" { if health_ratio == 1.0 { contents_desc.push_str(&format!("{} is in perfect health.\n", &language::caps_first(&item.pronouns.subject))); } else if health_ratio >= 0.75 { contents_desc.push_str(&format!("{} has some minor cuts and bruises.\n", &language::caps_first(&item.pronouns.subject))); } else if health_ratio >= 0.5 { contents_desc.push_str(&format!("{} has deep wounds all over {} body.\n", &language::caps_first(&item.pronouns.subject), &item.pronouns.possessive)); } else if health_ratio >= 0.25 { contents_desc.push_str(&format!("{} looks seriously injured.\n", &language::caps_first( &item.pronouns.subject))); } else { contents_desc.push_str(&format!("{} looks like {}'s on death's door.\n", &language::caps_first( &item.pronouns.subject), &item.pronouns.possessive)); } if ctx.trans.check_task_by_type_code("DelayedHealth", &format!("{}/{}/bandage", &item.item_type, &item.item_code) ).await? { contents_desc.push_str(&format!("{} is wrapped up in bandages.\n", &language::caps_first(&item.pronouns.subject)) ); } } else if item.item_type == "possession" { if health_ratio == 1.0 { contents_desc.push_str(&format!("{}'s in perfect condition.\n", &language::caps_first(&item.pronouns.subject))); } else if health_ratio >= 0.75 { contents_desc.push_str(&format!("{}'s slightly beaten up.\n", &language::caps_first(&item.pronouns.subject))); } else if health_ratio >= 0.5 { contents_desc.push_str(&format!("{}'s pretty beaten up.\n", &language::caps_first(&item.pronouns.subject))); } else if health_ratio >= 0.25 { contents_desc.push_str(&format!("{}'s seriously damaged.\n", &language::caps_first(&item.pronouns.subject))); } else { contents_desc.push_str(&format!("{}'s nearly completely destroyed.\n", &language::caps_first(&item.pronouns.subject))); } } } if item.item_type == "possession" { if let Some(charge_data) = item.possession_type.as_ref() .and_then(|pt| possession_data().get(&pt)) .and_then(|pd| pd.charge_data.as_ref()) { let unit = if item.charges == 1 { charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix } else { language::pluralise(charge_data.charge_name_prefix) + " " + charge_data.charge_name_suffix }; contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit)); } } ctx.trans.queue_for_session( ctx.session, Some(&format!("{}\n{}\n{}", &item.display_for_session(&ctx.session_dat), item.details_for_session(&ctx.session_dat).unwrap_or(""), contents_desc, )) ).await?; Ok(()) } fn exits_for(room: &room::Room) -> String { let exit_text: Vec = room.exits.iter().map(|ex| format!(ansi!("{}"), ex.direction.describe())).collect(); format!(ansi!("[ Exits: {} ]"), exit_text.join(" ")) } fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String { let exit_text: Vec = dynroom.exits.iter().map(|ex| format!(ansi!("{}"), ex.direction.describe())).collect(); format!(ansi!("[ Exits: {} ]"), exit_text.join(" ")) } pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item, room: &room::Room, contents: &str) -> UResult<()> { let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time"); ctx.trans.queue_for_session( ctx.session, Some(&flow_around(&render_map(room, 5, 5), 10, ansi!(" "), &word_wrap(&format!(ansi!("{} ({})\n{}.{}\n{}\n"), item.display_for_session(&ctx.session_dat), zone, item.details_for_session( &ctx.session_dat).unwrap_or(""), contents, exits_for(room)), |row| if row >= 5 { 80 } else { 68 }), 68)) ).await?; Ok(()) } pub async fn describe_dynroom(ctx: &VerbContext<'_>, item: &Item, dynzone: &dynzone::Dynzone, dynroom: &dynzone::Dynroom, contents: &str) -> UResult<()> { ctx.trans.queue_for_session( ctx.session, Some(&flow_around(&render_map_dyn(dynzone, dynroom, 5, 5), 10, ansi!(" "), &word_wrap(&format!(ansi!("{} ({})\n{}.{}\n{}\n"), item.display_for_session(&ctx.session_dat), dynzone.zonename, item.details_for_session( &ctx.session_dat).unwrap_or(""), contents, exits_for_dyn(dynroom)), |row| if row >= 5 { 80 } else { 68 }), 68)) ).await?; Ok(()) } async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult { if item.flags.contains(&ItemFlag::NoSeeContents) { return Ok(" It is too foggy to see who or what else is here.".to_owned()); } let mut buf = String::new(); let mut items = ctx.trans.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code)).await?; items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display)); let all_groups: Vec>> = items .iter() .group_by(|i| i.display_for_sentence(true, 1, false)) .into_iter() .map(|(_, g)|g.collect::>>()) .collect::>>>(); for group_items in all_groups { let head = &group_items[0]; let is_creature = head.item_type == "player" || head.item_type.starts_with("npc"); buf.push(' '); buf.push_str(&head.display_for_sentence(!ctx.session_dat.less_explicit_mode, group_items.len(), true)); buf.push_str(if group_items.len() > 1 { " are " } else { " is "}); match head.action_type { LocationActionType::Sitting => buf.push_str("sitting "), LocationActionType::Reclining => buf.push_str("reclining "), LocationActionType::Normal | LocationActionType::Attacking(_) if is_creature => { if head.is_dead { buf.push_str("lying "); } else { buf.push_str("standing "); } } _ => {} } buf.push_str("here"); if let LocationActionType::Attacking(subattack) = &head.action_type { match subattack { Subattack::Powerattacking => buf.push_str(", powerattacking "), Subattack::Feinting => buf.push_str(", feinting "), Subattack::Grabbing => buf.push_str(", grabbing "), Subattack::Wrestling => buf.push_str(", wrestling "), _ => buf.push_str(", attacking ") } match &head.active_combat.as_ref() .and_then(|ac| ac.attacking.clone()) .or_else(|| head.presence_target.clone()) { None => buf.push_str("someone"), Some(who) => match who.split_once("/") { None => buf.push_str("someone"), Some((ttype, tcode)) => match ctx.trans.find_item_by_type_code(ttype, tcode).await? { None => buf.push_str("someone"), Some(it) => buf.push_str( &it.display_for_session(&ctx.session_dat) ) } } } } buf.push('.'); } Ok(buf) } async fn direction_to_item( trans: &DBTrans, use_location: &str, direction: &Direction, ) -> UResult>> { // Firstly check dynamic exits, since they apply to rooms and dynrooms... if let Some(dynroom_result) = trans.find_exact_dyn_exit(use_location, direction).await? { return Ok(Some(Arc::new(dynroom_result))); } let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); if heretype == "dynroom" { let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? { None => user_error("Your current room has vanished!".to_owned())?, Some(v) => v }; let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() { Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code), _ => user_error("Your current room is invalid!".to_owned())? }; let dynzone = dynzone::dynzone_by_type() .get(&dynzone::DynzoneType::from_str(dynzone_code) .ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?) .ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?; let dynroom = dynzone.dyn_rooms.get(dynroom_code.as_str()) .ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?; let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction) .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; return match exit.target { dynzone::ExitTarget::ExitZone => { let (zonetype, zonecode) = old_dynroom_item.location.split_once("/") .ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?; let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await? .ok_or_else(|| UserError("Can't find your zone".to_owned()))?; let zone_exit = match zoneitem.special_data.as_ref() { Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) => user_error("That exit doesn't seem to go anywhere".to_owned())?, Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit, _ => user_error("The zone you are in has invalid data associated with it".to_owned())?, }; let (zone_exit_type, zone_exit_code) = zone_exit.split_once("/").ok_or_else( || UserError("Oops, that way out seems to be broken.".to_owned()))?; Ok(trans.find_item_by_type_code(zone_exit_type, zone_exit_code).await?) }, dynzone::ExitTarget::Intrazone { subcode } => { let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await? .ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?; Ok(Some(Arc::new(to_item))) } } } if heretype != "room" { user_error("Navigating outside rooms not yet supported.".to_owned())? } let room = room::room_map_by_code().get(herecode) .ok_or_else(|| UserError("Can't find your current location".to_owned()))?; let exit = room.exits.iter().find(|ex| ex.direction == *direction) .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; let new_room = room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?; Ok(trans.find_item_by_type_code("room", new_room.code).await?) } pub struct Verb; #[async_trait] impl UserVerb for Verb { async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { let player_item = get_player_item_or_fail(ctx).await?; let rem_trim = remaining.trim().to_lowercase(); let use_location = if player_item.is_dead { "room/repro_xv_respawn" } else { &player_item.location }; let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let item: Arc = if rem_trim == "" { ctx.trans.find_item_by_type_code(heretype, herecode).await? .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))? } else if let Some(dir) = Direction::parse(&rem_trim) { direction_to_item(&ctx.trans, use_location, &dir).await? .ok_or_else(|| UserError("There's nothing in that direction".to_owned()))? } else if rem_trim == "me" || rem_trim == "self" { player_item.clone() } else { search_item_for_user( &ctx, &ItemSearchParams { include_contents: true, include_loc_contents: true, limit: 1, ..ItemSearchParams::base(&player_item, &rem_trim) } ).await? }; if item.item_type == "room" { let room = room::room_map_by_code().get(item.item_code.as_str()) .ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?; describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?; } else if item.item_type == "dynroom" { let (dynzone, dynroom) = match &item.special_data { Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => { dynzone::DynzoneType::from_str(dynzone_code.as_str()) .and_then(|dz_t| dynzone::dynzone_by_type().get(&dz_t)) .and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr))) .ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))? }, _ => user_error("Expected dynroom to have DynroomData".to_owned())? }; describe_dynroom(ctx, &item, &dynzone, &dynroom, &list_room_contents(ctx, &item).await?).await?; } else { describe_normal_item(ctx, &item).await?; } Ok(()) } } static VERB_INT: Verb = Verb; pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;