Give every NPC a race and class, resolved at spawn time
NPCs can now optionally specify race and class in their TOML. When omitted, race is randomly selected from non-hidden races and class is determined by the race's default_class or picked randomly from compatible non-hidden classes. Race/class are re-rolled on each respawn for NPCs without fixed values, so killing the barkeep may bring back a different race next time. New hidden content (excluded from character creation): - Beast race: for animals (rats, etc.) with feral stats and no equipment slots - Peasant class: weak default for humanoid NPCs - Creature class: default for beasts/animals Existing races gain default_class fields (humanoids → peasant, beast → creature, dragon → random compatible). Look and examine commands now display NPC race and class. Made-with: Cursor
This commit is contained in:
30
src/world.rs
30
src/world.rs
@@ -104,6 +104,10 @@ pub struct NpcFile {
|
||||
#[serde(default)]
|
||||
pub faction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub race: Option<String>,
|
||||
#[serde(default)]
|
||||
pub class: Option<String>,
|
||||
#[serde(default)]
|
||||
pub respawn_secs: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub dialogue: Option<NpcDialogue>,
|
||||
@@ -244,6 +248,10 @@ pub struct RaceFile {
|
||||
#[serde(default)]
|
||||
pub metarace: Option<String>,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
#[serde(default)]
|
||||
pub default_class: Option<String>,
|
||||
#[serde(default)]
|
||||
pub stats: StatModifiers,
|
||||
#[serde(default)]
|
||||
pub body: BodyFile,
|
||||
@@ -290,6 +298,8 @@ pub struct ClassFile {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub hidden: bool,
|
||||
#[serde(default)]
|
||||
pub base_stats: ClassBaseStats,
|
||||
#[serde(default)]
|
||||
pub growth: ClassGrowth,
|
||||
@@ -395,6 +405,8 @@ pub struct Npc {
|
||||
pub room: String,
|
||||
pub base_attitude: Attitude,
|
||||
pub faction: Option<String>,
|
||||
pub fixed_race: Option<String>,
|
||||
pub fixed_class: Option<String>,
|
||||
pub respawn_secs: Option<u64>,
|
||||
pub greeting: Option<String>,
|
||||
pub combat: Option<NpcCombatStats>,
|
||||
@@ -437,6 +449,8 @@ pub struct Race {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub metarace: Option<String>,
|
||||
pub hidden: bool,
|
||||
pub default_class: Option<String>,
|
||||
pub stats: StatModifiers,
|
||||
pub size: String,
|
||||
pub weight: i32,
|
||||
@@ -462,6 +476,7 @@ pub struct Class {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub hidden: bool,
|
||||
pub base_stats: ClassBaseStats,
|
||||
pub growth: ClassGrowth,
|
||||
pub guild: Option<String>,
|
||||
@@ -535,6 +550,8 @@ impl World {
|
||||
races.push(Race {
|
||||
id, name: rf.name, description: rf.description,
|
||||
metarace: rf.metarace,
|
||||
hidden: rf.hidden,
|
||||
default_class: rf.default_class,
|
||||
stats: rf.stats,
|
||||
size: rf.body.size,
|
||||
weight: rf.body.weight,
|
||||
@@ -560,7 +577,7 @@ impl World {
|
||||
let mut classes = Vec::new();
|
||||
load_entities_from_dir(&world_dir.join("classes"), "class", &mut |id, content| {
|
||||
let cf: ClassFile = toml::from_str(content).map_err(|e| format!("Bad class {id}: {e}"))?;
|
||||
classes.push(Class { id, name: cf.name, description: cf.description, base_stats: cf.base_stats, growth: cf.growth, guild: cf.guild });
|
||||
classes.push(Class { id, name: cf.name, description: cf.description, hidden: cf.hidden, base_stats: cf.base_stats, growth: cf.growth, guild: cf.guild });
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -623,7 +640,12 @@ impl World {
|
||||
let combat = Some(nf.combat.map(|c| NpcCombatStats { max_hp: c.max_hp, attack: c.attack, defense: c.defense, xp_reward: c.xp_reward })
|
||||
.unwrap_or(NpcCombatStats { max_hp: 20, attack: 4, defense: 2, xp_reward: 5 }));
|
||||
let greeting = nf.dialogue.and_then(|d| d.greeting);
|
||||
npcs.insert(id.clone(), Npc { id: id.clone(), name: nf.name, description: nf.description, room: nf.room, base_attitude: nf.base_attitude, faction: nf.faction, respawn_secs: nf.respawn_secs, greeting, combat });
|
||||
npcs.insert(id.clone(), Npc {
|
||||
id: id.clone(), name: nf.name, description: nf.description, room: nf.room,
|
||||
base_attitude: nf.base_attitude, faction: nf.faction,
|
||||
fixed_race: nf.race, fixed_class: nf.class,
|
||||
respawn_secs: nf.respawn_secs, greeting, combat,
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -644,8 +666,8 @@ impl World {
|
||||
|
||||
if !rooms.contains_key(&manifest.spawn_room) { return Err(format!("Spawn room '{}' not found", manifest.spawn_room)); }
|
||||
for room in rooms.values() { for (dir, target) in &room.exits { if !rooms.contains_key(target) { return Err(format!("Room '{}' exit '{dir}' -> unknown '{target}'", room.id)); } } }
|
||||
if races.is_empty() { return Err("No races defined".into()); }
|
||||
if classes.is_empty() { return Err("No classes defined".into()); }
|
||||
if races.iter().filter(|r| !r.hidden).count() == 0 { return Err("No playable (non-hidden) races defined".into()); }
|
||||
if classes.iter().filter(|c| !c.hidden).count() == 0 { return Err("No playable (non-hidden) classes defined".into()); }
|
||||
|
||||
log::info!("World '{}': {} rooms, {} npcs, {} objects, {} races, {} classes, {} guilds, {} spells",
|
||||
manifest.name, rooms.len(), npcs.len(), objects.len(), races.len(), classes.len(), guilds.len(), spells.len());
|
||||
|
||||
Reference in New Issue
Block a user