architecture token naming - Drowbe/coffee-pub-blacksmith GitHub Wiki
Token Naming β Architecture
Audience: Contributors to the Blacksmith codebase.
Token auto-renaming picks a name from a RollTable appropriate to the creature's type and subtype, instead of one global table. The single global table remains as the failover, so behavior is unchanged until a GM configures the new mapping.
Files:
| File | Role |
|---|---|
resources/naming-taxonomy.json |
Single source of truth β canonical keys, labels, kind, aliases |
scripts/utility-token-naming.js |
Resolver: type/subtype read, canonicalize, cascade, table lookup |
scripts/settings.js |
Generates one RollTable dropdown per canonical key from the taxonomy |
scripts/manager-canvas.js (_onCreateToken) |
Calls the resolver during the rename flow |
scripts/blacksmith.js |
Loads the taxonomy in early ready, before registerSettings() |
Do not enumerate the keys in this document β the taxonomy file is the source of truth and it grows. Read the file rather than hardcoding a key list here.
1. Design
Explicit table-per-key + alias JSON + global fallback.
- Each canonical key gets its own RollTable dropdown in settings, defaulting to
none. - The taxonomy JSON maps any incoming type/subtype spelling β a canonical key, and is the source of the key list itself β so settings are generated from it and no key list is hardcoded anywhere.
- Point several keys at the same table by selecting it in multiple dropdowns. Reuse is free.
- Table references are stored by name (see Β§6), consistent with the existing
tokenNameTable.
tokenNameFormat, ignoredTokens, and fuzzyMatch are orthogonal β only the name source changes.
2. The cascade
Specific beats broad; stop at the first table that resolves:
subtypefield β canonical key β assigned table (structured data, strongest signal)- Specific name keyword β canonical key β assigned table
typefield β canonical key β assigned table (the broad bucket, e.g.humanoid)- Global fallback β the existing
tokenNameTablesetting - No table β no rename
Rung 2 only matches specific keys. Broad keys are skipped there so they can't beat a role or subtype word in the name β that's what lets "Cultist", "Goblin Boss", and "Human Cultist" resolve correctly.
Broad vs. specific is derived from the data, not from code. Each key carries "kind":
kind |
Tier |
|---|---|
type |
broad |
subtype, role |
specific |
Tiebreaks: the structured subtype field beats a role found in the name; within the name, the first
specific word wins (species usually leads β "Goblin Cultist" β goblinoid).
3. Resolution algorithm
read details.type:
type = (type.value === 'custom') ? type.custom : type.value
subtype = type.subtype // free text; may be empty
canonicalize via the taxonomy (normalized: lowercase, trimmed):
subKey = canonicalize(subtype)
specificKey = specific name keyword from the actor name (canonicalized)
typeKey = canonicalize(type)
table = assignedTable(subKey) // 1. subtype field
|| assignedTable(specificKey) // 2. specific name keyword
|| assignedTable(typeKey) // 3. type field
|| game.tables.getName(tokenNameTable) // 4. global failover
|| (no rename) // 5.
roll table β name β existing format/ignored/fuzzy logic (unchanged)
canonicalize() matches the key itself or any alias, so orc / Orcs / orcish all resolve to the
same key. The aliasβkey index is built once and cached; settings lookups are direct map reads.
4. Why the subtype set is small
A subtype earns its own key only if its name pool is genuinely distinct. Everything else folds in via
aliases or rides the type-level table β e.g. human, halfling, and goliath deliberately have no key, so
they cascade to the humanoid table, which is the intended generic result. Mixed heritage routes to the
more distinctive pool (half-elf β elf).
5. GM-supplied taxonomy
A GM can point namingTaxonomyJson at their own file (path + browse button; defaults to the bundled one).
Because the file defines which per-key dropdowns exist, the setting is requiresReload: true, and the
path is read raw β before registerSettings() β so the dropdowns rebuild from the chosen file. An invalid
or missing custom file falls back to the bundled default.
6. Decisions and rationale
- Filename
naming-taxonomy.jsonmirrors the other*-taxonomy.jsonfiles (tag-taxonomy.json,pin-taxonomy.json); "schema" was avoided because it connotes a validation schema. - Tables referenced by name, not UUID β consistent with
tokenNameTableand the name-keyedgetTableChoices()dropdown. Rename-fragility matches today's behavior and degrades gracefully via the cascade. Revisit at the compendium phase, where cross-pack refs need UUIDs. - Rejected: a single root-folder + first-word table-name convention (convention-fragile, silent failures, prefix collisions like Dragon/Dragonborn). Rejected: explicit per-subtype settings UI (can't enumerate open-ended subtypes).
7. Edge cases
type.value === 'custom'β usetype.custom.- Subtype not in the key set (e.g.
tiefling) β falls to thetypetable (humanoid), then global. - Assigned table renamed or deleted β that rung is skipped; the cascade continues.
- Swarm size is ignored for naming.
- Non-dnd5e system or missing type data β straight to the global fallback.
8. Backward compatibility
Nothing changes until at least one per-key table is set. With all keys at none, the cascade always lands
on tokenNameTable β identical to the previous behavior.