npm package: @series-inc/universal-avatar-renderer
Description: Phaser Scene Plugin for rendering static avatars with visual variant system
Author: Series Inc
Latest version: 1.0.0

# @series-inc/universal-avatar-renderer

> **Phaser Scene Plugin for rendering layered avatar images with visual variant system**

A production-ready, type-safe Phaser plugin for rendering customizable characters using a Container-based architecture with Items Catalog system.

[![Tests](https://img.shields.io/badge/tests-191%20passing-success)](./tests)
[![Coverage](https://img.shields.io/badge/coverage-~95%25-success)](#test-coverage)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
[![Phaser](https://img.shields.io/badge/Phaser-3.60%2B-orange)](https://phaser.io/)

---

## 📋 Table of Contents

- [Features](#-features)
- [Package Information](#-package-information)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Core Concepts](#-core-concepts)
- [JSON Format Reference](#-json-format-reference)
- [API Reference](#-api-reference)
- [Type Definitions](#-type-definitions)
- [Advanced Usage](#-advanced-usage)
  - [Loading Registries from JSON](#loading-registries-from-json)
  - [State Change Events](#state-change-events)
  - [Custom Variant Resolver](#custom-variant-resolver)
  - [Item Metadata System](#item-metadata-system)
  - [Accessing Internal Managers](#accessing-internal-managers)
  - [Closet System & Hot-Swapping](#closet-system--hot-swapping)
  - [Multi-Scene Management](#multi-scene-management)
- [Performance Considerations](#-performance-considerations)
- [Testing](#-testing)
- [Architecture](#-architecture)
- [Examples](#-examples)
- [Troubleshooting](#-troubleshooting)
  - [Common Issues](#common-issues)
  - [Error Reference](#error-reference)
  - [Debugging Tips](#debugging-tips)

---

## ✨ Features

- 🎨 **Layered Avatar Rendering** - Compose avatars from multiple image layers
- 🔄 **Hot-Swapping** - Change visual variants at runtime without re-rendering
- 📦 **Items Catalog System** - Reusable equipment items across characters
- 🎯 **Type-Safe** - Full TypeScript support with comprehensive type definitions
- ⚡ **Optimized Performance** - Smart caching and selective preloading
- 🧪 **Well-Tested** - 191 tests with ~95% coverage
- 🔌 **Phaser Plugin** - Seamless integration with Phaser 3 lifecycle
- 📊 **State Management** - Built-in visual state tracking with events
- 🔧 **Extensible Architecture** - Pluggable resource loaders for multiple formats
- 📐 **Local Transform Support** - Per-layer positioning, scaling, and effects for memory optimization

---

## 📦 Package Information

### Bundle & Format

| Property | Value |
|----------|-------|
| **Bundle Size** | ~50KB minified (~15KB gzipped) |
| **Module Format** | ESM (ECMAScript Modules) |
| **Tree-shakeable** | ✅ Yes |
| **TypeScript** | ✅ Full support with type definitions |
| **Side Effects** | None (pure functions + Phaser plugin) |

### Browser Compatibility

| Feature | Requirement |
|---------|-------------|
| **JavaScript** | ES2020+ |
| **WebGL** | Required (Phaser 3 requirement) |
| **Browsers** | All modern browsers (Chrome, Firefox, Safari, Edge) |
| **Mobile** | iOS Safari 12+, Chrome Android 80+ |
| **Node.js** | >=20.0.0 (for build tools) |

### Phaser Version Compatibility

| Phaser Version | UAR Support |
|----------------|-------------|
| **3.60.0 - 3.70.0** | ✅ Fully tested |
| **3.80.0+** | ✅ Compatible |
| **< 3.60.0** | ⚠️ Not tested (may work) |

### Dependencies

**Runtime Dependencies:** None (Phaser is a peer dependency)

**Peer Dependencies:**
- `phaser: ^3.60.0` - Required

**Dev Dependencies:** (Only for package development)
- TypeScript, ESLint, Vitest, tsdown

---

## 📦 Installation

### NPM/Yarn

```bash
npm install @series-inc/universal-avatar-renderer
# or
yarn add @series-inc/universal-avatar-renderer
```

### Peer Dependencies

```json
{
  "phaser": "^3.60.0"
}
```

---

## 🚀 Quick Start

### 1. Register the Plugin

```typescript
import Phaser from 'phaser';
import { AvatarRenderer } from '@series-inc/universal-avatar-renderer';

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  scene: {
    preload: preload,
    create: create,
  },
  plugins: {
    scene: [
      {
        key: 'AvatarRenderer',
        plugin: AvatarRenderer,
        mapping: 'avatarRenderer',
      },
    ],
  },
};

const game = new Phaser.Game(config);
```

### 2. Define Your Data

```typescript
import type {
  LayerSchemaRegistry,
  CharacterRegistry,
  ItemCatalog,
} from '@series-inc/universal-avatar-renderer';

// Define layer schema (body type)
const layerSchemas: LayerSchemaRegistry = {
  humanoid: {
    id: 'humanoid',
    displayName: 'Humanoid',
    layers: ['body', 'face', 'hair', 'outfit'],
    variantCategories: {
      expression: {
        displayName: 'Expression',
        values: ['default', 'happy', 'sad', 'angry'],
        defaultValue: 'default',
      },
    },
  },
};

// Define items catalog
const itemCatalog: ItemCatalog = {
  items: {
    item_body_default: {
      id: 'item_body_default',
      name: 'Default Body',
      compatibleSchemas: ['humanoid'],
      equipmentSlotId: 'body_slot',
      visuals: {
        body: {
          default: { resource: { type: 'static', id: 'body_d', uri: '/assets/body.png' } },
        },
      },
    },
    item_face_default: {
      id: 'item_face_default',
      name: 'Default Face',
      compatibleSchemas: ['humanoid'],
      equipmentSlotId: 'face_slot',
      visuals: {
        face: {
          default: { resource: { type: 'static', id: 'face_d', uri: '/assets/face_default.png' } },
          happy: { resource: { type: 'static', id: 'face_h', uri: '/assets/face_happy.png' } },
          sad: { resource: { type: 'static', id: 'face_s', uri: '/assets/face_sad.png' } },
        },
      },
    },
  },
};

// Define characters
const characters: CharacterRegistry = {
  player: {
    id: 'player',
    displayName: 'Player Character',
    layerSchemaId: 'humanoid',
    equipmentSlots: {
      body_slot: 'item_body_default',
      face_slot: 'item_face_default',
    },
  },
};
```

### 3. Use in Your Scene

```typescript
function preload(this: Phaser.Scene) {
  // Initialize the plugin
  this.avatarRenderer.initialize(layerSchemas, characters, itemCatalog);

  // Preload character assets
  this.avatarRenderer.preload(['player']);
}

function create(this: Phaser.Scene) {
  // Render character
  const container = this.avatarRenderer.render('player', {
    x: 400,
    y: 600,
    scale: 1.5,
    originX: 0.5,
    originY: 1.0,
  });

  // Update visual variant (hot-swap)
  this.avatarRenderer.updateVisualVariant('player', 'expression', 'happy');
}
```

---

## 🧩 Core Concepts

### Architecture Overview

```
┌─────────────────────────────────────────────────────────┐
│                    AvatarRenderer                        │
│                    (Scene Plugin)                        │
└────────────┬────────────────────────────────────────────┘
             │
    ┌────────┴────────┐
    │                 │
┌───▼────┐      ┌────▼─────┐      ┌──────────┐
│ Layer  │      │  Items   │      │  State   │
│ Schema │──────│ Catalog  │──────│ Manager  │
└────────┘      └──────────┘      └──────────┘
     │               │                   │
     │               │                   │
┌────▼───────────────▼───────────────────▼────┐
│         Character Registry                   │
│   (References items by ID from catalog)      │
└──────────────────────────────────────────────┘
```

### Key Components

1. **Layer Schema** - Defines rendering order and variant categories for a body type
2. **Items Catalog** - Reusable equipment items with visual variants
3. **Character Registry** - Character definitions that reference items by ID
4. **State Manager** - Tracks current visual state (expression, wetness, etc.)
5. **Container Manager** - Manages Phaser containers for each rendered character

---

## 📄 JSON Format Reference

While the Quick Start shows TypeScript definitions, in real projects you'll load data from JSON files. Here are complete, production-ready examples.

### Layer Schema JSON (`layer-schemas.json`)

```json
{
  "humanoid_v1": {
    "id": "humanoid_v1",
    "displayName": "Humanoid Base",
    "avatarCanvasSize": {
      "width": 1062,
      "height": 1674
    },
    "layers": ["body", "face", "hair_back", "outfit", "hair_front", "accessories"],
    "variantCategories": {
      "expression": {
        "displayName": "Expression",
        "description": "Character's emotional state",
        "values": ["default", "happy", "sad", "angry", "surprised"],
        "defaultValue": "default",
        "order": 1
      },
      "wetness": {
        "displayName": "Wetness",
        "description": "Water/rain effect",
        "values": ["default", "wet"],
        "defaultValue": "default",
        "order": 2
      }
    }
  },
  "fem_gen2a": {
    "id": "fem_gen2a",
    "displayName": "Female Body Type A",
    "layers": ["body", "face", "hair_b", "clothing", "hair_f", "props_b", "props_f"],
    "variantCategories": {
      "expression": {
        "displayName": "Expression",
        "values": ["NEUTRAL", "HAPPY", "SAD", "ANGRY", "SURPRISED"],
        "defaultValue": "NEUTRAL"
      }
    }
  }
}
```

### Item Catalog JSON (`item-catalog.json`)

```json
{
  "items": {
    "item_body_base": {
      "id": "item_body_base",
      "name": "Base Body",
      "compatibleSchemas": ["humanoid_v1"],
      "equipmentSlotId": "body_slot",
      "visuals": {
        "body": {
          "default": {
            "resource": {
              "type": "static",
              "id": "body_default",
              "uri": "/assets/avatars/body/base_default.png"
            }
          }
        }
      }
    },
    "item_face_expressive": {
      "id": "item_face_expressive",
      "name": "Expressive Face",
      "compatibleSchemas": ["humanoid_v1"],
      "equipmentSlotId": "face_slot",
      "visuals": {
        "face": {
          "default": {
            "resource": {
              "type": "static",
              "id": "face_default",
              "uri": "/assets/avatars/face/expressive_default.png"
            }
          },
          "happy": {
            "resource": {
              "type": "static",
              "id": "face_happy",
              "uri": "/assets/avatars/face/expressive_happy.png"
            }
          },
          "sad": {
            "resource": {
              "type": "static",
              "id": "face_sad",
              "uri": "/assets/avatars/face/expressive_sad.png"
            }
          },
          "angry": {
            "resource": {
              "type": "static",
              "id": "face_angry",
              "uri": "/assets/avatars/face/expressive_angry.png"
            }
          }
        }
      }
    },
    "item_hair_long_brown": {
      "id": "item_hair_long_brown",
      "name": "Long Brown Hair",
      "compatibleSchemas": ["humanoid_v1", "fem_gen2a"],
      "equipmentSlotId": "hair_slot",
      "visuals": {
        "hair_back": {
          "default": {
            "resource": {
              "type": "static",
              "id": "hair_back_brown",
              "uri": "/assets/avatars/hair/long_brown_back.png"
            }
          }
        },
        "hair_front": {
          "default": {
            "resource": {
              "type": "static",
              "id": "hair_front_brown",
              "uri": "/assets/avatars/hair/long_brown_front.png"
            }
          }
        }
      },
      "metadata": {
        "color": "brown",
        "length": "long",
        "price": 50,
        "unlockLevel": 1,
        "tags": ["casual", "natural"]
      }
    },
    "item_premium_dress": {
      "id": "item_premium_dress",
      "name": "Evening Gown",
      "compatibleSchemas": ["fem_gen2a"],
      "equipmentSlotId": "outfit_slot",
      "visuals": {
        "clothing": {
          "default": {
            "resource": {
              "type": "static",
              "id": "dress_evening_default",
              "uri": "/assets/avatars/clothing/evening_gown.png"
            }
          }
        }
      },
      "metadata": {
        "category": "formal",
        "isPremium": true,
        "price": 500,
        "currency": "gems",
        "rarity": "legendary",
        "season": "all",
        "tags": ["formal", "elegant", "premium"]
      }
    }
  }
}
```

### LocalTransform Support (Optional)

Resources can include optional `localTransform` properties for memory optimization and special effects:

```json
{
  "items": {
    "item_hair_cropped": {
      "id": "item_hair_cropped",
      "name": "Cropped Hair (Memory Optimized)",
      "compatibleSchemas": ["humanoid_v1"],
      "equipmentSlotId": "hair_slot",
      "visuals": {
        "hair_f": {
          "default": {
            "resource": {
              "type": "static",
              "id": "hair_cropped_default",
              "uri": "/assets/avatars/hair/cropped_400x600.png",
              "localTransform": {
                "offset": { "x": 0, "y": -500 },
                "scale": 1.2,
                "alpha": 0.95
              }
            }
          }
        }
      }
    }
  }
}
```

**Benefits:**
- **50-70% memory savings** - Use cropped assets instead of full canvas (e.g., 400×600px vs 1062×1674px)
- **Fine-tuning** - Adjust positioning without re-exporting assets
- **Special effects** - Blend modes, alpha, rotation per layer

**LocalTransform Properties:**
- `offset?: { x: number, y: number }` - Position relative to container origin
- `scale?: number` - Scale multiplier (combines with `RenderOptions.scale`)
- `rotation?: number` - Rotation in radians
- `alpha?: number` - Opacity (0-1)
- `blendMode?: number` - Phaser blend mode for special effects

**When to use:**
- ✅ Cropped assets for memory optimization
- ✅ Layer-specific adjustments during development
- ✅ Glow effects, overlays with blend modes
- ❌ Not needed for full-canvas assets (all defaults to 0)

**Working with Avatar Canvas Size:**

If your layer schema defines an `avatarCanvasSize`, the renderer will automatically set the dimensions of the Phaser Container. You can use it as reference for offsets:

```json
// layer-schemas.json
{
  "nanny_affair_humanoid": {
    "avatarCanvasSize": {
      "width": 1062,
      "height": 1674
    }
  }
}
```

**Calculating offset for cropped assets:**

When you crop an asset from the reference canvas:
1. Reference canvas: 1062×1674px
2. Cropped to: 406×675px (using Photoshop's "Trim Transparent Pixels")
3. Calculate offset:

```typescript
// If hair was at top of canvas:
// - Top trimmed: ~127px
// - Bottom trimmed: ~872px
// - Left/Right centered: ~328px each side

offset = {
  x: 52,    // Fine-tune horizontal position
  y: -873   // Negative of bottom trim amount
}
```

**Why negative Y?**
- Container origin is at bottom (originY: 1.0)
- Offset.y moves the anchor point UP (negative) or DOWN (positive)
- For cropped assets, you typically move UP to compensate for removed bottom pixels

---

### Character Registry JSON (`character-registry.json`)

```json
{
  "player": {
    "id": "player",
    "displayName": "Player Character",
    "layerSchemaId": "humanoid_v1",
    "equipmentSlots": {
      "body_slot": "item_body_base",
      "face_slot": "item_face_expressive",
      "hair_slot": "item_hair_long_brown"
    },
    "closetOfItems": [
      "item_premium_dress",
      "item_hair_blonde_short",
      "item_hair_red_curly"
    ]
  },
  "npc_shopkeeper": {
    "id": "npc_shopkeeper",
    "displayName": "Shopkeeper",
    "layerSchemaId": "humanoid_v1",
    "equipmentSlots": {
      "body_slot": "item_body_base",
      "face_slot": "item_face_friendly",
      "hair_slot": "item_hair_gray_short",
      "outfit_slot": "item_apron_green"
    }
  }
}
```

### File Organization Example

```
your-game/
├── public/
│   └── assets/
│       └── avatars/
│           ├── layer-schemas.json       # Layer definitions
│           ├── item-catalog.json        # All items
│           ├── character-registry.json  # All characters
│           └── images/
│               ├── body/
│               │   ├── base_default.png
│               │   └── ...
│               ├── face/
│               │   ├── expressive_default.png
│               │   ├── expressive_happy.png
│               │   └── ...
│               ├── hair/
│               │   ├── long_brown_back.png
│               │   ├── long_brown_front.png
│               │   └── ...
│               └── clothing/
│                   └── ...
```

### Loading in Phaser

```typescript
import { loadUARRegistries } from '@series-inc/universal-avatar-renderer';

class GameScene extends Phaser.Scene {
  async preload() {
    // Load all JSON registries from base path
    const registries = await loadUARRegistries('/assets/avatars');

    // Initialize UAR
    this.avatarRenderer.initialize(
      registries.layerSchemas,
      registries.characterRegistry,
      registries.itemCatalog
    );

    // Preload character assets
    this.avatarRenderer.preload(['player']);
  }
}
```

**Note:** The `loadUARRegistries()` function expects:
- `{basePath}/layer-schemas.json`
- `{basePath}/character-registry.json`
- `{basePath}/item-catalog.json`

---

## 📚 API Reference

### Main Plugin: `AvatarRenderer`

#### Initialization

##### `initialize(layerSchemas, characters, itemCatalog)`

Initialize the plugin with registries.

```typescript
initialize(
  layerSchemaRegistry: LayerSchemaRegistry,
  characterRegistry: CharacterRegistry,
  itemCatalog: ItemCatalog
): void
```

**Parameters:**
- `layerSchemaRegistry` - Layer schemas defining body types
- `characterRegistry` - Character definitions
- `itemCatalog` - Items catalog with equipment

**Example:**
```typescript
this.avatarRenderer.initialize(layerSchemas, characters, itemCatalog);
```

---

#### Asset Loading

##### `preload(characterIds?, options?)`

Preload character assets before rendering.

```typescript
preload(
  characterIds?: string[],
  options?: PreloadOptions
): void
```

**Parameters:**
- `characterIds` - Array of character IDs to preload (optional, defaults to all)
- `options` - Preload options for selective variant loading

**Examples:**
```typescript
// Preload all characters
this.avatarRenderer.preload();

// Preload specific characters
this.avatarRenderer.preload(['player', 'npc1']);

// Selective variant preloading
this.avatarRenderer.preload(['player'], {
  preloadVariants: {
    expression: ['default', 'happy', 'sad'],
  },
});
```

---

#### Rendering

##### `render(characterId, options)`

Render a character at the specified position.

```typescript
render(
  characterId: string,
  options: RenderOptions
): Phaser.GameObjects.Container
```

**Parameters:**
- `characterId` - Character ID to render
- `options` - Render options (position, scale, origin, depth)

**Returns:** Phaser Container with all character layers

**Example:**
```typescript
const container = this.avatarRenderer.render('player', {
  x: 400,
  y: 600,
  scale: 1.5,
  originX: 0.5,  // 0-1 (0.5 = center)
  originY: 1.0,  // 0-1 (1.0 = bottom)
  depth: 10,     // Optional z-index
});
```

---

#### Visual Variant Updates

##### `updateVisualVariant(characterId, categoryId, value)`

Update a visual variant (hot-swap texture).

```typescript
updateVisualVariant(
  characterId: string,
  variantCategoryId: VariantCategoryId,
  variantValue: VisualVariantValue
): void
```

**Example:**
```typescript
// Change expression
this.avatarRenderer.updateVisualVariant('player', 'expression', 'happy');

// Change wetness
this.avatarRenderer.updateVisualVariant('player', 'wetness', 'wet');
```

##### `getVisualState(characterId)`

Get current visual state of a character.

```typescript
getVisualState(
  characterId: string
): Record<VariantCategoryId, VisualVariantValue> | null
```

**Example:**
```typescript
const state = this.avatarRenderer.getVisualState('player');
console.log(state?.expression); // 'happy'
```

##### `resetVisualState(characterId)`

Reset character to default visual state.

```typescript
resetVisualState(characterId: string): void
```

---

#### Equipment Management

##### `equipItem(characterId, slotId, itemId)`

Equip a new item at runtime (hot-swap equipment).

```typescript
equipItem(
  characterId: string,
  equipmentSlotId: EquipmentSlotId,
  itemId: ItemId
): void
```

**Example:**
```typescript
// Change character's hair
this.avatarRenderer.equipItem('player', 'hair_slot', 'item_hair_blonde');
```

---

#### Container Management

##### `getContainer(characterId)`

Get the Phaser Container for a character.

```typescript
getContainer(characterId: string): Phaser.GameObjects.Container | null
```

##### `isRendered(characterId)`

Check if a character is currently rendered.

```typescript
isRendered(characterId: string): boolean
```

##### `getRenderedCharacterIds()`

Get all currently rendered character IDs.

```typescript
getRenderedCharacterIds(): string[]
```

##### `destroyCharacter(characterId, keepState?)`

Destroy a specific character.

```typescript
destroyCharacter(characterId: string, keepState?: boolean): void
```

**Parameters:**
- `keepState` - If true, preserves visual state for re-rendering (default: false)

##### `destroyAllCharacters(keepStates?)`

Destroy all rendered characters.

```typescript
destroyAllCharacters(keepStates?: boolean): void
```

---

#### Lifecycle Hooks

##### `boot()`

Called automatically by Phaser when scene starts. Registers lifecycle event listeners.

##### `shutdown()`

Called automatically by Phaser when scene shuts down (can be restarted).

##### `destroy()`

Called automatically by Phaser when scene is permanently destroyed. Cleans up all resources.

---

## 📐 Type Definitions

### Core Types

```typescript
// Basic identifiers
type LayerId = string;
type VariantCategoryId = string;
type VisualVariantValue = string;
type LayerSchemaId = string;
type ItemId = string;
type EquipmentSlotId = string;

// Image resource type discriminator
enum ImageResourceType {
  Static = 'static',
  Atlas = 'atlas',
  Spritesheet = 'spritesheet',
  ImageSequence = 'imageSequence',
  Skeletal = 'skeletal',
}

// Local transform configuration (optional)
type LocalTransform = {
  offset?: { x: number; y: number };  // Position relative to container
  scale?: number;                     // Scale multiplier
  rotation?: number;                  // Rotation in radians
  alpha?: number;                     // Opacity (0-1)
  blendMode?: Phaser.BlendModes | number;  // Blend mode
  origin?: { x: number; y: number };  // Custom pivot point (0-1 range)
}

// Base image resource
type BaseImageResource = {
  id: string;
  type: ImageResourceType;
  localTransform?: LocalTransform;
}

// Static image resource (PNG/JPG)
type StaticImageResource = BaseImageResource & {
  type: ImageResourceType.Static;
  uri: string;
}

// Image resource (discriminated union)
type ImageResource = StaticImageResource;

// Equipment slot definition
type EquipmentSlotDefinition = {
  [key: VisualVariantValue]: ImageResource;
}
```

### Layer Schema

```typescript
type LayerSchemaDefinition = {
  id: LayerSchemaId;
  displayName: string;
  layers: LayerId[];  // Render order (index 0 = bottom)
  variantCategories: Record<VariantCategoryId, VariantCategoryDefinition>;
  avatarCanvasSize?: {  // Optional reference canvas size
    width: number;
    height: number;
  };
}

type VariantCategoryDefinition = {
  displayName: string;
  description?: string;
  values: VisualVariantValue[];
  defaultValue: VisualVariantValue;
  order?: number;
}

type LayerSchemaRegistry = Record<LayerSchemaId, LayerSchemaDefinition>;
```

### Items Catalog

```typescript
type ItemData = {
  id: ItemId;
  name: string;
  compatibleSchemas: LayerSchemaId[];
  equipmentSlotId: EquipmentSlotId;
  visuals: Record<LayerId, Record<VisualVariantValue, { resource: ImageResource }>>;
  metadata?: ItemMetadata;
}

type ItemCatalog = {
  items: Record<ItemId, ItemData>;
}
```

### Character Definition

```typescript
type CharacterDefinition = {
  id: string;
  displayName: string;
  layerSchemaId: LayerSchemaId;
  equipmentSlots: Record<EquipmentSlotId, ItemId>;  // Slot → Item ID
  closetOfItems?: ItemId[];  // Optional items for hot-swapping
}

type CharacterRegistry = Record<string, CharacterDefinition>;
```

### Render Options

```typescript
type RenderOptions = {
  x: number;
  y: number;
  scale?: number;
  originX?: number;  // 0-1 (default: 0.5)
  originY?: number;  // 0-1 (default: 1.0)
  depth?: number;    // Optional z-index
  debug?: boolean;   // Show orange bounds (default: false)
}

type PreloadOptions = {
  preloadVariants?: Record<VariantCategoryId, VisualVariantValue[]>;
}
```

---

## 🎯 Advanced Usage

### Loading Registries from JSON

```typescript
import { loadUARRegistries } from '@series-inc/universal-avatar-renderer';

async function preload(this: Phaser.Scene) {
  // Load all registries from JSON files
  const registries = await loadUARRegistries('/assets/uar');

  this.avatarRenderer.initialize(
    registries.layerSchemas,
    registries.characterRegistry,
    registries.itemCatalog
  );

  this.avatarRenderer.preload();
}
```

### Debug Visualization

Enable visual debugging to see the logical bounds of your avatar:

```typescript
// Render with debug bounds visible
const container = this.avatarRenderer.render('player', {
  x: 400,
  y: 600,
  debug: true  // Shows orange rectangle around avatar
});
```

**What you'll see:**
- Orange rectangle representing the `avatarCanvasSize` from the schema
- Helps verify that cropped assets with `localTransform` are positioned correctly
- Disappears automatically when the character is destroyed

**Use cases:**
- ✅ Validating `localTransform.offset` values
- ✅ Verifying that `avatarCanvasSize` matches your character dimensions
- ✅ Debugging hit area issues
- ✅ Fine-tuning cropped asset positioning

### Custom Origin (Pivot Point) per Layer

Override the pivot point for individual layers. Useful for rotating accessories:

```json
{
  "resource": {
    "type": "static",
    "id": "hat_item",
    "uri": "/assets/hat.png",
    "localTransform": {
      "offset": { "x": 0, "y": -800 },
      "rotation": 0.1,
      "origin": { "x": 0.5, "y": 0.8 }  // Rotate around bottom-center of hat
    }
  }
}
```

**Default behavior:**
- If `origin` is NOT specified, uses global `RenderOptions.originX/originY`
- Default global origin: `(0.5, 1.0)` (center-bottom, feet of character)

**With custom origin:**
- The layer rotates/scales around its own pivot point
- Perfect for: swinging earrings, tilting hats, spinning accessories

**Example use case:**
```typescript
// Hat that tilts when character moves
"localTransform": {
  "rotation": Math.sin(time) * 0.1,  // Pendulum motion
  "origin": { "x": 0.5, "y": 0.9 }   // Pivot near top attachment point
}
```

### Auto Hit Area

When `avatarCanvasSize` is defined, the renderer automatically configures the container for input detection:

```json
{
  "avatarCanvasSize": {
    "width": 1062,
    "height": 1674
  }
}
```

**What happens automatically:**
1. Container dimensions are set to these values
2. Interactive hit area is calculated based on the origin
3. The character responds to click/touch events within the logical bounds

**Benefits:**
- ✅ No manual `setInteractive()` needed
- ✅ Hit area accounts for `originY: 1.0` (feet at bottom)
- ✅ Works correctly even with cropped assets

**Example:**
```typescript
const container = this.avatarRenderer.render('player', { x: 400, y: 600 });

// Container is automatically interactive
container.on('pointerdown', () => {
  console.log('Player clicked!');
});
```

### State Change Events

```typescript
// Listen to visual state changes
const unsubscribe = this.avatarRenderer.onStateChange((event) => {
  console.log('State changed:', {
    characterId: event.characterId,
    categoryId: event.categoryId,
    oldState: event.oldState,
    newState: event.newState,
    timestamp: event.timestamp,
  });
});

// Unsubscribe when done
unsubscribe();
```

### Custom Variant Resolver

```typescript
import type { IVariantResolver } from '@series-inc/universal-avatar-renderer';

class CustomResolver implements IVariantResolver {
  resolve(layerId, visualState, slotDef, schema) {
    // Custom resolution logic
    // Example: prioritize certain variants
    if (visualState.expression === 'angry' && slotDef['angry']) {
      return 'angry';
    }
    return slotDef['default'] ? 'default' : Object.keys(slotDef)[0];
  }
}

// Set custom resolver
this.avatarRenderer.setVariantResolver(new CustomResolver());
```

### Item Metadata System

#### Overview

Item metadata is an **optional**, flexible property that allows developers to attach custom information to catalog items. It's completely **game-agnostic** and can store any data your application needs.

```typescript
type ItemMetadata = Record<string, MetadataValue>;

// MetadataValue supports:
// - Primitives: string, number, boolean
// - Arrays: string[], number[], boolean[]
// - Objects: Record<string, string>, Record<string, unknown>
```

#### Common Use Cases

**1. Economy & Shop Systems**
```typescript
{
  id: 'item_premium_dress',
  metadata: {
    price: 500,
    currency: 'gems',
    isPremium: true,
    discount: 20,  // percentage
  }
}
```

**2. Unlock & Progression Systems**
```typescript
{
  id: 'item_legendary_armor',
  metadata: {
    unlockLevel: 25,
    rarity: 'legendary',
    requiredAchievements: ['complete_story', 'defeat_boss'],
  }
}
```

**3. Categorization & Filtering**
```typescript
{
  id: 'item_summer_outfit',
  metadata: {
    category: 'outfit',
    season: 'summer',
    tags: ['casual', 'beach', 'colorful'],
    style: 'modern',
  }
}
```

**4. Preload Priority & Performance**
```typescript
{
  id: 'item_main_character_skin',
  metadata: {
    preloadPriority: 'high',  // 'high' | 'medium' | 'low'
    alwaysLoaded: true,
    memorySize: 2048,  // KB
  }
}
```

**5. Custom Game Logic**
```typescript
{
  id: 'item_magical_robe',
  metadata: {
    statsBonus: { defense: 10, magic: 15 },
    specialAbility: 'fire_resistance',
    durability: 100,
    canBeCrafted: true,
  }
}
```

#### Querying Items by Metadata

Use the `ItemCatalogResolver` to query items based on metadata:

```typescript
// Get catalog resolver
const catalogResolver = this.avatarRenderer.getCatalogResolver();

// Query 1: Find all premium items
const premiumItems = catalogResolver.queryItems(item =>
  item.metadata?.isPremium === true
);

// Query 2: Find affordable items under 100 coins
const affordableItems = catalogResolver.queryItems(item =>
  typeof item.metadata?.price === 'number' &&
  item.metadata.price <= 100
);

// Query 3: Find all summer outfits
const summerOutfits = catalogResolver.queryItems(item =>
  item.metadata?.season === 'summer' &&
  item.equipmentSlotId === 'outfit_slot'
);

// Query 4: Complex query - unlockable by current player
const currentLevel = 15;
const unlockedItems = catalogResolver.queryItems(item => {
  const unlockLevel = item.metadata?.unlockLevel;
  return !unlockLevel || unlockLevel <= currentLevel;
});

// Query 5: Find items by tags
const beachItems = catalogResolver.queryItems(item => {
  const tags = item.metadata?.tags;
  return Array.isArray(tags) && tags.includes('beach');
});
```

#### Best Practices

**DO:**
- ✅ Keep metadata **optional** - not all items need it
- ✅ Use consistent property names across your catalog
- ✅ Document your metadata schema in your game
- ✅ Use TypeScript types for your custom metadata:

```typescript
// Define your game's metadata schema
type MyGameItemMetadata = {
  price?: number;
  rarity?: 'common' | 'rare' | 'legendary';
  unlockLevel?: number;
  tags?: string[];
}

// Use it when defining items (type-safe!)
const item: ItemData = {
  id: 'my_item',
  // ... other properties
  metadata: {
    price: 100,
    rarity: 'rare',
  } satisfies MyGameItemMetadata
}
```

**DON'T:**
- ❌ Don't use metadata for rendering logic (use `visuals` instead)
- ❌ Don't store large binary data (images, audio)
- ❌ Don't rely on metadata for core avatar functionality

#### Getting All Items

```typescript
// Get all items for custom processing
const allItems = catalogResolver.getAllItems();

// Example: Group items by category
const itemsByCategory = allItems.reduce((acc, item) => {
  const category = item.metadata?.category || 'uncategorized';
  if (!acc[category]) acc[category] = [];
  acc[category].push(item);
  return acc;
}, {});
```

#### Accessing Specific Items

```typescript
// Get single item
const item = catalogResolver.getItem('item_hair_blonde');
console.log(item?.metadata?.price);  // Access metadata safely

// Get multiple items
const items = catalogResolver.getItems([
  'item_skin_1',
  'item_skin_2',
  'item_skin_3'
]);

// Filter by metadata
const affordableSkins = items.filter(item =>
  (item.metadata?.price || 0) <= 50
);
```

---

### Accessing Internal Managers

```typescript
// Get catalog resolver for item queries
const catalogResolver = this.avatarRenderer.getCatalogResolver();
const item = catalogResolver?.getItem('item_hair_blonde');

// Get character registry for read-only access
const registry = this.avatarRenderer.getCharacterRegistry();
if (registry) {
  const character = registry['player'];
  console.log('Character equipment:', character?.equipmentSlots);
}
```

#### Advanced: Internal Components (for framework authors)

UAR exports internal components for advanced use cases. These are typically not needed for normal usage:

```typescript
import {
  StateManager,
  ContainerManager,
  VariantIndexer,
  ItemCatalogResolver
} from '@series-inc/universal-avatar-renderer';

// These are low-level components used internally by AvatarRenderer
// Use only if building custom avatar systems or extending functionality
```

**Exported Components:**
- `StateManager` - Manages visual state per character with event emission
- `ContainerManager` - Manages Phaser container lifecycle
- `VariantIndexer` - Reverse index for finding layers affected by variant categories
- `ItemCatalogResolver` - Resolves item IDs to visual resources

**Note:** These components are used internally by `AvatarRenderer`. Most applications should use the high-level API methods instead.

---

### Resource Loaders (Advanced)

UAR uses a pluggable loader architecture (Strategy Pattern) for extensibility:

```typescript
import {
  type IResourceLoader,
  ResourceLoaderRegistry,
  StaticImageLoader,
  ImageResourceType
} from '@series-inc/universal-avatar-renderer';

// The built-in registry handles resource loading
// Currently supports:
// - StaticImageLoader (PNG/JPG files)
//
// Future loaders (can be implemented):
// - AtlasImageLoader (TexturePacker format)
// - SpritesheetImageLoader (Grid-based animations)
// - ImageSequenceLoader (Frame sequences)
```

**Architecture Benefits:**
- **Open/Closed Principle** - Add new resource types without modifying core
- **Single Responsibility** - Each loader handles one format
- **Testable** - Each loader can be tested in isolation

**Custom Loader Example (Advanced):**

For framework authors or applications with specialized resource formats:

```typescript
class CustomResourceLoader implements IResourceLoader {
  canLoad(resource: ImageResource): boolean {
    // Check if this loader handles the resource type
    return resource.type === 'custom';
  }

  load(
    loader: Phaser.Loader.LoaderPlugin,
    cacheKey: string,
    resource: ImageResource
  ): void {
    // Custom loading logic
    loader.image(cacheKey, resource.uri);
  }

  createGameObject(
    scene: Phaser.Scene,
    cacheKey: string,
    resource: ImageResource
  ): Phaser.GameObjects.GameObject {
    // Custom game object creation
    return scene.add.image(0, 0, cacheKey);
  }
}

// Register custom loader
const registry = new ResourceLoaderRegistry();
registry.registerLoader(new CustomResourceLoader());
```

**Note:** Most applications won't need custom loaders. This is for framework authors or applications with specialized needs.

---

### Closet System & Hot-Swapping

#### What is the Closet System?

The **closet** is an optional array of item IDs that are **preloaded but not equipped** initially. This enables instant hot-swapping without loading delays during gameplay.

#### When to Use Closets

**✅ Good Use Cases:**
- Character customization screens
- Dynamic outfit changes during gameplay
- Skin tone selection
- Seasonal outfit variants
- Progressive unlocks that need to be ready instantly

**❌ Not Needed For:**
- One-time equipped items
- Items loaded only once at game start
- NPCs with fixed appearance

#### Basic Closet Setup

```typescript
// Character definition with closet
const characters = {
  player: {
    id: 'player',
    layerSchemaId: 'humanoid_v1',
    equipmentSlots: {
      body_slot: 'item_body_pale',        // Initially equipped
      outfit_slot: 'item_casual_shirt',   // Initially equipped
    },
    closetOfItems: [
      // Skin tones (not equipped, but ready to swap)
      'item_body_tan',
      'item_body_dark',

      // Outfits (not equipped, but ready to swap)
      'item_formal_suit',
      'item_party_dress',
      'item_sports_gear',
    ]
  }
};
```

#### Preloading Behavior

```typescript
// In preload phase:
this.avatarRenderer.preload(['player']);

// What happens:
// 1. ✅ Loads item_body_pale (equipped)
// 2. ✅ Loads item_casual_shirt (equipped)
// 3. ✅ Loads item_body_tan (closet)
// 4. ✅ Loads item_body_dark (closet)
// 5. ✅ Loads item_formal_suit (closet)
// 6. ✅ Loads item_party_dress (closet)
// 7. ✅ Loads item_sports_gear (closet)
//
// All items are in Phaser's texture cache and ready for instant use
```

#### Hot-Swapping from Closet

```typescript
class CustomizationScene extends Phaser.Scene {
  create() {
    // Render character with initial equipment
    this.avatarRenderer.render('player', { x: 400, y: 600 });

    // Hot-swap skin tone (instant - already loaded)
    this.createButton('Tan Skin', () => {
      this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
      // ⚡ Instant - no loading delay
    });

    // Hot-swap outfit (instant - already loaded)
    this.createButton('Formal Suit', () => {
      this.avatarRenderer.equipItem('player', 'outfit_slot', 'item_formal_suit');
      // ⚡ Instant - no loading delay
    });
  }
}
```

#### Dynamic vs Static Loading

```typescript
// ❌ WITHOUT CLOSET (causes loading delay)
const characterNoCloset = {
  id: 'player',
  equipmentSlots: {
    body_slot: 'item_body_pale',
  }
  // No closet - other items not preloaded
};

// During gameplay:
this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
// ⚠️ May cause brief delay if texture not loaded

// ✅ WITH CLOSET (instant swap)
const characterWithCloset = {
  id: 'player',
  equipmentSlots: {
    body_slot: 'item_body_pale',
  },
  closetOfItems: ['item_body_tan']  // Preloaded
};

// During gameplay:
this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
// ⚡ Instant - already in cache
```

#### Memory Considerations

The closet preloads textures into memory. Consider these factors:

```typescript
// Small closet (recommended for mobile)
closetOfItems: [
  'skin_variant_1',
  'skin_variant_2',
  'skin_variant_3'
]  // ~2-4MB depending on image sizes

// Large closet (desktop/high-end devices)
closetOfItems: [
  // 10 skin variants
  ...skinVariants,
  // 20 outfit variants
  ...outfitVariants,
  // 15 hair variants
  ...hairVariants
]  // ~20-40MB depending on image sizes
```

**Best Practice:** Use closet for items likely to be needed soon, not all possible items.

#### Selective Variant Preloading

Combine closet with selective variant preloading for fine-grained control:

```typescript
// Preload only specific variants of closet items
this.avatarRenderer.preload(['player'], {
  preloadVariants: {
    expression: ['default', 'happy'],  // Only 2 expressions
    // Other variants not loaded
  }
});

// This applies to both equipped items AND closet items
```

#### Advanced Pattern: Lazy Closet

For very large item sets, you can implement a "lazy closet" pattern:

```typescript
class CustomizationScene extends Phaser.Scene {
  private currentCategory = 'outfits';

  loadCategoryItems(category: 'outfits' | 'hair' | 'accessories') {
    // Dynamically update character's closet
    const itemsToLoad = this.getCategoryItems(category);

    // Load new category items
    this.load.image(itemsToLoad.map(item => /* ... */));
    this.load.once('complete', () => {
      // Items now available for hot-swapping
      this.currentCategory = category;
    });
    this.load.start();
  }

  getCategoryItems(category: string): string[] {
    // Return item IDs for category
    // This is application-specific logic
    return [];
  }
}
```

#### Checking Item Availability

```typescript
// Check if item exists in catalog
const catalogResolver = this.avatarRenderer.getCatalogResolver();
const item = catalogResolver?.getItem('item_body_tan');

if (!item) {
  console.error('Item not in catalog');
  return;
}

// Check if texture is loaded (from closet or currently equipped)
const textureKey = `${item.id}_${variantValue}`;
const isLoaded = this.textures.exists(textureKey);

if (isLoaded) {
  // Safe to equip
  this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
} else {
  // Need to load first
  console.warn('Item not in closet - will load on demand');
}
```

---

### Multi-Scene Management

#### Plugin Instance per Scene

The `AvatarRenderer` is a **Scene Plugin**, meaning each Phaser scene gets its own instance:

```typescript
class Scene1 extends Phaser.Scene {
  create() {
    // This is Scene1's instance
    this.avatarRenderer.render('player', { x: 100, y: 200 });
  }
}

class Scene2 extends Phaser.Scene {
  create() {
    // This is Scene2's instance (separate from Scene1)
    this.avatarRenderer.render('player', { x: 300, y: 400 });
  }
}
```

**Key Points:**
- ✅ Each scene has independent renderer state
- ✅ Registries need to be initialized per scene (or use global loading)
- ✅ Visual states are scene-specific
- ✅ Textures are shared across scenes (Phaser's global texture cache)

#### Initialization Strategies

**Strategy 1: Initialize per Scene (Simple)**

```typescript
class GameScene extends Phaser.Scene {
  async preload() {
    // Load registries
    const registries = await loadUARRegistries('/assets/avatars');

    // Initialize THIS scene's renderer
    this.avatarRenderer.initialize(
      registries.layerSchemas,
      registries.characterRegistry,
      registries.itemCatalog
    );

    this.avatarRenderer.preload(['player']);
  }
}
```

**Strategy 2: Global Initialization (Efficient)**

```typescript
// Global store for registries
class RegistryStore {
  static schemas: LayerSchemaRegistry | null = null;
  static characters: CharacterRegistry | null = null;
  static catalog: ItemCatalog | null = null;

  static async loadOnce(basePath: string) {
    if (!this.schemas) {
      const registries = await loadUARRegistries(basePath);
      this.schemas = registries.layerSchemas;
      this.characters = registries.characterRegistry;
      this.catalog = registries.itemCatalog;
    }
  }
}

// In boot/preloader scene
class BootScene extends Phaser.Scene {
  async create() {
    await RegistryStore.loadOnce('/assets/avatars');
    this.scene.start('GameScene');
  }
}

// In any other scene
class GameScene extends Phaser.Scene {
  preload() {
    // Reuse loaded registries
    this.avatarRenderer.initialize(
      RegistryStore.schemas!,
      RegistryStore.characters!,
      RegistryStore.catalog!
    );

    this.avatarRenderer.preload(['player']);
  }
}
```

#### Scene Transitions with State Preservation

**Preserving Visual State:**

```typescript
class Scene1 extends Phaser.Scene {
  shutdown() {
    // Destroy containers but KEEP visual states
    this.avatarRenderer.destroyAllCharacters(true);  // keepStates = true
  }
}

class Scene2 extends Phaser.Scene {
  create() {
    // Initialize renderer
    this.avatarRenderer.initialize(schemas, characters, catalog);
    this.avatarRenderer.preload(['player']);

    // Render character - visual state is restored!
    this.avatarRenderer.render('player', { x: 400, y: 600 });
    // Character appears with same expression/equipment as Scene1
  }
}
```

**Fresh State (Reset):**

```typescript
class Scene1 extends Phaser.Scene {
  shutdown() {
    // Destroy everything including states
    this.avatarRenderer.destroyAllCharacters(false);  // keepStates = false
  }
}

class Scene2 extends Phaser.Scene {
  create() {
    // Character will render with default state
    this.avatarRenderer.render('player', { x: 400, y: 600 });
    // Fresh state - default expression, original equipment
  }
}
```

#### Lifecycle Hooks

The plugin automatically handles Phaser's lifecycle:

```typescript
// ✅ Automatically called by Phaser
boot()     // When scene starts - registers listeners
shutdown() // When scene pauses/stops - can be restarted
destroy()  // When scene is permanently destroyed - cleans up everything

// You typically don't call these manually
// But you can if needed:
this.avatarRenderer.destroy();
```

#### Memory Management Across Scenes

**Important:** Phaser's texture cache is **global** across all scenes.

```typescript
// Scene 1 loads textures
class Scene1 extends Phaser.Scene {
  preload() {
    this.avatarRenderer.initialize(schemas, characters, catalog);
    this.avatarRenderer.preload(['player']);  // Loads textures
  }
}

// Scene 2 uses same textures (no re-download)
class Scene2 extends Phaser.Scene {
  preload() {
    this.avatarRenderer.initialize(schemas, characters, catalog);
    this.avatarRenderer.preload(['player']);  // ✅ Uses cached textures
  }
}

// Explicitly clear textures (rarely needed)
class Scene3 extends Phaser.Scene {
  shutdown() {
    // Remove specific character textures from global cache
    const characterData = this.avatarRenderer.getCharacterData('player');
    // Custom logic to remove textures if needed
    // Usually NOT necessary - Phaser manages memory well
  }
}
```

#### Parallel Scene Rendering

Multiple scenes can render different characters simultaneously:

```typescript
// Background scene (parallax, environment)
class BackgroundScene extends Phaser.Scene {
  create() {
    this.avatarRenderer.initialize(schemas, characters, catalog);
    this.avatarRenderer.render('npc_background', {
      x: 200,
      y: 400,
      depth: 1  // Behind UI
    });
  }
}

// UI scene (HUD, menus)
class UIScene extends Phaser.Scene {
  create() {
    this.avatarRenderer.initialize(schemas, characters, catalog);
    this.avatarRenderer.render('player_portrait', {
      x: 50,
      y: 50,
      scale: 0.3,
      depth: 100  // In front
    });
  }
}

// Both scenes can run in parallel
this.scene.run('BackgroundScene');
this.scene.run('UIScene');
```

#### Best Practices

```typescript
// ✅ DO: Initialize in preload()
class GoodScene extends Phaser.Scene {
  preload() {
    this.avatarRenderer.initialize(/* ... */);
    this.avatarRenderer.preload(['player']);
  }

  create() {
    this.avatarRenderer.render('player', { x: 400, y: 600 });
  }
}

// ❌ DON'T: Initialize in create()
class BadScene extends Phaser.Scene {
  create() {
    this.avatarRenderer.initialize(/* ... */);  // Too late
    this.avatarRenderer.preload(['player']);    // Async load in create!
    this.avatarRenderer.render('player', { x: 400, y: 600 });  // May fail
  }
}

// ✅ DO: Clean up on shutdown if preserving state
shutdown() {
  this.avatarRenderer.destroyAllCharacters(true);
}

// ✅ DO: Use global registry loading for efficiency
// (See Strategy 2 above)
```

---

## ⚡ Performance Considerations

Understanding how UAR works internally helps you write efficient code. This section explains architectural decisions and their performance implications.

> **Note:** This section focuses on design and architecture, not specific benchmarks. Performance characteristics may vary based on your game, assets, and target platform.

---

### Rendering Architecture

#### How Characters are Rendered

Each rendered character consists of:
- **1 Container** (`Phaser.GameObjects.Container`) - Holds all layers
- **N Images** (`Phaser.GameObjects.Image`) - One per layer in schema
- **Managed by ContainerManager** - Lifecycle and positioning

```typescript
// Example: Character with 7 layers
layerSchema: {
  layers: ['body', 'face', 'hair_b', 'clothing', 'hair_f', 'props_b', 'props_f']
}

// Results in:
// 1 Container containing 7 Image objects
```

#### Layer Count Impact

- Each layer = 1 Phaser Image object in the scene
- More layers = more objects to manage
- Layers are ordered by schema definition (index 0 = bottom, rendered first)

**Implication:**
```typescript
// Efficient: 5-7 layers (typical humanoid)
layers: ['body', 'face', 'hair_back', 'outfit', 'hair_front']

// Still efficient: 10-12 layers (detailed character)
layers: ['body', 'face', 'hair_b', 'outfit', 'hair_f', 'props_b', 'props_f', ...]

// Consider optimization: 20+ layers
// May want to combine some layers if they never change independently
```

---

### Hot-Swapping Mechanisms

#### Visual Variant Updates (`updateVisualVariant`)

**How it works:**
1. Queries `VariantIndexer` to find affected layers
2. Uses `VariantResolver` to determine new texture key
3. Changes texture on existing `Image` object
4. **No object creation/destruction**

```typescript
// FAST: Just swaps texture on existing Image
this.avatarRenderer.updateVisualVariant('player', 'expression', 'happy');
// Internally: imageObject.setTexture(newTextureKey)
```

**Performance characteristics:**
- ✅ Very fast (texture swap only)
- ✅ No garbage collection
- ✅ No container recreation
- ✅ Preferred for frequent changes (expressions, states)

#### Equipment Changes (`equipItem`)

**How it works:**
1. Validates item exists in catalog and is compatible with character schema
2. Updates character's `equipmentSlots` in registry (replaces item ID)
3. Rebuilds `EquipmentSlots` via `ItemCatalogResolver`
4. Calls `rerenderCharacter()` which iterates through layers
5. For each layer, swaps texture on existing `Image` object (if texture changed)
6. **Does NOT destroy/recreate container or images**

```typescript
// Changes equipment and updates visuals
this.avatarRenderer.equipItem('player', 'outfit_slot', 'item_dress_blue');
// Internally: Updates registry + swaps textures on existing sprites
```

**Performance characteristics:**
- ✅ Reuses existing container and Image objects
- ✅ Only changes textures that are different
- ✅ No object creation/destruction
- ⚠️ Iterates through all layers (vs targeted layer in updateVisualVariant)
- ✅ Use when changing equipped items (different visual resources)

#### Best Practice

```typescript
// ✅ CORRECT: Use variants for visual state changes
// Targets specific layers affected by variant category
this.avatarRenderer.updateVisualVariant('player', 'expression', 'happy');
this.avatarRenderer.updateVisualVariant('player', 'wetness', 'wet');

// ✅ CORRECT: Use equipItem for equipment changes
// Changes item reference, iterates all layers
this.avatarRenderer.equipItem('player', 'hair_slot', 'item_hair_blonde');
this.avatarRenderer.equipItem('player', 'outfit_slot', 'item_dress_red');

// ❌ INCORRECT: Creating separate items for each expression
// Wasteful - use visual variants instead
{
  id: 'item_face_happy',
  id: 'item_face_sad',
  id: 'item_face_angry'
}
// Should be ONE item with expression variants
```

---

### Closet System Design

#### What the Closet Does

The `closetOfItems` array specifies items to **preload but not equip initially**:

```typescript
{
  id: 'player',
  equipmentSlots: {
    body_slot: 'item_body_pale'  // ← Equipped AND preloaded
  },
  closetOfItems: [
    'item_body_tan',   // ← NOT equipped, but preloaded
    'item_body_dark'   // ← NOT equipped, but preloaded
  ]
}
```

**During `preload()`:**
1. Loads equipped items' textures
2. Loads closet items' textures
3. All textures stored in Phaser's global texture cache

**During `equipItem()`:**
- If item was in closet → texture already cached → instant
- If item wasn't in closet → may need to load texture → delay

#### Closet vs No Closet

```typescript
// WITHOUT CLOSET
{
  equipmentSlots: { body_slot: 'item_body_pale' }
  // Only item_body_pale loaded
}
// Later:
this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
// ⚠️ May need to load texture if not already cached

// WITH CLOSET
{
  equipmentSlots: { body_slot: 'item_body_pale' },
  closetOfItems: ['item_body_tan', 'item_body_dark']
  // All three loaded during preload
}
// Later:
this.avatarRenderer.equipItem('player', 'body_slot', 'item_body_tan');
// ✅ Instant - texture already in cache
```

#### When to Use Closet

**✅ Use closet for:**
- Items you'll `equipItem()` during gameplay
- Character customization options
- Skin tone variants
- Multiple outfit options
- Progressive unlocks that need instant swapping

**❌ Don't use closet for:**
- Items never used
- Items only needed in other scenes
- One-time equipment (won't change)

#### Closet Size Consideration

```typescript
// Small closet: A few alternatives
closetOfItems: ['skin_variant_1', 'skin_variant_2']
// Loads: ~2-3 additional items

// Medium closet: Customization options
closetOfItems: [...10 items]
// Loads: ~10 additional items

// Large closet: Full customization
closetOfItems: [...50 items]
// Loads: Many textures - ensure they're all actually used
```

**Recommendation:** Only add items to closet if you'll actually use `equipItem()` with them during gameplay.

---

### State Management

#### How State is Tracked

`StateManager` maintains visual state per character:

```typescript
// Internal state structure:
{
  player: {
    expression: 'happy',
    wetness: 'default',
    outfit_state: 'clean'
  }
}
```

#### State Persistence

```typescript
// Destroy but keep state
this.avatarRenderer.destroyCharacter('player', true);  // keepState = true
// State preserved: { expression: 'happy', wetness: 'default' }

// Later, re-render
this.avatarRenderer.render('player', { x: 400, y: 600 });
// ✅ Renders with preserved state (happy expression)

// Destroy and clear state
this.avatarRenderer.destroyCharacter('player', false);  // keepState = false
// State cleared: character will render with default state next time
```

#### Avoiding Redundant Updates

```typescript
// ❌ BAD: Always updates (even if already in that state)
setExpression(expr: string) {
  this.avatarRenderer.updateVisualVariant('player', 'expression', expr);
}

// ✅ GOOD: Check before updating
setExpression(expr: string) {
  const currentState = this.avatarRenderer.getVisualState('player');
  if (currentState?.expression !== expr) {
    this.avatarRenderer.updateVisualVariant('player', 'expression', expr);
  }
}
```

---

### Preload System

#### What `preload()` Does

```typescript
this.avatarRenderer.preload(['player']);

// Internally:
// 1. Get character definition from registry
// 2. Resolve equipped items via ItemCatalogResolver
// 3. Get all visual resources from items
// 4. Load closetOfItems resources
// 5. Call Phaser's load.image() for each texture
// 6. Optionally filter by PreloadOptions
```

#### Selective Variant Preloading

Built-in feature to load only specific variants:

```typescript
// Load ALL variants (default behavior)
this.avatarRenderer.preload(['player']);
// If face has 10 expressions, loads all 10

// Load ONLY specific variants
this.avatarRenderer.preload(['player'], {
  preloadVariants: {
    expression: ['default', 'happy', 'sad']
  }
});
// Only loads 3 expressions, not all 10
```

**Impact:**
- ✅ Fewer textures loaded = faster preload phase
- ✅ Less memory used
- ✅ Can still change to loaded variants at runtime
- ⚠️ Non-loaded variants won't render (will use 'default' or fail)

#### Preload Strategies

```typescript
// Strategy 1: Load everything (simple, memory-heavy)
this.avatarRenderer.preload(['player']);

// Strategy 2: Load common variants only (balanced)
this.avatarRenderer.preload(['player'], {
  preloadVariants: {
    expression: ['default', 'happy', 'sad', 'angry'],  // 4 common ones
    wetness: ['default']  // Skip 'wet' state
  }
});

// Strategy 3: Minimal preload (load on-demand later)
this.avatarRenderer.preload(['player'], {
  preloadVariants: {
    expression: ['default']  // Just default
  }
});
// Later, load additional variants as needed
```

---

### Lifecycle & Cleanup

#### Destruction Behavior

**`destroyCharacter(id, keepState)`:**
```typescript
// keepState = false (default)
this.avatarRenderer.destroyCharacter('player', false);
// - Destroys Container
// - Destroys all Image objects
// - Clears visual state
// - Character can be re-rendered with fresh state

// keepState = true
this.avatarRenderer.destroyCharacter('player', true);
// - Destroys Container
// - Destroys all Image objects
// - PRESERVES visual state in StateManager
// - Re-rendering restores state
```

**`destroyAllCharacters(keepStates)`:**
```typescript
// Same logic, applied to all rendered characters
this.avatarRenderer.destroyAllCharacters(true);
```

#### Scene Lifecycle Integration

UAR automatically hooks into Phaser's scene lifecycle:

```typescript
// Automatically called by Phaser
boot() {
  // When scene starts
  // Registers event listeners
}

shutdown() {
  // When scene stops/pauses (can restart)
  // Keeps plugin instance alive
  // You should manually destroy characters here
}

destroy() {
  // When scene is permanently destroyed
  // Full cleanup, plugin instance disposed
}
```

**Best practice:**

```typescript
class GameScene extends Phaser.Scene {
  shutdown() {
    // Clean up when scene stops
    this.avatarRenderer.destroyAllCharacters(true);  // Keep state for restart
  }

  // Don't override destroy() unless you have specific cleanup
  // UAR's destroy() will be called automatically
}
```

---

### Multi-Scene Architecture

#### Plugin Instances

AvatarRenderer is a **Scene Plugin** - key characteristics:

- Each scene gets its own `AvatarRenderer` instance
- Instances are independent (separate state, containers)
- Registries must be initialized per instance (or shared globally)

```typescript
class Scene1 extends Phaser.Scene {
  create() {
    // This is Scene1's AvatarRenderer instance
    this.avatarRenderer.render('player', { x: 100, y: 200 });
  }
}

class Scene2 extends Phaser.Scene {
  create() {
    // This is Scene2's AvatarRenderer instance (different from Scene1)
    this.avatarRenderer.render('player', { x: 300, y: 400 });
  }
}
```

#### Texture Cache is Global

Important: Phaser's texture cache is shared across ALL scenes:

```typescript
// Scene 1
this.avatarRenderer.preload(['player']);  // Loads textures into cache

// Scene 2 (later)
this.avatarRenderer.preload(['player']);  // Uses cached textures, no re-download
```

**Implication:**
- ✅ Textures loaded once, available everywhere
- ✅ No need to reload in each scene
- ✅ Memory efficient
- ⚠️ Texture cleanup only happens when explicitly removed

#### Efficient Multi-Scene Pattern

```typescript
// Global registry store
class RegistryStore {
  static schemas: LayerSchemaRegistry | null = null;
  static characters: CharacterRegistry | null = null;
  static catalog: ItemCatalog | null = null;

  static async loadOnce(basePath: string) {
    if (!this.schemas) {
      const registries = await loadUARRegistries(basePath);
      this.schemas = registries.layerSchemas;
      this.characters = registries.characterRegistry;
      this.catalog = registries.itemCatalog;
    }
  }
}

// Boot scene - load once
class BootScene extends Phaser.Scene {
  async create() {
    await RegistryStore.loadOnce('/assets/avatars');
    this.scene.start('GameScene');
  }
}

// Any game scene - reuse registries
class GameScene extends Phaser.Scene {
  preload() {
    // Reuse loaded registries (no re-parsing JSON)
    this.avatarRenderer.initialize(
      RegistryStore.schemas!,
      RegistryStore.characters!,
      RegistryStore.catalog!
    );

    // Textures already in cache if loaded by previous scene
    this.avatarRenderer.preload(['player']);
  }
}
```

---

### Variant Resolution

#### How `VariantResolver` Works

When rendering or updating variants:

1. Gets current visual state: `{ expression: 'happy', wetness: 'wet' }`
2. Gets available variants from `EquipmentSlotDefinition`
3. Iterates variant categories in priority order (from schema)
4. Returns first matching variant key
5. Falls back to `'default'` if no match

```typescript
// Example resolution:
// State: { expression: 'happy', wetness: 'wet' }
// Available variants: { default: {...}, happy: {...}, wet: {...} }
// Schema order: ['expression', 'wetness']  (expression checked first)
// Result: 'happy' (found before checking wetness)
```

#### Custom Resolution Strategy

You can provide custom variant resolution logic:

```typescript
import type { IVariantResolver } from '@series-inc/universal-avatar-renderer';

class PriorityResolver implements IVariantResolver {
  resolve(layerId, visualState, slotDef, schema) {
    // Custom logic: Always prefer wetness over expression
    if (visualState.wetness === 'wet' && slotDef['wet']) {
      return 'wet';
    }

    if (visualState.expression && slotDef[visualState.expression]) {
      return visualState.expression;
    }

    return 'default';
  }
}

// Set custom resolver
this.avatarRenderer.setVariantResolver(new PriorityResolver());
```

**Use cases:**
- Custom priority orders
- Complex fallback logic
- Game-specific variant rules

---

### Anti-Patterns to Avoid

#### ❌ Anti-Pattern 1: Re-rendering Every Frame

```typescript
// BAD: Creates/destroys containers every frame
update() {
  this.avatarRenderer.destroyCharacter('player');
  this.avatarRenderer.render('player', { x: 400, y: 600 });
  // Massive garbage collection, very slow
}

// GOOD: Render once, update variants
create() {
  this.avatarRenderer.render('player', { x: 400, y: 600 });
}

update() {
  if (shouldChangeExpression) {
    this.avatarRenderer.updateVisualVariant('player', 'expression', 'happy');
  }
}
```

---

#### ❌ Anti-Pattern 2: Using `equipItem()` for Visual States

```typescript
// BAD: Creates separate items for each expression state
const catalog = {
  items: {
    item_face_happy: { /* ... */ },
    item_face_sad: { /* ... */ },
    item_face_angry: { /* ... */ }
  }
};

changeExpression(expr: string) {
  // Iterates all layers, swaps multiple textures
  this.avatarRenderer.equipItem('player', 'face_slot', `item_face_${expr}`);
}

// GOOD: Use visual variant system
const catalog = {
  items: {
    item_face_default: {
      visuals: {
        face: {
          default: { /* ... */ },
          happy: { /* ... */ },
          sad: { /* ... */ },
          angry: { /* ... */ }
        }
      }
    }
  }
};

changeExpression(expr: string) {
  // Only swaps textures on affected layers (via VariantIndexer)
  this.avatarRenderer.updateVisualVariant('player', 'expression', expr);
}
```

**Why it matters:**
- `updateVisualVariant()` uses `VariantIndexer` to find affected layers only
- `equipItem()` iterates through ALL layers in schema
- Visual variants are the intended way to handle state changes

---

#### ❌ Anti-Pattern 3: Not Using Closet for Hot-Swapping

```typescript
// BAD: Items not in closet
{
  equipmentSlots: { outfit_slot: 'item_casual' },
  closetOfItems: []  // Empty closet
}

// During gameplay:
this.avatarRenderer.equipItem('player', 'outfit_slot', 'item_formal');
// ⚠️ May need to load texture, causing delay

// GOOD: Preload via closet
{
  equipmentSlots: { outfit_slot: 'item_casual' },
  closetOfItems: ['item_formal', 'item_party']  // Preloaded
}

// During gameplay:
this.avatarRenderer.equipItem('player', 'outfit_slot', 'item_formal');
// ✅ Instant - texture already loaded
```

---

#### ❌ Anti-Pattern 4: Initialize in `create()` Instead of `preload()`

```typescript
// BAD: Async operations in create()
create() {
  this.avatarRenderer.initialize(schemas, characters, catalog);
  this.avatarRenderer.preload(['player']);  // Async!
  this.avatarRenderer.render('player', { x: 400, y: 600 });  // May fail - not loaded yet
}

// GOOD: Use Phaser's lifecycle correctly
preload() {
  this.avatarRenderer.initialize(schemas, characters, catalog);
  this.avatarRenderer.preload(['player']);  // Loads during preload phase
}

create() {
  this.avatarRenderer.render('player', { x: 400, y: 600 });  // Textures ready
}
```

---

#### ❌ Anti-Pattern 5: Bloated Closet with Unused Items

```typescript
// BAD: Loads many items never used
{
  closetOfItems: [
    'item1', 'item2', 'item3', ... 'item100'  // 100 items!
  ]
}
// Most items never actually equipped during gameplay

// GOOD: Only preload what you'll use
{
  closetOfItems: [
    'item_skin_variant_1',
    'item_skin_variant_2',
    'item_hair_alternate'
  ]
}
// Only items actually used in this scene/mode
```

**Rule of thumb:** If you're not calling `equipItem()` with it, don't put it in closet.

---

#### ❌ Anti-Pattern 6: Not Cleaning Up on Scene Shutdown

```typescript
// BAD: Memory leak across scene transitions
class GameScene extends Phaser.Scene {
  shutdown() {
    // No cleanup!
  }
}
// Containers still in memory

// GOOD: Clean up properly
class GameScene extends Phaser.Scene {
  shutdown() {
    // De