diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index d6931fa3..80ac20cd 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -12,7 +12,10 @@ use crate::message_handler::user_commands::parsing::parse_offset; use crate::models::{ session::Session, user::User, - item::Item, + item::{ + Item, + LocationActionType, + }, task::{Task, TaskParse} }; use tokio_postgres::types::ToSql; @@ -630,10 +633,45 @@ impl DBTrans { pub async fn get_location_stats(&self, location: &str) -> DResult { Ok(serde_json::from_value(self.pg_trans()?.query_one( - "SELECT COUNT(*) AS total_count, SUM(details->>'weight') AS total_weight \ - FROM items WHERE location = $1", &[&location] + "SELECT JSON_BUILD_OBJECT('total_count', COUNT(*), 'total_weight', COALESCE(SUM(CAST(details->>'weight' AS NUMERIC)), 0)) \ + FROM items WHERE details->>'location' = $1", &[&location] ).await?.get(0))?) } + + pub async fn set_exclusive_action_type_to(&self, item: &Item, + new_action_type: &LocationActionType, + other_item_action_type: &LocationActionType) -> DResult<()> { + self.pg_trans()?.execute("UPDATE items SET details=\ + JSONB_SET(details, '{action_type}', $1) \ + WHERE details->>'location' = $2 AND \ + details->>'action_type' = $3", + &[&serde_json::to_value(other_item_action_type)?, + &item.location, + &serde_json::to_value(new_action_type)? + .as_str().unwrap() + ]).await?; + self.pg_trans()?.execute("UPDATE items SET details=\ + JSONB_SET(details, '{action_type}', $1) \ + WHERE details->>'item_type' = $2 AND \ + details->>'item_code' = $3", + &[&serde_json::to_value(new_action_type)?, + &item.item_type, + &item.item_code + ]).await?; + Ok(()) + } + + pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult>> { + if let Some(item) = self.pg_trans()?.query_opt( + "SELECT details FROM items WHERE \ + details->>'location' = $1 AND \ + details->>'action_type' = $2", + &[&location, + &serde_json::to_value(action_type)?.as_str().unwrap()]).await? { + return Ok(Some(Arc::new(serde_json::from_value::(item.get("details"))?))); + } + Ok(None) + } pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 389d03e7..f36892a0 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -28,6 +28,7 @@ mod register; pub mod say; mod whisper; mod who; +pub mod wield; pub struct VerbContext<'l> { pub session: &'l ListenerSession, @@ -125,6 +126,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "whisper" => whisper::VERB, "tell" => whisper::VERB, + "wield" => wield::VERB, "who" => who::VERB, }; diff --git a/blastmud_game/src/message_handler/user_commands/wield.rs b/blastmud_game/src/message_handler/user_commands/wield.rs new file mode 100644 index 00000000..7e3f5552 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/wield.rs @@ -0,0 +1,140 @@ +use super::{ + VerbContext, + UserVerb, + UserVerbRef, + UResult, + ItemSearchParams, + user_error, + get_player_item_or_fail, + search_item_for_user, +}; +use crate::{ + static_content::possession_type::possession_data, + regular_tasks::queued_command::{ + QueueCommandHandler, + QueueCommand, + queue_command + }, + models::item::{ + LocationActionType, + SkillType, + }, + services::{ + broadcast_to_room, + skills::skill_check_and_grind, + }, +}; +use async_trait::async_trait; +use std::time; + +pub struct QueueHandler; +#[async_trait] +impl QueueCommandHandler for QueueHandler { + async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) + -> UResult { + let player_item = get_player_item_or_fail(ctx).await?; + if player_item.is_dead { + user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Wield { 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 != format!("player/{}", player_item.item_code) { + user_error("You try to wield it but realise you no longer have it".to_owned())? + } + let msg_exp = format!("{} fumbles around with {} {}\n", + &player_item.display_for_sentence(true, 1, true), + &player_item.pronouns.possessive, + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} fumbles around with {} {}\n", + &player_item.display_for_sentence(false, 1, true), + &player_item.pronouns.possessive, + &item.display_for_sentence(false, 1, false)); + broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; + let mut draw_level: f64 = *player_item.total_skills.get(&SkillType::Quickdraw).to_owned().unwrap_or(&8.0); + let mut player_item_mut = (*player_item).clone(); + + let skill_result = + skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?; + if skill_result < -0.5 { + draw_level -= 2.0; + } else if skill_result < -0.25 { + draw_level -= 1.0; + } else if skill_result > 0.5 { + draw_level += 2.0; + } else if skill_result > 0.25 { + draw_level += 1.0; + } + ctx.trans.save_item_model(&player_item_mut).await?; + + let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0); + Ok(time::Duration::from_millis((wait_ticks * 500.0).round() as u64)) + } + + #[allow(unreachable_patterns)] + async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) + -> UResult<()> { + let player_item = get_player_item_or_fail(ctx).await?; + if player_item.is_dead { + user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Wield { 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 != format!("player/{}", player_item.item_code) { + user_error("You try to wield it but realise you no longer have it".to_owned())? + } + let msg_exp = format!("{} wields {} {}\n", + &player_item.display_for_sentence(true, 1, true), + &player_item.pronouns.possessive, + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} wields {} {}\n", + &player_item.display_for_sentence(false, 1, true), + &player_item.pronouns.possessive, + &item.display_for_sentence(false, 1, false)); + broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; + ctx.trans.set_exclusive_action_type_to(&item, + &LocationActionType::Wielded, + &LocationActionType::Normal).await?; + Ok(()) + } +} + +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 weapon = search_item_for_user(ctx, &ItemSearchParams { + include_contents: true, + ..ItemSearchParams::base(&player_item, &remaining) + }).await?; + if player_item.is_dead { + user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; + } + if weapon.action_type == LocationActionType::Wielded { + user_error("You're actually already wielding it.".to_owned())?; + } + if weapon.item_type != "possession" || + weapon.possession_type.as_ref() + .and_then(|poss_type| possession_data().get(&poss_type)) + .and_then(|poss_data| poss_data.weapon_data.as_ref()) + .is_none() { + user_error("You can't wield that!".to_owned())?; + } + queue_command(ctx, &QueueCommand::Wield { possession_id: weapon.item_code.clone() }).await?; + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index 4324b6f0..39f5a3b2 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -15,6 +15,7 @@ use crate::message_handler::user_commands::{ CommandHandlingError, UResult, movement, + wield, user_error, get_user_or_fail }; @@ -24,12 +25,14 @@ use once_cell::sync::OnceCell; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum QueueCommand { Movement { direction: Direction }, + Wield { possession_id: String } } impl QueueCommand { pub fn name(&self) -> &'static str { use QueueCommand::*; match self { - Movement {..} => "Movement" + Movement {..} => "Movement", + Wield {..} => "Wield", } } } @@ -44,7 +47,8 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que static REGISTRY: OnceCell> = OnceCell::new(); REGISTRY.get_or_init(|| vec!( - ("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)) + ("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), + ("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)) ).into_iter().collect()) } diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 5968adc0..b015ee33 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -291,7 +291,16 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> Ok(()) } -async fn what_wielded(_trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { +async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { + if let Some(item) = trans.find_by_action_and_location( + &format!("{}/{}", &who.item_type, &who.item_code), &LocationActionType::Wielded).await? { + if let Some(dat) = item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.weapon_data.as_ref()) { + return Ok(dat); + } + } + // TODO: Search inventory for wielded item first. if who.item_type == "npc" { if let Some(intrinsic) = npc_by_code().get(who.item_code.as_str()) diff --git a/blastmud_game/src/services/skills.rs b/blastmud_game/src/services/skills.rs index 185ace2a..e4fba67b 100644 --- a/blastmud_game/src/services/skills.rs +++ b/blastmud_game/src/services/skills.rs @@ -46,105 +46,105 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User target_item.total_skills.entry(SkillType::Appraise) .and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Appraise) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Blades) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Blades) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Bombs) .and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Bombs) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Chemistry) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Climb) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Climb) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Clubs) - .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); target_item.total_skills.entry(SkillType::Clubs) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Craft) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Dodge) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Dodge) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Fish) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Fish) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Fists) - .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); target_item.total_skills.entry(SkillType::Fists) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Focus) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Focus) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Fuck) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Fuck) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Hack) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Locksmith) .and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Locksmith) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Medic) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Persuade) .and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Persuade) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Pilot) .and_modify(|sk| *sk += brn * 0.5).or_insert(brn * 0.5); target_item.total_skills.entry(SkillType::Pilot) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Pistols) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Pistols) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Quickdraw) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Quickdraw) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Repair) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Rifles) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Rifles) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Scavenge) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Scavenge) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Science) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Sneak) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Sneak) - .and_modify(|sk| *sk += col * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += col * 0.5).or_insert(col * 0.5); target_item.total_skills.entry(SkillType::Spears) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); target_item.total_skills.entry(SkillType::Spears) - .and_modify(|sk| *sk += end * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += end * 0.5).or_insert(end * 0.5); target_item.total_skills.entry(SkillType::Swim) .and_modify(|sk| *sk += end).or_insert(brn); target_item.total_skills.entry(SkillType::Teach) .and_modify(|sk| *sk += brn).or_insert(brn); target_item.total_skills.entry(SkillType::Throw) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Throw) - .and_modify(|sk| *sk += brw * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += brw * 0.5).or_insert(brw * 0.5); target_item.total_skills.entry(SkillType::Track) .and_modify(|sk| *sk += sen).or_insert(brn); target_item.total_skills.entry(SkillType::Whips) - .and_modify(|sk| *sk += sen * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += sen * 0.5).or_insert(sen * 0.5); target_item.total_skills.entry(SkillType::Whips) - .and_modify(|sk| *sk += refl * 0.5).or_insert(brn * 0.5); + .and_modify(|sk| *sk += refl * 0.5).or_insert(refl * 0.5); // 5: Apply skill (de)buffs... for buff in &target_item.temporary_buffs { for impact in &buff.impacts {