Implement the first guns.

This commit is contained in:
Condorra 2024-06-08 00:20:45 +10:00
parent 8aff296e03
commit 78abdb761b
15 changed files with 815 additions and 105 deletions

View File

@ -753,6 +753,23 @@ impl DBTrans {
.collect())
}
pub async fn find_one_item_by_location<'a>(
self: &'a Self,
location: &'a str,
) -> DResult<Option<Arc<Item>>> {
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 {

View File

@ -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,

View File

@ -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,8 +200,28 @@ impl QueueCommandHandler for QueueHandler {
ctx.trans.save_item_model(&item_mut).await?;
if let Some(container) = container_opt {
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(())
}
}

View File

@ -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<PutCasesThatSkipContainer> {
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 <bold>put<reset> item <bold>in<reset> container").to_owned())?
}
@ -240,10 +322,11 @@ 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() {
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(
@ -252,13 +335,14 @@ impl UserVerb for Verb {
&into_what.display_for_sentence(1, false)),
)
).await?;
for page in pages {
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: page.item_code.clone(),
get_possession_id: subitem.item_code.clone(),
},
)
.await?;
@ -267,7 +351,7 @@ impl UserVerb for Verb {
&mut player_item_mut,
&QueueCommand::Put {
container_possession_id: into_what.item_code.clone(),
target_possession_id: page.item_code.clone(),
target_possession_id: subitem.item_code.clone(),
},
)
.await?;
@ -275,6 +359,7 @@ impl UserVerb for Verb {
continue;
}
}
}
queue_command(
ctx,

View File

@ -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))

View File

@ -342,6 +342,7 @@ pub enum ItemFlag {
AllowShare,
Invincible,
NoIdlePark,
DontCounterattack,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]

View File

@ -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,6 +254,15 @@ async fn process_attack(
}
}
if mean_damage < 1.0 {
let msg = format!(
"{}'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 {
let actual_damage_presoak = Normal::new(mean_damage, attack.stdev_damage)?
.sample(&mut rand::thread_rng())
.floor()
@ -307,8 +318,37 @@ async fn process_attack(
}
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<Item>,
) -> DResult<(Arc<Item>, &'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?;
}

View File

@ -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<NPC> {
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
})
}

View File

@ -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<NPC> {
@ -133,6 +139,32 @@ pub fn npc_list() -> Vec<NPC> {
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()
}

View File

@ -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

View File

@ -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<NPC> {
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()
}
]
}

View File

@ -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<PossessionType>,
}
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<PossessionType>,
pub container_flags: Vec<ContainerFlag>,
}
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<WeaponData>,
pub ammo_data: Option<WeaponAttackData>,
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<PossessionType, &'static Possessio
.chain(bottles::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(club::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(gun::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(keys::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(
corp_licence::data()

View File

@ -0,0 +1,227 @@
use super::{PossessionData, PossessionType, WeaponAttackData};
use crate::{
models::item::SkillType,
static_content::possession_type::{
AllowlistContainerCheck, ContainerData, ContainerFlag, DamageType, SkillScaling, WeaponData,
},
};
use once_cell::sync::OnceCell;
pub fn airsoft_round_checker() -> &'static AllowlistContainerCheck {
static C: OnceCell<AllowlistContainerCheck> = OnceCell::new();
&C.get_or_init(|| AllowlistContainerCheck {
allowed_types: vec![PossessionType::AirsoftRound],
})
}
pub fn ninemil_bullet_checker() -> &'static AllowlistContainerCheck {
static C: OnceCell<AllowlistContainerCheck> = OnceCell::new();
&C.get_or_init(|| AllowlistContainerCheck {
allowed_types: vec![PossessionType::NinemilSolidBullet],
})
}
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = 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()
}
),
]
})
}

View File

@ -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: <bgred><black>SR<reset>
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 <bold>list<reset> 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

View File

@ -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: <bgwhite><green>SS<reset>
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: <bgblack><white>==<reset>
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: <bgwhite><blue>ST<reset>
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