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:
74
src/game.rs
74
src/game.rs
@@ -27,28 +27,43 @@ pub struct Player {
|
||||
pub room_id: String,
|
||||
pub stats: PlayerStats,
|
||||
pub inventory: Vec<Object>,
|
||||
pub equipped_weapon: Option<Object>,
|
||||
pub equipped_armor: Option<Object>,
|
||||
pub equipped: HashMap<String, Object>,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn effective_attack(&self) -> i32 {
|
||||
let bonus = self
|
||||
.equipped_weapon
|
||||
.as_ref()
|
||||
.and_then(|w| w.stats.damage)
|
||||
.unwrap_or(0);
|
||||
self.stats.attack + bonus
|
||||
pub fn equipped_in_slot(&self, slot: &str) -> Option<&Object> {
|
||||
self.equipped.get(slot)
|
||||
}
|
||||
|
||||
pub fn effective_defense(&self) -> i32 {
|
||||
let bonus = self
|
||||
.equipped_armor
|
||||
.as_ref()
|
||||
.and_then(|a| a.stats.armor)
|
||||
pub fn total_equipped_damage(&self) -> i32 {
|
||||
self.equipped.values()
|
||||
.filter_map(|o| o.stats.damage)
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn total_equipped_armor(&self) -> i32 {
|
||||
self.equipped.values()
|
||||
.filter_map(|o| o.stats.armor)
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn effective_attack(&self, world: &World) -> i32 {
|
||||
let race_natural = world.races.iter()
|
||||
.find(|r| r.id == self.race_id)
|
||||
.map(|r| r.natural_attacks.iter().map(|a| a.damage).max().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
self.stats.defense + bonus
|
||||
let weapon_bonus = self.total_equipped_damage();
|
||||
let unarmed_or_weapon = weapon_bonus.max(race_natural);
|
||||
self.stats.attack + unarmed_or_weapon
|
||||
}
|
||||
|
||||
pub fn effective_defense(&self, world: &World) -> i32 {
|
||||
let race_natural = world.races.iter()
|
||||
.find(|r| r.id == self.race_id)
|
||||
.map(|r| r.natural_armor)
|
||||
.unwrap_or(0);
|
||||
self.stats.defense + self.total_equipped_armor() + race_natural
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +254,7 @@ impl GameState {
|
||||
room_id,
|
||||
stats,
|
||||
inventory: Vec::new(),
|
||||
equipped_weapon: None,
|
||||
equipped_armor: None,
|
||||
equipped: HashMap::new(),
|
||||
is_admin: false,
|
||||
},
|
||||
channel,
|
||||
@@ -259,14 +273,8 @@ impl GameState {
|
||||
) {
|
||||
let inventory: Vec<Object> =
|
||||
serde_json::from_str(&saved.inventory_json).unwrap_or_default();
|
||||
let equipped_weapon: Option<Object> = saved
|
||||
.equipped_weapon_json
|
||||
.as_deref()
|
||||
.and_then(|j| serde_json::from_str(j).ok());
|
||||
let equipped_armor: Option<Object> = saved
|
||||
.equipped_armor_json
|
||||
.as_deref()
|
||||
.and_then(|j| serde_json::from_str(j).ok());
|
||||
let equipped: HashMap<String, Object> =
|
||||
serde_json::from_str(&saved.equipped_json).unwrap_or_default();
|
||||
|
||||
let room_id = if self.world.rooms.contains_key(&saved.room_id) {
|
||||
saved.room_id
|
||||
@@ -294,8 +302,7 @@ impl GameState {
|
||||
room_id,
|
||||
stats,
|
||||
inventory,
|
||||
equipped_weapon,
|
||||
equipped_armor,
|
||||
equipped,
|
||||
is_admin: saved.is_admin,
|
||||
},
|
||||
channel,
|
||||
@@ -310,14 +317,8 @@ impl GameState {
|
||||
let p = &conn.player;
|
||||
let inv_json =
|
||||
serde_json::to_string(&p.inventory).unwrap_or_else(|_| "[]".into());
|
||||
let weapon_json = p
|
||||
.equipped_weapon
|
||||
.as_ref()
|
||||
.map(|w| serde_json::to_string(w).unwrap_or_else(|_| "null".into()));
|
||||
let armor_json = p
|
||||
.equipped_armor
|
||||
.as_ref()
|
||||
.map(|a| serde_json::to_string(a).unwrap_or_else(|_| "null".into()));
|
||||
let equipped_json =
|
||||
serde_json::to_string(&p.equipped).unwrap_or_else(|_| "{}".into());
|
||||
|
||||
self.db.save_player(&SavedPlayer {
|
||||
name: p.name.clone(),
|
||||
@@ -331,8 +332,7 @@ impl GameState {
|
||||
attack: p.stats.attack,
|
||||
defense: p.stats.defense,
|
||||
inventory_json: inv_json,
|
||||
equipped_weapon_json: weapon_json,
|
||||
equipped_armor_json: armor_json,
|
||||
equipped_json,
|
||||
is_admin: p.is_admin,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user