UNPKG

@reldens/skills

Version:
1,260 lines (1,011 loc) 31.4 kB
# LEVEL PROGRESSION SYSTEM ## Complete Guide to Levels, Experience, and Class Paths --- ## OVERVIEW The @reldens/skills package provides a sophisticated level progression system with automatic leveling, experience management, modifier application, and skill unlocking. This document covers the complete flow from level creation to class path progression. --- ## ARCHITECTURE ### Core Classes ``` Level (lib/level.js) LevelsSet (lib/levels-set.js) ClassPath (lib/class-path.js) SkillsServer (lib/server.js) ``` ### Hierarchy: - **Level**: Single level with modifiers and XP requirement - **LevelsSet**: Level collection with experience tracking and progression - **ClassPath**: LevelsSet + skill trees + label management - **SkillsServer**: ClassPath + server-client synchronization --- ## LEVEL CLASS **File:** `lib/level.js:9-33` ### Purpose Represents a single level in a progression system with associated modifiers and requirements. ### Properties ```javascript { key: 5, // Level number (integer) label: 'Expert', // Display name for this level requiredExperience: 1000, // XP needed to reach this level modifiers: [ // Modifiers applied at this level hpModifier, attackModifier ] } ``` ### Constructor ```javascript constructor(props) { this.key = sc.get(props, 'key', false); this.label = sc.get(props, 'label', this.key); this.requiredExperience = sc.get(props, 'requiredExperience', 0); this.modifiers = sc.get(props, 'modifiers', []); } ``` ### Key Points - `key` must be an integer (level number) - `label` defaults to key if not provided - `requiredExperience` is cumulative (total XP needed, not delta) - `modifiers` is an array of @reldens/modifiers Modifier instances - Level instances are immutable once created ### Example ```javascript import {Level} from '@reldens/skills'; import {Modifier} from '@reldens/modifiers'; let level5 = new Level({ key: 5, label: 'Expert Warrior', requiredExperience: 1000, modifiers: [ new Modifier({ key: 'level-5-hp', propertyKey: 'stats.hp', operation: '+', value: 50 }), new Modifier({ key: 'level-5-attack', propertyKey: 'stats.atk', operation: '+', value: 10 }) ] }); ``` --- ## LEVELSSET CLASS **File:** `lib/levels-set.js:11-298` ### Purpose Manages level progression, experience tracking, automatic leveling, and modifier application. ### Properties ```javascript { owner: playerEntity, // Entity with getPosition() method key: 'warrior-progression', // LevelsSet identifier levels: { // Object of Level instances 1: level1, 5: level5, 10: level10 }, currentLevel: 1, // Current level number currentExp: 0, // Current experience points autoFillRanges: true, // Auto-generate intermediate levels autoFillExperienceMultiplier: 1.5, // XP multiplier for auto-filled levels increaseLevelsWithExperience: true, // Auto level-up when XP threshold reached setRequiredExperienceLimit: false // Cap XP at max level } ``` ### Initialization **Method:** `init(props)` - File: `lib/levels-set.js:32-59` ```javascript let levelsSet = new LevelsSet(); await levelsSet.init({ owner: playerEntity, key: 'warrior-levels', levels: { 1: new Level({key: 1, requiredExperience: 0}), 5: new Level({key: 5, requiredExperience: 1000}), 10: new Level({key: 10, requiredExperience: 5000}) }, currentLevel: 1, currentExp: 0, autoFillRanges: true }); ``` **Initialization Flow:** ``` 1. Fire INIT_LEVEL_SET_START event 2. Set owner reference 3. Set key, events, currentLevel, currentExp 4. Call setLevels(props.levels) Auto-fill level ranges if enabled Sort levels by required experience 5. Apply current level modifiers 6. Fire INIT_LEVEL_SET_END event ``` ### Auto-Fill Level Ranges **Method:** `autoFillLevelRanges(levels)` - File: `lib/levels-set.js:96-160` **Purpose:** Automatically generate intermediate levels between defined levels. **Example:** ```javascript // Input levels: { 1: Level(key: 1, requiredExp: 0), 10: Level(key: 10, requiredExp: 5000) } // With autoFillRanges: true // Generates levels 2, 3, 4, 5, 6, 7, 8, 9 // Output: { 1: Level(key: 1, requiredExp: 0), 2: Level(key: 2, requiredExp: 0 * 1.5 = 0), 3: Level(key: 3, requiredExp: 0 * 1.5 = 0), // ... 9: Level(key: 9, requiredExp: calculated), 10: Level(key: 10, requiredExp: 5000) } ``` **Algorithm:** ``` For each gap between defined levels: 1. Identify startLevel and endLevel 2. Calculate totalGap = endLevel.requiredExp - startLevel.requiredExp 3. Calculate numberOfLevels = (endLevel.key - startLevel.key - 1) 4. For each intermediate level: Calculate baseIncrease = totalGap / (numberOfLevels + 1) Apply multiplier: expIncrease = baseIncrease * autoFillExperienceMultiplier^levelOffset Create new Level instance Fire GENERATED_LEVELS event 5. Add all levels to levels object ``` **Key Points:** - Only fills gaps, doesn't modify defined levels - Uses exponential growth with `autoFillExperienceMultiplier` - Empty modifiers array for auto-generated levels - Label defaults to level key number --- ### Experience Management #### Adding Experience **Method:** `addExperience(number)` - File: `lib/levels-set.js:162-224` ```javascript // Add 500 XP await levelsSet.addExperience(500); ``` **Flow:** ``` 1. Calculate newTotalExp = currentExp + number 2. Apply experience limit if setRequiredExperienceLimit enabled Cap at max level required experience 3. Set currentExp = newTotalExp 4. If increaseLevelsWithExperience enabled: Get next level data While currentExp >= nextLevel.requiredExperience: Level up Get next level data 5. Fire LEVEL_EXPERIENCE_ADDED event with: this, number, newTotalExp, currentLevelIndex, nextLevelIndex, nextLevelKey, nextLevel, nextLevelExp, isLevelUp 6. Return newTotalExp ``` **Parameters:** - `number` (required): Amount of XP to add - Returns: New total experience **Events Fired:** - `LEVEL_EXPERIENCE_ADDED` (always) - `LEVEL_UP` (if level increased) **Example:** ```javascript let levelsSet = new LevelsSet(); await levelsSet.init({ owner: player, levels: { 1: new Level({key: 1, requiredExperience: 0}), 2: new Level({key: 2, requiredExperience: 100}), 3: new Level({key: 3, requiredExperience: 300}) }, currentLevel: 1, currentExp: 0, increaseLevelsWithExperience: true }); // Add 250 XP - will level up from 1 to 3 await levelsSet.addExperience(250); console.log(levelsSet.currentLevel); // 3 console.log(levelsSet.currentExp); // 250 ``` #### Getting Next Level Experience **Method:** `getNextLevelExperience()` - File: `lib/levels-set.js:226-245` ```javascript let nextLevelXP = levelsSet.getNextLevelExperience(); console.log(`Need ${nextLevelXP} XP for next level`); ``` **Returns:** - Next level's required experience - `null` if at max level **Example:** ```javascript let levelsSet = new LevelsSet(); await levelsSet.init({ levels: { 1: new Level({key: 1, requiredExperience: 0}), 2: new Level({key: 2, requiredExperience: 100}) }, currentLevel: 1, currentExp: 50 }); let nextLevelXP = levelsSet.getNextLevelExperience(); // 100 let remaining = nextLevelXP - levelsSet.currentExp; // 50 ``` --- ### Level Up / Level Down #### Level Up **Method:** `levelUp()` - File: `lib/levels-set.js:247-257` ```javascript await levelsSet.levelUp(); ``` **Flow:** ``` 1. Fire LEVEL_UP event (params: this) 2. Increment currentLevel 3. Call applyLevelModifiers() ``` **Important:** - Does NOT check if next level exists - Does NOT validate XP requirements - Caller must ensure level up is valid - Usually called from addExperience() when XP threshold reached #### Level Down **Method:** `levelDown()` - File: `lib/levels-set.js:259-269` ```javascript await levelsSet.levelDown(); ``` **Flow:** ``` 1. Fire LEVEL_DOWN event (params: this) 2. Decrement currentLevel 3. Call applyLevelModifiers() ``` **Important:** - Does NOT check if previous level exists - Caller must ensure level down is valid - Use case: Death penalty, level drain effects --- ### Modifier Application **Method:** `applyLevelModifiers()` - File: `lib/levels-set.js:271-289` **Purpose:** Apply current level modifiers and revert old level modifiers. **Flow:** ``` 1. Fire LEVEL_APPLY_MODIFIERS event (params: this) 2. Get previous level from previousLevel property 3. Get current level from levels[currentLevel] 4. If previousLevel exists and has modifiers: Revert each modifier from owner sc.revertModifier(owner, modifier) 5. If currentLevel exists and has modifiers: Apply each modifier to owner sc.applyModifier(owner, modifier) 6. Update previousLevel = currentLevel ``` **Example:** ```javascript // Level 1 modifiers: +50 HP // Level 2 modifiers: +100 HP // Player at level 1: HP = base(100) + modifier(50) = 150 await levelsSet.levelUp(); // Goes to level 2 // After applyLevelModifiers(): // 1. Revert level 1 modifier: HP = 150 - 50 = 100 // 2. Apply level 2 modifier: HP = 100 + 100 = 200 ``` **Important:** - Modifiers are applied through @reldens/modifiers system - Uses `sc.applyModifier()` and `sc.revertModifier()` from @reldens/utils - Owner must have properties referenced by modifiers - If level has no modifiers, this step is skipped --- ### Event System **Events Fired by LevelsSet:** | Event | When | Parameters | |-------|------|------------| | `INIT_LEVEL_SET_START` | Before initialization | (this) | | `INIT_LEVEL_SET_END` | After initialization | (this) | | `SET_LEVELS` | After levels are set | (this, levels) | | `GENERATED_LEVELS` | After auto-filling range | (this, startLevel, endLevel, generatedLevels) | | `LEVEL_UP` | After level increases | (this) | | `LEVEL_DOWN` | After level decreases | (this) | | `LEVEL_APPLY_MODIFIERS` | Before modifier changes | (this) | | `LEVEL_EXPERIENCE_ADDED` | After XP added | (this, number, newTotal, currentIdx, nextIdx, nextKey, nextLevel, nextExp, isLevelUp) | **Listening to Events:** ```javascript levelsSet.listenEvent(SkillsEvents.LEVEL_UP, async (levelsSet) => { console.log('Leveled up!', levelsSet.currentLevel); // Show animation, play sound, award achievement }, 'my-level-up-listener'); levelsSet.listenEvent(SkillsEvents.LEVEL_EXPERIENCE_ADDED, async ( levelsSet, xpAdded, newTotal, currentIdx, nextIdx, nextKey, nextLevel, nextLevelXP, isLevelUp ) => { console.log(`Gained ${xpAdded} XP (total: ${newTotal})`); if(isLevelUp){ console.log('LEVEL UP!'); } else { console.log(`Next level at ${nextLevelXP} XP`); } }, 'xp-gain-listener'); ``` --- ## CLASSPATH CLASS **File:** `lib/class-path.js:11-315` ### Purpose Extends LevelsSet to provide class-based progression with skill trees and dynamic labels. ### Additional Properties ```javascript { // Inherited from LevelsSet: owner, key, levels, currentLevel, currentExp, ... // ClassPath-specific: label: 'Warrior', // Base display name currentLabel: 'Warrior', // Active label (changes with level) labelsByLevel: { // Level-specific labels 1: 'Novice Warrior', 5: 'Veteran Warrior', 10: 'Master Warrior' }, skillsByLevel: { // Skill instances by level 1: [swordSkill], 5: [shieldSkill, bashSkill], 10: [whirlwindSkill] }, skillsByLevelKeys: { // Skill keys by level (for serialization) 1: ['sword-attack'], 5: ['shield-block', 'bash'], 10: ['whirlwind'] }, currentSkills: { // Currently available skills 'sword-attack': swordSkill, 'shield-block': shieldSkill }, affectedProperty: 'hp' // Property affected by class skills } ``` ### Initialization **Method:** `init(props)` - File: `lib/class-path.js:46-80` ```javascript let classPath = new ClassPath(); await classPath.init({ owner: playerEntity, key: 'warrior', label: 'Warrior', labelsByLevel: { 1: 'Novice Warrior', 10: 'Master Warrior' }, levels: { 1: new Level({key: 1, requiredExperience: 0}), 10: new Level({key: 10, requiredExperience: 5000}) }, skillsByLevel: { 1: [swordSkill], 10: [masterSkill] }, currentLevel: 1, currentExp: 0 }); ``` **Initialization Flow:** ``` 1. Call parent LevelsSet.init() Sets up levels, XP, modifiers 2. Set label and currentLabel 3. Set labelsByLevel 4. Set skillsByLevel and skillsByLevelKeys 5. Set affectedProperty 6. Call setOwnerSkills() Populates currentSkills with skills up to current level 7. Fire INIT_CLASS_PATH_END event ``` --- ### Skill Management #### Setting Owner Skills **Method:** `setOwnerSkills(skills)` - File: `lib/class-path.js:82-123` **Purpose:** Initialize currentSkills based on current level or provided skills. **Flow:** ``` 1. If skills parameter provided: Use provided skills directly 2. Else if skillsByLevel defined: Collect all skills from levels <= currentLevel Build currentSkills object by skill key 3. Fire SET_SKILLS event (params: this, skills) 4. Return currentSkills ``` **Example:** ```javascript let classPath = new ClassPath(); await classPath.init({ owner: player, skillsByLevel: { 1: [skill1, skill2], 5: [skill3], 10: [skill4] }, currentLevel: 5 }); // currentSkills contains: skill1, skill2, skill3 // skill4 not available (requires level 10) ``` #### Adding Skills **Method:** `addSkills(skills)` - File: `lib/class-path.js:125-145` ```javascript await classPath.addSkills([newSkill1, newSkill2]); ``` **Flow:** ``` 1. Fire ADD_SKILLS_BEFORE event (params: this, skills) 2. For each skill in skills array: Add to currentSkills by skill.key 3. Fire ADD_SKILLS_AFTER event (params: this, skills) 4. Return currentSkills ``` **Use Cases:** - Level up unlocked new skills - Learned skill from trainer - Equipped item grants temporary skill #### Removing Skills **Method:** `removeSkills(skills)` - File: `lib/class-path.js:147-170` ```javascript await classPath.removeSkills([oldSkill1, oldSkill2]); ``` **Flow:** ``` 1. Fire REMOVE_SKILLS_BEFORE event (params: this, skills) 2. For each skill in skills array: Delete from currentSkills by skill.key 3. Fire REMOVE_SKILLS_AFTER event (params: this, skills) 4. Return currentSkills ``` **Use Cases:** - Skill forgotten or replaced - Temporary skill expired - Unequipped item removed skill --- ### Label Management **Labels change dynamically based on level progression.** **Method:** `updateLabelByLevel()` - File: `lib/class-path.js:172-182` ```javascript // Called automatically during levelUp() classPath.updateLabelByLevel(); ``` **Flow:** ``` 1. Check if labelsByLevel has entry for currentLevel 2. If yes: Set currentLabel = labelsByLevel[currentLevel] 3. Else: Keep currentLabel unchanged ``` **Example:** ```javascript let classPath = new ClassPath(); await classPath.init({ label: 'Warrior', labelsByLevel: { 1: 'Novice Warrior', 5: 'Veteran Warrior', 10: 'Master Warrior' }, currentLevel: 1 }); console.log(classPath.currentLabel); // 'Novice Warrior' await classPath.levelUp(); // Level 2 console.log(classPath.currentLabel); // 'Novice Warrior' (no label for level 2) // Add XP to reach level 5 await classPath.addExperience(1000); console.log(classPath.currentLabel); // 'Veteran Warrior' ``` --- ### Level Up Override **Method:** `levelUp()` - File: `lib/class-path.js:184-207` **ClassPath extends LevelsSet.levelUp() to add skill unlocking and label updates.** **Flow:** ``` 1. Call parent LevelsSet.levelUp() Fires LEVEL_UP event Increments currentLevel Applies level modifiers 2. Update label based on new level updateLabelByLevel() 3. Check if new skills available at this level If skillsByLevel[currentLevel] exists: addSkills(skillsByLevel[currentLevel]) Fires ADD_SKILLS_BEFORE Adds skills to currentSkills Fires ADD_SKILLS_AFTER ``` **Example:** ```javascript let classPath = new ClassPath(); await classPath.init({ owner: player, labelsByLevel: { 1: 'Novice', 5: 'Expert' }, skillsByLevel: { 1: [basicSkill], 5: [advancedSkill] }, levels: { 1: new Level({key: 1, requiredExperience: 0}), 5: new Level({key: 5, requiredExperience: 1000}) }, currentLevel: 4, currentExp: 999 }); // Add 1 XP to trigger level up await classPath.addExperience(1); // Results: // - currentLevel = 5 // - currentLabel = 'Expert' // - currentSkills includes advancedSkill // - Level 5 modifiers applied ``` --- ### Event System **Events Fired by ClassPath:** All LevelsSet events PLUS: | Event | When | Parameters | |-------|------|------------| | `INIT_CLASS_PATH_END` | After ClassPath initialization | (this) | | `SET_SKILLS` | After owner skills set | (this, skills) | | `ADD_SKILLS_BEFORE` | Before adding skills | (this, skills) | | `ADD_SKILLS_AFTER` | After adding skills | (this, skills) | | `REMOVE_SKILLS_BEFORE` | Before removing skills | (this, skills) | | `REMOVE_SKILLS_AFTER` | After removing skills | (this, skills) | **Example:** ```javascript classPath.listenEvent(SkillsEvents.INIT_CLASS_PATH_END, async (classPath) => { console.log('Class path initialized:', classPath.key); console.log('Starting skills:', Object.keys(classPath.currentSkills)); }, 'init-listener'); classPath.listenEvent(SkillsEvents.ADD_SKILLS_AFTER, async (classPath, skills) => { console.log('New skills unlocked:'); skills.forEach(skill => { console.log(`- ${skill.key}`); }); }, 'skill-unlock-listener'); ``` --- ## SKILLSSERVER CLASS **File:** `lib/server.js:11-85` ### Purpose Server-side wrapper for ClassPath with automatic client synchronization via Sender. ### Properties ```javascript { // All ClassPath properties... // SkillsServer-specific: client: roomClient, // Client object with send() and broadcast() sender: senderInstance // Sender instance for message broadcasting } ``` ### Initialization **Constructor:** `constructor(props)` - File: `lib/server.js:18-50` ```javascript import {SkillsServer} from '@reldens/skills/lib/server.js'; let skillsServer = new SkillsServer({ owner: playerEntity, client: roomClient, // Must have send() and broadcast() methods key: 'warrior', label: 'Warrior', levels: { 1: level1, 5: level5 }, skillsByLevel: { 1: [skill1], 5: [skill2] }, currentLevel: 1, currentExp: 0 }); ``` **Initialization Flow:** ``` 1. Validate client has send() method If missing: throw error 2. Validate client has broadcast() method If missing: throw error 3. Call parent ClassPath.init() Initializes levels, skills, XP 4. Create Sender instance Pass this (ClassPath) and client 5. Call sender.registerListeners() Sets up event listeners for client sync ``` **Client Validation:** ```javascript // Client must implement: class RoomClient { send(message) { // Send to this client only } broadcast(message) { // Send to all clients in room } } ``` --- ### Server-Client Synchronization **SkillsServer automatically broadcasts level/skill changes to clients via Sender.** **Registered Events (via Sender):** | Event | Message Action | Behavior | Data Sent | |-------|---------------|----------|-----------| | `INIT_CLASS_PATH_END` | `ACTION_INIT_CLASS_PATH_END` | SEND | level, label, XP, skills | | `LEVEL_UP` | `ACTION_LEVEL_UP` | SEND | level, label, skills, nextXP | | `LEVEL_EXPERIENCE_ADDED` | `ACTION_LEVEL_EXPERIENCE_ADDED` | SEND | current XP | | `SKILL_BEFORE_CAST` | `ACTION_SKILL_BEFORE_CAST` | BROADCAST | skill key, owner | | `SKILL_ATTACK_APPLY_DAMAGE` | `ACTION_SKILL_ATTACK_APPLY_DAMAGE` | BROADCAST | damage, target, skill | **Message Format:** ```javascript { act: 'rski.Lu', // Action constant (LEVEL_UP) owner: 'player-123', data: { lvl: 5, // Current level lab: 'Expert Warrior', // Current label ne: 2000, // Next level XP skl: ['sword', 'shield'] // Skill keys } } ``` **Example Usage:** ```javascript // Server side let skillsServer = new SkillsServer({ owner: player, client: roomClient, key: 'warrior', levels: levels, skillsByLevel: skillsByLevel }); // When player gains XP: await skillsServer.addExperience(500); // Automatically sends message to client: // { // act: 'rski.Ea', // owner: player.id, // data: {exp: 500} // } // If leveled up, also sends: // { // act: 'rski.Lu', // owner: player.id, // data: {lvl: 2, lab: 'Novice', ne: 200, skl: ['skill1', 'skill2']} // } ``` --- ## COMPLETE PROGRESSION FLOW ### Example: Player Progression from Level 1 to 10 ```javascript import {SkillsServer} from '@reldens/skills/lib/server.js'; import {Level} from '@reldens/skills/lib/level.js'; import {Modifier} from '@reldens/modifiers'; // 1. Define levels with modifiers let levels = { 1: new Level({ key: 1, label: 'Novice', requiredExperience: 0, modifiers: [ new Modifier({key: 'hp-1', propertyKey: 'stats.hp', operation: '+', value: 50}) ] }), 5: new Level({ key: 5, label: 'Veteran', requiredExperience: 1000, modifiers: [ new Modifier({key: 'hp-5', propertyKey: 'stats.hp', operation: '+', value: 150}), new Modifier({key: 'atk-5', propertyKey: 'stats.atk', operation: '+', value: 20}) ] }), 10: new Level({ key: 10, label: 'Master', requiredExperience: 5000, modifiers: [ new Modifier({key: 'hp-10', propertyKey: 'stats.hp', operation: '+', value: 300}), new Modifier({key: 'atk-10', propertyKey: 'stats.atk', operation: '+', value: 50}), new Modifier({key: 'def-10', propertyKey: 'stats.def', operation: '+', value: 30}) ] }) }; // 2. Define skills by level let skillsByLevel = { 1: [basicSwordSkill], 5: [powerAttackSkill, shieldBlockSkill], 10: [ultimateSkill] }; // 3. Initialize SkillsServer let skillsServer = new SkillsServer({ owner: player, client: roomClient, key: 'warrior-path', label: 'Warrior', labelsByLevel: { 1: 'Novice Warrior', 5: 'Veteran Warrior', 10: 'Master Warrior' }, levels: levels, skillsByLevel: skillsByLevel, currentLevel: 1, currentExp: 0, autoFillRanges: true, increaseLevelsWithExperience: true }); // 4. Player gains XP from killing monster await skillsServer.addExperience(250); // - currentExp = 250 // - Still level 1 // - Client receives: {act: 'rski.Ea', data: {exp: 250}} // 5. Player gains more XP await skillsServer.addExperience(750); // - currentExp = 1000 // - Levels up: 1 2 3 4 5 (auto-filled levels) // - currentLevel = 5 // - currentLabel = 'Veteran Warrior' // - Modifiers changed: -50 HP, +150 HP, +20 ATK = net +100 HP, +20 ATK // - Skills added: powerAttackSkill, shieldBlockSkill // - Client receives: // - {act: 'rski.Ea', data: {exp: 1000}} // - {act: 'rski.Lu', data: {lvl: 5, lab: 'Veteran Warrior', ...}} // 6. Player continues to level 10 await skillsServer.addExperience(4000); // - currentExp = 5000 // - Levels up: 5 6 7 8 9 10 // - currentLevel = 10 // - currentLabel = 'Master Warrior' // - Modifiers changed: -150 HP, -20 ATK, +300 HP, +50 ATK, +30 DEF // - Skills added: ultimateSkill // - Client receives level up messages // 7. At max level, can still gain XP but won't level up await skillsServer.addExperience(1000); // - currentExp = 6000 (above max required) // - Still level 10 // - Client receives: {act: 'rski.Ea', data: {exp: 6000}} ``` --- ## COMMON PATTERNS ### Pattern #1: Multi-Class System ```javascript let classes = { warrior: new SkillsServer({ owner: player, client: client, key: 'warrior', label: 'Warrior', levels: warriorLevels, skillsByLevel: warriorSkills }), mage: new SkillsServer({ owner: player, client: client, key: 'mage', label: 'Mage', levels: mageLevels, skillsByLevel: mageSkills }) }; // Level up specific class await classes.warrior.addExperience(500); await classes.mage.addExperience(300); ``` --- ### Pattern #2: Skill Tree with Prerequisites ```javascript let classPath = new ClassPath(); await classPath.init({ owner: player, skillsByLevel: { 1: [basicSkill], 5: [intermediateSkill], // Requires level 5 10: [advancedSkill] // Requires level 10 } }); // Check if player meets skill requirement function canLearnSkill(skill, classPath) { for(let [level, skills] of Object.entries(classPath.skillsByLevel)) { if(skills.includes(skill)) { return classPath.currentLevel >= parseInt(level); } } return false; } ``` --- ### Pattern #3: Prestige/Rebirth System ```javascript async function prestigeClass(classPath) { // Store current level for rewards let prestigeLevel = classPath.currentLevel; // Reset to level 1 while(classPath.currentLevel > 1) { await classPath.levelDown(); } classPath.currentExp = 0; // Apply prestige bonus (permanent modifier) let prestigeBonus = new Modifier({ key: `prestige-${prestigeLevel}`, propertyKey: 'stats.prestigeBonus', operation: '+', value: prestigeLevel * 10 }); sc.applyModifier(classPath.owner, prestigeBonus); return prestigeLevel; } ``` --- ### Pattern #4: Experience Gain with Modifiers ```javascript function calculateXpGain(baseXp, player) { let xpModifier = 1.0; // XP boost items if(player.hasItem('xp-potion')) { xpModifier += 0.5; // +50% XP } // Party bonus if(player.inParty) { xpModifier += 0.2; // +20% XP } // Level difference penalty let levelDiff = player.level - monster.level; if(levelDiff > 5) { xpModifier *= 0.5; // -50% XP for easy monsters } return Math.floor(baseXp * xpModifier); } // Usage let xpGained = calculateXpGain(100, player); await classPath.addExperience(xpGained); ``` --- ### Pattern #5: Skill Point System ```javascript class SkillPointClassPath extends ClassPath { constructor(props) { super(props); this.skillPoints = 0; } async levelUp() { await super.levelUp(); this.skillPoints += 1; // Gain 1 skill point per level } async learnSkill(skill, cost = 1) { if(this.skillPoints < cost) { return {error: 'Not enough skill points'}; } this.skillPoints -= cost; await this.addSkills([skill]); return {success: true}; } } ``` --- ## DEBUGGING LEVEL PROGRESSION ### Check Current State ```javascript console.log('=== ClassPath State ==='); console.log('Key:', classPath.key); console.log('Current Level:', classPath.currentLevel); console.log('Current Label:', classPath.currentLabel); console.log('Current XP:', classPath.currentExp); console.log('Next Level XP:', classPath.getNextLevelExperience()); console.log('Available Skills:', Object.keys(classPath.currentSkills)); console.log('Total Levels:', Object.keys(classPath.levels).length); ``` ### Track Level Changes ```javascript classPath.listenEvent(SkillsEvents.LEVEL_UP, async (cp) => { console.log('[LEVEL UP]', cp.currentLevel, '-', cp.currentLabel); console.log(' Skills:', Object.keys(cp.currentSkills)); }, 'debug-level-up'); classPath.listenEvent(SkillsEvents.LEVEL_EXPERIENCE_ADDED, async ( cp, xpAdded, newTotal, currentIdx, nextIdx, nextKey, nextLevel, nextXP, isLevelUp ) => { console.log('[XP GAIN]', `+${xpAdded}`, `(${newTotal}/${nextXP})`); if(isLevelUp) { console.log(' LEVELED UP!'); } }, 'debug-xp-gain'); ``` ### Verify Modifier Application ```javascript classPath.listenEvent(SkillsEvents.LEVEL_APPLY_MODIFIERS, async (cp) => { console.log('[MODIFIERS] Applying level', cp.currentLevel, 'modifiers'); let level = cp.levels[cp.currentLevel]; if(level && level.modifiers) { level.modifiers.forEach(mod => { console.log(` ${mod.propertyKey} ${mod.operation} ${mod.value}`); }); } }, 'debug-modifiers'); ``` --- ## COMMON PITFALLS ### ❌ PITFALL 1: Not Awaiting Level Operations ```javascript // BAD classPath.levelUp(); // Missing await console.log(classPath.currentLevel); // May not be updated yet // GOOD await classPath.levelUp(); console.log(classPath.currentLevel); // Guaranteed updated ``` --- ### ❌ PITFALL 2: Assuming Auto-Level-Up Without Flag ```javascript // BAD let classPath = new ClassPath(); await classPath.init({ levels: levels, currentLevel: 1 // increaseLevelsWithExperience not set (defaults to false) }); await classPath.addExperience(1000); console.log(classPath.currentLevel); // Still 1! No auto-level-up // GOOD await classPath.init({ levels: levels, currentLevel: 1, increaseLevelsWithExperience: true // Enable auto-level-up }); await classPath.addExperience(1000); console.log(classPath.currentLevel); // Leveled up correctly ``` --- ### ❌ PITFALL 3: Modifying levels After Initialization ```javascript // BAD await classPath.init({levels: levels}); classPath.levels[20] = new Level({key: 20, requiredExperience: 10000}); // LevelsByExperience not updated! Sorting broken! // GOOD levels[20] = new Level({key: 20, requiredExperience: 10000}); await classPath.init({levels: levels}); // Include new level in init ``` --- ### ❌ PITFALL 4: Reusing Skill Instances Across Classes ```javascript // BAD let sharedSkill = new Skill({owner: player}); let warriorPath = new ClassPath({skillsByLevel: {1: [sharedSkill]}}); let magePath = new ClassPath({skillsByLevel: {1: [sharedSkill]}}); // Skill has single owner reference - conflicts! // GOOD let warriorSkill = new Skill({owner: player, key: 'warrior-basic'}); let mageSkill = new Skill({owner: player, key: 'mage-basic'}); let warriorPath = new ClassPath({skillsByLevel: {1: [warriorSkill]}}); let magePath = new ClassPath({skillsByLevel: {1: [mageSkill]}}); ``` --- ## REFERENCES - Level: `lib/level.js:9-33` - LevelsSet: `lib/levels-set.js:11-298` - ClassPath: `lib/class-path.js:11-315` - SkillsServer: `lib/server.js:11-85` - Sender: `lib/server/sender.js:11-212` - Event Names: `lib/skills-events.js` - Event System: `.claude/event-system-architecture.md` - Skill Execution: `.claude/skill-execution-flow.md`