-
-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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.
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'sinstance. Passtrueas 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,UserTablesConfigreadsgetSystem("userTables", true).type.TABLE_TYPES). -
getSystemType(id)— returns the stored class for a system.
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.
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.
Two ordering rules are deliberate:
-
userTablesis 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. -
weaponMasteryis registered beforetoHitSystem. 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.
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 arangeModifiersmap. Implemented by the abstractToHitSystemBase, which the five variants extend. -
WeaponMasteryInterface(WeaponMastery.ts) —getAttackRollMod,getDamageMods,getDefenseACMod,getDefenseMasteries,getRanges, and more. -
Abstract bases —
ToHitSystemBase,ClassSystemBase,ArmorSystemBase,AbilityScoreBase, andBaseInitiative. 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.
Systems are pulled out of the registry wherever their rules are needed, not centralized behind one API:
-
Data preparation.
FDActorBase.prepareDerivedData()callsencumbranceSystem.prepareDerivedData(actor),actorMovement.prepareMovementRates(actor), andarmorSystem.prepareDerivedData(actor)so each system derives its slice of the actor's derived data. -
Combat.
fadeCombatgrabsinitiativeSystemin its constructor and delegates initiative rolling and turn sorting to it. -
Attack pipeline.
AttackDialogpullsweaponMasteryandtoHitSystemto build the attack,AttackRollChatBuildercallstoHitSystem.getToHitResults(...), andDamageRollChatBuilderusesdamageSystem. -
Character rules.
classSystemis used by actors, sheets, item classes, and dialogs (leveling, XP, saves);abilityScore,ancestrySystem, andabilityCheckare pulled in by actors, fields, and data models as needed. -
Saving throws.
FDActorSheetV2pullssavingThrowSystemto roll a save clicked on the sheet, andsavingThrowSystem's own static handler rolls for controlled tokens from saving-throw buttons in chat. The save targets those rolls check are set up byclassSystem(viasetupSavingThrows) andMonsterActor.#prepareSavingThrows. -
Miscellany.
fadeEffectreadsuserTablesfor condition data;FDActorSheetV2usesabilityCheckandmoraleCheck;fadeHandlebarsusesclassSystemin a helper.
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.
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
specialAbilityitem with categorysavewhosecustomSaveCodematchestype— and bails out if the caller lacksOWNERpermission or no such item exists. The items themselves are created on the actor byclassSystem.setupSavingThrows()(characters) orMonsterActor.#prepareSavingThrows()(monsters), which copy them from the compendium and fill in theirtargetfrom 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 configuredabilityMod, flipped in sign when the save is a roll-under, - active-effect modifiers summed from
actor.system.mod.save[type]andactor.system.mod.save.all— see the Active Effect Variables page.
- Rolls
saveItem.system.rollFormulaplus@modand posts the result as a chat message throughChatFactory, using the save item'starget,operator, androllMode.
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.
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.