use std::collections::BTreeMap; use super::{ follow::cancel_follow_by_leader, get_player_item_or_fail, get_user_or_fail, look, rent::recursively_destroy_or_move_item, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext, }; use crate::{ models::task::{Task, TaskDetails, TaskMeta}, regular_tasks::{TaskHandler, TaskRunContext}, services::{ combat::{corpsify_item, handle_death}, destroy_container, skills::calculate_total_stats_skills_for_user, }, DResult, }; use ansi::ansi; use async_trait::async_trait; use chrono::Utc; use rand::{distributions::Alphanumeric, Rng}; use std::time; async fn verify_code( ctx: &mut VerbContext<'_>, input: &str, base_command: &str, action_text: &str, ) -> UResult { let user_dat = ctx .user_dat .as_mut() .ok_or(UserError("Please log in first".to_owned()))?; let code = match &user_dat.danger_code { None => { let new_code = rand::thread_rng() .sample_iter(&Alphanumeric) .take(8) .map(char::from) .collect::(); user_dat.danger_code = Some(new_code.clone()); new_code } Some(code) => code.clone(), }; let input_tr = input.trim(); if input_tr == "" || !input_tr.starts_with("code ") { ctx.trans .queue_for_session( ctx.session, Some(&format!( ansi!("To verify you want to {}, type delete {} code {}\n"), action_text, base_command, code )), ) .await?; ctx.trans.save_user_model(&user_dat).await?; return Ok(false); } if input_tr["code ".len()..].trim() != code { ctx.trans .queue_for_session( ctx.session, Some(&format!( ansi!( "Your confirmation code didn't match! \ To verify you want to {}, type delete {} code {}\n" ), action_text, base_command, code )), ) .await?; ctx.trans.save_user_model(&user_dat).await?; return Ok(false); } Ok(true) } async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> { let mut player_item = (*get_player_item_or_fail(ctx).await?).clone(); let user_dat = ctx .user_dat .as_mut() .ok_or(UserError("Please log in first".to_owned()))?; handle_death(&ctx.trans, &mut player_item).await?; corpsify_item(&ctx.trans, &player_item).await?; player_item.death_data = None; player_item.location = "room/repro_xv_chargen".to_owned(); player_item.total_xp = ((player_item.total_xp as i64) - user_dat.experience.xp_change_for_this_reroll) .max(0) as u64; player_item.urges = Some(Default::default()); user_dat.experience.xp_change_for_this_reroll = 0; user_dat.raw_stats = BTreeMap::new(); user_dat.raw_skills = BTreeMap::new(); user_dat.wristpad_hacks = vec![]; user_dat.scan_codes = vec![]; calculate_total_stats_skills_for_user(&mut player_item, &user_dat); ctx.trans.save_user_model(&user_dat).await?; ctx.trans.save_item_model(&player_item).await?; look::VERB.handle(ctx, "look", "").await?; Ok(()) } #[derive(Clone)] pub struct DestroyUserHandler; pub static DESTROY_USER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DestroyUserHandler; #[async_trait] impl TaskHandler for DestroyUserHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let username = match &ctx.task.details { TaskDetails::DestroyUser { username } => username.clone(), _ => { return Ok(None); } }; let _user = match ctx.trans.find_by_username(&username).await? { None => return Ok(None), Some(u) => u, }; let player_item = match ctx .trans .find_item_by_type_code("player", &username.to_lowercase()) .await? { None => return Ok(None), Some(p) => p, }; cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?; destroy_container(&ctx.trans, &player_item).await?; for dynzone in ctx .trans .find_dynzone_for_owner(&format!("player/{}", &username.to_lowercase())) .await? { recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?; } ctx.trans.delete_user(&username).await?; Ok(None) } } pub struct Verb; #[async_trait] impl UserVerb for Verb { async fn handle( self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str, ) -> UResult<()> { let rtrim = remaining.trim(); let username = get_user_or_fail(ctx)?.username.clone(); if rtrim.starts_with("character forever") { if !verify_code(ctx, &rtrim["character forever".len()..], "character forever", "permanently destroy your character (after a one week reflection period), making the name available for other players").await? { return Ok(()); } ctx.trans .upsert_task(&Task { meta: TaskMeta { task_code: username.clone(), next_scheduled: Utc::now() + chrono::Duration::days(7), ..Default::default() }, details: TaskDetails::DestroyUser { username }, }) .await?; ctx.trans .queue_for_session( ctx.session, Some( "Puny human, your permanent destruction has been scheduled \ for one week from now! If you change your mind, just log \ in again before the week is up. After one week, your username \ will be available for others to take, and you will need to start \ again with a new character. This character will count towards the \ character limit for the week, but will not once it is deleted. \ Goodbye forever!\n", ), ) .await?; ctx.trans.queue_for_session(ctx.session, None).await?; } else if rtrim.starts_with("stats") { if !verify_code(ctx, &rtrim["stats".len()..], "stats", "kill your character, reset your stats and non-journal XP, and pick new stats to reclone with").await? { return Ok(()); } reset_stats(ctx).await?; } else { user_error( ansi!("Try delete character forever or delete stats") .to_owned(), )? } let user_dat = ctx .user_dat .as_mut() .ok_or(UserError("Please log in first".to_owned()))?; user_dat.danger_code = None; ctx.trans.save_user_model(&user_dat).await?; Ok(()) } } static VERB_INT: Verb = Verb; pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;