Implement scavenge command

This commit is contained in:
Condorra 2023-10-06 22:32:15 +11:00
parent 64b96f48ab
commit 925deba57e
10 changed files with 440 additions and 10 deletions

View File

@ -3,7 +3,7 @@ use crate::message_handler::ListenerSession;
use crate::models::{
consent::{Consent, ConsentType},
corp::{Corp, CorpCommType, CorpId, CorpMembership},
item::{Item, ItemFlag, LocationActionType},
item::{Item, ItemFlag, LocationActionType, Scavtype},
session::Session,
task::{Task, TaskParse},
user::User,
@ -788,6 +788,29 @@ impl DBTrans {
}
}
pub async fn find_scavhidden_item_by_location<'a>(
self: &'a Self,
location: &'a str,
scavtype: &Scavtype,
max_difficulty: i64,
) -> DResult<Option<Item>> {
match self
.pg_trans()?
.query_opt(
"SELECT details FROM items WHERE details->>'location' = $1 AND \
details->'action_type'->'Scavhidden'->'scavtype' = $2::JSONB AND \
(details->'action_type'->'Scavhidden'->>'difficulty')::INT8 <= $3 \
ORDER BY details->>'display'
LIMIT 1",
&[&location, &serde_json::to_value(scavtype)?, &max_difficulty],
)
.await?
{
None => Ok(None),
Some(i) => Ok(Some(serde_json::from_value(i.get("details"))?)),
}
}
pub async fn save_item_model(self: &Self, details: &Item) -> DResult<()> {
self.pg_trans()?
.execute(
@ -817,6 +840,17 @@ impl DBTrans {
Ok(())
}
pub async fn clean_scavhidden<'a>(self: &'a Self) -> DResult<()> {
self.pg_trans()?
.execute(
"DELETE FROM items WHERE \
details->'action_type'->'Scavhidden' IS NOT NULL",
&[],
)
.await?;
Ok(())
}
pub async fn find_session_for_player<'a>(
self: &'a Self,
item_code: &'a str,

View File

@ -64,6 +64,7 @@ pub mod rent;
mod report;
mod reset_spawns;
pub mod say;
pub mod scavenge;
mod score;
mod sign;
pub mod sit;
@ -225,6 +226,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"\'" => say::VERB,
"say" => say::VERB,
"scavenge" => scavenge::VERB,
"search" => scavenge::VERB,
"sc" => score::VERB,
"score" => score::VERB,

View File

@ -0,0 +1,147 @@
use super::{
drop::consider_expire_job_for_item, get_player_item_or_fail, user_error, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::{LocationActionType, Scavtype, SkillType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, CapacityLevel},
comms::broadcast_to_room,
skills::skill_check_and_grind,
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to search the area, but you realise your ghost hands aren't good for searching".to_owned(),
)?;
}
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
ansi!("<blue>{} starts methodically searching the area.<reset>\n"),
ctx.item.display_for_sentence(true, 1, true),
),
Some(&format!(
ansi!("<blue>{} starts methodically searching the area.<reset>\n"),
ctx.item.display_for_sentence(false, 1, true),
)),
)
.await?;
Ok(time::Duration::from_secs(3))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to search the area, but you realise your ghost hands aren't good for searching".to_owned(),
)?;
}
let scav = ctx
.item
.total_skills
.get(&SkillType::Scavenge)
.unwrap_or(&0.0)
.clone();
let found_opt = ctx
.trans
.find_scavhidden_item_by_location(
&ctx.item.location,
&Scavtype::Scavenge,
(scav * 100.0).max(0.0) as i64,
)
.await?;
let mut found = match found_opt {
None => user_error("You have a look, and you're pretty sure there's nothing to find here. [Try searching elsewhere, or come back later]".to_owned())?,
Some(v) => v,
};
let diff: f64 = (match found.action_type {
LocationActionType::Scavhidden { difficulty, .. } => difficulty,
_ => 800,
}) as f64
/ 100.0;
if skill_check_and_grind(&ctx.trans, &mut ctx.item, &SkillType::Scavenge, diff).await? < 0.0
{
// No crit fail for now, it is either yes or no.
match ctx.get_session().await? {
None => {},
Some((sess, _)) =>
ctx.trans.queue_for_session(&sess, Some("You give up searching, but you still have a feeling there is something here.\n")).await?
}
return Ok(());
}
found.action_type = LocationActionType::Normal;
match check_item_capacity(&ctx.trans, &ctx.item, found.weight).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
consider_expire_job_for_item(&ctx.trans, &found).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!("{} seems to have found {} - but can't carry it so drops it on the ground.\n",
ctx.item.display_for_sentence(true, 1, true),
found.display_for_sentence(true, 1, false),
),
Some(&format!(
"{} seems to have found {} - but can't carry it so drops it on the ground.\n",
ctx.item.display_for_sentence(false, 1, true),
found.display_for_sentence(false, 1, false),)),
).await?;
}
_ => {
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} seems to have found {}.\n",
ctx.item.display_for_sentence(true, 1, true),
found.display_for_sentence(true, 1, false),
),
Some(&format!(
"{} seems to have found {}.\n",
ctx.item.display_for_sentence(false, 1, true),
found.display_for_sentence(false, 1, false),
)),
)
.await?;
found.location = ctx.item.refstr();
}
}
ctx.trans.save_item_model(&found).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?;
queue_command_and_save(ctx, &player_item, &QueueCommand::Scavenge).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -261,6 +261,11 @@ pub enum Subattack {
Wrestling,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Scavtype {
Scavenge,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum LocationActionType {
Normal,
@ -270,6 +275,7 @@ pub enum LocationActionType {
Wielded,
Attacking(Subattack),
InstalledOnDoorAsLock(Direction),
Scavhidden { difficulty: u64, scavtype: Scavtype },
}
impl LocationActionType {
@ -277,6 +283,7 @@ impl LocationActionType {
use LocationActionType::*;
match self {
InstalledOnDoorAsLock(_) => false,
Scavhidden { .. } => false,
_ => true,
}
}

View File

@ -3,7 +3,8 @@ use super::{TaskHandler, TaskRunContext};
use crate::db::DBTrans;
use crate::message_handler::user_commands::{
close, cut, drink, drop, eat, fill, get, improvise, make, movement, open, put, recline, remove,
sit, stand, use_cmd, user_error, wear, wield, CommandHandlingError, UResult, VerbContext,
scavenge, sit, stand, use_cmd, user_error, wear, wield, CommandHandlingError, UResult,
VerbContext,
};
use crate::message_handler::ListenerSession;
use crate::models::session::Session;
@ -102,6 +103,7 @@ pub enum QueueCommand {
Remove {
possession_id: String,
},
Scavenge,
Sit {
item: Option<String>,
},
@ -143,6 +145,7 @@ impl QueueCommand {
Put { .. } => "Put",
Recline { .. } => "Recline",
Remove { .. } => "Remove",
Scavenge => "Scavenge",
Sit { .. } => "Sit",
Stand { .. } => "Stand",
Use { .. } => "Use",
@ -249,6 +252,10 @@ fn queue_command_registry(
"Remove",
&remove::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Scavenge",
&scavenge::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Sit",
&sit::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),

View File

@ -3,12 +3,12 @@ use crate::db::DBTrans;
use crate::{
message_handler::user_commands::rent::recursively_destroy_or_move_item,
models::{
item::{LiquidDetails, LiquidType},
item::{LiquidDetails, LiquidType, LocationActionType},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
},
regular_tasks::{TaskHandler, TaskRunContext},
services::Item,
static_content::{possession_type::PossessionType, StaticTask},
static_content::{possession_type::PossessionType, room::room_list, StaticTask},
DResult,
};
use async_recursion::async_recursion;
@ -17,6 +17,7 @@ use log::{info, warn};
use mockall_double::double;
use once_cell::sync::OnceCell;
use rand::{thread_rng, Rng};
use rand_distr::Distribution;
use std::collections::BTreeMap;
use std::time;
@ -168,6 +169,31 @@ pub async fn refresh_all_spawn_points(trans: &DBTrans) -> DResult<()> {
trans.save_item_model(&location_mut).await?;
}
}
// Also all scav spawns...
trans.clean_scavhidden().await?;
for room in room_list().iter() {
for scav in &room.scavtable {
if thread_rng().gen_bool(scav.p_present) {
let difflevel = {
let mut rng = thread_rng();
(rand_distr::Normal::new(scav.difficulty_mean, scav.difficulty_stdev)?
.sample(&mut rng)
* 100.0)
.max(0.0) as u64
};
let mut item: Item = scav.possession_type.clone().into();
item.item_code = format!("{}", trans.alloc_item_code().await?);
item.location = format!("room/{}", room.code);
item.action_type = LocationActionType::Scavhidden {
difficulty: difflevel,
scavtype: scav.scavtype.clone(),
};
trans.save_item_model(&item).await?;
}
}
}
Ok(())
}

View File

@ -24,6 +24,7 @@ mod corp_licence;
mod fangs;
mod food;
pub mod head_armour;
mod junk;
pub mod lock;
pub mod lower_armour;
mod meat;
@ -442,6 +443,7 @@ pub enum PossessionType {
Steak,
AnimalSkin,
SeveredHead,
RustySpike,
// Craft benches
KitchenStove,
// Recipes
@ -532,6 +534,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
.chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(food::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(junk::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(
head_armour::data()
.iter()

View File

@ -0,0 +1,19 @@
use super::{PossessionData, PossessionType};
use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| {
vec![(
PossessionType::RustySpike,
PossessionData {
display: "rusty metal spike",
aliases: vec!["spike"],
details:
"A sharp, rusty spike of metal. You might be able to make something with this, but it is otherwise useless",
weight: 250,
..Default::default()
},
)]
})
}

View File

@ -4,7 +4,7 @@ use crate::db::DBTrans;
use crate::{
message_handler::user_commands::UResult,
models::{
item::{DoorState, Item, ItemFlag},
item::{DoorState, Item, ItemFlag, Scavtype},
journal::JournalType,
user::WristpadHack,
},
@ -335,6 +335,14 @@ pub trait RoomEnterTrigger {
async fn handle_enter(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>;
}
pub struct Scavinfo {
pub possession_type: PossessionType,
pub p_present: f64, // probability it is there.
pub difficulty_mean: f64,
pub difficulty_stdev: f64,
pub scavtype: Scavtype,
}
pub struct Room {
pub zone: &'static str,
// Other zones where it can be seen on the map and accessed.
@ -359,6 +367,7 @@ pub struct Room {
pub wristpad_hack_allowed: Option<WristpadHack>,
pub journal: Option<JournalType>,
pub enter_trigger: Option<&'static (dyn RoomEnterTrigger + Sync + Send)>,
pub scavtable: Vec<Scavinfo>,
}
impl Default for Room {
@ -384,6 +393,7 @@ impl Default for Room {
wristpad_hack_allowed: None,
journal: None,
enter_trigger: None,
scavtable: vec![],
}
}
}
@ -555,6 +565,18 @@ mod test {
}
}
#[test]
fn room_shorts_should_be_display_length_2() {
assert_eq!(
room_list()
.iter()
.map(|r| (r.code, ansi::strip_special_characters(r.short)))
.filter(|(_c, s)| s.len() != 2)
.collect::<Vec<(&'static str, String)>>(),
vec![]
);
}
#[test]
fn room_map_by_code_should_have_repro_xv_chargen() {
assert_eq!(

File diff suppressed because it is too large Load Diff