@reldens/skills
Version:
473 lines (374 loc) • 12.1 kB
Markdown
# Architecture Overview
## Package Structure
```
/skills/
├── lib/
│ ├── skill.js # Base Skill class
│ ├── level.js # Single Level class
│ ├── levels-set.js # Level progression system
│ ├── class-path.js # Class with skill tree
│ ├── server.js # Server-side wrapper
│ ├── constants.js # Shared constants
│ ├── skills-events.js # Event definitions
│ ├── server/
│ │ └── sender.js # Server-to-client messages
│ ├── client/
│ │ └── receiver.js # Client message handler
│ └── types/
│ ├── attack.js # Damage-dealing skill
│ ├── effect.js # Modifier-applying skill
│ ├── physical-attack.js # Physics-enabled attack
│ ├── physical-effect.js # Physics-enabled effect
│ ├── physical-skill-runner.js # Physics execution helper
│ └── physical-properties-validator.js
├── tests/
│ ├── run-tests.js # Test runner
│ ├── utils/ # Test utilities
│ ├── fixtures/ # Test data and mocks
│ └── unit/ # Unit tests
├── .claude/
│ ├── settings.json # Claude permissions
│ ├── instructions.md # Development workflow
│ ├── testing-guide.md # Testing documentation
│ └── architecture.md # This file
├── index.js # Main export
├── package.json # Package metadata
└── CLAUDE.md # Package documentation
```
## Core Classes
### Inheritance Hierarchy
```
Skill (base class)
├── Attack (damage calculations)
│ └── PhysicalAttack (physics-enabled)
└── Effect (modifier application)
└── PhysicalEffect (physics-enabled)
LevelsSet (experience and progression)
└── ClassPath (skill tree system)
└── SkillsServer (server wrapper)
```
## Data Flow
### Skill Execution Flow
```
1. Client Request
└→ Player triggers skill
2. Validation Phase
├→ skill.validate()
│ ├→ Check isReady
│ ├→ Check canActivate
│ ├→ Check isCasting
│ ├→ Check usesLimit
│ └→ validateConditions()
│
└→ skill.execute(target)
├→ Fire SKILL_BEFORE_EXECUTE
├→ Check target existence
├→ onExecuteConditions() (custom)
├→ isValidRange(target)
├→ applyModifiers (owner effects)
└→ Fire SKILL_APPLY_OWNER_EFFECTS
3. Cast Time Handling
├→ If castTime > 0:
│ ├→ Fire SKILL_BEFORE_CAST
│ ├→ Set owner.isCasting = true
│ ├→ setTimeout(castTime)
│ └→ Fire SKILL_AFTER_CAST
└→ Else: Execute immediately
4. Skill Logic Execution
├→ Fire SKILL_BEFORE_RUN_LOGIC
├→ runSkillLogic() (class-specific)
│ ├→ Attack: applyDamageTo()
│ │ ├→ Calculate aim vs dodge
│ │ ├→ Check full dodge
│ │ ├→ Calculate damage (atk vs def)
│ │ ├→ Apply critical damage
│ │ └→ Fire SKILL_ATTACK_APPLY_DAMAGE
│ │
│ └→ Effect: applyModifiers()
│ ├→ Apply targetEffects[]
│ └→ Fire SKILL_EFFECT_TARGET_MODIFIERS
│
└→ Fire SKILL_AFTER_RUN_LOGIC
5. Completion
├→ Increment skill.uses
├→ onExecuteRewards() (custom)
└→ Fire SKILL_AFTER_EXECUTE
6. Server Communication (if SkillsServer)
└→ Sender broadcasts to clients
```
### Level Progression Flow
```
1. Experience Gain
└→ levelsSet.addExperience(amount)
├→ newTotalExp = currentExp + amount
├→ Check if threshold reached
└→ Fire LEVEL_EXPERIENCE_ADDED
2. Auto Level Up (if enabled)
└→ levelsSet.levelUp()
├→ currentLevel++
├→ applyLevelModifiers()
│ ├→ Get level instance
│ └→ Apply modifiers to owner
└→ Fire LEVEL_UP
3. Server Notification (if SkillsServer)
└→ Sender.onLevelUp()
└→ Send/Broadcast level data
```
### Class Path Initialization
```
1. Construction
└→ new ClassPath(props)
├→ Extends LevelsSet
├→ Set key, label, labelsByLevel
├→ Set skillsByLevel
└→ Initialize currentSkills
2. Initialization
└→ classPath.init(props)
├→ Call parent LevelsSet.init()
├→ Fire INIT_CLASS_PATH_END
└→ setOwnerSkills() (populate based on level)
3. Level Changes
└→ Override levelUp()
├→ Call parent LevelsSet.levelUp()
├→ Update currentLabel
├→ addSkills() (new level skills)
└→ Fire ADD_SKILLS_AFTER
└→ Override levelDown()
├→ removeSkills() (reverted level)
├→ Call parent LevelsSet.levelDown()
└→ Fire REMOVE_SKILLS_AFTER
```
## Event System
### Event Namespacing
All events are automatically namespaced by owner:
```javascript
// Event pattern
ownerEventKey.reldens.skills.eventName
// Example
skills.ownerId.player-123.reldens.skills.levelUp
```
### Event Categories
1. **Validation Events**
- VALIDATE_BEFORE
- VALIDATE_SUCCESS
- VALIDATE_FAIL
2. **Execution Events**
- SKILL_BEFORE_EXECUTE
- SKILL_AFTER_EXECUTE
- SKILL_BEFORE_RUN_LOGIC
- SKILL_AFTER_RUN_LOGIC
3. **Cast Events**
- SKILL_BEFORE_CAST
- SKILL_AFTER_CAST
4. **Combat Events**
- SKILL_ATTACK_APPLY_DAMAGE
- SKILL_EFFECT_TARGET_MODIFIERS
- SKILL_PHYSICAL_ATTACK_HIT
- SKILL_PHYSICAL_EFFECT_HIT
5. **Progression Events**
- LEVEL_UP
- LEVEL_DOWN
- LEVEL_EXPERIENCE_ADDED
- LEVEL_APPLY_MODIFIERS
6. **Class Path Events**
- INIT_CLASS_PATH_END
- SET_SKILLS
- ADD_SKILLS_BEFORE/AFTER
- REMOVE_SKILLS_BEFORE/AFTER
## Property Management
### Property Paths
Properties use path notation for nested access:
```javascript
// Flat property
propertyKey: 'hp'
// Accesses: owner.hp
// Nested property
propertyKey: 'stats/hp'
// Accesses: owner.stats.hp
// Deep nesting
propertyKey: 'stats/power/magical'
// Accesses: owner.stats.power.magical
```
### Property Manager Usage
```javascript
// Get property value
let hp = this.propertyManager.getPropertyValue(owner, 'stats/hp');
// Set property value
this.propertyManager.setOwnerProperty(owner, 'stats/hp', 90);
```
## Modifier Integration
### Modifier Application
Skills use modifiers from `/modifiers` package:
```javascript
const { Modifier, ModifierConst } = require('@reldens/modifiers');
// Create modifier
let modifier = new Modifier({
key: 'atk-boost',
propertyKey: 'stats/atk',
operation: ModifierConst.OPS.INC,
value: 5
});
// Apply to owner
modifier.apply(owner);
// Revert from owner
modifier.revert(owner);
```
### Owner vs Target Effects
```javascript
// Owner effects: Applied when skill executes
ownerEffects: [hpCostModifier, manaCostModifier]
// Target effects: Applied to target on hit
targetEffects: [damageModifier, stunModifier]
```
## Network Communication
### Sender Pattern (Server)
```javascript
// Server broadcasts skill events
sender.onSkillBeforeCast(skill, target)
└→ client.broadcast({
act: SkillsConst.ACTION_SKILL_BEFORE_CAST,
owner: ownerId,
data: {key, target}
})
```
### Receiver Pattern (Client)
```javascript
// Client receives and processes
receiver.processMessage(message)
└→ If action matches:
└→ Call mapped method
└→ this.onSkillBeforeCast(message)
```
### Message Format
```javascript
{
act: 'rski.ICpe', // Action constant
owner: 'player-123', // Owner ID
data: { // Compressed data
lvl: 5, // Level
lab: 'Warrior', // Label
exp: 1500, // Experience
ne: 2000, // Next level XP
skl: ['sword'] // Skills
}
}
```
## Critical Calculation Flows
### Damage Calculation
```
1. Get Properties Totals
├→ ownerAtk = sum(attackProperties)
├→ targetDef = sum(defenseProperties)
├→ ownerAim = sum(aimProperties)
└→ targetDodge = sum(dodgeProperties)
2. Dodge Check
├→ If dodgeFullEnabled && dodge > (aim * dodgeOverAimSuccess):
└→ Return false (dodged)
3. Base Damage
├→ If applyDirectDamage:
│ └→ damage = hitDamage
└→ Else:
└→ damage = calculateProportionDamage()
4. Attack vs Defense
├→ diff = ownerAtk - targetDef
├→ If diff > 0: Increase damage by percentage
└→ If diff < 0: Decrease damage by percentage
5. Dodge Proportion (if damageAffected)
├→ dodgeAimDiff = getDiffProportion(aim, dodge)
└→ damage = damage - (damage * dodgeAimDiff / 100)
6. Critical Damage
├→ critDamage = getCriticalDiff(damage)
├→ If criticalAffected && dodge > aim:
│ └→ Reduce critDamage by dodge proportion
└→ damage = damage + critDamage
7. Apply to Target
└→ newHp = currentHp - damage
```
### Experience-Based Leveling
```
1. Check Experience
└→ If newExp >= nextLevel.requiredExperience:
2. Level Up Loop
└→ For each level in levelsByExperience:
├→ If newExp >= level.requiredExperience:
│ └→ levelUp()
└→ Else: break
3. Apply Modifiers
└→ For each modifier in level.modifiers:
└→ modifier.apply(owner)
```
## Design Patterns
### Strategy Pattern
Different skill types implement the same interface:
```javascript
class Skill {
async runSkillLogic() { /* base implementation */ }
}
class Attack extends Skill {
async runSkillLogic() { /* damage logic */ }
}
class Effect extends Skill {
async runSkillLogic() { /* modifier logic */ }
}
```
### Observer Pattern
Event system allows loose coupling:
```javascript
// Register listener
skill.listenEvent(SkillsEvents.LEVEL_UP, callback);
// Fire event
skill.fireEvent(SkillsEvents.LEVEL_UP, data);
```
### Template Method Pattern
Base class defines workflow, subclasses implement steps:
```javascript
// Base workflow in Skill.execute()
async execute(target) {
await this.fireEvent(SKILL_BEFORE_EXECUTE);
// ... validation ...
await this.runSkillLogic(); // Implemented by subclass
await this.fireEvent(SKILL_AFTER_EXECUTE);
}
```
## Performance Considerations
1. **Event Listeners**: Always remove listeners when owner is destroyed
2. **Timers**: Clear skillActivationTimer and castingTimer properly
3. **Object Pooling**: Consider pooling for frequently created skills
4. **Property Caching**: Cache getPropertyValue results if called multiple times
5. **Modifier Management**: Limit number of active modifiers per entity
6. **Range Validation**: Use squared distance to avoid Math.sqrt when possible
## Extension Points
### Custom Skill Types
```javascript
class MyCustomSkill extends Skill {
constructor(props) {
super(props);
this.customProperty = props.customProperty;
}
async runSkillLogic() {
// Custom implementation
}
onExecuteConditions() {
// Custom validation
}
}
```
### Custom Level Progression
```javascript
class CustomLevelsSet extends LevelsSet {
async addExperience(number) {
// Custom XP calculation
let modifiedXp = this.calculateBonus(number);
return await super.addExperience(modifiedXp);
}
}
```
### Event Hooks
```javascript
// Hook into any event for custom logic
skill.listenEvent(SkillsEvents.SKILL_AFTER_EXECUTE, async (skill, target) => {
// Award achievement
// Play animation
// Update statistics
});
```