use crate::ansi; use crate::world::World; #[derive(Clone)] pub enum ChargenStep { AwaitingRace, AwaitingClass, Done { race_id: String, class_id: String }, } pub struct ChargenState { pub step: ChargenStep, pub race_id: Option, } impl ChargenState { pub fn new() -> Self { ChargenState { step: ChargenStep::AwaitingRace, race_id: None, } } pub fn prompt_text(&self, world: &World) -> String { match &self.step { ChargenStep::AwaitingRace => { let mut out = String::new(); out.push_str(&format!( "\r\n{}\r\n\r\n", ansi::bold("=== Choose Your Race ===") )); let visible_races: Vec<_> = world.races.iter().filter(|r| !r.hidden).collect(); for (i, race) in visible_races.iter().enumerate() { let mods = format_stat_mods(&race.stats); out.push_str(&format!( " {}{}.{} {} {}\r\n {}\r\n", ansi::BOLD, i + 1, ansi::RESET, ansi::color(ansi::CYAN, &race.name), if mods.is_empty() { String::new() } else { ansi::system_msg(&format!("({})", mods)) }, ansi::color(ansi::DIM, &race.description), )); let mut extras = Vec::new(); if race.size != "medium" { extras.push(format!("Size: {}", race.size)); } if !race.traits.is_empty() { extras.push(format!("Traits: {}", race.traits.join(", "))); } if race.natural_armor > 0 { extras.push(format!("Natural armor: {}", race.natural_armor)); } if !race.natural_attacks.is_empty() { let atks: Vec = race.natural_attacks.iter() .map(|a| format!("{} ({}dmg {})", a.name, a.damage, a.damage_type)) .collect(); extras.push(format!("Natural attacks: {}", atks.join(", "))); } if !race.vision.is_empty() { extras.push(format!("Vision: {}", race.vision.join(", "))); } if !extras.is_empty() { out.push_str(&format!( " {}\r\n", ansi::color(ansi::DIM, &extras.join(" | ")) )); } } out.push_str(&format!( "\r\n{}", ansi::color(ansi::YELLOW, "Enter number or name: ") )); out } ChargenStep::AwaitingClass => { let mut out = String::new(); out.push_str(&format!( "\r\n{}\r\n\r\n", ansi::bold("=== Choose Your Class ===") )); let visible_classes: Vec<_> = world.classes.iter().filter(|c| !c.hidden).collect(); for (i, class) in visible_classes.iter().enumerate() { let guild_info = class.guild.as_ref() .and_then(|gid| world.guilds.get(gid)) .map(|g| format!(" → joins {}", ansi::color(ansi::YELLOW, &g.name))) .unwrap_or_default(); out.push_str(&format!( " {}{}.{} {} {}{}\r\n {}\r\n {}HP:{} {}ATK:{} {}DEF:{}{}\r\n", ansi::BOLD, i + 1, ansi::RESET, ansi::color(ansi::CYAN, &class.name), ansi::system_msg(&format!( "(+{}hp/+{}atk/+{}def per level)", class.growth.hp_per_level, class.growth.attack_per_level, class.growth.defense_per_level, )), guild_info, ansi::color(ansi::DIM, &class.description), ansi::GREEN, class.base_stats.max_hp, ansi::RED, class.base_stats.attack, ansi::BLUE, class.base_stats.defense, ansi::RESET, )); } out.push_str(&format!( "\r\n{}", ansi::color(ansi::YELLOW, "Enter number or name: ") )); out } ChargenStep::Done { .. } => String::new(), } } pub fn handle_input(&mut self, input: &str, world: &World) -> Result { let input = input.trim(); match &self.step { ChargenStep::AwaitingRace => { let race = find_by_input( input, &world.races.iter().filter(|r| !r.hidden).map(|r| (r.id.clone(), r.name.clone())).collect::>(), ); match race { Some((id, name)) => { self.race_id = Some(id); self.step = ChargenStep::AwaitingClass; Ok(format!( "\r\n{}\r\n", ansi::system_msg(&format!("Race selected: {name}")) )) } None => Err(format!( "{}\r\n", ansi::error_msg("Invalid choice. Enter a number or name.") )), } } ChargenStep::AwaitingClass => { let class = find_by_input( input, &world .classes .iter() .filter(|c| !c.hidden) .map(|c| (c.id.clone(), c.name.clone())) .collect::>(), ); match class { Some((id, name)) => { let race_id = self.race_id.clone().unwrap(); self.step = ChargenStep::Done { race_id, class_id: id, }; Ok(format!( "\r\n{}\r\n", ansi::system_msg(&format!("Class selected: {name}")) )) } None => Err(format!( "{}\r\n", ansi::error_msg("Invalid choice. Enter a number or name.") )), } } ChargenStep::Done { .. } => Ok(String::new()), } } pub fn is_done(&self) -> bool { matches!(self.step, ChargenStep::Done { .. }) } pub fn result(&self) -> Option<(String, String)> { match &self.step { ChargenStep::Done { race_id, class_id } => { Some((race_id.clone(), class_id.clone())) } _ => None, } } } fn find_by_input(input: &str, options: &[(String, String)]) -> Option<(String, String)> { if let Ok(num) = input.parse::() { if num >= 1 && num <= options.len() { return Some(options[num - 1].clone()); } } let lower = input.to_lowercase(); options .iter() .find(|(_, name)| name.to_lowercase() == lower) .cloned() } fn format_stat_mods(stats: &crate::world::StatModifiers) -> String { let mut parts = Vec::new(); let fields = [ ("STR", stats.strength), ("DEX", stats.dexterity), ("CON", stats.constitution), ("INT", stats.intelligence), ("WIS", stats.wisdom), ("PER", stats.perception), ("CHA", stats.charisma), ]; for (label, val) in fields { if val != 0 { parts.push(format!( "{}{} {}", if val > 0 { "+" } else { "" }, val, label )); } } parts.join(", ") }