Skip to content

Registry

forelius edited this page Aug 14, 2026 · 2 revisions

The Registry

For developers. This page documents FaDe's internals — it's aimed at module authors and contributors, not players or GMs.

This page details game.fade.registry, the pluggable rule-system registry introduced in the Developer Guide. It is the place where FaDe keeps the game's math — how attacks hit, how damage applies, how initiative is rolled, how ability checks resolve — as swappable systems.

What the registry is

game.fade.registry is an instance of the fadeRegistry class (src/sys/registry/fadeRegistry.ts). It holds a plain dictionary of systems keyed by id. Each entry stores { id, type, instance }:

  • id — the string other code uses to look the system up.
  • type — the class (constructor) that produced the system.
  • instance — the live object code calls into (see below for the few exceptions).

The registry itself is created in the init hook as part of the game.fade object, and the default systems are registered immediately after system settings.

The API

Three methods cover everything:

  • registerSystem(id, instance, type) — adds a system. Because the dictionary is a plain object, registering under an existing id simply replaces the previous entry.
  • getSystem(id) — returns the system's instance. Pass true as a second argument to get the full { id, type, instance } object instead, which is useful when you need the class rather than the instance (for example, UserTablesConfig reads getSystem("userTables", true).type.TABLE_TYPES).
  • getSystemType(id) — returns the stored class for a system.

Two registration styles

Most systems are registered as instances: new DamageSystem(), new IndivInit(), and so on. Their collaborators and settings are captured on the instance when it is constructed.

A few systems are registered as the class itself, with no instance: wrestling, shove, and randomCharacter. These carry no per-instance state and expose static methods, so callers use them directly — game.fade.registry.getSystem("wrestling").calculateWrestlingRating(actor) invokes a static method.

How the defaults are chosen

registerDefaultSystems() runs during the init hook, after system settings have been registered. For each subsystem it reads the relevant setting and registers the matching implementation:

System id Registered implementations Selected by setting
userTables UserTables —
abilityScore AbilityScoreOriginal, AbilityScoreRetro abilityScoreMods
moraleCheck MoraleCheck —
abilityCheck AbilityCheck, TieredAbilityCheck abilityCheck
armorSystem ClassicArmorSystem —
damageSystem DamageSystem —
weaponMastery WeaponMasteryBase, WeaponMasteryHeroic weaponMastery
toHitSystem ToHitTHAC0, ToHitAAC, ToHitClassic, ToHitDarkDungeons, ToHitHeroic toHitSystem
encumbranceSystem BasicEncumbrance, ClassicEncumbrance, ExpertEncumbrance encumbrance
initiativeSystem IndivInit, GroupInit, AltGroupInit initiativeMode
wrestling Wrestling (static) —
shove Shove (static) —
savingThrowSystem SavingThrowSystem —
actorMovement ActorMovement —
ancestrySystem AncestrySystem —
classSystem SingleClassSystem, MultiClassSystem classSystem
randomCharacter RandomCharacter (static) —

Some implementations take constructor options that affect behavior; for example, BasicEncumbrance is registered as new BasicEncumbrance({ encSetting }) so it can adjust its logic for the basic encumbrance mode.

Registration order matters

Two ordering rules are deliberate:

  • userTables is registered first. Other systems read from user tables in their constructors — the to-hit system loads range modifiers, ability score systems load the ability-mods tables, ability checks load difficulty levels.
  • weaponMastery is registered before toHitSystem. The to-hit system's constructor captures the weapon mastery system as a collaborator.

The registerDefaultSystems source carries a TODO noting that the if/else selection blocks should not be necessary — ideally the registry itself would know how to select implementations.

Interfaces and shared bases

Each family of systems declares the contract its consumers rely on, either as an exported interface or an abstract base class:

  • ToHitInterface (ToHitSystem.ts) — getAttackRoll, getToHitResults, getLowestACHit, getDistance, getRange, plus a rangeModifiers map. Implemented by the abstract ToHitSystemBase, which the five variants extend.
  • WeaponMasteryInterface (WeaponMastery.ts) — getAttackRollMod, getDamageMods, getDefenseACMod, getDefenseMasteries, getRanges, and more.
  • Abstract bases — ToHitSystemBase, ClassSystemBase, ArmorSystemBase, AbilityScoreBase, and BaseInitiative. Variants extend the base and override only what differs, sharing the rest of the implementation.

Consumers type the result of getSystem() as the interface or base class rather than a concrete implementation — for example const toHitSystem: ToHitInterface = game.fade.registry.getSystem("toHitSystem"). That is what makes a system swappable without touching call sites.

How FaDe consumes systems

Systems are pulled out of the registry wherever their rules are needed, not centralized behind one API:

  • Data preparation. FDActorBase.prepareDerivedData() calls encumbranceSystem.prepareDerivedData(actor), actorMovement.prepareMovementRates(actor), and armorSystem.prepareDerivedData(actor) so each system derives its slice of the actor's derived data.
  • Combat. fadeCombat grabs initiativeSystem in its constructor and delegates initiative rolling and turn sorting to it.
  • Attack pipeline. AttackDialog pulls weaponMastery and toHitSystem to build the attack, AttackRollChatBuilder calls toHitSystem.getToHitResults(...), and DamageRollChatBuilder uses damageSystem.
  • Character rules. classSystem is used by actors, sheets, item classes, and dialogs (leveling, XP, saves); abilityScore, ancestrySystem, and abilityCheck are pulled in by actors, fields, and data models as needed.
  • Saving throws. FDActorSheetV2 pulls savingThrowSystem to roll a save clicked on the sheet, and savingThrowSystem's own static handler rolls for controlled tokens from saving-throw buttons in chat. The save targets those rolls check are set up by classSystem (via setupSavingThrows) and MonsterActor.#prepareSavingThrows.
  • Miscellany. fadeEffect reads userTables for condition data; FDActorSheetV2 uses abilityCheck and moraleCheck; fadeHandlebars uses classSystem in a helper.

Systems depend on each other

Systems themselves are consumers too. userTables is effectively a dependency of the whole registry: abilityScore, toHitSystem, and abilityCheck all read from it. toHitSystem uses weaponMastery, abilityScore, and damageSystem. damageSystem and armorSystem use abilityScore. initiativeSystem uses abilityScore for tie-breakers. savingThrowSystem calls into abilityScore for the save's ability modifier.

This is why the registry is populated during init rather than lazily: a system can fetch its collaborators by id at construction time because all defaults are already registered.

The saving throw system

SavingThrowSystem (src/sys/registry/SavingThrowSystem.ts) is registered as savingThrowSystem. It is a single concrete class with no variants and no interface or abstract base — it is registered unconditionally as new SavingThrowSystem().

execute({ actor, type, event }) is the entry point for rolling one of an actor's saves. It:

  • Looks up the actor's saving throw item — a specialAbility item with category save whose customSaveCode matches type — and bails out if the caller lacks OWNER permission or no such item exists. The items themselves are created on the actor by classSystem.setupSavingThrows() (characters) or MonsterActor.#prepareSavingThrows() (monsters), which copy them from the compendium and fill in their target from the class's save table.
  • Opens the save dialog (or skips it when the modifier key is held) and assembles the roll modifier from three sources:
    • the manual modifier entered in the dialog,
    • the ability-score modifier from abilityScore.getSavingThrowMod(...) — wisdom for magical effects, or the save item's configured abilityMod, flipped in sign when the save is a roll-under,
    • active-effect modifiers summed from actor.system.mod.save[type] and actor.system.mod.save.all — see the Active Effect Variables page.
  • Rolls saveItem.system.rollFormula plus @mod and posts the result as a chat message through ChatFactory, using the save item's target, operator, and rollMode.

The static handleSavingThrowRequest(event) is the click handler bound to saving-throw buttons in chat (.saving-roll); it rolls for every controlled token. Sheet clicks route through FDActorSheetV2's save handler, which calls execute() with the clicked save's customSaveCode.

Because the target, operator, and formula live on the actor's item rather than in the system, the class stays small — classSystem owns the save data, savingThrowSystem only rolls it.

Replacing a system

Because registration is a plain dictionary write, swapping a mechanic is a single call:

game.fade.registry.registerSystem("toHitSystem", new MyToHitSystem(), MyToHitSystem);

Last write wins. FaDe registers its defaults during init and fires afterFadeInit immediately after, so a module that swaps a system in afterFadeInit reliably overrides the default — the fade-white-box-fmag module does exactly this when it registers its own moraleCheck implementation. Any consumer that typed the system through its interface or base class keeps working as long as the replacement satisfies the same contract.

One caveat for module authors: systems that capture collaborators in their constructor (for example, the to-hit system captures weaponMastery) hold onto the instances that existed when they were built. Replacing a dependency later — say, registering a new weaponMastery after toHitSystem was already constructed — will not update the already-constructed system.

Clone this wiki locally