Flexible race system with slot-based equipment and dragon race

- Expand race TOML schema: 7 stats, body shape (size/weight/custom slots),
  natural armor and attacks with damage types, resistances, traits/disadvantages,
  regen multipliers, vision types, XP rate, guild compatibility
- Replace equipped_weapon/equipped_armor with slot-based HashMap<String, Object>
- Each race defines available equipment slots; default humanoid slots as fallback
- Combat uses natural weapons/armor from race when no gear equipped
- DB migration from old weapon/armor columns to equipped_json
- Add Dragon race: huge body, custom slots (forelegs/wings/tail), fire breath,
  natural armor 8, fire immune, slow XP rate for balance
- Update all existing races with expanded fields (traits, resistances, vision, regen)
- Objects gain optional slot field; kind=weapon/armor still works as fallback
- Update chargen to display race traits, size, natural attacks, vision
- Update stats display to show equipment and natural bonuses separately
- Update TESTING.md and AGENTS.md with race/slot system documentation

Made-with: Cursor
This commit is contained in:
AI Agent
2026-03-14 15:37:20 -06:00
parent 3f164e4697
commit 005c4faf08
18 changed files with 586 additions and 139 deletions

View File

@@ -44,6 +44,31 @@ impl ChargenState {
},
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<String> = 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{}",
@@ -179,6 +204,8 @@ fn format_stat_mods(stats: &crate::world::StatModifiers) -> String {
("CON", stats.constitution),
("INT", stats.intelligence),
("WIS", stats.wisdom),
("PER", stats.perception),
("CHA", stats.charisma),
];
for (label, val) in fields {
if val != 0 {