From 78abdb761b8ebc4eb097b66e787d43add4247255 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 8 Jun 2024 00:20:45 +1000 Subject: [PATCH] Implement the first guns. --- blastmud_game/src/db.rs | 33 +++ .../src/message_handler/user_commands.rs | 1 + .../src/message_handler/user_commands/get.rs | 28 ++- .../src/message_handler/user_commands/put.rs | 159 +++++++++--- .../user_commands/staff_show.rs | 2 +- blastmud_game/src/models/item.rs | 1 + blastmud_game/src/services/combat.rs | 190 ++++++++++----- blastmud_game/src/static_content/npc.rs | 2 + .../src/static_content/npc/melbs_npcs.rs | 36 ++- .../src/static_content/npc/melbs_npcs.yaml | 30 +++ .../src/static_content/npc/ronalds_house.rs | 108 +++++++++ .../src/static_content/possession_type.rs | 44 +++- .../src/static_content/possession_type/gun.rs | 227 ++++++++++++++++++ .../src/static_content/room/melbs.yaml | 19 ++ .../static_content/room/ronalds_house.yaml | 40 +++ 15 files changed, 815 insertions(+), 105 deletions(-) create mode 100644 blastmud_game/src/static_content/npc/ronalds_house.rs create mode 100644 blastmud_game/src/static_content/possession_type/gun.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 3376097..d2c9f7b 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -753,6 +753,23 @@ impl DBTrans { .collect()) } + pub async fn find_one_item_by_location<'a>( + self: &'a Self, + location: &'a str, + ) -> DResult>> { + Ok(self + .pg_trans()? + .query_opt( + "SELECT details FROM items WHERE details->>'location' = $1 \ + ORDER BY item_id + LIMIT 1", + &[&location], + ) + .await? + .and_then(|i| serde_json::from_value(i.get("details")).ok()) + .map(Arc::new)) + } + pub async fn count_items_by_location_type<'a>( self: &'a Self, location: &'a str, @@ -2053,6 +2070,22 @@ impl DBTrans { Ok(()) } + pub async fn destroy_one_item_by_location<'a>( + self: &'a Self, + location: &'a str, + ) -> DResult<()> { + self.pg_trans()? + .execute( + "DELETE FROM items WHERE item_id IN (\ + SELECT item_id FROM items WHERE details->>'location' = $1 \ + ORDER BY item_id + LIMIT 1)", + &[&location], + ) + .await?; + Ok(()) + } + pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); if let Some(trans) = trans_opt { diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 9cf198e..a52b552 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -234,6 +234,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "powerattack" => pow::VERB, "put" => put::VERB, + "recline" => recline::VERB, "remove" => remove::VERB, "rent" => rent::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/get.rs b/blastmud_game/src/message_handler/user_commands/get.rs index b67a992..eef281c 100644 --- a/blastmud_game/src/message_handler/user_commands/get.rs +++ b/blastmud_game/src/message_handler/user_commands/get.rs @@ -11,7 +11,7 @@ use crate::{ capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel}, comms::broadcast_to_room, }, - static_content::possession_type::possession_data, + static_content::possession_type::{possession_data, ContainerFlag}, }; use async_trait::async_trait; use std::time; @@ -172,7 +172,7 @@ impl QueueCommandHandler for QueueHandler { _ => user_error("Unexpected command".to_owned())?, }; - let possession_data = match item + let possession_dat = match item .possession_type .as_ref() .and_then(|pt| possession_data().get(&pt)) @@ -183,7 +183,7 @@ impl QueueCommandHandler for QueueHandler { Some(pd) => pd, }; - match check_item_capacity(ctx.trans, &ctx.item, possession_data.weight).await? { + match check_item_capacity(ctx.trans, &ctx.item, possession_dat.weight).await? { CapacityLevel::AboveItemLimit => { user_error("You just can't hold that many things!".to_owned())? } @@ -200,7 +200,27 @@ impl QueueCommandHandler for QueueHandler { ctx.trans.save_item_model(&item_mut).await?; if let Some(container) = container_opt { - recalculate_container_weight(&ctx.trans, &container).await?; + if container + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.container_data.as_ref()) + .map_or(false, |cd| { + cd.container_flags.contains(&ContainerFlag::DestroyOnEmpty) + }) + && ctx + .trans + .get_location_stats(&container.refstr()) + .await? + .total_count + == 0 + { + ctx.trans + .delete_item(&container.item_type, &container.item_code) + .await?; + } else { + recalculate_container_weight(&ctx.trans, &container).await?; + } } Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/put.rs b/blastmud_game/src/message_handler/user_commands/put.rs index 4e7dfcd..17cc511 100644 --- a/blastmud_game/src/message_handler/user_commands/put.rs +++ b/blastmud_game/src/message_handler/user_commands/put.rs @@ -3,7 +3,7 @@ use super::{ user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext, }; use crate::{ - models::item::{ItemFlag, LocationActionType}, + models::item::{Item, ItemFlag, LocationActionType}, regular_tasks::queued_command::{ queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, }, @@ -11,7 +11,7 @@ use crate::{ capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel}, comms::broadcast_to_room, }, - static_content::possession_type::possession_data, + static_content::possession_type::{possession_data, ContainerFlag}, }; use ansi::ansi; use async_trait::async_trait; @@ -133,7 +133,7 @@ impl QueueCommandHandler for QueueHandler { ); broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?; - let possession_data = match item + let possession_dat = match item .possession_type .as_ref() .and_then(|pt| possession_data().get(&pt)) @@ -144,7 +144,18 @@ impl QueueCommandHandler for QueueHandler { Some(pd) => pd, }; - match check_item_capacity(ctx.trans, &container, possession_data.weight).await? { + let is_loadable = container + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(&pt)) + .map_or(false, |pd| pd.weapon_data.is_some()); + match check_item_capacity(ctx.trans, &container, possession_dat.weight).await? { + CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened if is_loadable => { + user_error(format!( + "{} is already fully loaded", + container.display_for_sentence(1, true) + ))? + } CapacityLevel::AboveItemLimit => user_error(format!( "{} just can't hold that many things!", container.display_for_sentence(1, true), @@ -168,6 +179,74 @@ impl QueueCommandHandler for QueueHandler { } } +pub enum PutCasesThatSkipContainer { + BookInWorkstation, + AmmoInWeapon, +} + +impl PutCasesThatSkipContainer { + async fn announce( + &self, + ctx: &mut VerbContext<'_>, + into_what: &Item, + target: &Item, + ) -> UResult<()> { + match self { + Self::BookInWorkstation => { + ctx.trans + .queue_for_session(&ctx.session, + Some( + &format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n", + &target.display_for_sentence(1, false), + &into_what.display_for_sentence(1, false)), + ) + ).await?; + } + _ => {} + } + + Ok(()) + } +} + +fn check_for_special_put_case( + into_what: &Item, + target: &Item, +) -> Option { + if into_what.flags.contains(&ItemFlag::Bench) && target.flags.contains(&ItemFlag::Book) { + return Some(PutCasesThatSkipContainer::BookInWorkstation); + } + let into_pd = match into_what + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + { + None => return None, + Some(v) => v, + }; + if into_pd.container_data.is_none() { + return None; + } + let target_pd = match target + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + { + None => return None, + Some(v) => v, + }; + let target_cd = match target_pd.container_data.as_ref() { + None => return None, + Some(v) => v, + }; + if into_pd.weapon_data.is_some() && target_cd.container_flags.contains(&ContainerFlag::AmmoClip) + { + return Some(PutCasesThatSkipContainer::AmmoInWeapon); + } + + None +} + pub struct Verb; #[async_trait] impl UserVerb for Verb { @@ -188,7 +267,10 @@ impl UserVerb for Verb { remaining = remaining2; } - let (into_what, for_what) = match remaining.split_once(" in ") { + let (into_what, for_what) = match remaining + .split_once(" in ") + .or_else(|| remaining.split_once(" into ")) + { None => { user_error(ansi!("Try put item in container").to_owned())? } @@ -240,39 +322,42 @@ impl UserVerb for Verb { did_anything = true; - if into_what.flags.contains(&ItemFlag::Bench) && target.flags.contains(&ItemFlag::Book) - { - let pages = ctx.trans.find_items_by_location(&target.refstr()).await?; - if !pages.is_empty() { - ctx.trans - .queue_for_session(&ctx.session, - Some( - &format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n", - &target.display_for_sentence(1, false), - &into_what.display_for_sentence(1, false)), - ) - ).await?; - for page in pages { - queue_command( - ctx, - &mut player_item_mut, - &QueueCommand::GetFromContainer { - from_item_id: target.refstr(), - get_possession_id: page.item_code.clone(), - }, - ) - .await?; - queue_command( - ctx, - &mut player_item_mut, - &QueueCommand::Put { - container_possession_id: into_what.item_code.clone(), - target_possession_id: page.item_code.clone(), - }, - ) - .await?; + match check_for_special_put_case(&into_what, &target) { + None => {} + Some(special_put) => { + let subitems = ctx.trans.find_items_by_location(&target.refstr()).await?; + if !subitems.is_empty() { + ctx.trans + .queue_for_session(&ctx.session, + Some( + &format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n", + &target.display_for_sentence(1, false), + &into_what.display_for_sentence(1, false)), + ) + ).await?; + for subitem in subitems.iter().take(10) { + special_put.announce(ctx, &into_what, &target).await?; + queue_command( + ctx, + &mut player_item_mut, + &QueueCommand::GetFromContainer { + from_item_id: target.refstr(), + get_possession_id: subitem.item_code.clone(), + }, + ) + .await?; + queue_command( + ctx, + &mut player_item_mut, + &QueueCommand::Put { + container_possession_id: into_what.item_code.clone(), + target_possession_id: subitem.item_code.clone(), + }, + ) + .await?; + } + continue; } - continue; } } diff --git a/blastmud_game/src/message_handler/user_commands/staff_show.rs b/blastmud_game/src/message_handler/user_commands/staff_show.rs index 2e15e23..fe37f67 100644 --- a/blastmud_game/src/message_handler/user_commands/staff_show.rs +++ b/blastmud_game/src/message_handler/user_commands/staff_show.rs @@ -40,7 +40,7 @@ impl UserVerb for Verb { )); } } else { - user_error("Unknown subcommand.".to_owned())?; + user_error("Unknown subcommand. Try loc.".to_owned())?; } ctx.trans .queue_for_session(&ctx.session, Some(&resp)) diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index eac3428..67acc1c 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -342,6 +342,7 @@ pub enum ItemFlag { AllowShare, Invincible, NoIdlePark, + DontCounterattack, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 0741709..9c910bd 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -140,12 +140,9 @@ async fn start_next_attack( ctx: &mut TaskRunContext<'_>, attacker_item: &Item, victim_item: &Item, - weapon: &WeaponData, + normal_attack: &WeaponAttackData, ) -> DResult<()> { - let msg = &(weapon - .normal_attack - .start_message(&attacker_item, victim_item) - + ".\n"); + let msg = &(normal_attack.start_message(&attacker_item, victim_item) + ".\n"); broadcast_to_room(ctx.trans, &attacker_item.location, None, msg).await?; Ok(()) } @@ -212,7 +209,12 @@ async fn process_attack( ) .await? } else { - skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill) + let low_skill_penalty = (weapon.raw_min_to_learn - raw_skill).max(0.0); + skill_check_only( + &attacker_item, + &weapon.uses_skill, + victim_dodge_skill - low_skill_penalty, + ) } } else { skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill) @@ -252,63 +254,101 @@ async fn process_attack( } } - let actual_damage_presoak = Normal::new(mean_damage, attack.stdev_damage)? - .sample(&mut rand::thread_rng()) - .floor() - .max(1.0) as i64; - match attacker_item.active_combat.as_mut() { - Some(ac) => ac.attack_mode = AttackMode::NORMAL, - None => {} - } - ctx.trans.save_item_model(&attacker_item).await?; - let actual_damage = soak_damage( - &ctx.trans, - attack, - victim_item, - actual_damage_presoak as f64, - &part, - ) - .await? as i64; - let msg = attack.success_message(&attacker_item, victim_item, &part); - if actual_damage == 0 { + if mean_damage < 1.0 { let msg = format!( - "{}'s attack bounces off {}'s {}.\n", + "{}'s attempted attack on {}'s {} was completely ineffective.\n", &attacker_item.display_for_sentence(1, true), &victim_item.display_for_sentence(1, false), &part.display(victim_item.sex.clone()) ); - broadcast_to_room(&ctx.trans, &victim_item.location, None, &msg).await?; - } else if change_health(ctx.trans, -actual_damage, victim_item, &msg).await? { - ctx.trans.save_item_model(victim_item).await?; - return Ok(true); - } + broadcast_to_room(&ctx.trans, &victim_item.location, None, &msg).await? + } else { + let actual_damage_presoak = Normal::new(mean_damage, attack.stdev_damage)? + .sample(&mut rand::thread_rng()) + .floor() + .max(1.0) as i64; + match attacker_item.active_combat.as_mut() { + Some(ac) => ac.attack_mode = AttackMode::NORMAL, + None => {} + } + ctx.trans.save_item_model(&attacker_item).await?; + let actual_damage = soak_damage( + &ctx.trans, + attack, + victim_item, + actual_damage_presoak as f64, + &part, + ) + .await? as i64; + let msg = attack.success_message(&attacker_item, victim_item, &part); + if actual_damage == 0 { + let msg = format!( + "{}'s attack bounces off {}'s {}.\n", + &attacker_item.display_for_sentence(1, true), + &victim_item.display_for_sentence(1, false), + &part.display(victim_item.sex.clone()) + ); + broadcast_to_room(&ctx.trans, &victim_item.location, None, &msg).await?; + } else if change_health(ctx.trans, -actual_damage, victim_item, &msg).await? { + ctx.trans.save_item_model(victim_item).await?; + return Ok(true); + } - // Consider applying a crit effect... - let mut crit_rand = rand::thread_rng().gen::(); - for (p, eff_type) in &attack.crit_effects { - if victim_item.active_effects.iter().any(|e| e.0 == *eff_type) { - continue; - } - if *p >= crit_rand { - if let Some(effect_set) = default_effects_for_type().get(eff_type) { - run_effects( - ctx.trans, - &effect_set, - attacker_item, - weapon_item, - Some(victim_item), - 0.0, - ) - .await?; + // Consider applying a crit effect... + let mut crit_rand = rand::thread_rng().gen::(); + for (p, eff_type) in &attack.crit_effects { + if victim_item.active_effects.iter().any(|e| e.0 == *eff_type) { + continue; + } + if *p >= crit_rand { + if let Some(effect_set) = default_effects_for_type().get(eff_type) { + run_effects( + ctx.trans, + &effect_set, + attacker_item, + weapon_item, + Some(victim_item), + 0.0, + ) + .await?; + } + } else { + crit_rand -= *p; } - } else { - crit_rand -= *p; } + ctx.trans.save_item_model(victim_item).await?; } - ctx.trans.save_item_model(victim_item).await?; } - start_next_attack(ctx, &attacker_item, victim_item, weapon).await?; + let next_attack = if weapon_item + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .map(|pd| pd.container_data.is_some()) + .unwrap_or(false) + { + // Consume the ammo... + ctx.trans + .destroy_one_item_by_location(&weapon_item.refstr()) + .await?; + match ctx + .trans + .find_one_item_by_location(&weapon_item.refstr()) + .await? + .and_then(|ammo| { + ammo.possession_type + .as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.ammo_data.as_ref()) + }) { + None => &weapon.normal_attack, + Some(ammo_attack) => ammo_attack, + } + } else { + attack + }; + + start_next_attack(ctx, &attacker_item, victim_item, next_attack).await?; Ok(false) } @@ -468,7 +508,8 @@ impl TaskHandler for AttackTaskHandler { return Ok(None); } - let (weapon_it, weapon) = what_wielded(ctx.trans, &attacker_item).await?; + let (weapon_it, weapon, normal_attack) = + what_wielded_considering_ammo(ctx.trans, &attacker_item).await?; let mut attacker_item_mut = (*attacker_item).clone(); @@ -495,7 +536,8 @@ impl TaskHandler for AttackTaskHandler { if mode == AttackMode::FEINT { if process_feint(ctx, &mut attacker_item_mut, &mut victim_item).await? { - start_next_attack(ctx, &mut attacker_item_mut, &mut victim_item, &weapon).await?; + start_next_attack(ctx, &mut attacker_item_mut, &mut victim_item, normal_attack) + .await?; } } else { process_attack( @@ -507,10 +549,10 @@ impl TaskHandler for AttackTaskHandler { if let Some(pow) = weapon.power_attack.as_ref() { pow } else { - &weapon.normal_attack + normal_attack } } else { - &weapon.normal_attack + normal_attack }, &weapon, ) @@ -869,7 +911,6 @@ async fn what_wielded( } } - // 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()) @@ -886,6 +927,36 @@ async fn what_wielded( Ok((who.clone(), fist())) } +async fn what_wielded_considering_ammo( + trans: &DBTrans, + who: &Arc, +) -> DResult<(Arc, &'static WeaponData, &'static WeaponAttackData)> { + let (weapon_item, weapon_data) = what_wielded(trans, who).await?; + if weapon_item + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .map(|pd| pd.container_data.is_some()) + .unwrap_or(false) + { + // Check if it is loaded, and use the ammo instead... + match trans + .find_one_item_by_location(&weapon_item.refstr()) + .await? + .and_then(|ammo| { + ammo.possession_type + .as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.ammo_data.as_ref()) + }) { + None => Ok((weapon_item, weapon_data, &weapon_data.normal_attack)), + Some(ammo_attack) => Ok((weapon_item, weapon_data, ammo_attack)), + } + } else { + Ok((weapon_item, weapon_data, &weapon_data.normal_attack)) + } +} + fn attack_speed(who: &Item) -> time::Duration { let base_time = 5; @@ -959,8 +1030,8 @@ pub async fn start_attack_mut( &to_whom.display_for_sentence(1, false) )); - let (_, wielded) = what_wielded(trans, &Arc::new(by_whom.clone())).await?; - msg.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom) + ".\n")); + let (_, _, attack) = what_wielded_considering_ammo(trans, &Arc::new(by_whom.clone())).await?; + msg.push_str(&(attack.start_message(by_whom, to_whom) + ".\n")); broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg).await?; @@ -994,6 +1065,7 @@ pub async fn start_attack_mut( .as_ref() .and_then(|ac| ac.attacking.as_ref()) == None + && !to_whom.flags.contains(&ItemFlag::DontCounterattack) { start_attack_mut(trans, to_whom, by_whom).await?; } diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 9a5e4f4..1c6719a 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -41,6 +41,7 @@ pub mod computer_museum_npcs; mod melbs_npcs; mod northrad_npcs; mod roboporter; +mod ronalds_house; mod sewer_npcs; pub mod statbot; @@ -197,6 +198,7 @@ pub fn npc_list() -> &'static Vec { npcs.append(&mut roboporter::npc_list()); npcs.append(&mut computer_museum_npcs::npc_list()); npcs.append(&mut sewer_npcs::npc_list()); + npcs.append(&mut ronalds_house::npc_list()); npcs }) } diff --git a/blastmud_game/src/static_content/npc/melbs_npcs.rs b/blastmud_game/src/static_content/npc/melbs_npcs.rs index c3a6a53..e023773 100644 --- a/blastmud_game/src/static_content/npc/melbs_npcs.rs +++ b/blastmud_game/src/static_content/npc/melbs_npcs.rs @@ -2,7 +2,7 @@ use super::{NPCPronounType, NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC}; use crate::{ models::{ consent::ConsentType, - item::{LocationActionType, Pronouns, SkillType}, + item::{ItemFlag, LocationActionType, Pronouns, SkillType}, }, static_content::{ npc::{npc_pronoun_type_to_pronouns, KillBonus}, @@ -31,6 +31,12 @@ enum MelbsNPC { adjectives: String, spawn_room: String, }, + Target { + code: String, + name: String, + spawn_room: String, + dodge: f64, + }, } pub fn npc_list() -> Vec { @@ -133,6 +139,32 @@ pub fn npc_list() -> Vec { player_consents: vec!(ConsentType::Fight), ..Default::default() }, - } + MelbsNPC::Target { code, name, spawn_room, dodge } => { + NPC { + code: format!("melbs_target_{}", &code), + name: name.clone(), + pronouns: Pronouns { is_proper: true, ..Pronouns::default_inanimate() }, + description: "A robotic device that seems to be designed for target practice. It includes a stand, and has the vague shape of a robot torso. It seems designed to take a lot of damage, and to move to dodge bullets".to_owned(), + aliases: vec!("target".to_owned()), + spawn_location: format!("room/{}", spawn_room), + species: SpeciesType::Robot, + max_health: 10000, + extra_flags: vec![ItemFlag::DontCounterattack], + total_skills: SkillType::values() + .into_iter() + .map(|sk| { + ( + sk.clone(), + match sk { + SkillType::Dodge => dodge, + _ => 8.0 + } + ) + }).collect(), + player_consents: vec!(ConsentType::Fight), + ..Default::default() + } + } + }, ).collect() } diff --git a/blastmud_game/src/static_content/npc/melbs_npcs.yaml b/blastmud_game/src/static_content/npc/melbs_npcs.yaml index 65f10e7..49be648 100644 --- a/blastmud_game/src/static_content/npc/melbs_npcs.yaml +++ b/blastmud_game/src/static_content/npc/melbs_npcs.yaml @@ -546,3 +546,33 @@ code: "2" adjectives: huge frightening spawn_room: kings_office_hallway +- !Target + code: "1" + name: Practice Target 1 + spawn_room: melbs_shooting_range + dodge: 8.0 +- !Target + code: "2" + name: Practice Target 2 + spawn_room: melbs_shooting_range + dodge: 9.0 +- !Target + code: "3" + name: Practice Target 3 + spawn_room: melbs_shooting_range + dodge: 10.0 +- !Target + code: "4" + name: Practice Target 4 + spawn_room: melbs_shooting_range + dodge: 11.0 +- !Target + code: "5" + name: Practice Target 5 + spawn_room: melbs_shooting_range + dodge: 12.0 +- !Target + code: "6" + name: Practice Target 6 + spawn_room: melbs_shooting_range + dodge: 13.0 diff --git a/blastmud_game/src/static_content/npc/ronalds_house.rs b/blastmud_game/src/static_content/npc/ronalds_house.rs new file mode 100644 index 0000000..ec7e44a --- /dev/null +++ b/blastmud_game/src/static_content/npc/ronalds_house.rs @@ -0,0 +1,108 @@ +use crate::{ + models::{ + consent::ConsentType, + item::{Pronouns, SkillType}, + }, + static_content::{ + npc::{NPCSayInfo, NPCSayType}, + possession_type::PossessionType, + species::SpeciesType, + }, +}; + +use super::NPC; + +pub fn npc_list() -> Vec { + let ronald_says = vec![NPCSayInfo { + say_code: "ronald_babble", + frequency_secs: 10, + talk_type: NPCSayType::FromFixedList(vec![ + "Under the emperorer I had a whole squadran of security forces at my command... after his fall, I'm relegated to killing trespassers myself.", + "I once had a whole family going back 4 generations - twenty three people all up - executed because their snotty-nosed child looked at me funny. People feared me in those days. Now they break into my house.", + "The empire may have crumbled, but my will remains as strong as ever. You trespassers are nothing but pests to be eradicated.", + "The former emperor entrusted me with the green code share, and I will protect it with my life.", + "You think you're so clever, entering my home uninvited? I've seen entire families fall to ruin. A trespass here is a grave mistake.", + "This home is a mere fraction of what I once controlled. You're trespassing in the shadow of greatness, and shall pay.", + "Your life is forfeit. I'll make an example of your corpses to discourage others from following in your footsteps.", + "The code I guard is more valuable than all the wealth and resources you could ever dream of, and you shall not have it.", + "I once had a private army, now I must rely on my wits, my gun and my blade. They have never failed me.", + "You dare threaten me, the Praefect of a fallen empire? I'll show you what true power is.", + "Your intrusion is an affront to my dignity. Prepare to face my wrath.", + "I once commanded entire planets, now I must defend myself and my the green code share against pathetic invaders like you.", + "The late emperor trusted me with the green code share, and I will not let it fall into the wrong hands.", + "My people used to cower in fear of me. Now they dare to trespass on my property.", + "I once ruled an empire, now I must defend my home and my code against intruders like you.", + "Your life is forfeit. I'll make sure that this place remains a tomb for your bodies.", + "I once had an entire military at my command. Now I must defend myself and my code with this gun and blade.", + "Your trespassing is an insult to my memory and the legacy of the old empire.", + "Prepare to face the wrath of a Praefect who once commanded the might of an empire in this region.", + "I'll make sure that your bodies decorate the walls of this home as a warning to others.", + "Your life is forfeit. I'll make sure that your death serves a purpose - making others fear to be as foolish as you've been coming here.", + "You dare challenge me, the Praefect of a fallen empire? Prepare to face the barrel of my gun.", + "I'll make sure that your death is not in vain. I'll use it as an example to deter others from trespassing.", + "The old empire may be gone, but my determination remains.", + "You dare enter my home uninvited? Prepare to face the consequences.", + "Your trespassing is an act of treason against the legacy of the old empire.", + "I'll make sure that your death serves as a warning to others. This home is not a plaything for trespassers.", + ]), + }]; + + vec![ + NPC { + code: "ronalds_house_taipan".to_owned(), + name: "Brown-headed taipan".to_owned(), + description: "A large olive coloured snake, its head a darker brown. Its head tracks you as it tastes the air with its forked tongue. As it flicks its tongue, you see a glimpse of two needle-sharp fangs in the front of its mouth".to_owned(), + spawn_location: "room/ronalds_snakepit".to_owned(), + aliases: vec!["snake".to_owned(), "taipan".to_owned()], + aggression: 14, + aggro_pc_only: true, + max_health: 24, + intrinsic_weapon: Some(PossessionType::VenomousFangs), + total_xp: 15000, + total_skills: SkillType::values() + .into_iter() + .map(|sk| { + ( + sk.clone(), + match sk { + SkillType::Dodge => 14.0, + SkillType::Fists => 19.0, + _ => 8.0 + } + ) + }) + .collect(), + species: SpeciesType::Snake, + player_consents: vec![ConsentType::Fight], + ..Default::default() + }, + NPC { + code:"ronalds_house_ronald".to_owned(), + name:"Ronald Fairburn".to_owned(), + pronouns: Pronouns::default_male(), + description: "A tall man, probably in his fifties, but still looking fit. His hair shows tinges of gray, while his rugged face and piercing blue eyes give the impression this is a man who would make a heartless decision in an instant".to_owned(), + spawn_location: "room/ronalds_study".to_owned(), + spawn_possessions: vec![], + says: ronald_says, + aggression: 21, + aggro_pc_only: true, + max_health: 100, + total_xp: 20000, + total_skills: SkillType::values() + .into_iter() + .map(|sk| { + ( + sk.clone(), + match sk { + SkillType::Dodge => 14.0, + SkillType::Fists => 18.0, + _ => 8.0 + } + ) + }).collect(), + species: SpeciesType::Human, + player_consents: vec!(ConsentType::Fight), + ..Default::default() + } + ] +} diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 84370f2..fa15555 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -1,7 +1,7 @@ #[double] use crate::db::DBTrans; use crate::{ - message_handler::user_commands::{UResult, VerbContext}, + message_handler::user_commands::{user_error, UResult, VerbContext}, models::{ consent::ConsentType, effect::{EffectSet, EffectType}, @@ -26,6 +26,7 @@ mod club; mod corp_licence; mod fangs; mod food; +mod gun; pub mod head_armour; mod junk; mod keys; @@ -274,11 +275,38 @@ impl ContainerCheck for PermissiveContainerCheck { } } +pub struct AllowlistContainerCheck { + allowed_types: Vec, +} +impl ContainerCheck for AllowlistContainerCheck { + fn check_place(&self, container: &Item, item: &Item) -> UResult<()> { + if item + .possession_type + .as_ref() + .map(|pt| !self.allowed_types.contains(pt)) + .unwrap_or(true) + { + user_error(format!( + "You realise {} wasn't designed to hold that.", + container.display_for_sentence(1, false), + )) + } else { + Ok(()) + } + } +} + #[async_trait] pub trait BenchData { async fn check_make(&self, trans: &DBTrans, bench: &Item, recipe: &Item) -> UResult<()>; } +#[derive(Clone, Eq, PartialEq)] +pub enum ContainerFlag { + AmmoClip, + DestroyOnEmpty, +} + #[derive(Clone)] pub struct ContainerData { pub max_weight: u64, @@ -286,6 +314,7 @@ pub struct ContainerData { pub compression_ratio: f64, pub checker: &'static (dyn ContainerCheck + Sync + Send), pub default_contents: Vec, + pub container_flags: Vec, } impl Default for ContainerData { @@ -296,6 +325,7 @@ impl Default for ContainerData { compression_ratio: 1.0, checker: &PERMISSIVE_CONTAINER_CHECK, default_contents: vec![], + container_flags: vec![], } } } @@ -332,6 +362,7 @@ pub trait Describer { pub struct PossessionData { pub weapon_data: Option, + pub ammo_data: Option, pub display: &'static str, pub details: &'static str, pub aliases: Vec<&'static str>, @@ -361,6 +392,7 @@ impl Default for PossessionData { fn default() -> Self { Self { weapon_data: None, + ammo_data: None, display: "Thingy", details: "A generic looking thing", aliases: vec![], @@ -406,7 +438,7 @@ impl WeaponAttackData { } } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PossessionType { // Special values that substitute for possessions. Fangs, // Default weapon for certain animals @@ -436,6 +468,13 @@ pub enum PossessionType { NanobladeGladius, // Weapons: Clubs SpikedMace, + // Weapons: Guns & ammo + AirsoftRound, + AirsoftRoundFiftyBox, + AirsoftGun, + NinemilSolidBullet, + NinemilFiftyBox, + Z3000Pistol, // Medical MediumTraumaKit, EmptyMedicalBox, @@ -569,6 +608,7 @@ pub fn possession_data() -> &'static BTreeMap &'static AllowlistContainerCheck { + static C: OnceCell = OnceCell::new(); + &C.get_or_init(|| AllowlistContainerCheck { + allowed_types: vec![PossessionType::AirsoftRound], + }) +} + +pub fn ninemil_bullet_checker() -> &'static AllowlistContainerCheck { + static C: OnceCell = OnceCell::new(); + &C.get_or_init(|| AllowlistContainerCheck { + allowed_types: vec![PossessionType::NinemilSolidBullet], + }) +} + +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = OnceCell::new(); + &D.get_or_init(|| { + vec![ + ( + PossessionType::AirsoftRound, + PossessionData { + display: "airsoft round", + details: + "A soft plastic spherical projectile", + aliases: vec!["air-soft round", "air soft round", "ammo"], + weight: 8, + ammo_data: Some(WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim| { + format!( + "{} points {} colourful plastic gun at {}", + &attacker.display_for_sentence(1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part| { + format!( + "{} hits {}'s {} with an airsoft round", + &attacker.display_for_sentence(1, true), + &victim.display_for_sentence(1, false), + &part.display(victim.sex.clone()) + ) + })], + mean_damage: 1.0, + stdev_damage: 0.5, + base_damage_type: DamageType::Bullet, + ..Default::default() + }), + ..Default::default() + }, + ), + ( + PossessionType::AirsoftRoundFiftyBox, + PossessionData { + display: "box of airsoft rounds", + details: "A box of airsoft rounds, each a soft plastic spherical projectile", + aliases: vec!["air-soft round box", "air soft round box", "ammo"], + container_data: Some(ContainerData { + max_weight: 400, + base_weight: 1, + compression_ratio: 0.9, + default_contents: vec![PossessionType::AirsoftRound].repeat(50), + checker: airsoft_round_checker(), + container_flags: vec![ContainerFlag::DestroyOnEmpty, ContainerFlag::AmmoClip] + }), + ..Default::default() + } + ), + ( + PossessionType::AirsoftGun, + PossessionData { + display: "airsoft gun", + details: "A colourful looking pistol-type weapon. Apart from the colour scheme, it is a close replica of a normal powder-powered pistol", + aliases: vec!["gun", "air soft gun", "air-soft gun"], + weapon_data: Some( + WeaponData { + uses_skill: SkillType::Pistols, + raw_min_to_learn: 0.0, + raw_max_to_learn: 2.0, + normal_attack: WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim| { + format!( + "{} points {} colourful light-looking gun at {}", + &attacker.display_for_sentence(1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part| { + format!( + "There is a click as {} aims {} colourful unloaded gun at {} and pulls the trigger", + &attacker.display_for_sentence(1, true), + &victim.display_for_sentence(1, false), + &part.display(victim.sex.clone()) + ) + })], + mean_damage: 0.0, + stdev_damage: 0.0, + base_damage_type: DamageType::Bullet, + ..Default::default() + }, + power_attack: None + } + ), + container_data: Some(ContainerData { + max_weight: 96, + base_weight: 500, + compression_ratio: 1.0, + checker: airsoft_round_checker(), + ..Default::default() + }), + ..Default::default() + } + ), + ( + PossessionType::NinemilSolidBullet, + PossessionData { + display: "9mm solid bullet", + details: + "A nine millimetre bullet consisting of casing and a solid-tipped projectile", + aliases: vec!["bullet", "nine-millimetre bullet", "ammo"], + weight: 8, + ammo_data: Some(WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim| { + format!( + "{} points {} gun at {}", + &attacker.display_for_sentence(1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part| { + format!( + "Oww! {} blasts a 9mm hole through {}'s {}", + &attacker.display_for_sentence(1, true), + &victim.display_for_sentence(1, false), + &part.display(victim.sex.clone()) + ) + })], + mean_damage: 15.0, + stdev_damage: 15.0, + skill_scaling: vec![SkillScaling { + skill: SkillType::Pistols, + min_skill: 2.0, + mean_damage_per_point_over_min: 2.0, + }], + base_damage_type: DamageType::Bullet, + ..Default::default() + }), + ..Default::default() + }, + ), + ( + PossessionType::NinemilFiftyBox, + PossessionData { + display: "box of 9mm solid bullets", + details: "A box of nine millimetre bullets, each consisting of casing and a solid-tipped projectile", + aliases: vec!["bullet", "nine-millimetre bullet", "ammo"], + container_data: Some(ContainerData { + max_weight: 400, + base_weight: 1, + compression_ratio: 0.9, + default_contents: vec![PossessionType::NinemilSolidBullet].repeat(50), + checker: ninemil_bullet_checker(), + container_flags: vec![ContainerFlag::DestroyOnEmpty, ContainerFlag::AmmoClip] + }), + ..Default::default() + } + ), + ( + PossessionType::Z3000Pistol, + PossessionData { + display: "Z3000 pistol", + details: "A black metal pistol, featuring a cylindrical barrel, with a rectangular body and butt", + aliases: vec!["gun", "pistol"], + weapon_data: Some( + WeaponData { + uses_skill: SkillType::Pistols, + raw_min_to_learn: 2.0, + raw_max_to_learn: 5.0, + normal_attack: WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim| { + format!( + "{} points {} light-looking gun at {}", + &attacker.display_for_sentence(1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part| { + format!( + "There is a click as {} aims {} unloaded gun at {} and pulls the trigger", + &attacker.display_for_sentence(1, true), + &victim.display_for_sentence(1, false), + &part.display(victim.sex.clone()) + ) + })], + mean_damage: 0.0, + stdev_damage: 0.0, + base_damage_type: DamageType::Bullet, + ..Default::default() + }, + power_attack: None + } + ), + container_data: Some(ContainerData { + max_weight: 96, + base_weight: 500, + compression_ratio: 1.0, + checker: ninemil_bullet_checker(), + ..Default::default() + }), + ..Default::default() + } + ), + ] + }) +} diff --git a/blastmud_game/src/static_content/room/melbs.yaml b/blastmud_game/src/static_content/room/melbs.yaml index 4d00220..5e8cd27 100644 --- a/blastmud_game/src/static_content/room/melbs.yaml +++ b/blastmud_game/src/static_content/room/melbs.yaml @@ -1919,8 +1919,27 @@ - direction: east - direction: north target: !Custom room/chonkers_strength_hall + - direction: south should_caption: false scavtable: CityStreet +- zone: melbs + code: melbs_shooting_range + name: Bourke St Shooting Range + short: SR + grid_coords: + x: 8 + y: 4 + z: 0 + description: An indoor shooting range. The room is split into two halves by a red velvet rope, on which signs warning that the rope should not be crossed hang. The far side of the rope houses many bullet-riddled targets against a solid wall. The near side of the rope houses booths for shooters to stand in, as well as a desk, staffed by a cheery looking chap who is apparently selling equipment for use on the range [Use list to see stock for sale here] + stock_list: + - possession_type: AirsoftRoundFiftyBox + list_price: 50 + poverty_discount: false + - possession_type: AirsoftGun + list_price: 100 + poverty_discount: false + exits: + - direction: north - zone: melbs code: melbs_queenst_bourkest name: Queen St & Bourke St diff --git a/blastmud_game/src/static_content/room/ronalds_house.yaml b/blastmud_game/src/static_content/room/ronalds_house.yaml index b4841f9..5e44077 100644 --- a/blastmud_game/src/static_content/room/ronalds_house.yaml +++ b/blastmud_game/src/static_content/room/ronalds_house.yaml @@ -11,6 +11,7 @@ - direction: up target: !Custom room/northrad_g4 - direction: north + - direction: south repel_npc: true - zone: ronalds_house code: ronalds_health_room @@ -26,3 +27,42 @@ environment: passive_health: 10 passive_health_message: feels healthy +- zone: ronalds_house + code: ronalds_snakepit + name: Snake Pit + short: SS + grid_coords: + x: 0 + y: 1 + z: 0 + description: Here the hall is interrupted by a concrete pit set in the ground. It looks steep and smooth enough to contain a snake, but the edges are not so high as to stop a person entering. Above the pit hangs a drawbridge, apparently held up by a winch, but there is no obvious way to activate the drawbridge, so it looks like going through the snakepit is an intruder's only choice + exits: + - direction: north + - direction: south + needs_npc_cleared: + block_message: The taipan hisses and you narrowly avoid a bite. You realise you'll have to kill it to get past +- zone: ronalds_house + code: ronalds_hallway_1 + name: Hallway + should_caption: false + short: == + grid_coords: + x: 0 + y: 2 + z: 0 + description: A section of hallway that branches off to several rooms of the house. On a pillar hangs a framed but faded picture of two men, each with an evil smile, one recognisable as the former emperor, and the other clad in the nanoweave smartwear of the fallen empire's most trusted elite + exits: + - direction: south + - direction: east +- zone: ronalds_house + code: ronalds_study + name: Study + should_caption: true + short: ST + grid_coords: + x: -1 + y: 2 + z: 0 + description: A spacious study, the walls lined with massive electronic ink displays. An extremely fancy leather chair, featuring electronic controls, has prime place in the middle + exits: + - direction: west