Add SQLite persistence, per-player NPC attitude system, character creation, and combat
- Add trait-based DB layer (db.rs) with SQLite backend for easy future swapping - Player state persisted to SQLite: stats, inventory, equipment, room position - Returning players skip chargen and resume where they left off - Replace boolean hostile flag with 5-tier attitude system (friendly/neutral/wary/aggressive/hostile) - Per-player NPC attitudes stored in DB, shift on kills with faction propagation - Add character creation flow (chargen.rs) with data-driven races and classes from TOML - Add turn-based combat system (combat.rs) with XP, leveling, and NPC respawn - Add commands: take, drop, inventory, equip, use, examine, talk, attack, flee, stats - Add world data: 5 races, 4 classes, hostile NPCs (rat, thief), new items Made-with: Cursor
This commit is contained in:
194
src/chargen.rs
Normal file
194
src/chargen.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
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<String>,
|
||||
}
|
||||
|
||||
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 ===")
|
||||
));
|
||||
for (i, race) in world.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),
|
||||
));
|
||||
}
|
||||
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 ===")
|
||||
));
|
||||
for (i, class) in world.classes.iter().enumerate() {
|
||||
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,
|
||||
)),
|
||||
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<String, String> {
|
||||
let input = input.trim();
|
||||
match &self.step {
|
||||
ChargenStep::AwaitingRace => {
|
||||
let race = find_by_input(
|
||||
input,
|
||||
&world.races.iter().map(|r| (r.id.clone(), r.name.clone())).collect::<Vec<_>>(),
|
||||
);
|
||||
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()
|
||||
.map(|c| (c.id.clone(), c.name.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
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::<usize>() {
|
||||
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),
|
||||
];
|
||||
for (label, val) in fields {
|
||||
if val != 0 {
|
||||
parts.push(format!(
|
||||
"{}{} {}",
|
||||
if val > 0 { "+" } else { "" },
|
||||
val,
|
||||
label
|
||||
));
|
||||
}
|
||||
}
|
||||
parts.join(", ")
|
||||
}
|
||||
Reference in New Issue
Block a user