# Items System Implementation - Complete Documentation

## Overview

The Reldens Items System manages player inventory, equipment, and item modifiers. It uses the `@reldens/items-system` package for core functionality and integrates with the `@reldens/modifiers` package for stat modifications.

## Architecture

### Core Components

1. **ItemsServer** (`@reldens/items-system`) - Server-side inventory manager
2. **Inventory** (`@reldens/items-system`) - Base inventory container
3. **ItemBase** - Base class for all items
4. **Equipment** - Specialized item type for equippable items
5. **Modifier** (`@reldens/modifiers`) - Handles stat modifications
6. **StorageObserver** - Persists inventory changes to database

### Directory Structure

**lib/inventory/**
- **client/** - Client-side inventory UI and rendering
- **server/** - Server-side inventory logic
  - items-factory.js - Creates item instances from database models
  - message-actions.js - Handles equip/unequip/trade messages
  - models-manager.js - Database operations
  - storage-observer.js - Event listeners for persistence
  - plugin.js - Inventory feature plugin
  - **subscribers/** - Event subscribers
    - player-subscriber.js - Creates player inventory on login
    - player-death-subscriber.js
- constants.js

## Item Creation Flow

### When Player Logs In

**Entry Point**: `lib/inventory/server/plugin.js` line 50-51
```javascript
this.events.on('reldens.createPlayerStatsAfter', async (client, userModel, currentPlayer, room) => {
    await PlayerSubscriber.createPlayerInventory(client, currentPlayer, room, this.events, this.modelsManager);
});
```

**Sequence**:

1. **Player Stats Loaded** (`lib/users/server/plugin.js` lines 289-309)
   - Stats loaded from `players_stats` table
   - Set on `currentPlayer.stats` and `currentPlayer.statsBase`
   - Event `reldens.createPlayerStatsAfter` fires

2. **Inventory Creation** (`lib/inventory/server/subscribers/player-subscriber.js` lines 30-63)
   ```javascript
   let serverProps = {
       owner: currentPlayer,              // The player schema instance
       client: new ClientWrapper({client, room}),
       persistence: true,
       ownerIdProperty: 'player_id',
       eventsManager: events,
       modelsManager: modelsManager,
       itemClasses: {...},
       groupClasses: {...},
       itemsModelData: room.config.inventory.items
   };
   let inventoryServer = new ItemsServer(serverProps);
   inventoryServer.dataServer = new StorageObserver(inventoryServer.manager, modelsManager);
   ```

3. **Items Loading** (`lib/inventory/server/storage-observer.js` lines 169-182)
   ```javascript
   async loadOwnerItems(){
       let itemsModels = await this.modelsManager.loadOwnerItems(this.manager.getOwnerId());
       let itemsInstances = await ItemsFactory.fromModelsList(itemsModels, this.manager);
       await this.manager.fireEvent(ItemsEvents.LOADED_OWNER_ITEMS, this, itemsInstances, itemsModels);
       await this.manager.setItems(itemsInstances);
   }
   ```

4. **Item Instance Creation** (`lib/inventory/server/items-factory.js` lines 40-71)
   ```javascript
   static async fromModel(itemInventoryModel, manager){
       let itemClass = sc.get(
           manager.itemClasses,
           itemInventoryModel.related_items_item.key,
           manager.types.classByTypeId(itemInventoryModel.related_items_item.type)
       );
       let itemObj = new itemClass(itemProps);
       if (itemObj.isType(ItemsConst.TYPES.EQUIPMENT)) {
           itemObj.equipped = (1 === itemInventoryModel.is_active);  // Mark as equipped if active
       }
       await this.enrichWithModifiers(itemInventoryModel, itemObj, manager);
       return itemObj;
   }
   ```

5. **Modifier Creation** (`lib/inventory/server/items-factory.js` lines 79-93)
   ```javascript
   static async enrichWithModifiers(itemInventoryModel, itemObj, manager){
       let modifiers = {};
       for(let modifierData of itemInventoryModel.related_items_item.related_items_item_modifiers){
           if(modifierData.operation !== ModifierConst.OPS.SET){
               modifierData.value = Number(modifierData.value);
           }
           modifierData.target = manager.owner;  // Set target to currentPlayer
           modifiers[modifierData.id] = new Modifier(modifierData);
       }
       itemObj.modifiers = modifiers;
   }
   ```

### Critical Timing

- **BEFORE items load**: `currentPlayer.stats` is set (fresh object from database)
- **DURING item creation**: Modifiers get `target = manager.owner = currentPlayer`
- **AFTER items load**: Modifiers have correct reference to `currentPlayer.stats`

## Equipment Flow

### Manual Equip (User Action)

**Entry Point**: User clicks equip button, client sends message, server receives

1. **Message Reception** (`lib/inventory/server/message-actions.js` lines 71-73)
   ```javascript
   if(InventoryConst.ACTIONS.EQUIP === data.act){
       return await this.executeEquipAction(playerSchema, data);
   }
   ```

2. **Execute Equip Action** (`lib/inventory/server/message-actions.js` lines 360-373)
   ```javascript
   async executeEquipAction(playerSchema, data){
       let item = playerSchema.inventory.manager.items[data.idx];
       if(!item.equipped){
           this.unEquipPrevious(item.group_id, playerSchema.inventory.manager.items);  // Unequip same group
           await item.equip();  // Equip new item
           return true;
       }
       await item.unequip();  // If already equipped, unequip
       return true;
   }
   ```

3. **Item Equip Method** (`npm-packages/reldens-items/lib/item/type/equipment.js` lines 26-35)
   ```javascript
   async equip(applyMods){
       this.equipped = true;
       await this.manager.fireEvent(ItemsEvents.EQUIP_ITEM, this);
       if(applyMods === false || this.manager.applyModifiersAuto === false){
           return false;
       }
       await this.applyModifiers();  // Apply modifiers automatically
   }
   ```

4. **Apply Modifiers** (`npm-packages/reldens-items/lib/item/type/item-base.js` lines 90-105)
   ```javascript
   async changeModifiers(revert){
       await this.manager.fireEvent(ItemsEvents.EQUIP_BEFORE+(revert ? 'Revert': 'Apply')+'Modifiers', this);
       let modifiersKeys = Object.keys(this.modifiers);
       let methodName = revert ? 'revert' : 'apply';
       for(let i of modifiersKeys){
           this.modifiers[i][methodName](this.target);  // this.target is false, but modifier has its own target
       }
       return this.manager.fireEvent(ItemsEvents.EQUIP+(revert ? 'Reverted' : 'Applied')+'Modifiers', this);
   }
   ```

5. **Modifier Execute** (`npm-packages/reldens-modifiers/lib/modifier.js` lines 84-108)
   ```javascript
   execute(target, revert = false, useBasePropertyToGetValue = false, applyOnBaseProperty = false){
       // If target param is false, use this.target (set to currentPlayer in factory)
       if(target){
           this.target = target;
       }
       let newValue = this.getModifiedValue(revert, useBasePropertyToGetValue);
       let applyToProp = applyOnBaseProperty ? this.basePropertyKey : this.propertyKey;
       this.setOwnerProperty(applyToProp, newValue);  // Sets currentPlayer.stats.atk
       this.state = revert ? ModifierConst.MOD_REVERTED : ModifierConst.MOD_APPLIED;
       return true;
   }
   ```

6. **Property Manager Sets Value** (`npm-packages/reldens-modifiers/lib/property-manager.js` lines 22-34)
   ```javascript
   manageOwnerProperty(propertyOwner, propertyString, value){
       let propertyPathParts = propertyString.split('/');  // ['stats', 'atk']
       let childPropertyOwner = this.extractChildPropertyOwner(propertyOwner, propertyPathParts);  // Get stats object
       let propertyKey = propertyPathParts[propertyPathParts.length-1];  // 'atk'
       if('undefined' === typeof value && !sc.hasOwn(childPropertyOwner, propertyKey)){
           ErrorManager.error('Invalid property "'+propertyKey+'" from path: "'+propertyPathParts.join('/')+'"].');
       }
       if('undefined' !== typeof value){
           childPropertyOwner[propertyKey] = value;  // Sets stats.atk = newValue
       }
       return childPropertyOwner[propertyKey];
   }
   ```

7. **Stats Persistence** (`lib/inventory/server/storage-observer.js` lines 68-79)
   ```javascript
   this.manager.listenEvent(
       ItemsEvents.EQUIP+'AppliedModifiers',
       this.updateAppliedModifiers.bind(this),
       ...
   );

   async updateAppliedModifiers(item){
       return await this.modelsManager.onChangedModifiers(item, ModifierConst.MOD_APPLIED);
   }
   ```

8. **Persist Data** (`lib/inventory/server/models-manager.js` lines 127-131)
   ```javascript
   async onChangedModifiers(item, action){
       return await item.manager.owner.persistData({act: action, item: item});
   }
   ```

9. **Save Player Stats** (`lib/rooms/server/scene.js` lines 228-234)
   ```javascript
   currentPlayer.persistData = async (params) => {
       await this.savePlayedTime(currentPlayer);
       await this.savePlayerState(currentPlayer.sessionId);
       await this.savePlayerStats(currentPlayer, client);  // Saves stats to database
   };
   ```

10. **Client Update** (`lib/rooms/server/scene.js` lines 759-763)
    ```javascript
    client.send('*', {
        act: GameConst.PLAYER_STATS,
        stats: playerSchema.stats,
        statsBase: playerSchema.statsBase
    });
    ```

## Modifier Operations

From `@reldens/modifiers/lib/constants.js`:

**1. INC - Increase (flat)**
- Apply: `value + operand`
- Revert: `value - operand`

**2. DEC - Decrease**
- Apply: `value - operand`
- Revert: `value + operand`

**3. DIV - Divide**
- Apply: `value / operand`
- Revert: `value * operand`

**4. MUL - Multiply**
- Apply: `value * operand`
- Revert: `value / operand`

**5. INC_P - Increase by %**
- Apply: `value + (value * operand / 100)`
- Revert: Complex percentage revert

**6. DEC_P - Decrease by %**
- Apply: `value - (value * operand / 100)`
- Revert: Complex percentage revert

**7. SET - Set value**
- Apply: `operand`
- Revert: `false`

**8. METHOD - Custom method**
- Apply: Calls custom method on modifier
- Revert: Calls custom method

**9. SET_N - Set (alt)**
- Apply: `operand`
- Revert: `false`

### INC_P (Increase Percentage) Calculation

From `@reldens/modifiers/lib/calculator.js` lines 30-37:

**Apply**:
```javascript
return originalValue + Math.round(originalValue * operationValue / 100);
```
Example: atk=100, value=5 results in 100 + Math.round(100 * 5 / 100) = 100 + 5 = 105

**Revert**:
```javascript
let revertValue = Math.ceil(originalValue - (originalValue / (100 - operationValue)) * 100);
return originalValue + revertValue;
```
Example: atk=105, value=5 results in Math.ceil(105 - (105/95)*100) = Math.ceil(-5.26) = -5 then 105 + (-5) = 100

## Database Schema

### items_item (Item Definitions)
```sql
CREATE TABLE `items_item` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `key` varchar(255) NOT NULL,
    `type` int(11) NOT NULL,
    `group_id` int(10) unsigned DEFAULT NULL,
    `label` varchar(255) DEFAULT NULL,
    `description` text,
    `qty_limit` int(11) DEFAULT NULL,
    `uses_limit` int(11) DEFAULT NULL,
    `useTimeOut` int(11) DEFAULT NULL,
    `execTimeOut` int(11) DEFAULT NULL,
    `customData` text,
    PRIMARY KEY (`id`)
);
```

### items_item_modifiers (Item Modifier Definitions)
```sql
CREATE TABLE `items_item_modifiers` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `item_id` int(10) unsigned NOT NULL,
    `key` varchar(255) NOT NULL,
    `property_key` varchar(255) NOT NULL,
    `operation` int(11) NOT NULL,
    `value` varchar(255) NOT NULL,
    `maxProperty` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`),
    FOREIGN KEY (`item_id`) REFERENCES `items_item` (`id`)
);
```

- `item_id`: References the item this modifier belongs to
- `key`: Modifier identifier (e.g., 'atk')
- `property_key`: Path to property to modify (e.g., 'stats/atk')
- `operation`: Operation ID (1-9, see Modifier Operations table)
- `value`: Value to apply (as string, converted to number if not SET operation)
- `maxProperty`: Optional max value property path (e.g., 'statsBase/hp')

### items_inventory (Player Item Instances)
```sql
CREATE TABLE `items_inventory` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `owner_id` int(10) unsigned NOT NULL,
    `item_id` int(10) unsigned NOT NULL,
    `qty` int(11) NOT NULL,
    `remaining_uses` int(11) DEFAULT NULL,
    `is_active` tinyint(1) DEFAULT 0,
    PRIMARY KEY (`id`),
    FOREIGN KEY (`owner_id`) REFERENCES `players` (`id`),
    FOREIGN KEY (`item_id`) REFERENCES `items_item` (`id`)
);
```

- `owner_id`: Player ID who owns this item instance
- `item_id`: References the item definition
- `qty`: Quantity (-1 for unlimited)
- `remaining_uses`: Uses left (if item has uses limit)
- `is_active`: 1 if equipped, 0 if not (for equipment items only)

## Event Flow

### Equipment Events Sequence

1. `ItemsEvents.EQUIP_ITEM` - Fired when equip() starts
   - **Listener**: `StorageObserver.saveEquippedItemAsActive()` - Updates `is_active=1` in database

2. `ItemsEvents.EQUIP_BEFORE+'Apply'+'Modifiers'` - Before modifiers are applied
   - No default listeners

3. `ItemsEvents.EQUIP+'Applied'+'Modifiers'` - After modifiers are applied
   - **Listener**: `StorageObserver.updateAppliedModifiers()` - Calls `persistData()` to save stats

4. `reldens.playerPersistDataBefore` - Before data persistence
   - Custom hooks can intercept here

5. `reldens.savePlayerStatsUpdateClient` - After stats saved, before client update
   - **Listener**: `UsersPlugin.updateClientsWithPlayerStats()` - Updates life bar UI

6. Client receives `GameConst.PLAYER_STATS` message with updated stats

### Unequip Events Sequence

1. `ItemsEvents.UNEQUIP_ITEM` - Fired when unequip() starts
   - **Listener**: `StorageObserver.saveUnequippedItemAsInactive()` - Updates `is_active=0` in database

2. `ItemsEvents.EQUIP_BEFORE+'Revert'+'Modifiers'` - Before modifiers are reverted
   - No default listeners

3. `ItemsEvents.EQUIP+'Reverted'+'Modifiers'` - After modifiers are reverted
   - **Listener**: `StorageObserver.updateRevertedModifiers()` - Calls `persistData()` to save stats

4-6. Same persistence and client update flow as equip

## Testing Checklist

- Equip item - Stats increase correctly
- Unequip item - Stats revert to base value
- Logout with equipped item - Stats saved correctly
- Login with equipped item - Stats loaded with modifiers applied
- Unequip after login - Stats revert to base value correctly
- Multiple items in same group - Only one equipped at a time
- Percentage modifiers - Calculate correctly for different base values
- Flat modifiers - Add/subtract exact values
- Max/min property limits - Respect statsBase maximums

## Performance Considerations

- Modifiers are applied synchronously in a loop (item-base.js line 101-103)
- For items with many modifiers, this could cause brief delay
- Stats are saved to database after every equip/unequip operation
- Consider batching stats updates if players frequently swap equipment

## Extension Points

### Custom Item Types

Create custom item class extending ItemBase or Equipment:
```javascript
const Equipment = require('@reldens/items-system').ItemBase;

class MagicWeapon extends Equipment {
    async equip(applyMods){
        // Custom equip logic
        await super.equip(applyMods);
        // Post-equip custom logic
    }
}
```

Register in `server/customClasses/inventory/items`:
```javascript
itemClasses: {
    'magic_sword': MagicWeapon
}
```

### Custom Modifiers

Create custom modifier with METHOD operation:
```javascript
const { Modifier } = require('@reldens/modifiers');

class CustomModifier extends Modifier {
    customCalculation(modifier, propertyValue){
        // Your custom logic
        return newValue;
    }
}
```

Set in database:
```sql
INSERT INTO items_item_modifiers VALUES (
    NULL, item_id, 'custom', 'stats/custom', 8, 'customCalculation', NULL
);
```

### Event Hooks

Hook into any event for custom logic:
```javascript
events.on('reldens.createdPlayerSchema', async (client, userModel, currentPlayer, room) => {
    // Custom logic when player is created
});

inventoryServer.manager.listenEvent(ItemsEvents.EQUIP_ITEM, async (item) => {
    // Custom logic when any item is equipped
});
```

## References

- `@reldens/items-system` package: D:\dap\work\reldens\npm-packages\reldens-items
- `@reldens/modifiers` package: D:\dap\work\reldens\npm-packages\reldens-modifiers
- Sample data: D:\dap\work\reldens\src\migrations\production\reldens-sample-data-v4.0.0.sql
