use super::species::SpeciesType; #[double] use crate::db::DBTrans; use crate::{ models::{item::Item, journal::JournalType, user::User}, DResult, }; use async_trait::async_trait; use itertools::Itertools; use log::warn; use mockall_double::double; use once_cell::sync::OnceCell; use std::collections::BTreeMap; mod first_dog; mod killed_killbot; #[allow(unused)] pub enum KillSubscriptionType { SpecificNPCSpecies { species: SpeciesType }, SpecificNPC { code: &'static str }, } #[async_trait] pub trait JournalChecker { fn kill_subscriptions(&self) -> Vec; async fn handle_kill( &self, trans: &DBTrans, user: &mut User, player: &mut Item, victim: &Item, ) -> DResult; } pub struct JournalData { name: &'static str, details: &'static str, xp: u64, } pub fn journal_types() -> &'static BTreeMap { static JOURNAL_TYPES: OnceCell> = 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::TookOutAKillbot, JournalData { name: "Killed a killbot", details: "realising that in soviet melbs, you kill the killbots.", xp: 200 }), (JournalType::JoinedHackerClub, JournalData { name: "Joined the Hackers' club", details: "finding the secret Hackers' club.", xp: 250 }), (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> = OnceCell::new(); CHECKERS.get_or_init(|| vec![&first_dog::CHECKER, &killed_killbot::CHECKER]) } pub fn checkers_by_species( ) -> &'static BTreeMap> { static MAP: OnceCell>> = 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)) } _ => 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>> = 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, *jc)), _ => None, }) }) .group_by(|v| v.0); 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 { 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, }; 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?; user.experience.journals.completed_journals.insert(journal); // Note: Not counted as 'change for this reroll' since it is permanent. user.xp_adjusted(player, journal_data.xp as i64, trans, &sess) .await?; } Ok(true) } pub async fn check_journal_for_kill( trans: &DBTrans, player: &mut Item, victim: &Item, ) -> DResult { 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) }