@reldens/skills Event System
How the @reldens/skills package names, fires and listens to events: owner namespacing, remove keys, the full events catalog, event sequences and server to client synchronization.
Overview
The package is event-driven and uses EventsManagerSingleton from @reldens/utils:
- Single shared instance - skills, level sets, class paths and the Sender all use the same EventsManager (unless you pass your own in the events prop).
- Owner namespacing - events are prefixed with an owner key, so the listeners of one player are not triggered by another player.
- Remove keys registry - listeners registered with a remove key are tracked in a registry that is not cleared by removeAllListeners().
Event Naming
Every class builds the full event name with eventFullName(eventName), which returns getOwnerEventKey() + '.' + eventName.
- When the owner defines eventsPrefix, that value is the owner event key.
- Otherwise LevelsSet and ClassPath use skills.ownerId.{ownerId}, while Skill uses skill.ownerId.{ownerId}.
Define eventsPrefix on your owners: it keeps the skills and the class path of the same owner in one namespace, which the Sender needs to catch the cast and damage events fired by the skills. Reldens players define it.
Example with owner.eventsPrefix = 'skills.ownerId.player-123' and the level up event:
// SkillsEvents.LEVEL_UP = 'reldens.skills.levelUp'
classPath.eventFullName(SkillsEvents.LEVEL_UP);
// 'skills.ownerId.player-123.reldens.skills.levelUp'
Listener Registration
listenEvent(eventName, callback, removeKey, masterKey)
{
return this.events.onWithKey(this.eventFullName(eventName), callback, removeKey, masterKey);
}
- eventName (required) - base event name, for example SkillsEvents.LEVEL_UP.
- callback (required) - function executed when the event fires.
- removeKey (required) - unique identifier of this listener, used to remove it later.
- masterKey (optional) - groups listeners so they can be removed together (the Sender uses the owner event key).
Remove keys must be unique
If a remove key is already registered, onWithKey() returns false and the listener is not added (only a debug log is written). The remove keys registry is not cleared by removeAllListeners(), so a key used once stays taken until it is removed with offWithKey() or offByMasterKey().
// WRONG - the second registration is ignored because the remove key already exists
classPath.listenEvent(SkillsEvents.ADD_SKILLS_BEFORE, callback, 'before-listener');
classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_BEFORE, callback, 'before-listener');
// RIGHT - unique remove keys, built from the owner
classPath.listenEvent(
SkillsEvents.ADD_SKILLS_BEFORE,
callback,
classPath.getOwnerUniqueEventKey('addSkillsBefore'),
classPath.getOwnerEventKey()
);
classPath.listenEvent(
SkillsEvents.REMOVE_SKILLS_BEFORE,
callback,
classPath.getOwnerUniqueEventKey('removeSkillsBefore'),
classPath.getOwnerEventKey()
);
getOwnerUniqueEventKey(suffix) uses owner.eventUniqueKey() when available, otherwise skills.ownerId.{ownerId}.uKey.{time}, plus the suffix.
Event Firing
async fireEvent(eventName, ...args)
{
return await this.events.emit(this.eventFullName(eventName), ...args);
}
- Listeners run in registration order.
- When a listener returns a promise, emit() awaits it before calling the next one, so awaiting fireEvent() waits for every listener.
- When no listener exists, the emit completes immediately.
- Most events are awaited by the package. The exceptions are the events fired inside validate() and isInRange() (validate before / success / fail and the range events), which are fired without awaiting: synchronous listeners still run before the method returns, async listeners may finish later.
- The SkillsServer constructor starts classPath.init() without awaiting it, so the init events can complete after the constructor returns.
Events Catalog
All names start with reldens.skills. (SkillsEvents.PREF).
Level set events (LevelsSet and ClassPath)
- INIT_LEVEL_SET_START - initLevelSetStart - params: (levelsSet).
- INIT_LEVEL_SET_END - initLevelSetEnd - params: (levelsSet).
- SET_LEVELS - setLevels - params: (levelsSet, levels).
- GENERATED_LEVELS - generatedLevels - fired for each auto-generated level - params: (levelsSet).
- LEVEL_UP - levelUp - after the level increased and its modifiers were applied - params: (levelsSet).
- LEVEL_DOWN - levelDown - after the level modifiers were reverted and the level decreased - params: (levelsSet).
- LEVEL_APPLY_MODIFIERS - levelApplyModifiers - before the level modifiers are applied or reverted - params: (levelsSet, levelInstance).
- LEVEL_EXPERIENCE_ADDED - experienceAdded - params: (levelsSet, number, newTotalExp, currentLevelIndex, nextLevelIndex, nextLevelKey, nextLevel, nextLevelExp, isLevelUp).
Class path events (ClassPath)
- INIT_CLASS_PATH_END - initClassPathEnd - params: (classPath).
- SET_SKILLS - setSkills - after the current skills are calculated from the levels (not fired when currentSkills is preset) - params: (classPath).
- ADD_SKILLS_BEFORE / ADD_SKILLS_AFTER - addSkillBefore / addSkillAfter - params: (classPath, skills).
- REMOVE_SKILLS_BEFORE / REMOVE_SKILLS_AFTER - removeSkillBefore / removeSkillAfter - params: (classPath, skills).
Skill events (Skill and its types)
- VALIDATE_BEFORE - beforeValidate - params: (skill).
- VALIDATE_SUCCESS - validateSuccess - params: (skill). A synchronous listener can set skill.isValid = false to reject the validation.
- VALIDATE_FAIL - validateFail - an owner condition failed - params: (skill, failedCondition).
- SKILL_BEFORE_IN_RANGE - beforeIsInRange - params: (skill).
- SKILL_AFTER_IN_RANGE - afterIsInRange - not fired when the range is 0 (infinite) - params: (skill, interactionResult).
- SKILL_BEFORE_EXECUTE - beforeExecute - params: (skill, target).
- SKILL_APPLY_OWNER_EFFECTS - applyOwnerEffects - after the owner effects are applied - params: (skill, target).
- SKILL_BEFORE_CAST - beforeCast - params: (skill, target).
- SKILL_AFTER_CAST - afterCast - params: (skill, target, skillLogicResult).
- SKILL_BEFORE_RUN_LOGIC - beforeRunLogic - params: (skill, target).
- SKILL_AFTER_RUN_LOGIC - afterRanLogic - params: (skill, target).
- SKILL_AFTER_EXECUTE - afterExecute - params: (skill, target).
- SKILL_ATTACK_APPLY_DAMAGE - attackApplyDamage - params: (skill, target, damage, newValue).
- SKILL_EFFECT_TARGET_MODIFIERS - effectTargetModifiers - params: (skill).
- SKILL_PHYSICAL_ATTACK_HIT - physicalAttackOnHit - params: (skill, target).
- SKILL_PHYSICAL_EFFECT_HIT - physicalEffectOnHit - params: (skill, target).
Defined for integrations
LOADED_OWNER_SKILLS (loadedOwnerSkills) and EXECUTING_SKILL (executingSkill) are defined (with their action constants) but not fired by the package; your game can fire them.
Event Sequences
Skill validation
- VALIDATE_BEFORE
- VALIDATE_FAIL when an owner condition fails (the validation stops), otherwise VALIDATE_SUCCESS
Skill execution without cast time
- SKILL_BEFORE_EXECUTE
- SKILL_BEFORE_IN_RANGE / SKILL_AFTER_IN_RANGE (with rangeAutomaticValidation)
- SKILL_APPLY_OWNER_EFFECTS
- SKILL_BEFORE_RUN_LOGIC
- Type-specific events (see below)
- SKILL_AFTER_RUN_LOGIC
- SKILL_AFTER_EXECUTE
Skill execution with cast time
- SKILL_BEFORE_EXECUTE, the range events and SKILL_APPLY_OWNER_EFFECTS as above
- SKILL_BEFORE_CAST
- SKILL_AFTER_EXECUTE - fired right away, while the owner is still casting
- When the cast time ends: SKILL_BEFORE_RUN_LOGIC, type-specific events, SKILL_AFTER_RUN_LOGIC
- SKILL_AFTER_CAST with the logic result
Type-specific events
- Attack and Effect: the range events (their logic checks the range again with the current positions), then SKILL_ATTACK_APPLY_DAMAGE or SKILL_EFFECT_TARGET_MODIFIERS.
- PhysicalAttack and PhysicalEffect: the range events while launching; later, on collision, SKILL_PHYSICAL_ATTACK_HIT or SKILL_PHYSICAL_EFFECT_HIT followed by the parent Attack or Effect events.
Level set and class path initialization
- INIT_LEVEL_SET_START
- GENERATED_LEVELS for each auto-filled level (with autoFillRanges)
- SET_LEVELS
- INIT_LEVEL_SET_END
- ClassPath only: ADD_SKILLS_BEFORE / ADD_SKILLS_AFTER for each level with skills, SET_SKILLS, INIT_CLASS_PATH_END
Experience and levels
- For each level reached: ClassPath ADD_SKILLS_BEFORE / ADD_SKILLS_AFTER (when the level has skills), LEVEL_APPLY_MODIFIERS, LEVEL_UP
- LEVEL_EXPERIENCE_ADDED
Level down: ClassPath REMOVE_SKILLS_BEFORE / REMOVE_SKILLS_AFTER, then LEVEL_APPLY_MODIFIERS and LEVEL_DOWN.
Server to Client Synchronization
Sender (server)
The Sender registers its listeners on the ClassPath (with unique remove keys and the owner event key as master key):
this.classPath.listenEvent(
SkillsEvents.LEVEL_UP,
this.sendLevelUpData.bind(this),
this.classPath.getOwnerUniqueEventKey('levelUpSender'),
ownerEventKey
);
- The ClassPath level up fires LEVEL_UP.
- The Sender listener runs sendLevelUpData(classPath) and builds the message.
- The client send() (or broadcast()) transmits it.
- On the client, receiver.processMessage(message) calls the mapped handler.
{
act: 'rski.Lu',
owner: 'player-123',
data: {
lvl: 5,
lab: 'Warrior',
ne: 2000,
skl: ['sword', 'shield']
}
}
Receiver (client) action mapping
Default mapping between the action constants and the handler methods you can implement:
- rski.ILss - onInitLevelSetStart
- rski.ILse - onInitLevelSetEnd
- rski.ICpe - onInitClassPathEnd
- rski.Los - onLoadedOwnerSkills
- rski.Sk - onSetSkills
- rski.Sl - onSetLevels
- rski.Asb / rski.Asa - onAddSkillsBefore / onAddSkillsAfter
- rski.Rsb / rski.Rsa - onRemoveSkillsBefore / onRemoveSkillsAfter
- rski.Bv, rski.Vs, rski.Vf - onValidateBefore, onValidateSuccess, onValidateFail
- rski.Es - onExecutingSkill
- rski.Ea - onLevelExperienceAdded
- rski.Lu / rski.Ld - onLevelUp / onLevelDown
- rski.Apm - onLevelApplyModifiers
- rski.Bir / rski.Air - onSkillBeforeInRange / onSkillAfterInRange
- rski.Be / rski.Ae - onSkillBeforeExecute / onSkillAfterExecute
- rski.Bc / rski.Ac - onSkillBeforeCast / onSkillAfterCast
- rski.Ad - onSkillAttackApplyDamage
- rski.Brl / rski.Arl - onSkillBeforeRunLogic / onSkillAfterRunLogic
- rski.Pah / rski.Peh - onSkillPhysicalAttackHit / onSkillPhysicalEffectHit
The default Sender only sends rski.ICpe, rski.Lu, rski.Ea, rski.Bc and rski.Ad; the other actions are available for your own senders.
const { Receiver } = require('@reldens/skills');
class MyGameClient extends Receiver
{
onLevelUp(message)
{
console.log('Level up!', message.data.lvl);
}
}
let receiver = new MyGameClient({owner: player});
// for every message received from the server:
receiver.processMessage(message);
Debugging Tips
- Print the full event name to compare it with your listener: classPath.eventFullName(SkillsEvents.LEVEL_UP).
- Check the listenEvent() result: false means the listener was not registered (usually a duplicated remove key).
- Set EventsManagerSingleton.debug = 'all' (or a comma-separated list of event names, or a part of them such as reldens.skills) before the events are registered to log every listen and fire; the messages use the debug log level (for example RELDENS_LOG_LEVEL=9).
Rules
- Always await fireEvent() and the async methods that fire events.
- Use unique remove keys, for example with getOwnerUniqueEventKey(suffix).
- Define eventsPrefix on the owners so all their events share one namespace.
- Use a master key per owner so all its listeners can be removed at once when the owner is destroyed.
- Pass the complete context as event parameters, and document what each custom event sends.
- In tests, call the methods directly instead of testing through events, unless the event itself is under test (see the Skills Testing Guide).
Related Documentation
- @reldens/skills Architecture - Package overview.
- Skills Execution Flow - When each skill event is fired.
- Skills Level Progression - Level and class path events in context.
- Skills Testing Guide - Testing event-driven code.
- Events Manager - The shared events system, remove keys and master keys.
reldens