Add journal system
Also fix up bugs with navigation during death, and awarding payouts when you don't get any XP.
This commit is contained in:
parent
6ce1aff83e
commit
078519be95
@ -32,7 +32,12 @@ impl QueueCommandHandler for QueueHandler {
|
||||
_ => user_error("Unexpected queued command".to_owned())?
|
||||
};
|
||||
let player_item = get_player_item_or_fail(ctx).await?;
|
||||
match is_door_in_direction(&ctx.trans, &direction, &player_item).await? {
|
||||
let use_location = if player_item.death_data.is_some() {
|
||||
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
|
||||
} else {
|
||||
&player_item.location
|
||||
};
|
||||
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
|
||||
DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: false, .. }, .. } =>
|
||||
@ -51,7 +56,12 @@ impl QueueCommandHandler for QueueHandler {
|
||||
_ => user_error("Unexpected queued command".to_owned())?
|
||||
};
|
||||
let player_item = get_player_item_or_fail(ctx).await?;
|
||||
let (room_1, dir_in_room, room_2) = match is_door_in_direction(&ctx.trans, &direction, &player_item).await? {
|
||||
let use_location = if player_item.death_data.is_some() {
|
||||
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
|
||||
} else {
|
||||
&player_item.location
|
||||
};
|
||||
let (room_1, dir_in_room, room_2) = match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
|
||||
DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: false, .. }, .. } =>
|
||||
|
@ -351,7 +351,7 @@ impl UserVerb for Verb {
|
||||
ctx.trans.find_item_by_type_code(heretype, herecode).await?
|
||||
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
||||
match is_door_in_direction(&ctx.trans, &dir, &player_item).await? {
|
||||
match is_door_in_direction(&ctx.trans, &dir, use_location).await? {
|
||||
DoorSituation::NoDoor |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } |
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {},
|
||||
|
@ -190,7 +190,7 @@ pub async fn attempt_move_immediate(
|
||||
&orig_mover.location
|
||||
};
|
||||
|
||||
match is_door_in_direction(trans, direction, orig_mover).await? {
|
||||
match is_door_in_direction(trans, direction, use_location).await? {
|
||||
DoorSituation::NoDoor |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {},
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, room_with_door, .. } => {
|
||||
|
@ -75,10 +75,12 @@ impl TaskHandler for SwingShutHandler {
|
||||
|
||||
pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut VerbContext<'_>>,
|
||||
who: &Item, direction: &Direction) -> UResult<()> {
|
||||
if who.death_data.is_some() {
|
||||
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?;
|
||||
}
|
||||
let (room_1, dir_in_room, room_2) = match is_door_in_direction(trans, &direction, &who).await? {
|
||||
let use_location = if who.death_data.is_some() {
|
||||
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
|
||||
} else {
|
||||
&who.location
|
||||
};
|
||||
let (room_1, dir_in_room, room_2) = match is_door_in_direction(trans, &direction, use_location).await? {
|
||||
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } =>
|
||||
@ -171,7 +173,12 @@ impl QueueCommandHandler for QueueHandler {
|
||||
_ => user_error("Unexpected command".to_owned())?
|
||||
};
|
||||
let player_item = get_player_item_or_fail(ctx).await?;
|
||||
match is_door_in_direction(&ctx.trans, &direction, &player_item).await? {
|
||||
let use_location = if player_item.death_data.is_some() {
|
||||
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
|
||||
} else {
|
||||
&player_item.location
|
||||
};
|
||||
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
|
||||
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
|
||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } =>
|
||||
@ -217,13 +224,13 @@ pub enum DoorSituation {
|
||||
DoorOutOfRoom { state: DoorState, room_with_door: Arc<Item>, new_room: Arc<Item> } // No lockable.
|
||||
}
|
||||
|
||||
pub async fn is_door_in_direction(trans: &DBTrans, direction: &Direction, player_item: &Item) ->
|
||||
pub async fn is_door_in_direction(trans: &DBTrans, direction: &Direction, use_location: &str) ->
|
||||
UResult<DoorSituation> {
|
||||
let (loc_type_t, loc_type_c) = player_item.location.split_once("/")
|
||||
let (loc_type_t, loc_type_c) = use_location.split_once("/")
|
||||
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
|
||||
let cur_loc_item = trans.find_item_by_type_code(loc_type_t, loc_type_c).await?
|
||||
.ok_or_else(|| UserError("Can't find your current location anymore.".to_owned()))?;
|
||||
let new_loc_item = direction_to_item(trans, &player_item.location, direction).await?
|
||||
let new_loc_item = direction_to_item(trans, use_location, direction).await?
|
||||
.ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?;
|
||||
if let Some(door_state) =
|
||||
cur_loc_item.door_states.as_ref()
|
||||
|
@ -4,3 +4,4 @@ pub mod item;
|
||||
pub mod task;
|
||||
pub mod consent;
|
||||
pub mod corp;
|
||||
pub mod journal;
|
||||
|
28
blastmud_game/src/models/journal.rs
Normal file
28
blastmud_game/src/models/journal.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use std::collections::BTreeSet;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum JournalType {
|
||||
SlayedMeanDog,
|
||||
Died
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum JournalInProgress {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct JournalState {
|
||||
pub completed_journals: BTreeSet<JournalType>,
|
||||
pub in_progress_journals: Vec<JournalInProgress>,
|
||||
}
|
||||
|
||||
impl Default for JournalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
completed_journals: BTreeSet::new(),
|
||||
in_progress_journals: vec!(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
use super::item::{SkillType, StatType};
|
||||
use super::{
|
||||
item::{SkillType, StatType},
|
||||
journal::JournalState
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@ -11,9 +14,10 @@ pub struct UserTermData {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct UserExperienceData {
|
||||
pub spent_xp: u64, // Since last chargen complete.
|
||||
pub completed_journals: BTreeMap<String, DateTime<Utc>>,
|
||||
pub journals: JournalState,
|
||||
pub xp_change_for_this_reroll: i64,
|
||||
pub crafted_items: BTreeMap<String, u64>
|
||||
}
|
||||
@ -54,7 +58,7 @@ impl Default for UserExperienceData {
|
||||
fn default() -> Self {
|
||||
UserExperienceData {
|
||||
spent_xp: 0,
|
||||
completed_journals: BTreeMap::new(),
|
||||
journals: Default::default(),
|
||||
xp_change_for_this_reroll: 0,
|
||||
crafted_items: BTreeMap::new(),
|
||||
}
|
||||
|
@ -8,11 +8,13 @@ use crate::{
|
||||
models::{
|
||||
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
|
||||
task::{Task, TaskMeta, TaskDetails},
|
||||
journal::JournalType,
|
||||
},
|
||||
static_content::{
|
||||
possession_type::{WeaponData, possession_data, fist},
|
||||
npc::npc_by_code,
|
||||
species::species_info_map,
|
||||
journals::{check_journal_for_kill, award_journal_if_needed}
|
||||
},
|
||||
message_handler::user_commands::{user_error, UResult},
|
||||
regular_tasks::{TaskRunContext, TaskHandler},
|
||||
@ -165,25 +167,22 @@ pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item:
|
||||
None => return Ok(()),
|
||||
Some(r) => r
|
||||
};
|
||||
if by_item.total_xp >= for_item.total_xp {
|
||||
trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?;
|
||||
return Ok(());
|
||||
}
|
||||
let xp_gain =
|
||||
(((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as u64)
|
||||
.min(100);
|
||||
|
||||
if xp_gain == 0 {
|
||||
trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
by_item.total_xp += xp_gain;
|
||||
let mut user = match trans.find_by_username(&by_item.item_code).await? {
|
||||
None => return Ok(()),
|
||||
Some(r) => r
|
||||
};
|
||||
user.experience.xp_change_for_this_reroll += xp_gain as i64;
|
||||
let xp_gain = if by_item.total_xp >= for_item.total_xp {
|
||||
0
|
||||
} else {
|
||||
let xp_gain =
|
||||
(((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as u64)
|
||||
.min(100);
|
||||
|
||||
by_item.total_xp += xp_gain;
|
||||
user.experience.xp_change_for_this_reroll += xp_gain as i64;
|
||||
xp_gain
|
||||
};
|
||||
|
||||
// Now consider kill bonuses...
|
||||
if for_item.item_type == "npc" {
|
||||
@ -196,7 +195,11 @@ pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item:
|
||||
}
|
||||
|
||||
trans.save_user_model(&user).await?;
|
||||
trans.queue_for_session(&session, Some(&format!("You gained {} experience points!\n", xp_gain))).await?;
|
||||
if xp_gain == 0 {
|
||||
trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?;
|
||||
} else {
|
||||
trans.queue_for_session(&session, Some(&format!("You gained {} experience points!\n", xp_gain))).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -217,6 +220,7 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
.map(|sp| sp.corpse_butchers_into.clone()).unwrap_or_else(|| vec!()),
|
||||
..Default::default()
|
||||
});
|
||||
let vic_is_npc = whom.item_type == "npc";
|
||||
if let Some(ac) = &whom.active_combat {
|
||||
let at_str = ac.attacking.clone();
|
||||
for attacker in ac.attacked_by.clone().iter() {
|
||||
@ -224,6 +228,9 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
if let Some(aitem) = trans.find_item_by_type_code(atype, acode).await? {
|
||||
let mut new_aitem = (*aitem).clone();
|
||||
consider_reward_for(trans, &mut new_aitem, &whom).await?;
|
||||
if vic_is_npc {
|
||||
check_journal_for_kill(trans, &mut new_aitem, whom).await?;
|
||||
}
|
||||
stop_attacking_mut(trans, &mut new_aitem, whom, true).await?;
|
||||
trans.save_item_model(&new_aitem).await?;
|
||||
}
|
||||
@ -237,7 +244,7 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if whom.item_type == "npc" {
|
||||
if vic_is_npc {
|
||||
trans.upsert_task(&Task {
|
||||
meta: TaskMeta {
|
||||
task_code: whom.item_code.clone(),
|
||||
@ -250,6 +257,15 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
}).await?;
|
||||
} else if whom.item_type == "player" {
|
||||
trans.revoke_until_death_consent(&whom.item_code).await?;
|
||||
match trans.find_by_username(&whom.item_code).await? {
|
||||
None => {},
|
||||
Some(mut user) => {
|
||||
if award_journal_if_needed(trans, &mut user, whom, JournalType::Died).await? {
|
||||
trans.save_user_model(&user).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ pub mod dynzone;
|
||||
pub mod npc;
|
||||
pub mod possession_type;
|
||||
pub mod species;
|
||||
pub mod journals;
|
||||
mod fixed_item;
|
||||
|
||||
pub struct StaticItem {
|
||||
|
177
blastmud_game/src/static_content/journals.rs
Normal file
177
blastmud_game/src/static_content/journals.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use crate::{
|
||||
DResult,
|
||||
models::{
|
||||
user::User,
|
||||
item::Item,
|
||||
journal::JournalType,
|
||||
}
|
||||
};
|
||||
use std::collections::{BTreeMap};
|
||||
use once_cell::sync::OnceCell;
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
use log::warn;
|
||||
use async_trait::async_trait;
|
||||
use super::species::SpeciesType;
|
||||
use itertools::Itertools;
|
||||
|
||||
mod first_dog;
|
||||
|
||||
#[allow(unused)]
|
||||
pub enum KillSubscriptionType {
|
||||
SpecificNPCSpecies { species: SpeciesType },
|
||||
SpecificNPC { code: &'static str },
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait JournalChecker {
|
||||
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType>;
|
||||
async fn handle_kill(
|
||||
&self,
|
||||
trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
victim: &Item
|
||||
) -> DResult<bool>;
|
||||
}
|
||||
|
||||
pub struct JournalData {
|
||||
name: &'static str,
|
||||
details: &'static str,
|
||||
xp: u64,
|
||||
|
||||
}
|
||||
|
||||
pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
|
||||
static JOURNAL_TYPES: OnceCell<BTreeMap<JournalType, JournalData>> = OnceCell::new();
|
||||
return JOURNAL_TYPES.get_or_init(|| vec!(
|
||||
(JournalType::SlayedMeanDog, JournalData {
|
||||
name: "Slayed a mean dog",
|
||||
details: "killing a mean street dog for the first time.",
|
||||
xp: 100
|
||||
}),
|
||||
(JournalType::Died, JournalData {
|
||||
name: "Carked it",
|
||||
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
|
||||
xp: 150
|
||||
})
|
||||
).into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> {
|
||||
static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new();
|
||||
CHECKERS.get_or_init(|| vec!(
|
||||
&first_dog::CHECKER
|
||||
))
|
||||
}
|
||||
|
||||
pub fn checkers_by_species() ->
|
||||
&'static BTreeMap<SpeciesType,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
||||
{
|
||||
static MAP: OnceCell<BTreeMap<SpeciesType,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
OnceCell::new();
|
||||
MAP.get_or_init(|| {
|
||||
let species_groups = journal_checkers().iter().flat_map(
|
||||
|jc|
|
||||
jc.kill_subscriptions().into_iter()
|
||||
.filter_map(|sub|
|
||||
match sub {
|
||||
KillSubscriptionType::SpecificNPCSpecies { species } =>
|
||||
Some((species.clone(), jc.clone())),
|
||||
_ => None
|
||||
})
|
||||
).group_by(|v| v.0.clone());
|
||||
species_groups
|
||||
.into_iter()
|
||||
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn checkers_by_npc() ->
|
||||
&'static BTreeMap<&'static str,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
||||
{
|
||||
static MAP: OnceCell<BTreeMap<&'static str,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
OnceCell::new();
|
||||
MAP.get_or_init(|| {
|
||||
let npc_groups = journal_checkers().iter().flat_map(
|
||||
|jc|
|
||||
jc.kill_subscriptions().into_iter()
|
||||
.filter_map(|sub|
|
||||
match sub {
|
||||
KillSubscriptionType::SpecificNPC { code } =>
|
||||
Some((code.clone(), jc.clone())),
|
||||
_ => None
|
||||
})
|
||||
).group_by(|v| v.0.clone());
|
||||
npc_groups
|
||||
.into_iter()
|
||||
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn award_journal_if_needed(trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
journal: JournalType) -> DResult<bool> {
|
||||
if user.experience.journals.completed_journals.contains(&journal) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let journal_data = match journal_types().get(&journal) {
|
||||
None => {
|
||||
warn!("Tried to award journal type {:#?} that doesn't exist.", &journal);
|
||||
return Ok(false);
|
||||
},
|
||||
Some(v) => v
|
||||
};
|
||||
user.experience.journals.completed_journals.insert(journal);
|
||||
// Note: Not counted as 'change for this reroll' since it is permanent.
|
||||
player.total_xp += journal_data.xp;
|
||||
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
|
||||
trans.queue_for_session(
|
||||
&sess,
|
||||
Some(&format!("Journal earned: {} - You earned {} XP for {}\n",
|
||||
journal_data.name, journal_data.xp, journal_data.details)
|
||||
)).await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn check_journal_for_kill(trans: &DBTrans,
|
||||
player: &mut Item,
|
||||
victim: &Item) -> DResult<bool> {
|
||||
if player.item_type != "player" {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut user = match trans.find_by_username(&player.item_code).await? {
|
||||
None => return Ok(false),
|
||||
Some(u) => u
|
||||
};
|
||||
|
||||
let mut did_work = false;
|
||||
|
||||
if let Some(checkers) = checkers_by_species().get(&victim.species) {
|
||||
for checker in checkers {
|
||||
did_work = did_work ||
|
||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
||||
}
|
||||
}
|
||||
if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) {
|
||||
for checker in checkers {
|
||||
did_work = did_work ||
|
||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if did_work {
|
||||
trans.save_user_model(&user).await?;
|
||||
}
|
||||
Ok(did_work)
|
||||
}
|
37
blastmud_game/src/static_content/journals/first_dog.rs
Normal file
37
blastmud_game/src/static_content/journals/first_dog.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use super::{JournalChecker, KillSubscriptionType, award_journal_if_needed};
|
||||
use crate::{
|
||||
DResult,
|
||||
static_content::species::SpeciesType,
|
||||
models::{
|
||||
user::User,
|
||||
item::Item,
|
||||
journal::JournalType,
|
||||
}
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
||||
pub struct FirstDogChecker;
|
||||
#[async_trait]
|
||||
impl JournalChecker for FirstDogChecker {
|
||||
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> {
|
||||
vec!(
|
||||
KillSubscriptionType::SpecificNPCSpecies {
|
||||
species: SpeciesType::Dog
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle_kill(
|
||||
&self,
|
||||
trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
_victim: &Item
|
||||
) -> DResult<bool> {
|
||||
award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await
|
||||
}
|
||||
}
|
||||
|
||||
pub static CHECKER: FirstDogChecker = FirstDogChecker;
|
Loading…
Reference in New Issue
Block a user