@reldens/skills Architecture

Overview of the @reldens/skills package: skill types, levels and class paths, server and client communication, damage calculation and extension points.

Overview

@reldens/skills provides the skills, experience and leveling system used by Reldens. It can be attached to any owner entity and includes:

  • Level sets with experience tracking, automatic level up and level modifiers.
  • Class paths with skill trees (skills unlocked by level) and labels by level.
  • Skill types: base skills, attacks with damage calculation, effects (buffs / debuffs) and physics-based attacks and effects.
  • Combat mechanics: critical hits, aim vs dodge, attack vs defense, range validation, cooldowns and cast time.
  • Server to client synchronization through a Sender (server) and a Receiver (client).
  • Events for every step, namespaced by owner.
npm install @reldens/skills

Dependencies: @reldens/modifiers (Modifier, Condition, PropertyManager, Calculator) and @reldens/utils (Shortcuts, Logger, EventsManagerSingleton, InteractionArea).

const {
    Skill, Attack, Effect, PhysicalAttack, PhysicalEffect,
    Level, LevelsSet, ClassPath,
    Receiver, SkillsEvents, SkillConst
} = require('@reldens/skills');
// the server wrapper is required from its own file:
const SkillsServer = require('@reldens/skills/lib/server');

Owner requirements

  • getPosition() returning {x, y} - required by skills, level sets, class paths and the server wrapper.
  • An ID property, id by default (configurable with ownerIdProperty).
  • Optional eventsPrefix - used as the event namespace for the owner (recommended, see the event system).
  • Optional eventUniqueKey() - used to build unique listener keys.
  • executePhysicalSkill(target, skill) - required for physical skills.

Package Structure

  • lib/skill.js - base Skill class.
  • lib/level.js - single Level.
  • lib/levels-set.js - level progression and experience.
  • lib/class-path.js - class path with skill tree.
  • lib/server.js - SkillsServer wrapper.
  • lib/constants.js - skill types, states, actions and behaviors.
  • lib/skills-events.js - event names.
  • lib/server/sender.js - server to client messages.
  • lib/client/receiver.js - client message handler.
  • lib/types/ - attack.js, effect.js, physical-attack.js, physical-effect.js, physical-skill-runner.js, physical-properties-validator.js.

Core Classes

  • Skill (base class)
    • Attack - damage calculation
      • PhysicalAttack - executed on collision
    • Effect - modifiers applied to the target
      • PhysicalEffect - executed on collision
  • LevelsSet - experience and progression
    • ClassPath - adds the skill tree and labels by level
  • Level - data container: key, label, required experience and modifiers.
  • SkillsServer - wraps a ClassPath (property classPath) and a Sender (property client).
  • Receiver - client-side message processor.

See Skills Class Hierarchy for the properties and methods of each class.

Data Flow

Skill execution

  1. The game validates the skill with skill.validate(): cooldown, owner casting state, owner conditions and uses limit. A successful validation starts the cooldown timer.
  2. skill.execute(target) fires the before execute event, sets the target, runs onExecuteConditions() and the automatic range validation (when enabled).
  3. Owner effects are applied to the owner.
  4. With a cast time, the owner is marked as casting and the logic runs when the timer ends; otherwise it runs immediately.
  5. runSkillLogic() runs the type-specific behavior: damage for attacks, target modifiers for effects, the physics call for physical skills.
  6. The uses counter increases, onExecuteRewards() runs and the after execute event is fired.
  7. When a SkillsServer is used, the Sender sends the cast and damage messages to the clients.

See Skills Execution Flow for every step and event.

Level progression

  1. addExperience(amount) adds the experience to the current total.
  2. When increaseLevelsWithExperience is enabled (default) and the total reaches the next level requirement, levelUp() runs for every level reached.
  3. Each level up increases the level, applies the new level modifiers to the owner and fires the level up event. A ClassPath also adds the skills and label of the new level.
  4. The experience added event is fired with the new total.
  5. When a SkillsServer is used, the Sender sends the level up and experience messages to the owner client.

See Skills Level Progression for the details.

Class path initialization

  1. new ClassPath({owner}) stores the owner and the events manager.
  2. await classPath.init(props) runs the LevelsSet initialization (levels, current level and experience).
  3. Sets the key (required), label, labels by level and current label.
  4. Sets the skills by level and fills the current skills with every skill up to the current level.
  5. Fires the class path init end event.

Event System

All events are defined in SkillsEvents with the reldens.skills. prefix and are fired through the shared EventsManager, namespaced by owner: {ownerEventKey}.reldens.skills.{eventName}, where the owner event key is owner.eventsPrefix when defined.

Event categories:

  • Validation: validate before / success / fail.
  • Execution: before / after execute, before / after run logic, apply owner effects, range checks.
  • Cast: before / after cast.
  • Combat: attack apply damage, effect target modifiers, physical attack / effect hit.
  • Progression: level set init, set levels, generated levels, level up / down, level apply modifiers, experience added.
  • Class path: init end, set skills, add / remove skills before and after.

See Skills Event System for the full catalog, parameters and listener rules.

Property Paths

Every property name used by the package (affected property, attack / defense / aim / dodge properties, range properties, modifier and condition property keys) is a @reldens/modifiers path with / as separator:

  • hp - owner.hp
  • stats/hp - owner.stats.hp
  • stats/power/magical - owner.stats.power.magical

Reading a property that does not exist throws an error, so make sure the owner and target objects contain every configured path.

Modifier Integration

  • Level modifiers - applied to the owner on level up and reverted on level down.
  • ownerConditions - Condition instances validated against the owner in validate().
  • ownerEffects - modifiers applied to the owner when the skill executes (for example, a mana cost), without critical.
  • targetEffects - Effect skill modifiers applied to the target, with critical.
const { Modifier, ModifierConst } = require('@reldens/modifiers');

let manaCost = new Modifier({
    key: 'fireball-mana-cost',
    propertyKey: 'stats/mp',
    operation: ModifierConst.OPS.DEC,
    value: 10,
    minValue: 0
});
let atkBuff = new Modifier({
    key: 'atk-boost',
    propertyKey: 'stats/atk',
    operation: ModifierConst.OPS.INC,
    value: 5
});

Network Communication

Sender (server)

Created by SkillsServer when a client is provided. It listens to the ClassPath events and calls the client send() or broadcast() with messages shaped as {act, owner, data}:

  • Class path init end - send - level, label, experience, skill keys, next level experience and the level label.
  • Level up - send - level, label, new skill keys and next level experience.
  • Experience added - send - current experience.
  • Skill before cast - broadcast - skill key and the owner position.
  • Attack apply damage - broadcast - damage.

The cast and damage messages also include extraData when the owner implements getSkillExtraData({skill, target}).

{
    act: 'rski.Lu',
    owner: 'player-123',
    data: {
        lvl: 5,
        lab: 'Warrior',
        skl: ['sword', 'shield'],
        ne: 2000
    }
}

Receiver (client)

Processes messages whose action starts with rski. and calls the mapped handler method, for example onLevelUp(message). Extend it and implement the handlers you need; see Skills Event System for the action to method mapping.

Damage Calculation

Attack skills calculate the damage when the logic runs:

  1. Totals - the owner aim and the target dodge are the sum of their configured properties (or combined with the operators in propertiesTotalOperators).
  2. Full dodge - when dodgeFullEnabled (default true) and dodge > aim * dodgeOverAimSuccess, the attack is dodged and no damage is applied.
  3. Target check - without allowEffectBelowZero, no damage is applied when the affected property is already 0 or lower.
  4. Base damage - hitDamage as is when applyDirectDamage is true; otherwise it is adjusted by the attack vs defense difference:
    • Attack higher than defense: the damage increases by the difference as a percentage of the defense (capped at 99%).
    • Defense higher than attack: the damage decreases by the difference as a percentage of the attack (capped at 99%).
    • With damageAffected and dodge higher than aim, the damage is reduced by the dodge over aim proportion.
  5. Critical - the critical bonus is calculated on the damage; with criticalAffected and dodge higher than aim, the bonus is reduced by the same proportion. The bonus is added to the damage.
  6. Apply - the damage is subtracted from the affected property (clamped to 0 unless allowEffectBelowZero) and the attack apply damage event is fired with (skill, target, damage, newValue).

Design Patterns

  • Template method - Skill.execute() defines the workflow and calls the hooks onExecuteConditions(), runSkillLogic() and onExecuteRewards().
  • Strategy - each skill type implements its own runSkillLogic().
  • Observer - every step fires events through the shared EventsManager, so features can react without coupling (persistence, chat messages, animations).

Extension Points

Custom skill types

const { Skill } = require('@reldens/skills');

class MyCustomSkill extends Skill
{

    constructor(props)
    {
        super(props);
        this.customProperty = props.customProperty;
    }

    onExecuteConditions()
    {
        // return false to cancel the execution
        return true;
    }

    async runSkillLogic()
    {
        // this.owner and this.target are available here
        return true;
    }

    async onExecuteRewards()
    {
        // optional rewards after a successful execution
    }

}

Custom level progression

const { ClassPath } = require('@reldens/skills');

class BonusExperienceClassPath extends ClassPath
{

    async addExperience(number)
    {
        return await super.addExperience(Math.floor(number * 1.2));
    }

}

Event hooks

skill.listenEvent(
    SkillsEvents.SKILL_AFTER_EXECUTE,
    async (skill, target) => {
        // award achievements, update statistics, etc.
    },
    skill.getOwnerUniqueEventKey('achievementsListener'),
    skill.getOwnerEventKey()
);

Performance Considerations

  1. Remove the owner listeners when the owner is destroyed (for example, by master key with the EventsManager).
  2. Clear the timers you no longer need: skill.skillActivationTimer (cooldown) and owner.castingTimer (cast time).
  3. Keep the property paths short and the number of active modifiers per entity limited; every property read navigates the full path.
  4. Reuse skill instances per owner instead of creating new ones for every execution.

Related Documentation

Go Up