use super::{ item::{BuffImpact, Item, SkillType, StatType}, journal::JournalState, }; #[double] use crate::db::DBTrans; use crate::{message_handler::ListenerSession, DResult}; use chrono::{DateTime, Utc}; use mockall_double::double; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct UserTermData { pub accepted_terms: BTreeMap>, pub terms_complete: bool, // Recalculated on accept and login. pub last_presented_term: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(default)] pub struct UserExperienceData { pub spent_xp: u64, // Since last chargen complete. pub journals: JournalState, pub xp_change_for_this_reroll: i64, pub crafted_items: BTreeMap, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub enum UserFlag { Staff, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum WristpadHack { Superdork, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] pub struct WristpadHackData { pub name: &'static str, pub buff: Vec, } pub fn wristpad_hack_data() -> &'static BTreeMap { static D: OnceCell> = OnceCell::new(); D.get_or_init(|| { vec![( WristpadHack::Superdork, WristpadHackData { name: "Superdork", buff: vec![ BuffImpact::ChangeStat { stat: StatType::Brains, magnitude: 3.0, }, BuffImpact::ChangeStat { stat: StatType::Cool, magnitude: -1.0, }, ], }, )] .into_iter() .collect() }) } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(default)] pub struct User { pub username: String, pub password_hash: String, // bcrypted. pub email: String, pub player_item_id: i64, pub registered_at: Option>, pub banned_until: Option>, pub abandoned_at: Option>, pub chargen_last_completed_at: Option>, pub terms: UserTermData, pub experience: UserExperienceData, pub raw_skills: BTreeMap, pub raw_stats: BTreeMap, pub wristpad_hacks: Vec, pub last_skill_improve: BTreeMap>, pub last_page_from: Option, pub credits: u64, pub danger_code: Option, pub user_flags: Vec, // Reminder: Consider backwards compatibility when updating this. } static NO_BUFFS: Vec = vec![]; pub fn xp_to_hack_slots(xp: u64) -> u64 { if xp >= 1000000 { 14 } else if xp >= 500000 { 13 } else if xp >= 100000 { 12 } else if xp >= 70000 { 11 } else if xp >= 40000 { 10 } else if xp >= 20000 { 9 } else if xp >= 10000 { 8 } else if xp >= 8000 { 7 } else if xp >= 6000 { 6 } else if xp >= 4000 { 5 } else if xp >= 2000 { 4 } else if xp >= 1500 { 3 } else if xp >= 1000 { 2 } else if xp >= 500 { 1 } else { 0 } } impl User { pub fn wristpad_hack_buffs<'a>(self: &'a Self) -> impl Iterator + 'a { self.wristpad_hacks.iter().flat_map(|h| { wristpad_hack_data() .get(h) .map(|hd| &hd.buff) .unwrap_or_else(|| &NO_BUFFS) }) } pub async fn adjust_xp_for_reroll( self: &mut User, item: &mut Item, change: i64, trans: &DBTrans, sess: &ListenerSession, ) -> DResult<()> { self.experience.xp_change_for_this_reroll += change; self.xp_adjusted(item, change, trans, sess).await } pub async fn xp_adjusted( self: &mut User, item: &mut Item, change: i64, trans: &DBTrans, sess: &ListenerSession, ) -> DResult<()> { let old_slots = xp_to_hack_slots(item.total_xp); if change >= 0 { item.total_xp += change as u64; } else if (-change) as u64 <= item.total_xp { item.total_xp -= (-change) as u64; } else { item.total_xp = 0; } let new_slots = xp_to_hack_slots(item.total_xp); if new_slots > old_slots { trans .queue_for_session( sess, Some("You just earned a new hack slot on your wristpad!\n"), ) .await?; } Ok(()) } } impl Default for UserTermData { fn default() -> Self { UserTermData { accepted_terms: BTreeMap::new(), terms_complete: false, last_presented_term: None, } } } impl Default for UserExperienceData { fn default() -> Self { UserExperienceData { spent_xp: 0, journals: Default::default(), xp_change_for_this_reroll: 0, crafted_items: BTreeMap::new(), } } } impl Default for User { fn default() -> Self { User { username: "unknown".to_owned(), password_hash: "unknown".to_owned(), email: "unknown".to_owned(), player_item_id: 0, registered_at: None, banned_until: None, abandoned_at: None, chargen_last_completed_at: None, terms: UserTermData::default(), experience: UserExperienceData::default(), raw_skills: BTreeMap::new(), raw_stats: BTreeMap::new(), wristpad_hacks: vec![], last_skill_improve: BTreeMap::new(), last_page_from: None, credits: 500, danger_code: None, user_flags: vec![], } } }