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:
144
src/commands.rs
144
src/commands.rs
@@ -545,19 +545,25 @@ async fn cmd_inventory(pid: usize, state: &SharedState) -> CommandResult {
|
||||
None => return simple("Error\r\n"),
|
||||
};
|
||||
let mut out = format!("\r\n{}\r\n", ansi::bold("=== Inventory ==="));
|
||||
if let Some(ref w) = conn.player.equipped_weapon {
|
||||
out.push_str(&format!(
|
||||
" Weapon: {} {}\r\n",
|
||||
ansi::color(ansi::CYAN, &w.name),
|
||||
ansi::system_msg(&format!("(+{} dmg)", w.stats.damage.unwrap_or(0)))
|
||||
));
|
||||
}
|
||||
if let Some(ref a) = conn.player.equipped_armor {
|
||||
out.push_str(&format!(
|
||||
" Armor: {} {}\r\n",
|
||||
ansi::color(ansi::CYAN, &a.name),
|
||||
ansi::system_msg(&format!("(+{} def)", a.stats.armor.unwrap_or(0)))
|
||||
));
|
||||
if !conn.player.equipped.is_empty() {
|
||||
out.push_str(&format!(" {}\r\n", ansi::color(ansi::DIM, "Equipped:")));
|
||||
let mut slots: Vec<(&String, &crate::world::Object)> = conn.player.equipped.iter().collect();
|
||||
slots.sort_by_key(|(s, _)| (*s).clone());
|
||||
for (slot, obj) in &slots {
|
||||
let bonus = if let Some(dmg) = obj.stats.damage {
|
||||
format!(" (+{} dmg)", dmg)
|
||||
} else if let Some(arm) = obj.stats.armor {
|
||||
format!(" (+{} def)", arm)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
out.push_str(&format!(
|
||||
" {}: {} {}\r\n",
|
||||
ansi::color(ansi::YELLOW, slot),
|
||||
ansi::color(ansi::CYAN, &obj.name),
|
||||
ansi::system_msg(&bonus),
|
||||
));
|
||||
}
|
||||
}
|
||||
if conn.player.inventory.is_empty() {
|
||||
out.push_str(&format!(" {}\r\n", ansi::system_msg("(empty)")));
|
||||
@@ -588,6 +594,17 @@ async fn cmd_equip(pid: usize, target: &str, state: &SharedState) -> CommandResu
|
||||
return simple("Equip what?\r\n");
|
||||
}
|
||||
let mut st = state.lock().await;
|
||||
|
||||
// Extract race slots before mutable borrow
|
||||
let race_id = match st.players.get(&pid) {
|
||||
Some(c) => c.player.race_id.clone(),
|
||||
None => return simple("Error\r\n"),
|
||||
};
|
||||
let race_slots: Vec<String> = st.world.races.iter()
|
||||
.find(|r| r.id == race_id)
|
||||
.map(|r| r.slots.clone())
|
||||
.unwrap_or_else(|| crate::world::DEFAULT_HUMANOID_SLOTS.iter().map(|s| s.to_string()).collect());
|
||||
|
||||
let conn = match st.players.get_mut(&pid) {
|
||||
Some(c) => c,
|
||||
None => return simple("Error\r\n"),
|
||||
@@ -609,47 +626,46 @@ async fn cmd_equip(pid: usize, target: &str, state: &SharedState) -> CommandResu
|
||||
};
|
||||
let obj = conn.player.inventory.remove(idx);
|
||||
let name = obj.name.clone();
|
||||
let kind = obj.kind.as_deref().unwrap_or("").to_string();
|
||||
match kind.as_str() {
|
||||
"weapon" => {
|
||||
if let Some(old) = conn.player.equipped_weapon.take() {
|
||||
conn.player.inventory.push(old);
|
||||
}
|
||||
conn.player.equipped_weapon = Some(obj);
|
||||
st.save_player_to_db(pid);
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"You equip the {} as your weapon.\r\n",
|
||||
ansi::color(ansi::CYAN, &name)
|
||||
),
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
|
||||
let slot = if let Some(ref s) = obj.slot {
|
||||
s.clone()
|
||||
} else {
|
||||
match obj.kind.as_deref() {
|
||||
Some("weapon") => "main_hand".into(),
|
||||
Some("armor") => "torso".into(),
|
||||
_ => {
|
||||
conn.player.inventory.push(obj);
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("You can't equip the {}.", name))
|
||||
));
|
||||
}
|
||||
}
|
||||
"armor" => {
|
||||
if let Some(old) = conn.player.equipped_armor.take() {
|
||||
conn.player.inventory.push(old);
|
||||
}
|
||||
conn.player.equipped_armor = Some(obj);
|
||||
st.save_player_to_db(pid);
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"You equip the {} as armor.\r\n",
|
||||
ansi::color(ansi::CYAN, &name)
|
||||
),
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
conn.player.inventory.push(obj);
|
||||
simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("You can't equip the {}.", name))
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if !race_slots.contains(&slot) {
|
||||
conn.player.inventory.push(obj);
|
||||
return simple(&format!(
|
||||
"{}\r\n",
|
||||
ansi::error_msg(&format!("Your body doesn't have a {} slot.", slot))
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(old) = conn.player.equipped.remove(&slot) {
|
||||
conn.player.inventory.push(old);
|
||||
}
|
||||
conn.player.equipped.insert(slot.clone(), obj);
|
||||
let _ = conn;
|
||||
st.save_player_to_db(pid);
|
||||
CommandResult {
|
||||
output: format!(
|
||||
"You equip the {} in your {} slot.\r\n",
|
||||
ansi::color(ansi::CYAN, &name),
|
||||
ansi::color(ansi::YELLOW, &slot),
|
||||
),
|
||||
broadcasts: Vec::new(),
|
||||
kick_targets: Vec::new(),
|
||||
quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,20 +1110,22 @@ async fn cmd_stats(pid: usize, state: &SharedState) -> CommandResult {
|
||||
s.max_hp,
|
||||
ansi::RESET
|
||||
));
|
||||
let race_natural_atk = st.world.races.iter()
|
||||
.find(|r| r.id == p.race_id)
|
||||
.map(|r| r.natural_attacks.iter().map(|a| a.damage).max().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let race_natural_def = st.world.races.iter()
|
||||
.find(|r| r.id == p.race_id)
|
||||
.map(|r| r.natural_armor)
|
||||
.unwrap_or(0);
|
||||
let equip_dmg = p.total_equipped_damage();
|
||||
let equip_arm = p.total_equipped_armor();
|
||||
out.push_str(&format!(
|
||||
" {} {} (+{} equip) {} {} (+{} equip)\r\n",
|
||||
" {} {} (+{} equip, +{} natural) {} {} (+{} equip, +{} natural)\r\n",
|
||||
ansi::color(ansi::DIM, "ATK:"),
|
||||
s.attack,
|
||||
p.equipped_weapon
|
||||
.as_ref()
|
||||
.and_then(|w| w.stats.damage)
|
||||
.unwrap_or(0),
|
||||
s.attack, equip_dmg.max(race_natural_atk), race_natural_atk,
|
||||
ansi::color(ansi::DIM, "DEF:"),
|
||||
s.defense,
|
||||
p.equipped_armor
|
||||
.as_ref()
|
||||
.and_then(|a| a.stats.armor)
|
||||
.unwrap_or(0)
|
||||
s.defense, equip_arm, race_natural_def,
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" {} {}\r\n",
|
||||
|
||||
Reference in New Issue
Block a user