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())?
|
_ => user_error("Unexpected queued command".to_owned())?
|
||||||
};
|
};
|
||||||
let player_item = get_player_item_or_fail(ctx).await?;
|
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::NoDoor => user_error("There is no door to close.".to_owned())?,
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
||||||
DoorSituation::DoorOutOfRoom { 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())?
|
_ => user_error("Unexpected queued command".to_owned())?
|
||||||
};
|
};
|
||||||
let player_item = get_player_item_or_fail(ctx).await?;
|
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::NoDoor => user_error("There is no door to close.".to_owned())?,
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } |
|
||||||
DoorSituation::DoorOutOfRoom { 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?
|
ctx.trans.find_item_by_type_code(heretype, herecode).await?
|
||||||
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||||
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
} 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::NoDoor |
|
||||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } |
|
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } |
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {},
|
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {},
|
||||||
|
@ -190,7 +190,7 @@ pub async fn attempt_move_immediate(
|
|||||||
&orig_mover.location
|
&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::NoDoor |
|
||||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {},
|
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {},
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, room_with_door, .. } => {
|
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<'_>>,
|
pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut VerbContext<'_>>,
|
||||||
who: &Item, direction: &Direction) -> UResult<()> {
|
who: &Item, direction: &Direction) -> UResult<()> {
|
||||||
if who.death_data.is_some() {
|
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())?;
|
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
|
||||||
}
|
} else {
|
||||||
let (room_1, dir_in_room, room_2) = match is_door_in_direction(trans, &direction, &who).await? {
|
&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::NoDoor => user_error("There is no door to open.".to_owned())?,
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
||||||
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } =>
|
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } =>
|
||||||
@ -171,7 +173,12 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
_ => user_error("Unexpected command".to_owned())?
|
_ => user_error("Unexpected command".to_owned())?
|
||||||
};
|
};
|
||||||
let player_item = get_player_item_or_fail(ctx).await?;
|
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::NoDoor => user_error("There is no door to open.".to_owned())?,
|
||||||
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } |
|
||||||
DoorSituation::DoorOutOfRoom { 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.
|
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> {
|
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()))?;
|
.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?
|
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()))?;
|
.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()))?;
|
.ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?;
|
||||||
if let Some(door_state) =
|
if let Some(door_state) =
|
||||||
cur_loc_item.door_states.as_ref()
|
cur_loc_item.door_states.as_ref()
|
||||||
|
@ -4,3 +4,4 @@ pub mod item;
|
|||||||
pub mod task;
|
pub mod task;
|
||||||
pub mod consent;
|
pub mod consent;
|
||||||
pub mod corp;
|
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 serde::{Serialize, Deserialize};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use super::item::{SkillType, StatType};
|
use super::{
|
||||||
|
item::{SkillType, StatType},
|
||||||
|
journal::JournalState
|
||||||
|
};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -11,9 +14,10 @@ pub struct UserTermData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct UserExperienceData {
|
pub struct UserExperienceData {
|
||||||
pub spent_xp: u64, // Since last chargen complete.
|
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 xp_change_for_this_reroll: i64,
|
||||||
pub crafted_items: BTreeMap<String, u64>
|
pub crafted_items: BTreeMap<String, u64>
|
||||||
}
|
}
|
||||||
@ -54,7 +58,7 @@ impl Default for UserExperienceData {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
UserExperienceData {
|
UserExperienceData {
|
||||||
spent_xp: 0,
|
spent_xp: 0,
|
||||||
completed_journals: BTreeMap::new(),
|
journals: Default::default(),
|
||||||
xp_change_for_this_reroll: 0,
|
xp_change_for_this_reroll: 0,
|
||||||
crafted_items: BTreeMap::new(),
|
crafted_items: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@ use crate::{
|
|||||||
models::{
|
models::{
|
||||||
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
|
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
|
||||||
task::{Task, TaskMeta, TaskDetails},
|
task::{Task, TaskMeta, TaskDetails},
|
||||||
|
journal::JournalType,
|
||||||
},
|
},
|
||||||
static_content::{
|
static_content::{
|
||||||
possession_type::{WeaponData, possession_data, fist},
|
possession_type::{WeaponData, possession_data, fist},
|
||||||
npc::npc_by_code,
|
npc::npc_by_code,
|
||||||
species::species_info_map,
|
species::species_info_map,
|
||||||
|
journals::{check_journal_for_kill, award_journal_if_needed}
|
||||||
},
|
},
|
||||||
message_handler::user_commands::{user_error, UResult},
|
message_handler::user_commands::{user_error, UResult},
|
||||||
regular_tasks::{TaskRunContext, TaskHandler},
|
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(()),
|
None => return Ok(()),
|
||||||
Some(r) => r
|
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? {
|
let mut user = match trans.find_by_username(&by_item.item_code).await? {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
Some(r) => r
|
Some(r) => r
|
||||||
};
|
};
|
||||||
|
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;
|
user.experience.xp_change_for_this_reroll += xp_gain as i64;
|
||||||
|
xp_gain
|
||||||
|
};
|
||||||
|
|
||||||
// Now consider kill bonuses...
|
// Now consider kill bonuses...
|
||||||
if for_item.item_type == "npc" {
|
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.save_user_model(&user).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?;
|
trans.queue_for_session(&session, Some(&format!("You gained {} experience points!\n", xp_gain))).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
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!()),
|
.map(|sp| sp.corpse_butchers_into.clone()).unwrap_or_else(|| vec!()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
let vic_is_npc = whom.item_type == "npc";
|
||||||
if let Some(ac) = &whom.active_combat {
|
if let Some(ac) = &whom.active_combat {
|
||||||
let at_str = ac.attacking.clone();
|
let at_str = ac.attacking.clone();
|
||||||
for attacker in ac.attacked_by.clone().iter() {
|
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? {
|
if let Some(aitem) = trans.find_item_by_type_code(atype, acode).await? {
|
||||||
let mut new_aitem = (*aitem).clone();
|
let mut new_aitem = (*aitem).clone();
|
||||||
consider_reward_for(trans, &mut new_aitem, &whom).await?;
|
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?;
|
stop_attacking_mut(trans, &mut new_aitem, whom, true).await?;
|
||||||
trans.save_item_model(&new_aitem).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 {
|
trans.upsert_task(&Task {
|
||||||
meta: TaskMeta {
|
meta: TaskMeta {
|
||||||
task_code: whom.item_code.clone(),
|
task_code: whom.item_code.clone(),
|
||||||
@ -250,6 +257,15 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
|||||||
}).await?;
|
}).await?;
|
||||||
} else if whom.item_type == "player" {
|
} else if whom.item_type == "player" {
|
||||||
trans.revoke_until_death_consent(&whom.item_code).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ pub mod dynzone;
|
|||||||
pub mod npc;
|
pub mod npc;
|
||||||
pub mod possession_type;
|
pub mod possession_type;
|
||||||
pub mod species;
|
pub mod species;
|
||||||
|
pub mod journals;
|
||||||
mod fixed_item;
|
mod fixed_item;
|
||||||
|
|
||||||
pub struct StaticItem {
|
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