@reldens/skills
Version:
620 lines (523 loc) • 23.5 kB
Markdown
## 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