The identity model: archetypes, not people
The content tree — the contract for the
data/ tree.
Archetype files carry NO person-name semantics — no names, no races. A living NPC is composed at generation:
- Archetype primitives (
creatures/humanoids/) — base scores, level, proficiencies, gear, CR; - a Race rolled from region culture tables, whose ability adjustments are COMPOSED onto the archetype’s base scores, and whose size and speed replace the printed human assumptions. This is the SRD’s own model: the blocks are “Medium humanoid (any race)”, and Appendix MM-B says so directly — “These stat blocks can be used to represent both human and nonhuman NPCs” and “Racial Traits. You can add racial traits to an NPC. For example, a halfling druid might have a speed of 25 feet and the Lucky trait.” (p. 395);
- a name drawn from that race’s register in
data/names/— see the race key contract below.
Worked example — a dwarf guard: base 13/12/12/10/11/10, dwarf +2 Con →
13/12/14/10/11/10; Con mod +2 makes HP floor(2 × 4.5) + 2 × 2 = 13; speed 25 (dwarf), size Medium keeps the d8; AC stays 16 (Dex mod
still +1 under Max2); alignment rolls under the block’s constraint with
the dwarf bias (+0.3 moral, +0.5 ethic); the name comes from
names/dwarf.ron.
The rationale, from the designer: templates never carry identity, so every generated NPC is a real someone with a story — in this game they actually do.
The race key contract
data/races/<id>.srd.ron and data/names/<id>.ron share the filename
id: races/half_orc.srd.ron ↔ names/half_orc.ron. Name generation
falls back to the HUMAN tables (names/human.ron) for any race
without a name register — a modded race works immediately and sounds
human until given a voice. The built-in half-elf is the exception: without
its own register it draws an elf given name and an elf or human surname.
(names/human.ron landed with the name tables,
extracted from the engine’s names.rs frontier tables; soul::register_name
is the wiring, and the fallback target is now this data register.) The
fallback is deliberate: a missing
register is a soft gap, not a boot error (contrast with gear ids, where
a dangling reference IS a boot error — a guard without his spear is a
bug; a race without a dialect is a default).
The name provider is tiered (design seam only — no generator code
lives in data/): tier 1, the curated tables plus combinatorial
bynames with a living-collision epithet rule (two living “Durin”s force
an epithet); tier 2, the documented overflow for population scale —
deterministic markov chains trained on OUR OWN registers (never on
licensed corpora), so generated names stay in each race’s phonology.
data/ carries only the registers; tiers live in the engine when they
land.
AlignmentConstraint encodes the blocks’ printed alignment lines as
generation constraints on the axes (±1/3 label thresholds):
Any unconstrained; NonLawful ethic ≤ +1/3; NonGood moral ≤ +1/3;
Chaotic ethic < −1/3. The three current blocks use Any (commoner,
guard) and NonLawful (bandit); NonGood and Chaotic cover the rest
of Appendix MM-B’s phrasing (“any non-good alignment”, “any chaotic
alignment”) for when those blocks arrive.