Start work on a new computer museum zone. The puzzle and reward are

still TODO.
This commit is contained in:
Condorra 2023-09-17 22:13:19 +10:00
parent 752af74337
commit 4467707d4a
20 changed files with 579 additions and 107 deletions

View File

@ -99,10 +99,11 @@ static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
\t<bold>look<reset> (or <bold>l<reset>) to look around - follow it with an exit or\n\ \t<bold>look<reset> (or <bold>l<reset>) to look around - follow it with an exit or\n\
\t\ta character / item name for detail on that.\n\ \t\ta character / item name for detail on that.\n\
\t<bold>lmap<reset> - get a map showing exits and places."), \t<bold>lmap<reset> - get a map showing exits and places."),
"talk" => "talk" => TALK_HELP,
ansi!("Use:\n\ "say" => TALK_HELP,
\t<bold>'<reset>message to send message to the room.\n\ "whisper" => TALK_HELP,
\t<bold>-<reset>user message to whisper to someone."), "page" => TALK_HELP,
"reply" => TALK_HELP,
"possessions" => "possessions" =>
ansi!("Use:\n\ ansi!("Use:\n\
\t<bold>get<reset> item to pick something up from the ground.\n\ \t<bold>get<reset> item to pick something up from the ground.\n\
@ -278,6 +279,14 @@ static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
disallow command."), disallow command."),
}; };
static TALK_HELP: &'static str = ansi!(
"Use:\n\
\t<bold>'<reset>message to send message to the room.\n\
\t<bold>-<reset>user message to whisper to someone.\n\
\t<bold>p<reset> user message to page someone on your wristpad.\n\
\t<bold>reply<reset> message to page back the last person to page you."
);
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {

View File

@ -47,6 +47,7 @@ pub async fn describe_normal_item(
let all_groups: Vec<Vec<&Arc<Item>>> = items let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter() .iter()
.filter(|it| it.action_type != LocationActionType::Worn)
.group_by(|i| (i.display_for_sentence(true, 1, false), &i.action_type)) .group_by(|i| (i.display_for_sentence(true, 1, false), &i.action_type))
.into_iter() .into_iter()
.map(|(_, g)| g.collect::<Vec<&Arc<Item>>>()) .map(|(_, g)| g.collect::<Vec<&Arc<Item>>>())

View File

@ -28,6 +28,7 @@ use crate::{
}, },
static_content::{ static_content::{
dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget}, dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget},
npc::check_for_instant_aggro,
room::{self, Direction, ExitClimb, ExitType, MaterialType}, room::{self, Direction, ExitClimb, ExitType, MaterialType},
}, },
DResult, DResult,
@ -657,6 +658,8 @@ async fn attempt_move_immediate(
} }
} }
check_for_instant_aggro(&ctx.trans, &mut ctx.item).await?;
Ok(true) Ok(true)
} }

View File

@ -41,14 +41,13 @@ impl UserVerb for Verb {
player_item.health, player_item.health,
maxh maxh
)); ));
let (hunger, thirst, bladder, stress) = match player_item.urges.as_ref() { let (hunger, thirst, stress) = match player_item.urges.as_ref() {
None => (0, 0, 0, 0), None => (0, 0, 0),
Some(Urges { Some(Urges {
hunger, hunger,
thirst, thirst,
bladder,
stress, stress,
}) => (hunger.value, thirst.value, bladder.value, stress.value), }) => (hunger.value, thirst.value, stress.value),
}; };
msg.push_str(&format!( msg.push_str(&format!(
ansi!("<bold>Hunger [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"), ansi!("<bold>Hunger [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
@ -68,13 +67,6 @@ impl UserVerb for Verb {
stress / 100, stress / 100,
100 100
)); ));
if bladder >= 7500 {
msg.push_str("Your bladder is so full it hurts!\n");
} else if bladder >= 5000 {
msg.push_str("You really need the toilet!\n");
} else if bladder >= 2500 {
msg.push_str("Your bladder is slightly full.\n");
}
msg.push_str(&format!( msg.push_str(&format!(
ansi!("<bold>Credits <green>${}<reset>\n"), ansi!("<bold>Credits <green>${}<reset>\n"),

View File

@ -361,7 +361,6 @@ impl Default for Urge {
#[serde(default)] #[serde(default)]
pub struct Urges { pub struct Urges {
pub hunger: Urge, pub hunger: Urge,
pub bladder: Urge,
pub thirst: Urge, pub thirst: Urge,
pub stress: Urge, pub stress: Urge,
} }
@ -370,7 +369,6 @@ impl Default for Urges {
fn default() -> Self { fn default() -> Self {
Self { Self {
hunger: Default::default(), hunger: Default::default(),
bladder: Default::default(),
thirst: Default::default(), thirst: Default::default(),
stress: Default::default(), stress: Default::default(),
} }

View File

@ -342,19 +342,19 @@ impl TaskHandler for AttackTaskHandler {
let weapon = what_wielded(ctx.trans, &attacker_item).await?; let weapon = what_wielded(ctx.trans, &attacker_item).await?;
if process_attack( process_attack(
ctx, ctx,
&mut attacker_item, &mut attacker_item,
&mut victim_item, &mut victim_item,
&weapon.normal_attack, &weapon.normal_attack,
&weapon, &weapon,
) )
.await? .await?;
{
Ok(None) // We re-check this on the next tick, rather than going off if the victim
} else { // died. That prevents a bug when re-focusing where we re-schedule and then
Ok(Some(attack_speed(&attacker_item))) // re-delete the task.
} Ok(Some(attack_speed(&attacker_item)))
} }
} }
@ -513,7 +513,6 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
None => {} None => {}
Some(urges) => { Some(urges) => {
urges.hunger = Default::default(); urges.hunger = Default::default();
urges.bladder = Default::default();
urges.thirst = Default::default(); urges.thirst = Default::default();
urges.stress = Default::default(); urges.stress = Default::default();
} }

View File

@ -384,11 +384,11 @@ pub async fn skill_check_and_grind(
let gap = calc_level_gap(who, skill, diff_level); let gap = calc_level_gap(who, skill, diff_level);
let result = skill_check_fn(gap); let result = skill_check_fn(gap);
// If the skill gap is 0, probability of learning is 0.5 // If the skill gap is 0, probability of learning is 0.75
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease). // If the skill gap is 1, probability of learning is 0.6 (20% less), and so on (exponential decrease).
const LAMBDA: f64 = -0.2231435513142097; // log 0.8 const LAMBDA: f64 = -0.2231435513142097; // log 0.8
if who.item_type == "player" if who.item_type == "player"
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap.abs() as f64)).exp() && rand::thread_rng().gen::<f64>() < 0.75 * (LAMBDA * (gap.abs() as f64)).exp()
{ {
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? { if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
if let Some(mut user) = trans.find_by_username(&who.item_code).await? { if let Some(mut user) = trans.find_by_username(&who.item_code).await? {

View File

@ -112,36 +112,6 @@ pub async fn hunger_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
Ok(()) Ok(())
} }
pub async fn bladder_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.bladder) {
None => return Ok(()),
Some(u) => u,
};
if !urge_threshold_check(&urge) {
return Ok(());
}
if who.item_type == "player" && urge.value > urge.last_value {
let msg = if urge.value < 5000 {
"You feel a slight pressure building in your bladder."
} else if urge.value < 7500 {
"You've really got to find a toilet soon."
} else if urge.value < 10000 {
"You're absolutely busting!"
} else {
"You can't hold your bladder any longer."
};
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
trans
.queue_for_session(&sess, Some(&format!("{}\n", msg)))
.await?;
}
}
// TODO soil the room
Ok(())
}
pub async fn thirst_changed(trans: &DBTrans, who: &Item) -> DResult<()> { pub async fn thirst_changed(trans: &DBTrans, who: &Item) -> DResult<()> {
let urge = match who.urges.as_ref().map(|urg| &urg.thirst) { let urge = match who.urges.as_ref().map(|urg| &urg.thirst) {
None => return Ok(()), None => return Ok(()),
@ -272,10 +242,6 @@ impl TaskHandler for TickUrgesTaskHandler {
for item in ctx.trans.get_urges_crossed_milestones("hunger").await? { for item in ctx.trans.get_urges_crossed_milestones("hunger").await? {
hunger_changed(&ctx.trans, &item).await?; hunger_changed(&ctx.trans, &item).await?;
} }
ctx.trans.apply_urge_tick("bladder").await?;
for item in ctx.trans.get_urges_crossed_milestones("bladder").await? {
bladder_changed(&ctx.trans, &item).await?;
}
ctx.trans.apply_urge_tick("thirst").await?; ctx.trans.apply_urge_tick("thirst").await?;
for item in ctx.trans.get_urges_crossed_milestones("thirst").await? { for item in ctx.trans.get_urges_crossed_milestones("thirst").await? {
thirst_changed(&ctx.trans, &item).await?; thirst_changed(&ctx.trans, &item).await?;
@ -387,10 +353,6 @@ pub async fn recalculate_urge_growth(_trans: &DBTrans, item: &mut Item) -> DResu
growth: 0, growth: 0,
..old_urges.thirst ..old_urges.thirst
}, // To do: climate based? }, // To do: climate based?
bladder: Urge {
growth: 42,
..old_urges.bladder
},
stress: Urge { stress: Urge {
growth: (-(cool.max(7.0) - 7.0) * 10.0 * relax_action_factor) as i16, growth: (-(cool.max(7.0) - 7.0) * 10.0 * relax_action_factor) as i16,
..old_urges.stress ..old_urges.stress

View File

@ -18,6 +18,7 @@ pub mod species;
pub struct StaticItem { pub struct StaticItem {
pub item_code: &'static str, pub item_code: &'static str,
pub initial_item: Box<dyn Fn() -> Item>, pub initial_item: Box<dyn Fn() -> Item>,
pub extra_items_on_create: Box<dyn Fn(&Item) -> Box<dyn Iterator<Item = Item>>>,
} }
pub struct StaticTask { pub struct StaticTask {
@ -102,8 +103,13 @@ async fn refresh_static_items(pool: &DBPool) -> DResult<()> {
} }
for new_item_code in expected_set.difference(&existing_items) { for new_item_code in expected_set.difference(&existing_items) {
info!("Creating item {:?}", new_item_code); info!("Creating item {:?}", new_item_code);
tx.create_item(&(expected_items.get(new_item_code).unwrap().initial_item)()) let expected_item = expected_items.get(new_item_code).unwrap();
.await?; let new_item = &(expected_item.initial_item)();
tx.create_item(new_item).await?;
for mut item in (expected_item.extra_items_on_create)(&new_item) {
item.item_code = format!("{}", tx.alloc_item_code().await?);
tx.create_item(&item).await?;
}
} }
for existing_item_code in expected_set.intersection(&existing_items) { for existing_item_code in expected_set.intersection(&existing_items) {
tx.limited_update_static_item(&(expected_items tx.limited_update_static_item(&(expected_items

View File

@ -98,5 +98,6 @@ pub fn static_items() -> Box<dyn Iterator<Item = StaticItem>> {
}, },
..Item::default() ..Item::default()
}), }),
extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())),
})) }))
} }

View File

@ -13,14 +13,14 @@ use crate::{
}, },
models::{ models::{
consent::ConsentType, consent::ConsentType,
item::{Item, ItemFlag, Pronouns, SkillType, StatType}, item::{Item, ItemFlag, LocationActionType, Pronouns, SkillType, StatType},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence}, task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
}, },
regular_tasks::{ regular_tasks::{
queued_command::{queue_command_for_npc_and_save, MovementSource, QueueCommand}, queued_command::{queue_command_for_npc_and_save, MovementSource, QueueCommand},
TaskHandler, TaskRunContext, TaskHandler, TaskRunContext,
}, },
services::combat::{corpsify_item, start_attack}, services::combat::{corpsify_item, start_attack, start_attack_mut},
DResult, DResult,
}; };
use async_trait::async_trait; use async_trait::async_trait;
@ -33,6 +33,7 @@ use std::collections::BTreeMap;
use std::time; use std::time;
use uuid::Uuid; use uuid::Uuid;
pub mod computer_museum_npcs;
mod melbs_citizen; mod melbs_citizen;
mod melbs_dog; mod melbs_dog;
mod roboporter; mod roboporter;
@ -85,16 +86,24 @@ pub struct HireData {
pub handler: &'static (dyn HireHandler + Sync + Send), pub handler: &'static (dyn HireHandler + Sync + Send),
} }
pub struct NPCSpawnPossession {
what: PossessionType,
action_type: LocationActionType,
wear_layer: i64, // 0 is on top, higher under.
}
pub struct NPC { pub struct NPC {
pub code: &'static str, pub code: &'static str,
pub name: &'static str, pub name: &'static str,
pub pronouns: Pronouns, pub pronouns: Pronouns,
pub description: &'static str, pub description: &'static str,
pub spawn_location: &'static str, pub spawn_location: &'static str,
pub spawn_possessions: Vec<NPCSpawnPossession>,
pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>, pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>,
pub aliases: Vec<&'static str>, pub aliases: Vec<&'static str>,
pub says: Vec<NPCSayInfo>, pub says: Vec<NPCSayInfo>,
pub aggression: u64, pub aggression: u64, // 1-20 sets aggro time, >= 21 = instant aggro.
pub aggro_pc_only: bool,
pub max_health: u64, pub max_health: u64,
pub intrinsic_weapon: Option<PossessionType>, pub intrinsic_weapon: Option<PossessionType>,
pub total_xp: u64, pub total_xp: u64,
@ -116,6 +125,7 @@ impl Default for NPC {
pronouns: Pronouns::default_animate(), pronouns: Pronouns::default_animate(),
description: "default", description: "default",
spawn_location: "default", spawn_location: "default",
spawn_possessions: vec![],
message_handler: None, message_handler: None,
aliases: vec![], aliases: vec![],
says: vec![], says: vec![],
@ -131,6 +141,7 @@ impl Default for NPC {
.collect(), .collect(),
total_stats: vec![].into_iter().collect(), total_stats: vec![].into_iter().collect(),
aggression: 0, aggression: 0,
aggro_pc_only: false,
max_health: 24, max_health: 24,
intrinsic_weapon: None, intrinsic_weapon: None,
species: SpeciesType::Human, species: SpeciesType::Human,
@ -159,6 +170,7 @@ pub fn npc_list() -> &'static Vec<NPC> {
npcs.append(&mut melbs_citizen::npc_list()); npcs.append(&mut melbs_citizen::npc_list());
npcs.append(&mut melbs_dog::npc_list()); npcs.append(&mut melbs_dog::npc_list());
npcs.append(&mut roboporter::npc_list()); npcs.append(&mut roboporter::npc_list());
npcs.append(&mut computer_museum_npcs::npc_list());
npcs npcs
}) })
} }
@ -185,37 +197,50 @@ pub fn npc_say_info_by_npc_code_say_code(
} }
pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> { pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
Box::new(npc_list().iter().map(|c| StaticItem { Box::new(npc_list().iter().map(|c| {
item_code: c.code, StaticItem {
initial_item: Box::new(|| { item_code: c.code,
let mut flags: Vec<ItemFlag> = c.extra_flags.clone(); initial_item: Box::new(|| {
if c.hire_data.is_some() { let mut flags: Vec<ItemFlag> = c.extra_flags.clone();
flags.push(ItemFlag::Hireable); if c.hire_data.is_some() {
// Revise if we ever want NPCs to attack for-hire NPCs. flags.push(ItemFlag::Hireable);
flags.push(ItemFlag::NPCsDontAttack); // Revise if we ever want NPCs to attack for-hire NPCs.
} flags.push(ItemFlag::NPCsDontAttack);
Item { }
item_code: c.code.to_owned(), Item {
item_type: "npc".to_owned(), item_code: c.code.to_owned(),
display: c.name.to_owned(), item_type: "npc".to_owned(),
details: Some(c.description.to_owned()), display: c.name.to_owned(),
location: c.spawn_location.to_owned(), details: Some(c.description.to_owned()),
is_static: true, location: c.spawn_location.to_owned(),
pronouns: c.pronouns.clone(), is_static: true,
total_xp: c.total_xp.clone(), pronouns: c.pronouns.clone(),
total_skills: c.total_skills.clone(), total_xp: c.total_xp.clone(),
total_stats: c.total_stats.clone(), total_skills: c.total_skills.clone(),
species: c.species.clone(), total_stats: c.total_stats.clone(),
health: c.max_health.clone(), species: c.species.clone(),
flags, health: c.max_health.clone(),
aliases: c flags,
.aliases aliases: c
.iter() .aliases
.map(|a| (*a).to_owned()) .iter()
.collect::<Vec<String>>(), .map(|a| (*a).to_owned())
..Item::default() .collect::<Vec<String>>(),
} ..Item::default()
}), }
}),
extra_items_on_create: Box::new(|npc_item| {
let npc_ref = npc_item.refstr();
Box::new(c.spawn_possessions.iter().map(move |spawn_item| {
let mut pos_it: Item = spawn_item.what.clone().into();
pos_it.location = npc_ref.clone();
pos_it.action_type = spawn_item.action_type.clone();
pos_it.action_type_started =
Some(Utc::now() - chrono::Duration::seconds(spawn_item.wear_layer));
pos_it
}))
}),
}
})) }))
} }
@ -277,6 +302,8 @@ pub fn npc_wander_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
) )
} }
const NONINSTANT_AGGRESSION: u64 = 20;
pub fn npc_aggro_tasks() -> Box<dyn Iterator<Item = StaticTask>> { pub fn npc_aggro_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
Box::new( Box::new(
npc_list() npc_list()
@ -286,7 +313,8 @@ pub fn npc_aggro_tasks() -> Box<dyn Iterator<Item = StaticTask>> {
task_code: c.code.to_owned(), task_code: c.code.to_owned(),
initial_task: Box::new(|| { initial_task: Box::new(|| {
let mut rng = thread_rng(); let mut rng = thread_rng();
let aggro_time = (rng.gen_range(450..550) as u64) / c.aggression; let aggro_time =
(rng.gen_range(450..550) as u64) / c.aggression.min(NONINSTANT_AGGRESSION);
Task { Task {
meta: TaskMeta { meta: TaskMeta {
task_code: c.code.to_owned(), task_code: c.code.to_owned(),
@ -475,6 +503,10 @@ impl TaskHandler for NPCAggroTaskHandler {
} }
Some(r) => r, Some(r) => r,
}; };
let npc = match npc_by_code().get(npc_code.as_str()) {
None => return Ok(None),
Some(r) => r,
};
if item.death_data.is_some() if item.death_data.is_some()
|| item || item
.active_combat .active_combat
@ -488,7 +520,7 @@ impl TaskHandler for NPCAggroTaskHandler {
let vic_opt = items_loc let vic_opt = items_loc
.iter() .iter()
.filter(|it| { .filter(|it| {
(it.item_type == "player" || it.item_type == "npc") (it.item_type == "player" || (!npc.aggro_pc_only && it.item_type == "npc"))
&& it.death_data.is_none() && it.death_data.is_none()
&& !it.flags.contains(&ItemFlag::NPCsDontAttack) && !it.flags.contains(&ItemFlag::NPCsDontAttack)
&& it.active_climb.is_none() && it.active_climb.is_none()
@ -507,6 +539,43 @@ impl TaskHandler for NPCAggroTaskHandler {
} }
pub static AGGRO_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCAggroTaskHandler; pub static AGGRO_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &NPCAggroTaskHandler;
pub async fn check_for_instant_aggro(trans: &DBTrans, player_item: &mut Item) -> DResult<()> {
for item in &trans.find_items_by_location(&player_item.location).await? {
if item.item_type == player_item.item_type && item.item_code == player_item.item_code {
continue;
}
if item.item_type != "npc" {
continue;
}
let npc = match npc_by_code().get(item.item_code.as_str()) {
None => continue,
Some(r) => r,
};
if npc.aggression <= NONINSTANT_AGGRESSION {
continue;
}
if npc.aggro_pc_only && player_item.item_type != "player" {
continue;
}
if item.death_data.is_some()
|| item
.active_combat
.as_ref()
.map(|ac| ac.attacking.is_some())
.unwrap_or(false)
{
continue;
}
let mut item_mut: Item = (**item).clone();
match start_attack_mut(trans, &mut item_mut, player_item).await {
Ok(()) | Err(CommandHandlingError::UserError(_)) => {}
Err(CommandHandlingError::SystemError(e)) => Err(e)?,
}
trans.save_item_model(&item_mut).await?;
}
Ok(())
}
pub struct NPCRecloneTaskHandler; pub struct NPCRecloneTaskHandler;
#[async_trait] #[async_trait]
impl TaskHandler for NPCRecloneTaskHandler { impl TaskHandler for NPCRecloneTaskHandler {
@ -535,6 +604,15 @@ impl TaskHandler for NPCRecloneTaskHandler {
npc_item.health = npc.max_health; npc_item.health = npc.max_health;
npc_item.location = npc.spawn_location.to_owned(); npc_item.location = npc.spawn_location.to_owned();
ctx.trans.save_item_model(&npc_item).await?; ctx.trans.save_item_model(&npc_item).await?;
for spawn_item in &npc.spawn_possessions {
let mut item: Item = spawn_item.what.clone().into();
item.location = npc_item.refstr();
item.item_code = format!("{}", ctx.trans.alloc_item_code().await?);
item.action_type = spawn_item.action_type.clone();
ctx.trans.create_item(&item).await?;
}
return Ok(None); return Ok(None);
} }
} }

View File

@ -0,0 +1,119 @@
use super::{NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC};
use crate::{
models::{
consent::ConsentType,
item::{LocationActionType, Pronouns, SkillType},
},
static_content::{npc::KillBonus, possession_type::PossessionType, species::SpeciesType},
};
pub fn npc_list() -> Vec<NPC> {
use NPCSayType::FromFixedList;
let geek_stdsay = NPCSayInfo {
say_code: "babble",
frequency_secs: 120,
talk_type: FromFixedList(vec!(
(false, "I wish I could get into the secret hackers club - I heard they have a machine that hacks your wristpad to make you a super smart dork."),
(false, "The basement here is pretty dangerous with all those robots."),
(false, "I overhead a real dork bragging about how he managed to get all four rings onto the rightmost virtual peg. No idea what that was about!"),
(false, "Why is it that all the dorks around here keep whispering stuff to each other about Towers of Hanoi?"),
))
};
let killbot_stdsay = NPCSayInfo {
say_code: "babble",
frequency_secs: 20,
talk_type: FromFixedList(vec!(
(false, "My mission is to EXTERMINATE"),
(false, "Intruder kill mode active"),
(false, "Recoverable Error: Preset Kill Limit not set. Treating as unlimited"),
(false, "404 action module STUN not found. Falling back to action KILL underscore WITH underscore EXTREME underscore PREJUDICE"),
))
};
macro_rules! geek_gender_word {
(default_male) => {
"guy"
};
(default_female) => {
"gal"
};
}
macro_rules! geek {
($code: expr, $adj: expr, $spawn: expr, $pronouns: ident) => {
NPC {
code: concat!("computer_museum_geek_", $code),
name: concat!($adj, " geek"),
pronouns: Pronouns { is_proper: false, ..Pronouns::$pronouns() },
description: concat!("A geeky looking ", geek_gender_word!($pronouns), " with horn-rimmed glasses, and a button-down shirt. In the pocket is a pocket protector"),
spawn_location: concat!("room/computer_museum_", $spawn),
spawn_possessions: vec![
NPCSpawnPossession {
what: PossessionType::Shirt,
action_type: LocationActionType::Worn,
wear_layer: 0,
},
NPCSpawnPossession {
what: PossessionType::Jeans,
action_type: LocationActionType::Worn,
wear_layer: 0,
},
],
message_handler: None,
wander_zones: vec!("computer_museum"),
says: vec!(geek_stdsay.clone()),
player_consents: vec!(ConsentType::Medicine),
..Default::default()
}
}
}
macro_rules! killbot {
($code: expr, $adj: expr, $spawn: expr) => {
NPC {
code: concat!("computer_museum_killbot_", $code),
name: concat!($adj, " Killbot"),
species: SpeciesType::Robot,
pronouns: Pronouns {
is_proper: false,
..Pronouns::default_inanimate()
},
description: concat!("A mean looking robot, complete with a vicious spiked mace and powerful solonoids for smashing it hard into whatever unfortunate victim this bot targets"),
spawn_location: concat!("room/computer_museum_", $spawn),
spawn_possessions: vec![],
aggression: 21,
aggro_pc_only: true,
total_xp: 3000,
total_skills: SkillType::values()
.into_iter()
.map(|sk| {
(
sk.clone(),
if &sk == &SkillType::Clubs { 12.0 } else { 10.0 },
)
})
.collect(),
intrinsic_weapon: Some(PossessionType::SpikedMace),
message_handler: None,
wander_zones: vec!["computer_museum"],
kill_bonus: Some(KillBonus {
msg: "On your wristpad: Those robots have been the bane of our existence at the museum. Thanks for taking one out! Here's a small token of our gratitude.",
payment: 120,
}),
says: vec![killbot_stdsay.clone()],
player_consents: vec![ConsentType::Fight],
..Default::default()
}
};
}
vec![
geek!("1", "excited", "lobby", default_male),
geek!("2", "mesmerised", "lobby", default_female),
killbot!("3", "abhorrent", "hw_1"),
killbot!("4", "berserk", "hw_2"),
killbot!("5", "vicious", "hw_3"),
killbot!("6", "murderous", "club_door"),
]
}

View File

@ -1,5 +1,11 @@
use super::{NPCSayInfo, NPCSayType, NPC}; use super::{NPCSayInfo, NPCSayType, NPCSpawnPossession, NPC};
use crate::models::{consent::ConsentType, item::Pronouns}; use crate::{
models::{
consent::ConsentType,
item::{LocationActionType, Pronouns},
},
static_content::possession_type::PossessionType,
};
pub fn npc_list() -> Vec<NPC> { pub fn npc_list() -> Vec<NPC> {
use NPCSayType::FromFixedList; use NPCSayType::FromFixedList;
@ -26,6 +32,18 @@ pub fn npc_list() -> Vec<NPC> {
pronouns: $pronouns, pronouns: $pronouns,
description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harsh reality of post-apocalyptic life", description: "A fairly ordinary looking citizen of Melbs, clearly weary from the harsh reality of post-apocalyptic life",
spawn_location: concat!("room/melbs_", $spawn), spawn_location: concat!("room/melbs_", $spawn),
spawn_possessions: vec![
NPCSpawnPossession {
what: PossessionType::Shirt,
action_type: LocationActionType::Worn,
wear_layer: 0,
},
NPCSpawnPossession {
what: PossessionType::Jeans,
action_type: LocationActionType::Worn,
wear_layer: 0,
},
],
message_handler: None, message_handler: None,
wander_zones: vec!("melbs"), wander_zones: vec!("melbs"),
says: vec!(melbs_citizen_stdsay.clone()), says: vec!(melbs_citizen_stdsay.clone()),

View File

@ -19,6 +19,7 @@ mod benches;
mod blade; mod blade;
mod books; mod books;
mod bottles; mod bottles;
mod club;
mod corp_licence; mod corp_licence;
mod fangs; mod fangs;
mod food; mod food;
@ -398,7 +399,9 @@ pub enum PossessionType {
// Armour // Armour
RustyMetalPot, RustyMetalPot,
HockeyMask, HockeyMask,
Shirt,
LeatherJacket, LeatherJacket,
Jeans,
LeatherPants, LeatherPants,
// Weapons: Whips // Weapons: Whips
AntennaWhip, AntennaWhip,
@ -411,6 +414,8 @@ pub enum PossessionType {
Electroblade, Electroblade,
FlameScimitar, FlameScimitar,
NanobladeGladius, NanobladeGladius,
// Weapons: Clubs
SpikedMace,
// Medical // Medical
MediumTraumaKit, MediumTraumaKit,
EmptyMedicalBox, EmptyMedicalBox,
@ -511,6 +516,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
.chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(bottles::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(bottles::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(trauma_kit::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( .chain(
corp_licence::data() corp_licence::data()
.iter() .iter()

View File

@ -0,0 +1,48 @@
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::{models::item::SkillType, static_content::possession_type::DamageType};
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::SpikedMace,
PossessionData {
display: "spiked mace",
details: "A heavy metal mace with vicious looking spikes",
aliases: vec!["club", "mace"],
weight: 3000,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Clubs,
raw_min_to_learn: 2.0,
raw_max_to_learn: 4.0,
normal_attack: WeaponAttackData {
start_messages: vec![Box::new(|attacker, victim, exp| {
format!(
"{} aims {} spiked mace at {}",
&attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false),
)
})],
success_messages: vec![Box::new(|attacker, victim, part, exp| {
format!(
"{}'s spiked mace smashes with force into {}'s {}",
&attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone())
)
})],
mean_damage: 3.0,
stdev_damage: 3.0,
base_damage_type: DamageType::Beat,
other_damage_types: vec![(0.25, DamageType::Pierce)],
..Default::default()
},
..Default::default()
}),
..Default::default()
},
)]
})
}

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use super::{DamageType, PossessionData, PossessionType, SoakData, WearData}; use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
use crate::static_content::species::BodyPart; use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -5,6 +7,23 @@ use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new(); static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(
PossessionType::Jeans,
PossessionData {
display: "pair of jeans",
details: "A fairly plain pair of jeans that probably provides no protection against anything except fashion crimes",
aliases: vec!["jeans", "pants"],
weight: 200,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Groin,
BodyPart::Legs),
thickness: 2.0,
dodge_penalty: 0.0,
soaks: BTreeMap::new()
}),
..Default::default()
}
),
( (
PossessionType::LeatherPants, PossessionType::LeatherPants,
PossessionData { PossessionData {

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use super::{DamageType, PossessionData, PossessionType, SoakData, WearData}; use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
use crate::static_content::species::BodyPart; use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -5,6 +7,24 @@ use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new(); static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(
PossessionType::Shirt,
PossessionData {
display: "shirt",
details: "A fairly plain shirt that probably provides no protection against anything except fashion crimes",
aliases: vec!("shirt"),
weight: 70,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Arms,
BodyPart::Chest,
BodyPart::Back),
thickness: 2.0,
dodge_penalty: 0.0,
soaks: BTreeMap::new(),
}),
..Default::default()
}
),
( (
PossessionType::LeatherJacket, PossessionType::LeatherJacket,
PossessionData { PossessionData {

View File

@ -11,6 +11,7 @@ use std::collections::BTreeMap;
mod chonkers; mod chonkers;
mod cok_murl; mod cok_murl;
mod computer_museum;
mod melbs; mod melbs;
mod repro_xv; mod repro_xv;
mod special; mod special;
@ -50,6 +51,11 @@ pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> {
display: "Chonker's Gym", display: "Chonker's Gym",
outdoors: false, outdoors: false,
}, },
Zone {
code: "computer_museum",
display: "Computer Museum",
outdoors: false,
},
] ]
.into_iter() .into_iter()
.map(|x| (x.code, x)) .map(|x| (x.code, x))
@ -357,6 +363,7 @@ pub fn room_list() -> &'static Vec<Room> {
rooms.append(&mut cok_murl::room_list()); rooms.append(&mut cok_murl::room_list());
rooms.append(&mut chonkers::room_list()); rooms.append(&mut chonkers::room_list());
rooms.append(&mut special::room_list()); rooms.append(&mut special::room_list());
rooms.append(&mut computer_museum::room_list());
rooms.into_iter().collect() rooms.into_iter().collect()
}) })
} }
@ -398,6 +405,7 @@ pub fn room_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
flags: r.item_flags.clone(), flags: r.item_flags.clone(),
..Item::default() ..Item::default()
}), }),
extra_items_on_create: Box::new(|_| Box::new(std::iter::empty())),
})) }))
} }

View File

@ -0,0 +1,173 @@
use super::{Direction, Exit, ExitTarget, GridCoords, Room, SecondaryZoneRecord};
use ansi::ansi;
pub fn room_list() -> Vec<Room> {
vec!(
Room {
zone: "computer_museum",
secondary_zones: vec!(
SecondaryZoneRecord {
zone: "melbs",
short: ansi!("<bgyellow><blue>CM<reset>"),
grid_coords: GridCoords { x: 2, y: -3, z: 0 },
caption: Some("Computer Museum")
}
),
code: "computer_museum_lobby",
name: "Lobby",
short: ansi!("<bgblack><white><lt>=<reset>"),
description: ansi!("A large room, full of glass cases containing computer equipment from various eras, from ancient dusty looking machines, to miniturised modern equipment. Some of the computer equipment is even powered up, showing scrolling text and various demonstration images. A dusty staircase, above which is hung a faded sign saying <red>'Danger, robots in use, do not enter'<reset>, leads down"),
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
exits: vec!(
Exit {
direction: Direction::WEST,
target: ExitTarget::Custom("room/melbs_kingst_20"),
..Default::default()
},
Exit {
direction: Direction::DOWN,
..Default::default()
},
),
should_caption: true,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_club_stairwell",
name: "Stairwell",
short: ansi!("<bgblack><yellow>>=<reset>"),
description: ansi!("This appears to be the start of a long corridor. Floor, walls, and ceiling are all made of concrete. A plain concrete staircase leads up. Painted on the floor to the east is a red line adorned with the text <red>DANGER ROBOTS DO NOT CROSS<reset>. The corridor echoes with screams of torment and the sound of metal cutting into flesh. You can just make out a door at the east end of the corridor"),
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 0, z: -1 },
exits: vec!(
Exit {
direction: Direction::UP,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
repel_npc: true,
should_caption: true,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_hw_1",
name: "Corridor Segment 1",
short: ansi!("<bgblack><yellow>==<reset>"),
description: ansi!("This appears to be part of a long corridor. Floor, walls, and ceiling are all made of concrete. Painted on the ground to the west is the text <red>DANGER ROBOTS DO NOT CROSS<reset>. The corridor continues to the east. To the east, you see some kind of marking on the concrete wall. The corridor echoes with screams of torment and the sound of metal cutting into flesh. You can make out a door at the east end of the corridor, with some kind of electronic screen on it. A plaque on the wall tells you this is Corridor Segment 1"),
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: -1 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_hw_2",
name: "Corridor Segment 2",
short: ansi!("<bgblack><yellow>==<reset>"),
description: ansi!("This appears to be part of a long corridor. Floor, walls, and ceiling are all made of concrete. The corridor continues to the east and west. The corridor echoes with screams of torment and the sound of metal cutting into flesh. A stain, apparently done in blood, says in scrawled, panicked looking writing: <yellow><bgblack>Beware Robots! Discs to third tower<reset>. You can see a door at the east end of the corridor, with some kind of electronic screen on it, showing some kind of towers. A plaque on the wall tells you this is Corridor Segment 2"),
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 0, z: -1 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_hw_3",
name: "Corridor Segment 3",
short: ansi!("<bgblack><yellow>==<reset>"),
description: ansi!("This appears to be part of a long corridor. Floor, walls, and ceiling are all made of concrete. Painted on the ground to the west is the text <red>DANGER ROBOTS DO NOT CROSS<reset>. The corridor continues to the east and west. To the west, you see some kind of marking on the concrete wall. The corridor echoes with screams of torment and the sound of metal cutting into flesh. You can make out a door at the east end of the corridor, with some kind of electronic screen on it, showing some kind of towers. A plaque on the wall tells you this is Corridor Segment 3"),
description_less_explicit: None,
grid_coords: GridCoords { x: 3, y: 0, z: -1 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_club_door",
name: "Doorwell",
short: ansi!("<bgblack><yellow>/\\<reset>"),
description: ansi!("This appears to be a security zone protecting access to a door to the north. A long corridor stretches to the west. The corridor echoes with screams of torment and the sound of metal cutting into flesh.\n\
Mounted on the door is a large screen, showing primative ASCII art of some towers, \
with with some kind of electronic screen on it, showing some kind of towers with \
coloured disks stacked on them.\n\
The disks are arranged as follows:\n\
Tower 1: A small <green>green disk<reset>, atop a medium sized <yellow>yellow disk<reset>, \
atop a large <bgblack><white>white disk<reset>, atop an extra large <red>red disk<reset>.\n\
Tower 2: Empty\n\
Tower 3: Empty"),
description_less_explicit: None,
grid_coords: GridCoords { x: 4, y: 0, z: -1 },
exits: vec!(
Exit {
direction: Direction::NORTH,
..Default::default()
},
Exit {
direction: Direction::WEST,
..Default::default()
},
),
should_caption: true,
..Default::default()
},
Room {
zone: "computer_museum",
secondary_zones: vec![],
code: "computer_museum_hackers_club",
name: "Hackers' Club",
short: ansi!("<bgblack><green>HC<reset>"),
description: ansi!("A room full of beeping and whirring equipment. One shiny stainless steel piece of equipment really catches your eye. It has a large plaque on it saying: Wristpad hacking unit - intelligence upgrade program"),
description_less_explicit: None,
grid_coords: GridCoords { x: 4, y: -1, z: -1 },
exits: vec!(
Exit {
direction: Direction::SOUTH,
..Default::default()
},
),
should_caption: true,
..Default::default()
},
)
}

View File

@ -52,7 +52,14 @@ pub fn room_list() -> Vec<Room> {
}, },
Room { Room {
zone: "melbs", zone: "melbs",
secondary_zones: vec!(), secondary_zones: vec!(
SecondaryZoneRecord {
zone: "computer_museum",
short: ansi!("<bggreen><white>EX<reset>"),
grid_coords: GridCoords { x: -1, y: 0, z: 0 },
caption: Some("Melbs"),
}
),
code: "melbs_kingst_20", code: "melbs_kingst_20",
name: "King Street - 20 block", name: "King Street - 20 block",
short: ansi!("<yellow>||<reset>"), short: ansi!("<yellow>||<reset>"),
@ -68,6 +75,11 @@ pub fn room_list() -> Vec<Room> {
direction: Direction::SOUTH, direction: Direction::SOUTH,
..Default::default() ..Default::default()
}, },
Exit {
direction: Direction::EAST,
target: ExitTarget::Custom("room/computer_museum_lobby"),
..Default::default()
},
), ),
should_caption: false, should_caption: false,
..Default::default() ..Default::default()