@reldens/modifiers
Version:
1,110 lines (910 loc) • 26.9 kB
Markdown
# Modifiers System - Deep Technical Architecture
This document provides comprehensive technical details about the @reldens/modifiers package internals.
## Core Execution Flow
### Complete Modifier Application Flow
When you call `modifier.apply(target)`, here's the complete execution path:
```
modifier.apply(target, useBasePropertyToGetValue, applyOnBaseProperty)
↓
modifier.execute(target, revert=false, useBasePropertyToGetValue, applyOnBaseProperty)
↓
1. Validate target exists (line 86-88)
2. Validate conditions if present (line 90-96)
3. Override target if provided (line 99-101)
4. Calculate new value (line 103)
↓
getModifiedValue(revert, useBasePropertyToGetValue)
↓
a. Determine which property to read from (line 134)
b. Get current value via PropertyManager (line 136)
c. Calculate using Calculator (line 138)
d. Handle special operations SET/SET_N/METHOD (line 140-151)
e. Apply min/max limits (line 153)
↓
applyModifierLimits(propertyValue)
↓
- Check minValue (line 153-155)
- Check maxValue (line 156-158)
- Check minProperty (line 159-164)
- Check maxProperty (line 165-170)
- Return clamped value (line 171)
5. Determine which property to apply to (line 104)
6. Apply value via PropertyManager (line 105)
7. Update state to MOD_APPLIED (line 106)
8. Return true (line 107)
```
### Property Path Navigation Flow
**Example:** Modifying `stats/combat/attack` on a player object
```javascript
let player = {
stats: {
combat: {
attack: 50,
defense: 30
}
}
};
modifier.apply(player);
```
**Internal Flow:**
```
PropertyManager.setOwnerProperty(player, 'stats/combat/attack', 75)
↓
manageOwnerProperty(player, 'stats/combat/attack', 75)
↓
1. Split path: ['stats', 'combat', 'attack'] (line 24)
2. Extract parent object (line 25)
↓
extractChildPropertyOwner(player, ['stats', 'combat', 'attack'])
↓
a. Copy array and pop last element: ['stats', 'combat'] (line 44-45)
b. If empty array, return player (single-level property) (line 46-48)
c. Navigate through path parts (line 49-55)
- Check player.stats exists → currentOwner = player.stats
- Check player.stats.combat exists → currentOwner = player.stats.combat
d. Return player.stats.combat (the PARENT of 'attack')
3. Get final property name: 'attack' (line 26)
4. Validate property exists when getting (line 27-29)
5. Set value: player.stats.combat['attack'] = 75 (line 30-32)
6. Return new value: 75 (line 33)
```
**Critical Understanding:**
- `extractChildPropertyOwner` returns the PARENT object, NOT the property value
- For `['stats', 'combat', 'attack']` it returns `player.stats.combat`
- This allows setting `player.stats.combat.attack` directly
- Validation only happens when GETTING values (value is undefined)
- When SETTING values, no validation is performed (allows creating new properties)
## Base Property Operations Deep Dive
### The Two Parameters
**useBasePropertyToGetValue:**
- Controls which property to READ from for calculation
- `true`: Read from `basePropertyKey`
- `false`: Read from `propertyKey`
**applyOnBaseProperty:**
- Controls which property to WRITE result to
- `true`: Write to `basePropertyKey`
- `false`: Write to `propertyKey`
### Four Possible Combinations
**1. Both false (default):**
```javascript
// Read from propertyKey, write to propertyKey
player.attack = 50;
modifier.apply(target, false, false);
// Reads from attack (50), calculates, writes to attack
```
**2. useBasePropertyToGetValue=true, applyOnBaseProperty=false:**
```javascript
// Read from basePropertyKey, write to propertyKey
player.currentHealth = 50;
player.baseHealth = 100;
modifier.apply(target, true, false);
// Reads from baseHealth (100), calculates, writes to currentHealth
```
**3. useBasePropertyToGetValue=false, applyOnBaseProperty=true:**
```javascript
// Read from propertyKey, write to basePropertyKey
player.currentHealth = 50;
player.baseHealth = 100;
modifier.apply(target, false, true);
// Reads from currentHealth (50), calculates, writes to baseHealth
```
**4. Both true:**
```javascript
// Read from basePropertyKey, write to basePropertyKey
player.baseHealth = 100;
modifier.apply(target, true, true);
// Reads from baseHealth (100), calculates, writes to baseHealth
```
### Real-World Use Cases
**Percentage-based healing from max health:**
```javascript
let player = {
currentHealth: 30,
maxHealth: 100
};
let healingPotion = new Modifier({
key: 'healing-potion',
propertyKey: 'currentHealth',
basePropertyKey: 'maxHealth',
operation: ModifierConst.OPS.INC_P,
value: 50 // 50% of max health
});
// Calculate 50% of maxHealth (100) = 50
// Apply to currentHealth: 30 + 50 = 80
healingPotion.apply(player, true, false);
// Result: currentHealth = 80
```
**Equipment that modifies base stats:**
```javascript
let player = {
currentStrength: 50,
baseStrength: 100
};
let strengthRing = new Modifier({
key: 'strength-ring',
propertyKey: 'currentStrength',
basePropertyKey: 'baseStrength',
operation: ModifierConst.OPS.INC,
value: 25
});
// Calculate from currentStrength: 50 + 25 = 75
// Apply to baseStrength (permanent base increase)
strengthRing.apply(player, false, true);
// Result: baseStrength = 75
```
## Calculator Operations Deep Dive
### INC_P (Increase Percentage) - The Tricky One
**Apply Formula** (calculator.js line 30-37):
```javascript
// originalValue = 100, operationValue = 50
let percentage = originalValue * operationValue / 100; // 100 * 50 / 100 = 50
let roundedPercentage = Math.round(percentage); // 50
return originalValue + roundedPercentage; // 100 + 50 = 150
```
**Revert Formula** (calculator.js line 30-37):
```javascript
// currentValue = 150 (after applying +50%)
// We need to get back to original 100
// Formula: originalValue = currentValue / (1 + percentage/100)
let divider = 1 + (operationValue / 100); // 1 + (50/100) = 1.5
let result = originalValue / divider; // 150 / 1.5 = 100
return Math.round(result); // 100
```
**Why Rounding Matters:**
```javascript
// Without rounding
100 * 33 / 100 = 33.33... → 133.33...
// With rounding
100 * 33 / 100 = 33.33... → 33 → 133
// Revert
133 / 1.33 = 100 (exact)
```
### DEC_P (Decrease Percentage)
**Apply Formula:**
```javascript
// originalValue = 100, operationValue = 25
let percentage = originalValue * operationValue / 100; // 100 * 25 / 100 = 25
return originalValue - Math.round(percentage); // 100 - 25 = 75
```
**Revert Formula:**
```javascript
// currentValue = 75 (after -25%)
// Formula: originalValue = currentValue / (1 - percentage/100)
let divider = 1 - (operationValue / 100); // 1 - 0.25 = 0.75
return Math.round(originalValue / divider); // 75 / 0.75 = 100
```
### All Operations Summary
| Operation | Apply | Revert | Example (100, 25) |
|-----------|-------|--------|-------------------|
| INC | a + b | a - b | 100 + 25 = 125 → 125 - 25 = 100 |
| DEC | a - b | a + b | 100 - 25 = 75 → 75 + 25 = 100 |
| MUL | a * b | a / b | 100 * 2 = 200 → 200 / 2 = 100 |
| DIV | a / b | a * b | 100 / 2 = 50 → 50 * 2 = 100 |
| INC_P | a + (a*b/100) | a / (1+b/100) | 150 → 100 |
| DEC_P | a - (a*b/100) | a / (1-b/100) | 75 → 100 |
| SET | b | false | Sets to b → Reverts to false |
| SET_N | b | false | Sets to b → Reverts to false |
## Modifier Limits System
### Execution Order
Limits are applied AFTER calculation but BEFORE setting the value:
```
1. Calculate new value
2. Apply modifier limits ← YOU ARE HERE
3. Set value on target
```
### Four Types of Limits
**1. minValue (fixed numeric minimum):**
```javascript
let modifier = new Modifier({
key: 'damage',
propertyKey: 'health',
operation: ModifierConst.OPS.DEC,
value: 150,
minValue: 0 // Health cannot go below 0
});
// health = 100
// Calculate: 100 - 150 = -50
// Apply limit: max(-50, 0) = 0
// Result: health = 0
```
**2. maxValue (fixed numeric maximum):**
```javascript
let modifier = new Modifier({
key: 'heal',
propertyKey: 'health',
operation: ModifierConst.OPS.INC,
value: 100,
maxValue: 150 // Health cannot exceed 150
});
// health = 100
// Calculate: 100 + 100 = 200
// Apply limit: min(200, 150) = 150
// Result: health = 150
```
**3. minProperty (dynamic minimum from target property):**
```javascript
let target = {
health: 50,
minHealth: 10
};
let modifier = new Modifier({
key: 'damage',
propertyKey: 'health',
operation: ModifierConst.OPS.DEC,
value: 100,
minProperty: 'minHealth' // Use minHealth as floor
});
// Calculate: 50 - 100 = -50
// Read minHealth: 10
// Apply limit: max(-50, 10) = 10
// Result: health = 10
```
**4. maxProperty (dynamic maximum from target property):**
```javascript
let target = {
health: 50,
maxHealth: 100
};
let modifier = new Modifier({
key: 'heal',
propertyKey: 'health',
operation: ModifierConst.OPS.INC,
value: 80,
maxProperty: 'maxHealth' // Use maxHealth as ceiling
});
// Calculate: 50 + 80 = 130
// Read maxHealth: 100
// Apply limit: min(130, 100) = 100
// Result: health = 100
```
### Limit Check Order (applyModifierLimits method)
```javascript
1. Check minValue (if it's a number)
2. Check maxValue (if it's a number)
3. Check minProperty (if defined and property exists)
4. Check maxProperty (if defined and property exists)
```
**Important:** All applicable limits are checked sequentially. A value could be adjusted multiple times:
```javascript
let modifier = new Modifier({
propertyKey: 'health',
operation: ModifierConst.OPS.SET,
value: 200,
minValue: 0,
maxValue: 150,
maxProperty: 'maxHealth' // target.maxHealth = 120
});
// Flow:
// Initial: 200
// After minValue check: 200 (no change, 200 > 0)
// After maxValue check: 150 (clamped to 150)
// After maxProperty check: 120 (clamped to maxHealth)
// Final result: 120
```
## Condition System Deep Dive
### Condition Validation Flow
```javascript
let condition = new Condition({
key: 'level-check',
propertyKey: 'level',
conditional: ModifierConst.COMPARE.GE,
value: 10
});
condition.isValidOn(target);
```
**Execution:**
```
1. Get target property value via PropertyManager (line 42)
- Supports deep paths: 'stats/combat/level'
2. Get comparison method name (line 43)
- GE → 'ge'
3. Execute comparison method (line 44)
- this.ge() → returns true/false
4. Return validation result
```
### All Comparison Methods
**EQ (Equal)** - condition.js line 48-51:
```javascript
eq(){
return this.targetPropertyValue === this.value;
}
// Example: 10 === 10 → true
```
**NE (Not Equal)** - condition.js line 53-56:
```javascript
ne(){
return this.targetPropertyValue !== this.value;
}
// Example: 10 !== 5 → true
```
**LT (Less Than)** - condition.js line 58-61:
```javascript
lt(){
return this.targetPropertyValue < this.value;
}
// Example: 5 < 10 → true
```
**GT (Greater Than)** - condition.js line 63-66:
```javascript
gt(){
return this.targetPropertyValue > this.value;
}
// Example: 15 > 10 → true
```
**LE (Less or Equal)** - condition.js line 68-71:
```javascript
le(){
return this.targetPropertyValue <= this.value;
}
// Example: 10 <= 10 → true
```
**GE (Greater or Equal)** - condition.js line 73-76:
```javascript
ge(){
return this.targetPropertyValue >= this.value;
}
// Example: 10 >= 10 → true
```
### Multiple Conditions
When a modifier has multiple conditions, ALL must be valid:
```javascript
let levelCondition = new Condition({
key: 'min-level',
propertyKey: 'level',
conditional: ModifierConst.COMPARE.GE,
value: 10
});
let healthCondition = new Condition({
key: 'min-health',
propertyKey: 'health',
conditional: ModifierConst.COMPARE.GT,
value: 50
});
let modifier = new Modifier({
key: 'powerful-attack',
propertyKey: 'damage',
operation: ModifierConst.OPS.MUL,
value: 2,
conditions: [levelCondition, healthCondition]
});
// Validation (modifier.js validateConditions method line 116-129):
// for each condition:
// - Check it's a Condition instance
// - Call condition.isValidOn(target)
// - If ANY fails, return false
// If ALL pass, return true
```
### Deep Property Conditions
Conditions support the same path notation as modifiers:
```javascript
let player = {
stats: {
combat: {
level: 15
}
}
};
let condition = new Condition({
key: 'combat-level-check',
propertyKey: 'stats/combat/level', // Deep path
conditional: ModifierConst.COMPARE.GE,
value: 10
});
condition.isValidOn(player); // true (15 >= 10)
```
## PropertyManager Internal Mechanics
### The extractChildPropertyOwner Method
**Purpose:** Navigate to the PARENT object owning the final property.
**Critical Concept:** It does NOT return the property value, it returns the object containing the property.
```javascript
let player = {
stats: {
combat: {
attack: 50
}
}
};
// For path 'stats/combat/attack'
let parts = ['stats', 'combat', 'attack'];
let parent = extractChildPropertyOwner(player, parts);
// Returns: player.stats.combat (NOT 50!)
```
**Step-by-Step Execution:**
```javascript
extractChildPropertyOwner(propertyOwner, propertyPathParts)
```
**Line 44-45:** Copy array and remove last element
```javascript
let pathToParent = [...propertyPathParts]; // ['stats', 'combat', 'attack']
pathToParent.pop(); // ['stats', 'combat']
```
**Line 46-48:** Handle single-level properties
```javascript
if(0 === pathToParent.length){
return propertyOwner; // No navigation needed
}
```
**Line 49-55:** Navigate through path
```javascript
let currentOwner = propertyOwner; // Start at root
for(let propertyName of pathToParent){ // ['stats', 'combat']
// Iteration 1: propertyName = 'stats'
if(!sc.hasOwn(currentOwner, propertyName)){
ErrorManager.error('Invalid property...');
}
currentOwner = currentOwner[propertyName]; // currentOwner = player.stats
// Iteration 2: propertyName = 'combat'
if(!sc.hasOwn(currentOwner, propertyName)){
ErrorManager.error('Invalid property...');
}
currentOwner = currentOwner[propertyName]; // currentOwner = player.stats.combat
}
return currentOwner; // Returns player.stats.combat
```
**Line 56:** Return parent object
```javascript
return currentOwner; // The object containing the final property
```
### Why This Design?
**Allows direct property assignment:**
```javascript
let parent = extractChildPropertyOwner(player, ['stats', 'combat', 'attack']);
// parent = player.stats.combat
let propertyKey = 'attack';
parent[propertyKey] = 75; // player.stats.combat.attack = 75
```
**Enables validation before access:**
```javascript
if(!sc.hasOwn(parent, propertyKey)){
ErrorManager.error('Property does not exist!');
}
```
### manageOwnerProperty - The Orchestrator
This method coordinates getting/setting values:
**Getting a value (value parameter is undefined):**
```javascript
manageOwnerProperty(player, 'stats/combat/attack', undefined)
↓
1. Split path: ['stats', 'combat', 'attack']
2. Get parent: player.stats.combat
3. Get property name: 'attack'
4. Validate property exists (line 27-29)
5. Return value: player.stats.combat.attack
```
**Setting a value:**
```javascript
manageOwnerProperty(player, 'stats/combat/attack', 75)
↓
1. Split path: ['stats', 'combat', 'attack']
2. Get parent: player.stats.combat
3. Get property name: 'attack'
4. Skip validation (value is defined)
5. Set value: player.stats.combat.attack = 75
6. Return new value: 75
```
**Why validation only on GET:**
- Getting: Property must exist to read it
- Setting: Creating new properties is allowed
## Type System and Value Parsing
### Type Constants (constants.js)
```javascript
TYPES: {
INT: 'integer',
STRING: 'string'
}
```
### Value Parsing Flow
**Constructor** (modifier.js line 23-24):
```javascript
this.originalValue = props.value; // Store original
this.value = this.parseValue(props.value); // Parse based on type
```
**parseValue Method** (modifier.js line 54-63):
```javascript
parseValue(value){
if(this.type === ModifierConst.TYPES.INT){
value = Number(value); // Convert to number
}
if(this.type === ModifierConst.TYPES.STRING){
value = String(value); // Convert to string
}
return value;
}
```
**Examples:**
```javascript
// INT type (default)
new Modifier({
key: 'test',
propertyKey: 'attack',
operation: ModifierConst.OPS.INC,
value: '25' // String input
});
// this.value = 25 (number)
// STRING type
new Modifier({
key: 'test',
propertyKey: 'status',
operation: ModifierConst.OPS.SET,
type: ModifierConst.TYPES.STRING,
value: 123 // Number input
});
// this.value = '123' (string)
```
### Min/Max Value Parsing
**Constructor** (modifier.js line 26-27):
```javascript
this.minValue = sc.hasOwn(props, 'minValue') ? this.parseValue(props.minValue) : false;
this.maxValue = sc.hasOwn(props, 'maxValue') ? this.parseValue(props.maxValue) : false;
```
**Why parse min/max values?**
- Ensures type consistency
- Allows string inputs from database/config
- Prevents type comparison issues
## State Management System
### State Constants (constants.js)
```javascript
MOD_MISSING_KEY: 101,
MOD_MISSING_PROPERTY_KEY: 102,
MOD_MISSING_OPERATION: 103,
MOD_MISSING_VALUE: 104,
MOD_READY: 105,
MOD_APPLIED: 106,
MOD_REVERTED: 107,
MOD_UNDEFINED_TARGET: 108,
MOD_INVALID_CONDITIONS: 109,
MOD_MISSING_CONDITION_INSTANCE: 110,
MOD_MODIFIER_ERROR: 111
```
### State Lifecycle
```
Constructor → determineState() → Initial State
↓
MOD_READY / MOD_MISSING_* / Error State
↓
apply() or revert()
↓
MOD_APPLIED / MOD_REVERTED / Error State
```
### determineState Method (modifier.js line 37-52)
**Validation Order:**
```javascript
1. Check key exists → MOD_MISSING_KEY
2. Check propertyKey exists → MOD_MISSING_PROPERTY_KEY
3. Check operation exists → MOD_MISSING_OPERATION
4. Check value exists → MOD_MISSING_VALUE
5. All valid → MOD_READY
```
### Runtime State Changes
**During execution:**
```javascript
execute(target, revert){
// State can change during execution:
if(!target){
this.state = ModifierConst.MOD_UNDEFINED_TARGET;
return false;
}
if(conditions invalid){
this.state = ModifierConst.MOD_INVALID_CONDITIONS;
return false;
}
// Success:
this.state = revert ? ModifierConst.MOD_REVERTED : ModifierConst.MOD_APPLIED;
return true;
}
```
**During condition validation:**
```javascript
validateConditions(target){
if(!(condition instanceof Condition)){
this.state = ModifierConst.MOD_MISSING_CONDITION_INSTANCE;
return false;
}
if(!condition.isValidOn(target)){
this.state = ModifierConst.MOD_INVALID_CONDITIONS;
return false;
}
}
```
**During method operation:**
```javascript
if(this.operation === ModifierConst.OPS.METHOD){
if(!sc.hasOwn(this, this.value) || 'function' !== typeof this[this.value]){
this.state = ModifierConst.MOD_MODIFIER_ERROR;
return false;
}
}
```
## Integration Patterns
### With @reldens/items-system
Items use modifiers for equipment stats:
```javascript
// Item has modifiers
let sword = {
key: 'iron-sword',
modifiers: {
'1': new Modifier({
key: 'sword-attack',
propertyKey: 'stats/atk',
operation: ModifierConst.OPS.INC,
value: 25
}),
'2': new Modifier({
key: 'sword-speed',
propertyKey: 'stats/spd',
operation: ModifierConst.OPS.DEC,
value: 5
})
}
};
// When equipped:
for(let modifierId in item.modifiers){
let modifier = item.modifiers[modifierId];
modifier.apply(player);
}
// When unequipped:
for(let modifierId in item.modifiers){
let modifier = item.modifiers[modifierId];
modifier.revert(player);
}
```
### With @reldens/skills
Skills use modifiers for temporary buffs:
```javascript
let skill = {
key: 'berserker-rage',
duration: 10000, // 10 seconds
modifiers: {
attack_boost: new Modifier({
key: 'rage-attack',
propertyKey: 'stats/atk',
operation: ModifierConst.OPS.INC_P,
value: 50 // +50% attack
}),
defense_penalty: new Modifier({
key: 'rage-defense',
propertyKey: 'stats/def',
operation: ModifierConst.OPS.DEC_P,
value: 30 // -30% defense
})
}
};
// On skill cast:
for(let key in skill.modifiers){
skill.modifiers[key].apply(player);
}
// After duration:
setTimeout(() => {
for(let key in skill.modifiers){
skill.modifiers[key].revert(player);
}
}, skill.duration);
```
### Custom Method Operations
Extend Modifier class for complex calculations:
```javascript
const { Modifier, ModifierConst } = require('@reldens/modifiers');
class CriticalHitModifier extends Modifier
{
criticalCalculation(modifier, currentValue)
{
let baseDamage = currentValue;
let critChance = this.target.stats.critChance || 0.1;
let critMultiplier = this.target.stats.critMultiplier || 2;
if(Math.random() < critChance){
return Math.floor(baseDamage * critMultiplier);
}
return baseDamage;
}
}
let critModifier = new CriticalHitModifier({
key: 'crit-system',
propertyKey: 'calculatedDamage',
operation: ModifierConst.OPS.METHOD,
value: 'criticalCalculation' // Method name
});
critModifier.apply(combatContext);
```
## Common Gotchas and Solutions
### 1. Using basePropertyKey Without Setting It
**Problem:**
```javascript
let modifier = new Modifier({
key: 'test',
propertyKey: 'currentHealth',
// No basePropertyKey specified
operation: ModifierConst.OPS.INC_P,
value: 50
});
modifier.apply(target, true, false); // useBasePropertyToGetValue = true
```
**What Happens:**
- basePropertyKey defaults to propertyKey (line 20)
- Reads from currentHealth, not maxHealth
- Unexpected calculation
**Solution:**
```javascript
let modifier = new Modifier({
key: 'test',
propertyKey: 'currentHealth',
basePropertyKey: 'maxHealth', // Explicitly set
operation: ModifierConst.OPS.INC_P,
value: 50
});
```
### 2. Forgetting to Revert Modifiers
**Problem:**
```javascript
// Apply modifier
modifier.apply(player);
// Player changes equipment
// Modifier never reverted!
// Stats permanently modified
```
**Solution:**
```javascript
// Always pair apply with revert
modifier.apply(player);
// Later...
modifier.revert(player);
```
### 3. Modifying Wrong Property with Paths
**Problem:**
```javascript
let player = {
stats: {
combat: { attack: 50 }
}
};
let modifier = new Modifier({
key: 'test',
propertyKey: 'stats/combat/atk', // TYPO: 'atk' not 'attack'
operation: ModifierConst.OPS.INC,
value: 25
});
modifier.apply(player);
// Creates stats.combat.atk = 25 (NEW property!)
// stats.combat.attack = 50 (unchanged)
```
**Solution:**
- Double-check property paths
- Use constants for property names
- Validate in tests
### 4. Condition Type Mismatches
**Problem:**
```javascript
let player = { level: 10 }; // Number
let condition = new Condition({
key: 'level-check',
propertyKey: 'level',
conditional: ModifierConst.COMPARE.EQ,
type: ModifierConst.TYPES.STRING, // Wrong type!
value: '10'
});
condition.isValidOn(player); // false! (10 !== '10')
```
**Solution:**
```javascript
let condition = new Condition({
key: 'level-check',
propertyKey: 'level',
conditional: ModifierConst.COMPARE.EQ,
type: ModifierConst.TYPES.INT, // Correct type
value: 10
});
```
### 5. Percentage Operations on Zero
**Problem:**
```javascript
let player = { damage: 0 };
let modifier = new Modifier({
key: 'damage-boost',
propertyKey: 'damage',
operation: ModifierConst.OPS.INC_P,
value: 100 // +100%
});
modifier.apply(player);
// Result: damage = 0 (0 + 100% of 0 = 0)
```
**Solution:**
- Use absolute values (INC) for base increases
- Or set base value first, then apply percentages
## Testing Patterns
### Test Structure
Tests are organized by class and functionality:
```
tests/
unit/
test-calculator.js - Calculator operations
test-condition.js - Condition validation
test-modifier.js - Modifier application/revert
test-property-manager.js - Property navigation
test-constants.js - Constants validation
fixtures/
test-helpers.js - Mock objects and helpers
```
### Key Test Scenarios
**1. Basic Operations:**
- Each operation type (INC, DEC, MUL, DIV, INC_P, DEC_P, SET)
- Apply and revert
- State changes
**2. Property Navigation:**
- Simple properties ('health')
- Nested properties ('stats/health')
- Deep nested properties ('stats/combat/attack')
- Error handling for invalid paths
**3. Conditions:**
- All comparison operators
- Multiple conditions
- Nested property conditions
- Type handling (INT vs STRING)
**4. Edge Cases:**
- Zero values
- Negative values
- Very large numbers
- Decimal values
- Type conversions
**5. Integration:**
- Modifiers with conditions
- Modifiers with limits
- Base property operations
## Performance Considerations
### PropertyManager Caching
PropertyManager does NOT cache property lookups. Each access navigates the full path:
```javascript
// Every call navigates full path
modifier.apply(player); // Navigates to property
modifier.revert(player); // Navigates again
```
**For high-frequency modifications:**
- Consider caching parent objects
- Or use direct property access when paths are static
### Condition Validation
Conditions are validated on every apply/revert:
```javascript
// With 5 conditions:
modifier.apply(player);
// Validates all 5 conditions
// Then applies modifier
```
**Optimization:**
- Use minimal conditions
- Or cache validation results if conditions don't change
### Modifier Reuse
Modifier instances are reusable:
```javascript
let modifier = new Modifier({
key: 'heal',
propertyKey: 'health',
operation: ModifierConst.OPS.INC,
value: 50
});
// Use on multiple targets
modifier.apply(player1);
modifier.apply(player2);
modifier.apply(player3);
```
**Benefit:**
- Single instance for common modifiers
- Reduced memory footprint
- Consistent behavior