Traits Reference
Complete documentation for tabs/traits.json.
Schema
interface Trait {
name: string // ✅ Display name, must match object key
description: string // ✅ Player-facing description shown during selection
traitNarrativeEffects: string // ✅ AI narrator's primary reference for trait effects
attributes: Array<{attribute: string, modifier: number}> // ✅ Additive attribute modifiers
skills: Array<{skill: string, modifier: number}> // ✅ Additive skill level modifiers
resources: Array<{resource: string, modifier: number}> // ✅ Additive resource max modifiers
startingItems: InventoryDefinition[] // ✅ Items granted on permanent trait acquisition (not via triggers); item must be an existing item key
abilities: string[] // ✅ Ability names unlocked (deduplicated if multiple sources)
requirements: TraitRequirement[] // ⚠️ Prerequisites for level-up trait picks (defaults to [] if omitted)
unlockedBy: string[] // ✅ Prerequisites (OR logic): existing trait keys or story start ids
excludedBy: string[] // ✅ Conflicts: existing trait keys or story start ids
vulnerabilities?: string[] // ✅ Damage types that deal 1.5x damage to the player (can be empty [])
resistances?: string[] // ✅ Damage types that deal 0.5x damage to the player (can be empty [])
immunities?: string[] // ✅ Damage types that deal 0x damage to the player (can be empty [])
}
Configs written before the schema change may use quirk instead of traitNarrativeEffects; the value is migrated verbatim. Write new configs with traitNarrativeEffects.
Legend
- ✅ Predefine-able: Can be set in config, preserved via spread
- ⚠️ Calculated default: Has fallback logic if not predefined
TraitModifier Schema
interface AttributeModifier {
attribute: string // Key from attributeSettings.attributes
modifier: number // Additive value (positive or negative)
}
interface SkillModifier {
skill: string // Key from skills.json
modifier: number // Additive value to skill level
}
interface ResourceModifier {
resource: string // Key from resourceSettings.resources
modifier: number // Additive value to resource max
}
All modifiers are additive and stack across multiple traits:
character.attributes[attribute] += modifier
character.skills[skill].level += modifier
character.characterResources[resource].max += modifier
InventoryDefinition Schema
interface InventoryDefinition {
item: string // Key from items.json
quantity: number // How many to grant. A negative quantity removes that many of the item from the character instead (useful for traits/backgrounds that take away default gear). 0 is a no-op
}
TraitRequirement Schema
Same format as ability requirements. A requirement is one of two shapes. Most types name a target via variable; characterLevel has no target, so it omits variable entirely.
// resource | attribute | skill | trait
interface ReferencedTraitRequirement {
type: 'resource' | 'attribute' | 'skill' | 'trait' // What to check
variable: string // Name of the requirement target
amount: number // Required value (ignored for trait type)
}
// characterLevel — no variable
interface CharacterLevelTraitRequirement {
type: 'characterLevel'
amount: number // Required character level
}
| Type | Shape | Checks | Example |
|---|---|---|---|
resource | with variable | Resource max >= amount | { type: "resource", variable: "mana", amount: 50 } |
attribute | with variable | Attribute value >= amount | { type: "attribute", variable: "strength", amount: 14 } |
skill | with variable | Skill level >= amount | { type: "skill", variable: "melee", amount: 3 } |
trait | with variable | Has trait with that name | { type: "trait", variable: "fire affinity", amount: 1 } |
characterLevel | no variable | Character level >= amount | { type: "characterLevel", amount: 5 } |
Requirements gate level-up trait picks only. Starting trait selection at character creation ignores requirements: as far as this field is concerned, a player can pick any trait its category offers. unlockedBy and excludedBy do apply at character creation, following the evaluation order in Trait Dependencies below. If a trait must never be a starting option, keep it out of every traitCategories entry rather than relying on requirements.
Damage Modifiers (vulnerabilities, resistances, immunities)
These fields give the player character typed-damage modifiers in combat, the same way npcTypes gives them to NPCs:
| Modifier | Multiplier | Effect |
|---|---|---|
| vulnerability | 1.5x | +50% damage taken |
| resistance | 0.5x | -50% damage taken |
| immunity | 0x | No damage taken |
- The player’s effective lists are the union across all their traits (starting traits, level-up picks, and trigger-applied traits alike).
- Stacking with buffs: a static vulnerability plus a matching vulnerability buff becomes 2x; a static resistance plus a matching resistance buff becomes 0.25x (75% reduction).
- Precedence when the same damage type appears in multiple lists: immunity beats vulnerability, and vulnerability beats resistance.
- Values must match entries in
combatSettings.damageTypesintabs/settings.json. Mismatched values produce validation warnings and have no combat effect.
TraitCategory Schema
interface TraitCategory {
name: string // ✅ Display name shown to player
maxSelections: number // ✅ Maximum traits player can select across the whole category, subcategories included. 0 blocks all selection in the category
traits: string[] // ✅ Array of trait keys (not display names); ungrouped root traits
description?: string // ✅ Player-facing text explaining the category
subcategories?: TraitSubcategory[] // ✅ Optional groups shown inside the category
}
interface TraitSubcategory {
name: string // ✅ Display name for the group
traits: string[] // ✅ Array of trait keys in this group
description?: string // ✅ Player-facing text explaining the group
}
Categories group traits and limit selections during character creation. Players must select between 0 and maxSelections traits from each category; the limit applies to the category as a whole, however its traits are grouped. In character creation, subcategory traits are listed first, then the ungrouped root traits.
Category selection at character creation ignores requirements (level-up picks only). unlockedBy and excludedBy constrain which traits each category offers: see Trait Dependencies below. Category order in traitCategories is meaningful for those dependencies.
Trait Dependencies (unlockedBy and excludedBy)
Both fields list ids of traits or story starts:
unlockedBy(OR logic): the trait is only offered when at least one listed dependency is satisfied, that is, a listed trait is selected or a listed story start is the chosen one.excludedBy: the trait is withheld when any listed dependency is satisfied.
Evaluation order matters. The story start is chosen first, then trait categories are evaluated in the order they appear in traitCategories:
- A story start dependency always takes effect.
- A dependency on another trait only constrains choices in categories that come after the referenced trait’s own category.
- A dependency that is neither an earlier-category trait nor a story start leaves the trait unconditionally unlocked.
Changing an earlier selection, whether the story start or a trait in an earlier category, removes later selections that are no longer eligible. Both fields also filter the level-up pick list (see Level-Up Trait Picks below).
Modifier Application Order
When traits are selected during character creation:
1. Attribute modifiers → applied to base attributes
↓
2. Resource initialization (uses modified attributes via attributeStatModifiers)
↓
3. Skill modifiers → applied to skill levels
↓
4. Resource modifiers → applied to resource max values
↓
5. Abilities → added to character (deduplicated)
↓
6. Starting items → queued for granting
Why order matters: Attribute modifiers apply BEFORE resources initialize. If a trait gives +2 STR and attributeStatModifiers grants +2 health per STR, the character gets +4 bonus health from the attribute boost.
Trait Removal
When traits are removed dynamically (via triggers):
- Attribute modifiers subtracted
- Skill modifiers subtracted
- Resource modifiers subtracted (current may drop if above new max)
- Items NOT removed (remain in inventory)
- Abilities may be removed if no other source grants them
Low Attribute Traits
attributeSettings.lowAttributeTraits can auto-apply traits when attributes drop too low:
lowAttributeTraits: {
"strength": "weak-bodied",
"intelligence": "simple-minded"
}
If an attribute drops to/below lowAttributeThreshold, the corresponding trait is automatically applied.
Level-Up Trait Picks
Traits can also be earned during play through level-up picks, configured in progressionSettings in tabs/settings.json:
progressionSettings.levelUpTraitPoollists the trait names eligible as level-up rewards.- A cadence setting controls how often picks are granted (default: 1 pick every 10 levels).
When a player has pending picks, the game lists every pool trait that:
- The character doesn’t already have
- Passes its
unlockedBy/excludedByconditions (see Trait Dependencies above) - Meets all its
requirements
The player picks one, and it applies immediately exactly like a starting trait — skill/attribute/resource modifiers, abilities, and starting items all take effect.
Notes:
- An empty
levelUpTraitPoolmeans no picks are ever granted, regardless of cadence. - This is the ONLY place
requirementsmatters;unlockedByandexcludedByalso shape character creation (see Trait Dependencies above). - Level-up traits don’t need to appear in any
traitCategoriesentry; a trait can be pool-only (never a starting option) or both.
See the settings documentation for the full progressionSettings schema.
Dynamic Trait Changes
Triggers can add/remove traits via player-traits effect:
{
type: 'player-traits',
operator: 'add',
value: 'cursed'
}
A trigger-granted trait applies its attribute/skill/resource modifiers and abilities but does NOT grant its startingItems; only permanent acquisition paths (character creation and level-up picks) hand out starting items. Trait bonuses are reconciled as net deltas, so toggling or swapping traits never double-applies a modifier.
Cross-References
| Field | References |
|---|---|
attributes[].attribute | attributeSettings.attributes in tabs/settings.json |
skills[].skill | tabs/skills.json |
resources[].resource | resourceSettings.resources in tabs/settings.json |
startingItems[].item | tabs/items.json (validated against item keys) |
abilities[] | tabs/abilities.json |
requirements[].variable (resource) | resourceSettings.resources in tabs/settings.json |
requirements[].variable (attribute) | attributeSettings.attributes in tabs/settings.json |
requirements[].variable (skill) | tabs/skills.json |
requirements[].variable (trait) | Keys from traits in same file |
vulnerabilities[], resistances[], immunities[] | combatSettings.damageTypes in tabs/settings.json |
unlockedBy[] | Keys from traits in same file, or story start ids from tabs/story-starts.json |
excludedBy[] | Keys from traits in same file, or story start ids from tabs/story-starts.json |
traitCategories[].traits[] | Keys from traits in same file |
traitCategories[].subcategories[].traits[] | Keys from traits in same file |
progressionSettings.levelUpTraitPool (in tabs/settings.json) | Keys from traits in same file |