UNPKG

@reldens/skills

Version:
620 lines (523 loc) 23.5 kB
## Package Overview **@reldens/skills** (v0.47.0) is a comprehensive skills, leveling, and combat system that provides: - Level sets with automatic progression and experience management - Class paths with skill trees and level-dependent unlocks - Multiple skill types (base, attack, effect, physical variants) - Combat mechanics (damage calculation, critical hits, dodge system) - Real-time server-client synchronization via Sender/Receiver pattern - Event-driven architecture for custom logic integration - Modifiers system integration via @reldens/modifiers This package is part of the Reldens MMORPG Platform ecosystem and can be attached to any entity owner with position methods. ## Comprehensive Documentation **Detailed technical documentation is available in the `.claude/` directory:** - **[Event System Architecture](.claude/event-system-architecture.md)** - Complete event system documentation including EventsManagerSingleton, removeKey registry, event naming, and testing anti-patterns - **[Skill Execution Flow](.claude/skill-execution-flow.md)** - Step-by-step skill execution phases, type-specific logic, and complete sequence diagrams - **[Testing Patterns](.claude/testing-patterns.md)** - Best practices, anti-patterns, and critical lessons learned from test suite debugging - **[Level Progression](.claude/level-progression.md)** - Complete leveling system guide including auto-fill, experience management, and class paths - **[Class Hierarchy](.claude/class-hierarchy.md)** - Architecture reference with inheritance, composition, and integration patterns **Always consult these documents when working with the corresponding systems.** ## Key Commands ```bash # Run all tests npm test # Run tests in watch mode npm run test:watch # Run specific tests with filter npm run test:filter=skill # Run tests with coverage npm run test:coverage ``` ## Architecture ### Package Structure ``` lib/ ├── skill.js # Base Skill class ├── level.js # Single level with modifiers ├── levels-set.js # Level progression system ├── class-path.js # Skill tree with class progression ├── server.js # SkillsServer wrapper ├── constants.js # Shared constants and action names ├── skills-events.js # Event name definitions ├── server/ │ └── sender.js # Server-to-client message sender ├── client/ │ └── receiver.js # Client message receiver └── types/ ├── attack.js # Damage-based skills ├── effect.js # Modifier-based skills ├── physical-attack.js # Physics-enabled attacks ├── physical-effect.js # Physics-enabled effects ├── physical-properties-validator.js └── physical-skill-runner.js ``` ### Core Classes **Skill** (`lib/skill.js:12`): - Base class for all skills - Provides validation, execution, range checking - Event system integration - Owner/target relationship management - Critical hit system (chance, multiplier, fixed value) - Cooldown/delay system (`skillDelay`, `canActivate`) - Cast time support with `castTime` property - Owner conditions validation - Owner effects application - Properties: - `key`: Unique skill identifier - `owner`: Entity that owns the skill (must have `getPosition()` method) - `target`: Target entity (can be fixed or passed on execution) - `range`: Skill range (0 = infinite range) - `rangeAutomaticValidation`: Auto-validate range before execution - `allowSelfTarget`: Allow targeting the owner - `usesLimit`: Maximum uses (0 = unlimited) - `ownerConditions`: Array of Condition instances - `ownerEffects`: Array of modifiers applied to owner on execution - `criticalChance`, `criticalMultiplier`, `criticalFixedValue` **Level** (`lib/level.js:9`): - Represents a single level with: - `key`: Integer level number - `modifiers`: Array of modifiers applied at this level - `label`: Display name for the level - `requiredExperience`: XP required to reach this level **LevelsSet** (`lib/levels-set.js:11`): - Manages level progression and experience - Auto-fills level ranges if `autoFillRanges` enabled - Experience-based automatic leveling - Modifier application/reversion on level up/down - Properties: - `owner`: Entity with `getPosition()` method - `levels`: Object of Level instances - `currentLevel`: Current level number - `currentExp`: Current experience points - `autoFillRanges`: Auto-generate intermediate levels - `autoFillExperienceMultiplier`: XP multiplier for auto-generated levels (default: 1.5) - `increaseLevelsWithExperience`: Auto level up when XP threshold reached - `levelsByExperience`: Sorted array of level keys by required XP - `setRequiredExperienceLimit`: Cap XP at max level requirement - Methods: - `levelUp()`: Increase level, apply modifiers - `levelDown()`: Decrease level, revert modifiers - `addExperience(number)`: Add XP, auto-level if enabled - `getNextLevelExperience()`: Get XP required for next level **ClassPath** (`lib/class-path.js:11`): - Extends LevelsSet - Provides skill tree functionality - Level-based skill unlocking - Dynamic label changes per level - Properties: - `key`: Class path identifier - `label`: Base display name - `labelsByLevel`: Object of {levelKey: 'label'} for level-specific names - `currentLabel`: Active display name - `skillsByLevel`: Object of {levelKey: [Skill instances]} - `skillsByLevelKeys`: Object of {levelKey: [skill keys]} - `currentSkills`: Object of currently available skills - `affectedProperty`: Property affected by class skills (e.g., 'hp') - Methods: - `addSkills(skills)`: Add skills to current set - `removeSkills(skills)`: Remove skills from current set - `setOwnerSkills(skills)`: Initialize owner's skill set based on current level **SkillsServer** (`lib/server.js:11`): - Server-side wrapper for ClassPath - Integrates Sender for client communication - Validates client has `send()` and `broadcast()` methods - Automatically initializes ClassPath and registers listeners ### Skill Types **Attack** (`lib/types/attack.js:14`): - Extends Skill for damage-based skills - Damage calculation with attack vs defense properties - Dodge system with aim vs dodge properties - Critical damage affected by aim/dodge ratio - Properties: - `affectedProperty`: Target property to modify (e.g., 'hp') - `hitDamage`: Base damage at 100% - `applyDirectDamage`: Skip calculations, apply hitDamage directly - `attackProperties`: Array of owner properties for attack total - `defenseProperties`: Array of target properties for defense total - `aimProperties`: Array of owner properties for aim total - `dodgeProperties`: Array of target properties for dodge total - `dodgeFullEnabled`: Enable complete dodge if dodge > (aim * dodgeOverAimSuccess) - `dodgeOverAimSuccess`: Dodge threshold multiplier (default: 1) - `damageAffected`: Apply dodge/aim ratio to damage - `criticalAffected`: Apply dodge/aim ratio to critical damage - `propertiesTotalOperators`: Custom operators for property totals - `allowEffectBelowZero`: Allow negative values on affected property - Damage Calculation: 1. Calculate owner aim and target dodge totals 2. Check full dodge: `dodge > (aim * dodgeOverAimSuccess)` 3. Calculate base damage: `hitDamage` modified by attack vs defense diff 4. Apply dodge/aim ratio if `damageAffected` enabled 5. Calculate critical damage with dodge/aim ratio if `criticalAffected` 6. Apply total damage to `affectedProperty` **Effect** (`lib/types/effect.js:14`): - Extends Skill for modifier-based skills (buffs/debuffs) - Applies modifiers to target with critical chance - Properties: - `targetEffects`: Array of modifiers applied to target - Logic: Validates range, applies modifiers with critical calculation **PhysicalAttack** (`lib/types/physical-attack.js:16`): - Extends Attack for physics-enabled attacks - Requires collision detection system - Properties: - `magnitude`: Physics force/magnitude - `objectWidth`: Collision object width - `objectHeight`: Collision object height - `validateTargetOnHit`: Verify target matches on collision - Requires owner to implement `executePhysicalSkill(target, skill)` method - Uses PhysicalSkillRunner for execution - Fires `SKILL_PHYSICAL_ATTACK_HIT` event on collision - Calls parent `runSkillLogic()` in `executeOnHit(target)` callback **PhysicalEffect** (`lib/types/physical-effect.js:16`): - Extends Effect for physics-enabled effects - Same physics properties as PhysicalAttack - Uses PhysicalSkillRunner for execution - Fires `SKILL_PHYSICAL_EFFECT_HIT` event on collision ### Communication System **Sender** (`lib/server/sender.js:11`): - Server-to-client message broadcaster - Registers event listeners on ClassPath events - Sends compressed data to client - Behaviors: - `BEHAVIOR_SEND`: Send to owner client only - `BEHAVIOR_BROADCAST`: Broadcast to all clients - `BEHAVIOR_BOTH`: Both send and broadcast - Registered Events: - `INIT_CLASS_PATH_END`: Send initial class data (level, label, XP, skills) - `LEVEL_UP`: Send level up data (new level, label, skills, next XP) - `LEVEL_EXPERIENCE_ADDED`: Send current XP - `SKILL_BEFORE_CAST`: Broadcast skill cast start - `SKILL_ATTACK_APPLY_DAMAGE`: Broadcast damage application - Message Format: ```javascript { act: 'rski.ICpe', // Action constant owner: ownerId, // Owner ID data: { // Compressed data lvl: 5, // Level lab: 'Warrior', // Label exp: 1500, // Current XP ne: 2000, // Next level XP skl: ['sword', 'shield'] // Skill keys } } ``` **Receiver** (`lib/client/receiver.js:10`): - Client-side message processor - Maps action constants to method names - Validates message action prefix (`rski.`) - Properties: - `actions`: Object mapping action constants to method names - `avoidDefaults`: Skip default action-to-method mapping - Default Method Mapping: Each action constant maps to `on[ActionName]` method - Usage: Extend and implement handler methods (e.g., `onLevelUp(message)`) ### Events System All events are prefixed with `reldens.skills.` and appended with owner event key. **IMPORTANT:** See `.claude/event-system-architecture.md` for complete event system documentation including: - EventsManagerSingleton architecture and removeKey registry - Event naming conventions (owner-namespaced) - Listener registration patterns - Testing anti-patterns and race conditions - Critical rules for event usage **Level Set Events** (fired by LevelsSet): - `INIT_LEVEL_SET_START`: Before level set initialization - `INIT_LEVEL_SET_END`: After level set initialization - `SET_LEVELS`: After levels are set - `GENERATED_LEVELS`: After auto-filling a level range - `LEVEL_UP`: After level increases - `LEVEL_DOWN`: After level decreases - `LEVEL_APPLY_MODIFIERS`: Before applying level modifiers - `LEVEL_EXPERIENCE_ADDED`: After XP added (args: number, newTotal, currentLevelIndex, nextLevelIndex, nextLevelKey, nextLevel, nextLevelExp, isLevelUp) **Class Path Events** (fired by ClassPath): - `INIT_CLASS_PATH_END`: After class path initialization - `SET_SKILLS`: After owner skills set - `ADD_SKILLS_BEFORE`: Before adding skills - `ADD_SKILLS_AFTER`: After adding skills - `REMOVE_SKILLS_BEFORE`: Before removing skills - `REMOVE_SKILLS_AFTER`: After removing skills **Skill Execution Events** (fired by Skill): - `VALIDATE_BEFORE`: Before skill validation - `VALIDATE_SUCCESS`: After successful validation - `VALIDATE_FAIL`: After validation failure (args: skill, failedCondition) - `SKILL_BEFORE_IN_RANGE`: Before range check - `SKILL_AFTER_IN_RANGE`: After range check (args: skill, interactionResult) - `SKILL_BEFORE_EXECUTE`: Before skill execution - `SKILL_APPLY_OWNER_EFFECTS`: After applying owner effects - `SKILL_BEFORE_CAST`: Before cast time starts - `SKILL_AFTER_CAST`: After cast time completes - `SKILL_BEFORE_RUN_LOGIC`: Before skill logic execution - `SKILL_AFTER_RUN_LOGIC`: After skill logic execution - `SKILL_AFTER_EXECUTE`: After skill execution complete **Attack/Effect Events**: - `SKILL_ATTACK_APPLY_DAMAGE`: After damage applied (args: skill, target, damage, newValue) - `SKILL_EFFECT_TARGET_MODIFIERS`: After effect modifiers applied - `SKILL_PHYSICAL_ATTACK_HIT`: On physical attack collision - `SKILL_PHYSICAL_EFFECT_HIT`: On physical effect collision **Event Naming Convention**: Events are automatically namespaced by owner: ```javascript // Event fired as: 'skills.ownerId.123.reldens.skills.levelUp' skill.fireEvent(SkillsEvents.LEVEL_UP, this); // Listen to specific owner's events: skill.listenEvent(SkillsEvents.LEVEL_UP, callback, removeKey, masterKey); ``` ### Constants **Skill Types** (`constants.js:10`): - `SKILL.TYPE.BASE`: Base skill (1) - `SKILL.TYPE.ATTACK`: Attack skill (2) - `SKILL.TYPE.EFFECT`: Effect skill (3) - `SKILL.TYPE.PHYSICAL_ATTACK`: Physical attack (4) - `SKILL.TYPE.PHYSICAL_EFFECT`: Physical effect (5) **Skill States** (`constants.js:53`): - `PHYSICAL_SKILL_INVALID_TARGET`: Target validation failed - `PHYSICAL_SKILL_RUN_LOGIC`: Running physical skill logic - `OUT_OF_RANGE`: Target out of range - `CAN_NOT_ACTIVATE`: Skill on cooldown or owner casting - `DODGED`: Attack dodged - `APPLYING_DAMAGE`: Damage calculation in progress - `APPLIED_DAMAGE`: Damage applied successfully - `APPLIED_CRITICAL_DAMAGE`: Critical damage applied - `APPLYING_EFFECTS`: Effects being applied - `APPLIED_EFFECTS`: Effects applied successfully - `EXECUTE_PHYSICAL_ATTACK`: Physical attack executing - `TARGET_NOT_AVAILABLE`: Target undefined or invalid **Action Constants** (`constants.js:7`): All action constants use `rski.` prefix (Reldens Skills) for network messages: - `ACTION_INIT_CLASS_PATH_END`: 'rski.ICpe' - `ACTION_LEVEL_UP`: 'rski.Lu' - `ACTION_LEVEL_EXPERIENCE_ADDED`: 'rski.Ea' - (See constants.js:24-51 for complete list) **Behaviors** (`constants.js:20`): - `BEHAVIOR_SEND`: Send to owner only - `BEHAVIOR_BROADCAST`: Broadcast to all - `BEHAVIOR_BOTH`: Both send and broadcast ## Dependencies - **@reldens/modifiers** (^0.33.0): Provides PropertyManager, Condition, Calculator, and modifier system - **@reldens/utils** (^0.54.0): Provides Shortcuts (sc), Logger, EventsManagerSingleton, InteractionArea ## Test Suite The package includes a comprehensive test suite using Node.js built-in test runner. **See `.claude/testing-patterns.md` for comprehensive testing best practices, anti-patterns, and lessons learned.** ### Test Structure ``` tests/ ├── run-tests.js # Test runner with filtering and coverage support ├── utils/ │ └── test-helpers.js # Helper functions for tests ├── fixtures/ │ ├── mocks/ # Mock classes (MockOwner, MockTarget, MockClient) │ ├── skills/ # Skill test data fixtures │ └── levels/ # Level test data fixtures └── unit/ ├── test-skill.js # Skill class tests ├── test-level.js # Level class tests ├── test-levels-set.js # LevelsSet class tests ├── test-class-path.js # ClassPath class tests ├── test-server.js # SkillsServer tests ├── types/ │ ├── test-attack.js # Attack skill type tests │ ├── test-effect.js # Effect skill type tests │ ├── test-physical-attack.js # PhysicalAttack tests │ └── test-physical-effect.js # PhysicalEffect tests ├── server/ │ └── test-sender.js # Sender tests └── client/ └── test-receiver.js # Receiver tests ``` ### Running Tests ```bash # Run all tests npm test # Watch mode for development npm run test:watch # Filter specific tests npm run test:filter=attack # Run only attack-related tests npm run test:filter=skill # Run only skill tests # Coverage report npm run test:coverage ``` ### Test Patterns - **Unit Tests**: Test individual classes and methods in isolation - **Mock Objects**: Use MockOwner, MockTarget, and MockClient for testing - **Fixtures**: Reusable test data in fixtures directory - **Event Testing**: Verify event firing and listening - **Async Testing**: Proper handling of async/await patterns - **Edge Cases**: Test boundary conditions and error states ### Critical Testing Rules #### Rule #1: NEVER Test Through Events Unless Testing the Event System **❌ WRONG:** ```javascript it('should send level up message', async () => { await classPath.levelUp(); // Fires event asynchronously assert.strictEqual(mockClient.sentMessages.length, 1); // Race condition! }); ``` **✅ CORRECT:** ```javascript it('should send level up message', async () => { await sender.sendLevelUpData(classPath); // Direct method call assert.strictEqual(mockClient.sentMessages.length, 1); // Deterministic }); ``` #### Rule #2: NEVER Use sleep() to Fix Race Conditions **❌ WRONG:** ```javascript await classPath.levelUp(); await TestHelpers.sleep(10); // DON'T DO THIS! assert.strictEqual(mockClient.sentMessages.length, 1); ``` **✅ CORRECT (for testing methods):** ```javascript await sender.sendLevelUpData(classPath); // Direct method call assert.strictEqual(mockClient.sentMessages.length, 1); ``` **✅ CORRECT (for testing timers):** ```javascript it('should restore canActivate after delay', async () => { skill.validate(); await TestHelpers.sleep(150); // Testing actual timer feature assert.strictEqual(skill.canActivate, true); }); ``` #### Rule #3: ALWAYS Use Unique removeKeys **❌ WRONG:** ```javascript // Test 1 classPath.listenEvent(SkillsEvents.ADD_SKILLS_BEFORE, callback, 'before-listener'); // Test 2 (after clearEventListeners) classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_BEFORE, callback, 'before-listener'); // ❌ removeKey collision - registration fails silently ``` **✅ CORRECT:** ```javascript // Test 1 classPath.listenEvent(SkillsEvents.ADD_SKILLS_BEFORE, callback, 'add-before-listener'); // Test 2 classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_BEFORE, callback, 'remove-before-listener'); // ✓ Unique removeKeys ``` **Summary:** - ❌ Don't test through events (use direct method calls) - ❌ Don't use `sleep()` to fix race conditions - ✅ Do use `sleep()` to test timer features (skillDelay, castTime) - ✅ Always use unique removeKeys across all tests ## Common Development Patterns ### Creating a Custom Skill ```javascript const { Skill } = require('@reldens/skills'); class MyCustomSkill extends Skill { constructor(props) { super(props); // Custom properties } async runSkillLogic() { // Implement skill behavior // Access this.owner, this.target // Use this.applyModifiers(), this.applyCriticalValue() return true; // Success } onExecuteConditions() { // Custom validation before execution return true; // Valid } } ``` ### Setting Up a Class Path ```javascript const { ClassPath, Level } = require('@reldens/skills'); let levels = { 1: new Level({ key: 1, modifiers: [/* modifier instances */], requiredExperience: 0 }), 5: new Level({ key: 5, modifiers: [/* modifier instances */], requiredExperience: 1000 }) }; let classPath = new ClassPath({ owner: playerEntity, key: 'warrior', label: 'Warrior', levels: levels, autoFillRanges: true, // Auto-generate levels 2, 3, 4 currentLevel: 1, currentExp: 0, skillsByLevel: { 1: [swordSkill], 5: [shieldSkill] } }); await classPath.init({ owner: playerEntity, key: 'warrior', levels: levels }); ``` ### Server-Side Integration ```javascript const { SkillsServer } = require('@reldens/skills'); let skillsServer = new SkillsServer({ owner: playerEntity, client: roomClient, // Must have send() and broadcast() methods key: 'warrior', levels: levels, skillsByLevel: skillsByLevel }); ``` ### Client-Side Integration ```javascript const { Receiver } = require('@reldens/skills/lib/client/receiver'); class MySkillsUI extends Receiver { onLevelUp(message) { console.log('Level up!', message.data.lvl); // Update UI with new level, label, skills } onLevelExperienceAdded(message) { console.log('XP gained:', message.data.exp); // Update XP bar } } let receiver = new MySkillsUI({owner: playerEntity}); gameClient.onMessage('*', (message) => receiver.processMessage(message)); ``` ### Hooking into Events ```javascript // Listen to skill events skill.listenEvent(SkillsEvents.SKILL_AFTER_EXECUTE, async (skill, target) => { console.log('Skill executed:', skill.key); // Custom logic after skill execution }, 'myUniqueKey', skill.getOwnerEventKey()); // Listen to level events classPath.listenEvent(SkillsEvents.LEVEL_UP, async (classPath) => { console.log('Leveled up to:', classPath.currentLevel); // Award achievement, show animation, etc. }, 'levelUpListener', classPath.getOwnerEventKey()); ``` ## Important Notes - **Owner Requirements**: All owners must implement `getPosition()` returning `{x, y}` object - **Physical Skills**: Require owner to implement `executePhysicalSkill(target, skill)` method - **Event-Driven**: All major operations fire events for custom logic integration - **Standalone Package**: Can be used independently from main Reldens platform - **Server Authoritative**: For multiplayer games, always validate skills on server - **Modifiers Integration**: Uses @reldens/modifiers Condition and Modifier instances - **Range System**: Range 0 = infinite range, uses InteractionArea for circular validation - **Critical System**: Independent chance, multiplier, and fixed value properties - **Auto-Leveling**: Automatically levels up when XP threshold reached if enabled - **Experience Cap**: Can limit XP to max level requirement with `setRequiredExperienceLimit` ## Analysis Approach When working on code issues: - Always investigate thoroughly before making changes - Read related files completely before proposing solutions - Trace execution flows and dependencies - Provide proof for issues, never guess or assume - Verify file contents before creating patches - Never jump to early conclusions - A variable with an unexpected value is not an issue, it is the result of a previous issue - Consult `.claude/` documentation for detailed technical information on each system