> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update-1771983334664.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={null}
// 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={null}
interface NPCData {
  id: string;                    // Unique identifier (e.g., "goblin")
  name: string;                  // Display name (e.g., "Goblin")
  description: string;           // NPC description
  category: NPCCategory;         // "mob" | "boss" | "neutral" | "quest"
  faction?: string;              // Faction (e.g., "monster", "town")
  levelRange?: [number, number]; // Min/max level range for mobs

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

  combat: {
    attackable: boolean;         // Can be attacked
    aggressive?: boolean;        // Attacks players
    retaliates?: boolean;        // Fights back when attacked
    aggroRange?: number;         // Aggro detection range
    combatRange?: number;        // Attack range
    leashRange?: number;         // Max distance from spawn
    attackSpeedTicks?: number;   // Attack speed
    respawnTicks?: number;       // Respawn delay in ticks
  };

  movement: {
    type: MovementType;          // "stationary" | "wander" | "patrol"
    speed: number;               // Movement speed
    wanderRadius: number;        // Wander distance from spawn
  };

  services?: {
    enabled: boolean;            // Provides services
    types: string[];             // Service types (e.g., "bank", "shop")
  };

  dialogue?: {
    entryNodeId: string;         // Starting dialogue node
    nodes: DialogueNode[];       // Dialogue tree
  };

  appearance: {
    modelPath: string;           // Path to VRM/GLB model
    iconPath?: string;           // Icon path
    scale: number;               // Model scale
  };

  spawnBiomes?: string[];        // Where NPC can spawn
  drops?: DropTable;             // Loot drops (for mobs)
}
```

***

## NPC Categories

| Category  | Description        | Example                       |
| --------- | ------------------ | ----------------------------- |
| `mob`     | Hostile enemy      | Goblin, Bandit, Dark Wizard   |
| `boss`    | Powerful enemy     | Giant Spider, Dragon          |
| `neutral` | Non-combat NPC     | Shopkeeper, Bank Clerk        |
| `quest`   | Quest giver/target | Captain Rowan, Forester Wilma |

***

## Movement Types

NPCs have different movement patterns:

```typescript theme={null}
type MovementType =
  | "stationary"  // Does not move
  | "wander"      // Random movement within radius
  | "patrol";     // Follows predefined path
```

| Type         | Behavior                                                  |
| ------------ | --------------------------------------------------------- |
| `stationary` | NPC stays in one location (e.g., shopkeepers, bankers)    |
| `wander`     | NPC randomly moves within `wanderRadius` from spawn point |
| `patrol`     | NPC follows a predefined patrol route                     |

***

## Combat System

### Aggression Behavior

Mobs use the `combat.aggressive` flag to determine if they attack players:

* **Aggressive Mobs**: Automatically attack nearby players within `aggroRange`
* **Passive Mobs**: Only attack when provoked
* **Level-Based Aggro**: Some mobs use `levelRange` to determine valid targets

### Combat Properties

| Property           | Description                                         |
| ------------------ | --------------------------------------------------- |
| `attackable`       | Whether players can attack this NPC                 |
| `aggressive`       | Whether NPC attacks players on sight                |
| `retaliates`       | Whether NPC fights back when attacked               |
| `aggroRange`       | Detection radius for aggressive behavior            |
| `combatRange`      | Attack range (1 for melee, higher for ranged/magic) |
| `leashRange`       | Max distance from spawn before resetting            |
| `attackSpeedTicks` | Ticks between attacks (4 ticks = 2.4 seconds)       |
| `respawnTicks`     | Ticks until respawn after death                     |

***

## Services System

NPCs can provide services to players:

```typescript theme={null}
interface Services {
  enabled: boolean;
  types: string[];  // "bank", "shop", "quest", etc.
}
```

### Service Types

| Type    | Description          | Example NPC                   |
| ------- | -------------------- | ----------------------------- |
| `bank`  | Opens bank interface | Bank Clerk                    |
| `shop`  | Opens shop interface | Shopkeeper, Dommik            |
| `quest` | Quest giver          | Captain Rowan, Forester Wilma |

***

## Dialogue System

NPCs can have multi-node dialogue trees:

```typescript theme={null}
interface Dialogue {
  entryNodeId: string;
  nodes: DialogueNode[];
}

interface DialogueNode {
  id: string;
  text: string;
  responses?: DialogueResponse[];
}

interface DialogueResponse {
  text: string;
  nextNodeId?: string;
  effect?: string;  // "openBank", "openStore", etc.
}
```

### Example Dialogue Flow

```json theme={null}
{
  "entryNodeId": "greeting",
  "nodes": [
    {
      "id": "greeting",
      "text": "Welcome to the bank! How may I help you today?",
      "responses": [
        {
          "text": "I'd like to access my bank.",
          "nextNodeId": "open_bank",
          "effect": "openBank"
        },
        {
          "text": "Goodbye.",
          "nextNodeId": "farewell"
        }
      ]
    },
    {
      "id": "open_bank",
      "text": "Of course! Here are your belongings."
    },
    {
      "id": "farewell",
      "text": "Take care, adventurer!"
    }
  ]
}
```

***

## Drop Tables

Each NPC has a `DropTable` defining loot:

```typescript theme={null}
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={null}
// 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 VRM models from the assets repository:

### Mob Models

| Model Path                                        | Used For     |
| ------------------------------------------------- | ------------ |
| `asset://models/mobs/goblin/goblin.vrm`           | Goblins      |
| `asset://models/mobs/bandit/bandit.vrm`           | Bandits      |
| `asset://models/mobs/barbarian/barbarian.vrm`     | Barbarians   |
| `asset://models/mobs/dark-ranger/dark-ranger.vrm` | Dark Rangers |
| `asset://models/mobs/dark-wizard/dark-wizard.vrm` | Dark Wizards |
| `asset://models/mobs/gaurd/gaurd.vrm`             | Guards       |

### NPC Models

| Model Path                                              | Used For                             |
| ------------------------------------------------------- | ------------------------------------ |
| `asset://models/npcs/captain-rowan/captain-rowan.vrm`   | Captain Rowan (quest giver)          |
| `asset://models/npcs/forester-wilma/forester-wilma.vrm` | Forester Wilma (woodcutting trainer) |
| `asset://models/npcs/fisherman-pete/fisherman-pete.vrm` | Fisherman Pete (fishing trainer)     |
| `asset://models/npcs/torvin/torvin.vrm`                 | Torvin (smithing trainer)            |
| `asset://models/npcs/banker/banker.vrm`                 | Bank Clerk                           |
| `asset://models/npcs/shopkeeper/shopkeeper.vrm`         | Shopkeeper                           |
| `asset://models/npcs/dommik/Dommik.vrm`                 | Dommik (crafting supplier)           |
| `asset://models/npcs/horvik/Horvik.vrm`                 | Horvik (armor shop)                  |
| `asset://models/npcs/Lowe/Lowe.vrm`                     | Lowe (bowyer)                        |
| `asset://models/npcs/Zamorin/Zamorin.vrm`               | Wizard Zamorin (magic trainer)       |
| `asset://models/npcs/tanner-ellis/tanner-ellis.vrm`     | Tanner Ellis (leather goods)         |
| `asset://avatars/avatar-male-01.vrm`                    | Generic male NPCs                    |
| `asset://avatars/avatar-female-01.vrm`                  | Generic female NPCs                  |

***

## Helper Functions

### Get NPC by ID

```typescript theme={null}
export function getNPCById(npcId: string): NPCData | null {
  return ALL_NPCS.get(npcId) || null;
}
```

### Get NPCs by Category

```typescript theme={null}
export function getNPCsByCategory(category: NPCCategory): NPCData[] {
  return Array.from(ALL_NPCS.values()).filter(
    (npc) => npc.category === category
  );
}
```

### Get NPCs by Biome

```typescript theme={null}
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={null}
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={null}
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={null}
// 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={null}
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

### Hostile Mob (Goblin)

```json theme={null}
{
  "id": "goblin",
  "name": "Goblin",
  "description": "A weak goblin creature, perfect for beginners",
  "category": "mob",
  "faction": "monster",
  "levelRange": [1, 3],
  "stats": {
    "level": 2,
    "health": 5,
    "attack": 1,
    "strength": 1,
    "defense": 1,
    "defenseBonus": 0,
    "ranged": 1,
    "magic": 1
  },
  "combat": {
    "attackable": true,
    "aggressive": true,
    "retaliates": true,
    "aggroRange": 4,
    "combatRange": 1,
    "leashRange": 7,
    "attackSpeedTicks": 4,
    "respawnTicks": 35
  },
  "movement": {
    "type": "wander",
    "speed": 3.33,
    "wanderRadius": 5
  },
  "drops": {
    "defaultDrop": {
      "enabled": true,
      "itemId": "bones",
      "quantity": 1
    },
    "common": [
      {
        "itemId": "coins",
        "minQuantity": 5,
        "maxQuantity": 15,
        "chance": 1.0,
        "rarity": "common"
      }
    ],
    "uncommon": [
      {
        "itemId": "bronze_sword",
        "minQuantity": 1,
        "maxQuantity": 1,
        "chance": 0.1,
        "rarity": "uncommon"
      }
    ]
  },
  "appearance": {
    "modelPath": "asset://models/goblin/goblin.vrm",
    "iconPath": "asset://icons/npcs/goblin.png",
    "scale": 0.75
  },
  "spawnBiomes": ["forest", "plains"]
}
```

### Service NPC (Bank Clerk)

```json theme={null}
{
  "id": "bank_clerk",
  "name": "Bank Clerk",
  "description": "A helpful bank clerk who manages deposits and withdrawals",
  "category": "neutral",
  "faction": "town",
  "combat": {
    "attackable": false
  },
  "movement": {
    "type": "stationary",
    "speed": 0,
    "wanderRadius": 0
  },
  "services": {
    "enabled": true,
    "types": ["bank"]
  },
  "dialogue": {
    "entryNodeId": "greeting",
    "nodes": [
      {
        "id": "greeting",
        "text": "Welcome to the bank! How may I help you today?",
        "responses": [
          {
            "text": "I'd like to access my bank.",
            "nextNodeId": "open_bank",
            "effect": "openBank"
          },
          {
            "text": "Goodbye.",
            "nextNodeId": "farewell"
          }
        ]
      },
      {
        "id": "open_bank",
        "text": "Of course! Here are your belongings."
      },
      {
        "id": "farewell",
        "text": "Take care, adventurer!"
      }
    ]
  },
  "appearance": {
    "modelPath": "asset://avatars/avatar-male-01.vrm",
    "iconPath": "asset://icons/npcs/shopkeeper.png",
    "scale": 1.0
  }
}
```

***

## 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="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)
