> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# NPC Data Structure

> How NPCs and mobs are defined in Hyperscape

# NPC Data Structure

NPCs (Non-Player Characters) and mobs are defined in **JSON manifests** and loaded at runtime. This data-driven approach allows content to be modified without code changes.

<Info>
  NPC data is managed in `packages/shared/src/data/npcs.ts` and loaded from `world/assets/manifests/npcs.json`.
</Info>

## Data Loading

NPCs are **NOT hardcoded**. The `ALL_NPCS` map is populated at runtime:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From npcs.ts
export const ALL_NPCS: Map<string, NPCData> = new Map();

// Populated by DataManager from JSON
DataManager.loadNPCs(); // Reads world/assets/manifests/npcs.json
```

***

## NPC Data Schema

Each NPC has the following structure:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface NPCData {
  id: string;                    // Unique identifier (e.g., "goblin_warrior")
  name: string;                  // Display name (e.g., "Goblin Warrior")
  category: NPCCategory;         // "mob" | "boss" | "neutral" | "quest"
  modelPath: string;             // Path to GLB model

  stats: {
    level: number;               // Combat level (1-126)
    attack: number;              // Attack level
    strength: number;            // Strength level
    defense: number;             // Defense level
    health: number;              // Max HP
    ranged?: number;             // Ranged level (for ranged mobs)
    magic?: number;              // Magic level (for magic mobs)
  };

  combat: {
    attackable: boolean;         // Can be attacked by players
    aggressive: boolean;         // Initiates combat
    retaliates: boolean;         // Fights back when attacked
    aggroRange: number;          // Detection range in tiles
    combatRange: number;         // Attack range in tiles
    leashRange: number;          // Max chase distance from spawn
    attackSpeedTicks: number;    // Ticks between attacks
    respawnTime: number;         // Respawn delay in ms
    xpReward: number;            // XP granted on kill
    poisonous: boolean;          // Can poison players
    immuneToPoison: boolean;     // Immune to poison damage
    attackType?: "melee" | "ranged" | "magic";  // Attack type (default: "melee")
    spellId?: string;            // Spell ID for magic mobs (e.g., "wind_strike")
    arrowId?: string;            // Arrow ID for ranged mobs (e.g., "bronze_arrow")
  };

  appearance: {
    modelPath: string;           // Path to GLB/VRM model
    iconPath?: string;           // Minimap icon
    scale: number;               // Model scale multiplier
    tint?: string;               // Hex color tint
    heldWeaponModel?: string;    // Weapon GLB to attach to hand (e.g., "asset://weapons/bow.glb")
  };

  aggression: {
    type: AggressionType;        // Aggro behavior
    maxLevel?: number;           // For level_gated type
  };

  spawnBiomes: string[];         // Where NPC can spawn
  respawnTime?: number;          // Respawn delay in seconds

  drops: DropTable;              // Loot drops
}
```

***

## NPC Categories

| Category  | Description        | Example              |
| --------- | ------------------ | -------------------- |
| `mob`     | Hostile enemy      | Goblin, Bandit       |
| `boss`    | Powerful enemy     | Giant Spider, Dragon |
| `neutral` | Non-combat NPC     | Shopkeeper, Banker   |
| `quest`   | Quest giver/target | Quest NPC, Guard     |

***

## Aggression Types

NPCs have different aggression behaviors:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
type AggressionType =
  | "passive"           // Never attacks first
  | "aggressive"        // Attacks players below double its level
  | "always_aggressive" // Attacks all players
  | "level_gated";      // Only attacks below specific level
```

### Aggro Rules

| Type                | Behavior                                       |
| ------------------- | ---------------------------------------------- |
| `passive`           | Never initiates combat                         |
| `aggressive`        | Attacks if player level \< 2 × NPC level       |
| `always_aggressive` | Attacks all players regardless of level        |
| `level_gated`       | Attacks if player level ≤ `maxLevel` threshold |

***

## Drop Tables

Each NPC has a `DropTable` defining loot:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface DropTable {
  defaultDrop: {
    enabled: boolean;
    itemId: string;
    quantity: number;
  };
  always: Drop[];      // 100% drop rate
  common: Drop[];      // High chance
  uncommon: Drop[];    // Medium chance
  rare: Drop[];        // Low chance
  veryRare: Drop[];    // Very low chance
}

interface Drop {
  itemId: string;
  minQuantity: number;
  maxQuantity: number;
  chance: number;      // 0.0 to 1.0
}
```

### Drop Calculation

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From npcs.ts
export function calculateNPCDrops(npcId: string): Array<{ itemId: string; quantity: number }> {
  const npc = getNPCById(npcId);
  if (!npc) return [];

  const drops: Array<{ itemId: string; quantity: number }> = [];

  // Default drop (always if enabled)
  if (npc.drops.defaultDrop.enabled) {
    drops.push({
      itemId: npc.drops.defaultDrop.itemId,
      quantity: npc.drops.defaultDrop.quantity,
    });
  }

  // Roll for each tier
  const processDrop = (drop: Drop) => {
    if (Math.random() < drop.chance) {
      const quantity = Math.floor(
        Math.random() * (drop.maxQuantity - drop.minQuantity + 1) + drop.minQuantity
      );
      drops.push({ itemId: drop.itemId, quantity });
    }
  };

  npc.drops.always.forEach(processDrop);
  npc.drops.common.forEach(processDrop);
  npc.drops.uncommon.forEach(processDrop);
  npc.drops.rare.forEach(processDrop);
  npc.drops.veryRare.forEach(processDrop);

  return drops;
}
```

***

## Available 3D Models

NPCs use rigged GLB models from `/assets/world/forge/`:

| Model Path                 | Used For                     |
| -------------------------- | ---------------------------- |
| `goblin/goblin_rigged.glb` | Goblins                      |
| `thug/thug_rigged.glb`     | Bandits, thugs               |
| `human/human_rigged.glb`   | Guards, knights, shopkeepers |
| `troll/troll_rigged.glb`   | Hobgoblins                   |
| `imp/imp_rigged.glb`       | Dark warriors                |

***

## Helper Functions

### Get NPC by ID

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export function getNPCById(npcId: string): NPCData | null {
  return ALL_NPCS.get(npcId) || null;
}
```

### Get NPCs by Category

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export function getNPCsByCategory(category: NPCCategory): NPCData[] {
  return Array.from(ALL_NPCS.values()).filter(
    (npc) => npc.category === category
  );
}
```

### Get NPCs by Biome

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export function getNPCsByBiome(biome: string): NPCData[] {
  return Array.from(ALL_NPCS.values()).filter((npc) =>
    npc.spawnBiomes?.includes(biome)
  );
}
```

### Get NPCs by Level Range

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export function getNPCsByLevelRange(minLevel: number, maxLevel: number): NPCData[] {
  return Array.from(ALL_NPCS.values()).filter(
    (npc) => npc.stats.level >= minLevel && npc.stats.level <= maxLevel
  );
}
```

### Check if NPC Can Drop Item

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export function canNPCDropItem(npcId: string, itemId: string): boolean {
  const npc = getNPCById(npcId);
  if (!npc) return false;

  // Check default drop
  if (npc.drops.defaultDrop.enabled && npc.drops.defaultDrop.itemId === itemId) {
    return true;
  }

  // Check all drop tiers
  const allDrops = [
    ...npc.drops.always,
    ...npc.drops.common,
    ...npc.drops.uncommon,
    ...npc.drops.rare,
    ...npc.drops.veryRare,
  ];

  return allDrops.some((drop) => drop.itemId === itemId);
}
```

***

## Combat Level Calculation

NPC combat level is calculated from stats:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From npcs.ts
export function calculateNPCCombatLevel(stats: NPCStats): number {
  const base = 0.25 * (stats.defense + stats.health + 1);
  const melee = 0.325 * (stats.attack + stats.strength);
  const ranged = 0.325 * Math.floor((stats.ranged || 1) * 1.5);

  return Math.floor(base + Math.max(melee, ranged));
}
```

***

## Spawn Constants

Global spawn settings:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export const NPC_SPAWN_CONSTANTS = {
  DEFAULT_RESPAWN_TIME: 30,    // 30 seconds
  BOSS_RESPAWN_TIME: 300,      // 5 minutes
  MAX_NPCS_PER_ZONE: 50,
  AGGRO_CHECK_INTERVAL: 600,   // Every tick (600ms)
};
```

***

## Example NPC Definitions

### Melee Mob (Default)

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "id": "goblin_warrior",
  "name": "Goblin Warrior",
  "category": "mob",
  "stats": {
    "level": 5,
    "attack": 5,
    "strength": 5,
    "defense": 5,
    "health": 20
  },
  "combat": {
    "attackable": true,
    "aggressive": true,
    "retaliates": true,
    "aggroRange": 4,
    "combatRange": 1,
    "leashRange": 10,
    "attackSpeedTicks": 4,
    "respawnTime": 30000,
    "xpReward": 20,
    "poisonous": false,
    "immuneToPoison": false
  },
  "appearance": {
    "modelPath": "goblin/goblin_rigged.glb",
    "scale": 1.0
  },
  "aggression": {
    "type": "aggressive"
  },
  "spawnBiomes": ["forest", "plains"],
  "drops": {
    "defaultDrop": {
      "enabled": true,
      "itemId": "bones",
      "quantity": 1
    },
    "common": [
      { "itemId": "bronze_dagger", "minQuantity": 1, "maxQuantity": 1, "chance": 0.25 }
    ]
  }
}
```

### Magic Mob

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "id": "dark_wizard",
  "name": "Dark Wizard",
  "category": "mob",
  "stats": {
    "level": 20,
    "attack": 1,
    "strength": 1,
    "defense": 10,
    "health": 40,
    "magic": 25
  },
  "combat": {
    "attackable": true,
    "aggressive": true,
    "retaliates": true,
    "aggroRange": 5,
    "combatRange": 10,
    "leashRange": 12,
    "attackSpeedTicks": 5,
    "respawnTime": 45000,
    "xpReward": 50,
    "poisonous": false,
    "immuneToPoison": false,
    "attackType": "magic",
    "spellId": "fire_strike"
  },
  "appearance": {
    "modelPath": "wizard/wizard_rigged.glb",
    "scale": 1.0,
    "heldWeaponModel": "asset://weapons/staff.glb"
  },
  "drops": {
    "defaultDrop": {
      "enabled": true,
      "itemId": "bones",
      "quantity": 1
    },
    "common": [
      { "itemId": "fire_rune", "minQuantity": 5, "maxQuantity": 15, "chance": 0.4 },
      { "itemId": "air_rune", "minQuantity": 10, "maxQuantity": 20, "chance": 0.4 }
    ]
  }
}
```

<Info>
  **Magic Mobs**: The `magic` stat determines damage output. The `spellId` must reference a valid spell from the spell manifest. Mobs have infinite runes and don't consume resources when casting.
</Info>

### Ranged Mob

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "id": "dark_ranger",
  "name": "Dark Ranger",
  "category": "mob",
  "stats": {
    "level": 15,
    "attack": 1,
    "strength": 1,
    "defense": 8,
    "health": 35,
    "ranged": 20
  },
  "combat": {
    "attackable": true,
    "aggressive": true,
    "retaliates": true,
    "aggroRange": 5,
    "combatRange": 7,
    "leashRange": 10,
    "attackSpeedTicks": 4,
    "respawnTime": 40000,
    "xpReward": 40,
    "poisonous": false,
    "immuneToPoison": false,
    "attackType": "ranged",
    "arrowId": "bronze_arrow"
  },
  "appearance": {
    "modelPath": "ranger/ranger_rigged.glb",
    "scale": 1.0,
    "heldWeaponModel": "asset://weapons/shortbow.glb"
  },
  "drops": {
    "defaultDrop": {
      "enabled": true,
      "itemId": "bones",
      "quantity": 1
    },
    "common": [
      { "itemId": "bronze_arrow", "minQuantity": 5, "maxQuantity": 15, "chance": 0.5 }
    ]
  }
}
```

<Info>
  **Ranged Mobs**: The `ranged` stat determines damage output. The `arrowId` must reference a valid arrow from the ammunition manifest. Mobs have infinite arrows and don't consume ammunition when firing.
</Info>

***

## Combat Type Configuration

### Attack Type Fields

**For Melee Mobs (default):**

* No special fields required
* Uses `attack` and `strength` stats
* `combatRange` defaults to 1 tile
* Plays `COMBAT` or `SWORD_SWING` animation
* Immediate hit (0 tick delay)

**For Ranged Mobs:**

* `attackType`: `"ranged"` (required)
* `arrowId`: Required (e.g., `"bronze_arrow"`, `"iron_arrow"`, `"steel_arrow"`)
* `ranged`: Required stat for damage calculation
* `combatRange`: Typically 7-10 tiles
* `attackSpeedTicks`: Typically 4 ticks (2.4 seconds)
* `heldWeaponModel`: Optional bow GLB for visuals (e.g., `"asset://weapons/shortbow.glb"`)
* Plays `RANGE` animation
* Arrow launch delay: 400ms
* Hit delay: `1 + floor((3 + distance) / 6)` ticks

**For Magic Mobs:**

* `attackType`: `"magic"` (required)
* `spellId`: Required (e.g., `"wind_strike"`, `"fire_bolt"`, `"water_strike"`)
* `magic`: Required stat for damage calculation
* `combatRange`: Typically 10 tiles
* `attackSpeedTicks`: Typically 5 ticks (3.0 seconds)
* `heldWeaponModel`: Optional staff GLB for visuals (e.g., `"asset://weapons/staff.glb"`)
* Plays `SPELL_CAST` animation
* Spell launch delay: 600ms
* Hit delay: `1 + floor((1 + distance) / 3)` ticks

<Warning>
  **Missing Configuration**: If a mob has `attackType: "magic"` but no `spellId`, or `attackType: "ranged"` but no `arrowId`, the attack will be skipped with a console warning. Always configure both the attack type and the corresponding resource ID.
</Warning>

### Held Weapon Models

The `heldWeaponModel` field attaches a 3D weapon to the mob's hand bone:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "appearance": {
    "heldWeaponModel": "asset://weapons/shortbow.glb"
  }
}
```

**Supported Formats:**

* Uses Asset Forge attachment metadata (same as player equipment)
* Supports V1 (direct attachment) and V2 (pre-baked matrix) formats
* Weapons are cached and shared across mobs of the same type
* Automatically attaches to VRM `rightHand` bone (or custom bone from metadata)

**Weapon Cache System:**

* Static `_weaponCache` in `MobVisualManager` shares loaded GLB scenes
* `_pendingLoads` map deduplicates concurrent fetches for the same URL
* First mob to load a weapon caches the scene, subsequent mobs clone from cache
* Eliminates duplicate network requests when multiple mobs of the same type spawn
* Cache cleared on world teardown via `MobNPCSpawnerSystem.destroy()`
* Weapons use `clone(true)` to share geometry/material buffers (GPU efficient)

**Available Weapon Models:**

* `asset://weapons/shortbow.glb` - Shortbow (for ranged mobs)
* `asset://weapons/staff.glb` - Magic staff (for magic mobs)
* `asset://weapons/sword.glb` - Sword (for melee mobs)
* Custom weapons from Asset Forge

**Attachment Metadata:**
Weapons exported from Asset Forge include attachment metadata:

* `vrmBoneName`: Target bone name (default: `"rightHand"`)
* `version`: Metadata format version (1 or 2)
* `relativeMatrix`: Pre-baked 4×4 transform matrix (V2 format only)

**Cleanup:**
Weapons are properly cleaned up when mobs are destroyed:

* `removeFromParent()` detaches weapon from bone
* Geometry and materials are NOT disposed (shared across mob instances)
* JavaScript garbage collection handles the rest

## Adding New NPCs

<Steps>
  <Step title="Add to JSON Manifest">
    Add entry to `world/assets/manifests/npcs.json`
  </Step>

  <Step title="Choose or Create Model">
    Use existing model or generate new one in 3D Asset Forge
  </Step>

  <Step title="Configure Combat Type">
    Set `attackType`, `spellId`/`arrowId`, and `heldWeaponModel` if using ranged/magic
  </Step>

  <Step title="Restart Server">
    Server must restart to reload manifests
  </Step>
</Steps>

<Warning>
  **DO NOT** add NPC data directly to `npcs.ts`. Keep all content in JSON manifests for data-driven design.
</Warning>

***

## Related Documentation

* [Item Data Structure](/wiki/data/items)
* [Combat System](/wiki/game-systems/combat)
* [World Areas](/wiki/data/world-areas)
