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:
AI Agent
2026-03-14 16:32:27 -06:00
parent 598360ac95
commit 7b6829b1e8
17 changed files with 240 additions and 38 deletions

View File

@@ -29,7 +29,8 @@ impl ChargenState {
"\r\n{}\r\n\r\n",
ansi::bold("=== Choose Your Race ===")
));
for (i, race) in world.races.iter().enumerate() {
let visible_races: Vec<_> = world.races.iter().filter(|r| !r.hidden).collect();
for (i, race) in visible_races.iter().enumerate() {
let mods = format_stat_mods(&race.stats);
out.push_str(&format!(
" {}{}.{} {} {}\r\n {}\r\n",
@@ -82,7 +83,8 @@ impl ChargenState {
"\r\n{}\r\n\r\n",
ansi::bold("=== Choose Your Class ===")
));
for (i, class) in world.classes.iter().enumerate() {
let visible_classes: Vec<_> = world.classes.iter().filter(|c| !c.hidden).collect();
for (i, class) in visible_classes.iter().enumerate() {
let guild_info = class.guild.as_ref()
.and_then(|gid| world.guilds.get(gid))
.map(|g| format!(" → joins {}", ansi::color(ansi::YELLOW, &g.name)))
@@ -126,7 +128,7 @@ impl ChargenState {
ChargenStep::AwaitingRace => {
let race = find_by_input(
input,
&world.races.iter().map(|r| (r.id.clone(), r.name.clone())).collect::<Vec<_>>(),
&world.races.iter().filter(|r| !r.hidden).map(|r| (r.id.clone(), r.name.clone())).collect::<Vec<_>>(),
);
match race {
Some((id, name)) => {
@@ -149,6 +151,7 @@ impl ChargenState {
&world
.classes
.iter()
.filter(|c| !c.hidden)
.map(|c| (c.id.clone(), c.name.clone()))
.collect::<Vec<_>>(),
);