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:
172
src/world.rs
172
src/world.rs
@@ -134,11 +134,15 @@ pub struct ObjectFile {
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
#[serde(default)]
|
||||
pub slot: Option<String>,
|
||||
#[serde(default)]
|
||||
pub takeable: bool,
|
||||
#[serde(default)]
|
||||
pub stats: Option<ObjectStatsFile>,
|
||||
}
|
||||
|
||||
// --- Race TOML schema ---
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct StatModifiers {
|
||||
#[serde(default)]
|
||||
@@ -151,6 +155,86 @@ pub struct StatModifiers {
|
||||
pub intelligence: i32,
|
||||
#[serde(default)]
|
||||
pub wisdom: i32,
|
||||
#[serde(default)]
|
||||
pub perception: i32,
|
||||
#[serde(default)]
|
||||
pub charisma: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct BodyFile {
|
||||
#[serde(default = "default_size")]
|
||||
pub size: String,
|
||||
#[serde(default)]
|
||||
pub weight: i32,
|
||||
#[serde(default)]
|
||||
pub slots: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_size() -> String {
|
||||
"medium".into()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct NaturalAttack {
|
||||
#[serde(default)]
|
||||
pub damage: i32,
|
||||
#[serde(default = "default_damage_type")]
|
||||
pub r#type: String,
|
||||
#[serde(default)]
|
||||
pub cooldown_ticks: Option<i32>,
|
||||
}
|
||||
|
||||
fn default_damage_type() -> String {
|
||||
"physical".into()
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct NaturalFile {
|
||||
#[serde(default)]
|
||||
pub armor: i32,
|
||||
#[serde(default)]
|
||||
pub attacks: HashMap<String, NaturalAttack>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct RegenFile {
|
||||
#[serde(default = "default_one")]
|
||||
pub hp: f32,
|
||||
#[serde(default = "default_one")]
|
||||
pub mana: f32,
|
||||
#[serde(default = "default_one")]
|
||||
pub endurance: f32,
|
||||
}
|
||||
|
||||
fn default_one() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct GuildCompatibilityFile {
|
||||
#[serde(default)]
|
||||
pub good: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub average: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub poor: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub restricted: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct RaceMiscFile {
|
||||
#[serde(default)]
|
||||
pub lifespan: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub diet: Option<String>,
|
||||
#[serde(default)]
|
||||
pub xp_rate: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub natural_terrain: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub vision: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -158,9 +242,29 @@ pub struct RaceFile {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub metarace: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stats: StatModifiers,
|
||||
#[serde(default)]
|
||||
pub body: BodyFile,
|
||||
#[serde(default)]
|
||||
pub natural: NaturalFile,
|
||||
#[serde(default)]
|
||||
pub resistances: HashMap<String, f32>,
|
||||
#[serde(default)]
|
||||
pub traits: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub disadvantages: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub regen: RegenFile,
|
||||
#[serde(default)]
|
||||
pub guild_compatibility: GuildCompatibilityFile,
|
||||
#[serde(default)]
|
||||
pub misc: RaceMiscFile,
|
||||
}
|
||||
|
||||
// --- Class TOML schema ---
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct ClassBaseStats {
|
||||
#[serde(default)]
|
||||
@@ -238,16 +342,47 @@ pub struct Object {
|
||||
pub description: String,
|
||||
pub room: Option<String>,
|
||||
pub kind: Option<String>,
|
||||
pub slot: Option<String>,
|
||||
pub takeable: bool,
|
||||
pub stats: ObjectStats,
|
||||
}
|
||||
|
||||
pub const DEFAULT_HUMANOID_SLOTS: &[&str] = &[
|
||||
"head", "neck", "torso", "legs", "feet", "main_hand", "off_hand", "finger", "finger",
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NaturalAttackDef {
|
||||
pub name: String,
|
||||
pub damage: i32,
|
||||
pub damage_type: String,
|
||||
pub cooldown_ticks: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Race {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub metarace: Option<String>,
|
||||
pub stats: StatModifiers,
|
||||
pub size: String,
|
||||
pub weight: i32,
|
||||
pub slots: Vec<String>,
|
||||
pub natural_armor: i32,
|
||||
pub natural_attacks: Vec<NaturalAttackDef>,
|
||||
pub resistances: HashMap<String, f32>,
|
||||
pub traits: Vec<String>,
|
||||
pub disadvantages: Vec<String>,
|
||||
pub regen_hp: f32,
|
||||
pub regen_mana: f32,
|
||||
pub regen_endurance: f32,
|
||||
pub guild_compatibility: GuildCompatibilityFile,
|
||||
pub lifespan: Option<i32>,
|
||||
pub diet: Option<String>,
|
||||
pub xp_rate: f32,
|
||||
pub natural_terrain: Vec<String>,
|
||||
pub vision: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -280,7 +415,36 @@ impl World {
|
||||
let mut races = Vec::new();
|
||||
load_entities_from_dir(&world_dir.join("races"), "race", &mut |id, content| {
|
||||
let rf: RaceFile = toml::from_str(content).map_err(|e| format!("Bad race {id}: {e}"))?;
|
||||
races.push(Race { id, name: rf.name, description: rf.description, stats: rf.stats });
|
||||
let slots = if rf.body.slots.is_empty() {
|
||||
DEFAULT_HUMANOID_SLOTS.iter().map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
rf.body.slots
|
||||
};
|
||||
let natural_attacks = rf.natural.attacks.into_iter().map(|(name, a)| {
|
||||
NaturalAttackDef { name, damage: a.damage, damage_type: a.r#type, cooldown_ticks: a.cooldown_ticks }
|
||||
}).collect();
|
||||
races.push(Race {
|
||||
id, name: rf.name, description: rf.description,
|
||||
metarace: rf.metarace,
|
||||
stats: rf.stats,
|
||||
size: rf.body.size,
|
||||
weight: rf.body.weight,
|
||||
slots,
|
||||
natural_armor: rf.natural.armor,
|
||||
natural_attacks,
|
||||
resistances: rf.resistances,
|
||||
traits: rf.traits,
|
||||
disadvantages: rf.disadvantages,
|
||||
regen_hp: rf.regen.hp,
|
||||
regen_mana: rf.regen.mana,
|
||||
regen_endurance: rf.regen.endurance,
|
||||
guild_compatibility: rf.guild_compatibility,
|
||||
lifespan: rf.misc.lifespan,
|
||||
diet: rf.misc.diet,
|
||||
xp_rate: rf.misc.xp_rate.unwrap_or(1.0),
|
||||
natural_terrain: rf.misc.natural_terrain,
|
||||
vision: rf.misc.vision,
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -325,7 +489,11 @@ impl World {
|
||||
load_entities_from_dir(®ion_path.join("objects"), ®ion_name, &mut |id, content| {
|
||||
let of: ObjectFile = toml::from_str(content).map_err(|e| format!("Bad object {id}: {e}"))?;
|
||||
let stats = of.stats.unwrap_or_default();
|
||||
objects.insert(id.clone(), Object { id: id.clone(), name: of.name, description: of.description, room: of.room, kind: of.kind, takeable: of.takeable, stats: ObjectStats { damage: stats.damage, armor: stats.armor, heal_amount: stats.heal_amount } });
|
||||
objects.insert(id.clone(), Object {
|
||||
id: id.clone(), name: of.name, description: of.description, room: of.room,
|
||||
kind: of.kind, slot: of.slot, takeable: of.takeable,
|
||||
stats: ObjectStats { damage: stats.damage, armor: stats.armor, heal_amount: stats.heal_amount },
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user