Skip to main content

Item Data Structure

Items are defined in JSON manifests and loaded at runtime. Items are now organized into separate files by type for better maintainability.
Item data is managed in packages/shared/src/data/items.ts and loaded from world/assets/manifests/items/ directory.

Data Loading

Items are NOT hardcoded. The ITEMS map is populated at runtime from multiple manifest files:
// From items.ts
export const ITEMS: Map<string, Item> = new Map();

// Populated by DataManager from JSON files
DataManager.loadItems(); // Reads all files in world/assets/manifests/items/

Manifest Organization

Items are split into separate files by category:
manifests/items/
├── weapons.json      # Combat weapons (swords, axes, etc.)
├── tools.json        # Skilling tools (hatchets, pickaxes, fishing rods)
├── resources.json    # Gathered materials (ores, logs, bars, raw fish)
├── food.json         # Cooked consumables
└── misc.json         # Currency, burnt food, junk items

Directory-Based Loading

Items are organized by category in packages/server/world/assets/manifests/items/:
  • weapons.json - Swords, axes, bows, etc.
  • tools.json - Hatchets, pickaxes, fishing rods, hammer, tinderbox
  • resources.json - Ores, bars, logs, raw fish
  • food.json - Cooked food, raw food, burnt food
  • misc.json - Coins, junk items, quest items
Atomic Loading: All 5 category files must exist or the system falls back to legacy items.json. Duplicate Detection: The loader validates that no item ID appears in multiple category files.

Item Schema

Each item has the following structure:
interface Item {
  id: string;                    // Unique identifier (e.g., "bronze_sword")
  name: string;                  // Display name (e.g., "Bronze Sword")
  type: ItemType;                // Category
  tier?: string;                 // Equipment tier (bronze, steel, mithril, etc.)
  rarity: ItemRarity;            // Common, Uncommon, Rare, etc.
  modelPath?: string;            // Path to GLB model
  tier?: string;                 // Metal tier (bronze, iron, steel, etc.)

  // Inventory properties
  stackable: boolean;            // Can stack in inventory
  tradeable: boolean;            // Can be traded/sold
  value: number;                 // Base value in coins

  // Equipment properties (for weapons/armor)
  requirements?: ItemRequirement;
  bonuses?: ItemStats;           // Renamed from stats
  attackType?: AttackType;
  weaponType?: WeaponType;
  equipSlot?: EquipmentSlotName; // Where item equips
  attackSpeed?: number;          // In milliseconds
  range?: number;                // Attack range
  equipSlot?: string;            // Equipment slot (weapon, shield, head, body, etc.)

  // Tool properties (for gathering tools)
  tool?: {
    skill: "woodcutting" | "mining" | "fishing";
    priority: number;            // Higher = better tool
    rollTicks?: number;          // Mining: ticks between roll attempts
  };

  // OSRS-accurate inventory actions
  inventoryActions?: string[];   // Context menu actions (e.g., ["Eat", "Use", "Drop"])

  // Tool properties (for gathering tools)
  tool?: {
    skill: "woodcutting" | "mining" | "fishing";
    priority: number;            // Higher priority tools used first
    rollTicks?: number;          // Ticks between gather attempts
  };

  // Processing properties (embedded recipe data)
  cooking?: CookingRecipeData;
  firemaking?: FiremakingRecipeData;
  smelting?: SmeltingRecipeData;
  smithing?: SmithingRecipeData;

  // Noted item support
  isNoted?: boolean;
  baseItemId?: string;           // For noted items
  notedItemId?: string;          // For base items

  // Processing properties
  cooking?: CookingData;         // For raw food items
  firemaking?: FiremakingData;   // For logs
  smelting?: SmeltingData;       // For bars
  smithing?: SmithingData;       // For smithable items
  
  // Ground item display overrides
  modelScale?: number;           // Scale for 3D model when displayed as ground item (default: 0.3)
  groundOffset?: number;         // Ground item Y positioning (see below)
}

Tier-Based Requirements

Items with a tier property automatically derive level requirements from tier-requirements.json:
{
  "id": "steel_sword",
  "name": "Steel Sword",
  "type": "weapon",
  "tier": "steel",
  "equipSlot": "weapon",
  "attackType": "MELEE"
  // requirements auto-derived: { attack: 5 }
}
The TierDataProvider maps tiers to skill requirements, eliminating redundant requirement definitions.

Item Types

type ItemType =
  | "weapon"      // Swords, axes, bows → weapons.json
  | "armor"       // Helmets, bodies, legs → (future)
  | "tool"        // Hatchets, pickaxes, fishing rods → tools.json
  | "consumable"  // Food, potions → food.json
  | "resource"    // Logs, ore, fish, bars → resources.json
  | "currency"    // Coins → misc.json
  | "junk"        // Burnt food → misc.json
  | "misc";       // Other items → misc.json

Tier-Based Equipment System

Items now use a centralized tier system defined in tier-requirements.json. Instead of hardcoding level requirements in each item, equipment references a tier:
{
  "id": "bronze_sword",
  "name": "Bronze Sword",
  "type": "weapon",
  "tier": "bronze",  // References tier-requirements.json
  // ... other properties
}
The system automatically looks up requirements from tier-requirements.json:

Melee Tiers

TierAttackDefence
bronze/iron11
steel55
black1010
mithril2020
adamant3030
rune4040
dragon6060

Tool Tiers

TierAttackWoodcuttingMining
bronze/iron111
steel566
mithril202121
adamant303131
rune404141
dragon606161
This centralized approach ensures OSRS-accurate requirements and makes it easy to add new tiers without modifying individual items.

Item Rarity

type ItemRarity =
  | "common"
  | "uncommon"
  | "rare"
  | "epic"
  | "legendary";

Equipment Stats

For weapons and armor:
interface ItemStats {
  attack?: number;       // Attack bonus
  strength?: number;     // Strength bonus (max hit)
  defense?: number;      // Defense bonus
  ranged?: number;       // Ranged attack bonus
  rangedStrength?: number; // Ranged damage bonus
  prayer?: number;       // Prayer bonus
}

Skill Requirements

Items can require skill levels to use:
interface ItemRequirement {
  attack?: number;
  strength?: number;
  defense?: number;
  ranged?: number;
  woodcutting?: number;
  mining?: number;
  fishing?: number;
}

Weapon Types

type WeaponType =
  | "sword"
  | "scimitar"
  | "longsword"
  | "dagger"
  | "axe"
  | "mace"
  | "warhammer"
  | "2h"
  | "bow"
  | "crossbow"
  | "staff";

type AttackType = "melee" | "ranged" | "magic";

Attack Speed

Weapons have different attack speeds:
Weapon TypeSpeed (ms)Speed (ticks)
Dagger18003
Scimitar18003
Sword24004
Longsword24004
Battleaxe30005
2H Sword36006
Shortbow18003
Longbow30005

Helper Functions

Item Type Detection

The item-helpers.ts module provides utilities for detecting item types:
// From item-helpers.ts
export function isFood(item: Item | null): boolean {
  if (!item) return false;
  return (
    item.type === "consumable" &&
    typeof item.healAmount === "number" &&
    item.healAmount > 0 &&
    !item.id.includes("potion")
  );
}

export function isPotion(item: Item | null): boolean {
  if (!item) return false;
  return item.type === "consumable" && item.id.includes("potion");
}

export function isBone(item: Item | null): boolean {
  if (!item) return false;
  return item.id === "bones" || item.id.endsWith("_bones");
}

export function isWeapon(item: Item | null): boolean {
  if (!item) return false;
  return (
    item.equipSlot === "weapon" ||
    item.equipSlot === "2h" ||
    item.is2h === true ||
    item.weaponType != null
  );
}

export function isShield(item: Item | null): boolean {
  if (!item) return false;
  return item.equipSlot === "shield";
}

export function usesWield(item: Item | null): boolean {
  return isWeapon(item) || isShield(item);
}

export function usesWear(item: Item | null): boolean {
  if (!item) return false;
  if (!item.equipable && !item.equipSlot) return false;
  return !usesWield(item);
}

export function isNotedItem(item: Item | null): boolean {
  if (!item) return false;
  return item.isNoted === true || item.id.endsWith("_noted");
}

Primary Action Detection

// Get primary action for left-click
export function getPrimaryAction(
  item: Item | null,
  isNoted: boolean,
): PrimaryActionType {
  if (isNoted) return "use";

  // Check manifest first
  const manifestAction = getPrimaryActionFromManifest(item);
  if (manifestAction) return manifestAction;

  // Fallback to heuristic detection
  if (isFood(item)) return "eat";
  if (isPotion(item)) return "drink";
  if (isBone(item)) return "bury";
  if (usesWield(item)) return "wield";
  if (usesWear(item)) return "wear";

  return "use";
}

Get Item by ID

export function getItem(itemId: string): Item | null {
  return ITEMS.get(itemId) || null;
}

Get Items by Type

export function getItemsByType(type: ItemType): Item[] {
  return Array.from(ITEMS.values()).filter((item) => item.type === type);
}

// Convenience functions
export function getWeapons(): Item[] {
  return getItemsByType("weapon");
}

export function getArmor(): Item[] {
  return getItemsByType("armor");
}

export function getTools(): Item[] {
  return getItemsByType("tool");
}

export function getConsumables(): Item[] {
  return getItemsByType("consumable");
}

export function getResources(): Item[] {
  return getItemsByType("resource");
}

Get Items by Skill Requirement

export function getItemsBySkill(skill: string): Item[] {
  return Array.from(ITEMS.values()).filter(
    (item) => item.requirements && item.requirements[skill as keyof ItemRequirement]
  );
}

Get Items by Level Requirement

export function getItemsByLevel(level: number): Item[] {
  return Array.from(ITEMS.values()).filter((item) => {
    if (!item.requirements) return true;
    return Object.values(item.requirements).every((req) =>
      typeof req === "number" ? req <= level : true
    );
  });
}

Shop Items

Items available in general stores:
export const SHOP_ITEMS = [
  "bronze_hatchet",
  "fishing_rod",
  "tinderbox",
  "arrows",
];

Noted Items

Items can have “noted” variants for efficient storage:
// Check if item can be noted
export function canBeNoted(itemId: string): boolean {
  const item = ITEMS.get(itemId);
  return item?.stackable === false && item?.notedItemId !== undefined;
}

// Get base item from noted item
export function getBaseItem(itemId: string): Item | null {
  const item = ITEMS.get(itemId);
  if (!item) return null;

  if (item.isNoted && item.baseItemId) {
    return ITEMS.get(item.baseItemId) || null;
  }
  return item;
}

// Get noted variant of item
export function getNotedItem(itemId: string): Item | null {
  const item = ITEMS.get(itemId);
  if (!item || item.isNoted) return null;

  if (item.notedItemId) {
    return ITEMS.get(item.notedItemId) || null;
  }
  return null;
}

// Check if item ID is a noted variant
export function isNotedItemId(itemId: string): boolean {
  return itemId.endsWith("_noted");
}

// Get base item ID from noted ID
export function getBaseItemId(itemId: string): string {
  if (isNotedItemId(itemId)) {
    return itemId.replace(/_noted$/, "");
  }
  return itemId;
}

Example Item Definitions

Weapon (weapons.json)

{
  "id": "bronze_sword",
  "name": "Bronze Sword",
  "type": "weapon",
  "tier": "bronze",
  "value": 100,
  "weight": 2,
  "equipSlot": "weapon",
  "weaponType": "SWORD",
  "attackType": "MELEE",
  "attackSpeed": 4,
  "attackRange": 1,
  "description": "A basic sword made of bronze",
  "examine": "A basic sword made of bronze",
  "tradeable": true,
  "rarity": "common",
  "modelPath": "asset://models/sword-bronze/sword-bronze.glb",
  "equippedModelPath": "asset://models/sword-steel/sword-steel-aligned.glb",
  "iconPath": "asset://models/sword-bronze/concept-art.png",
  "bonuses": {
    "attack": 4,
    "strength": 3,
    "defense": 0,
    "ranged": 0
  }
}

Tool (tools.json)

Tools include a tool object specifying the skill and priority. The tier system automatically derives level requirements:
{
  "id": "bronze_hatchet",
  "name": "Bronze Hatchet",
  "type": "tool",
  "tier": "bronze",
  "tool": {
    "skill": "woodcutting",
    "priority": 1
  },
  "value": 50,
  "weight": 1,
  "equipSlot": "weapon",
  "weaponType": "AXE",
  "attackType": "MELEE",
  "attackSpeed": 5,
  "attackRange": 1,
  "description": "A basic hatchet for chopping trees",
  "examine": "A basic hatchet for chopping trees",
  "tradeable": true,
  "rarity": "common",
  "modelPath": "asset://models/hatchet-bronze/hatchet-bronze.glb",
  "iconPath": "asset://models/hatchet-bronze/concept-art.png",
  "bonuses": {
    "attack": 4,
    "strength": 3,
    "defense": 0,
    "ranged": 0
  }
}
The tier: "bronze" automatically gives this tool attack: 1 and woodcutting: 1 requirements from tier-requirements.json. Tools with equipSlot: "weapon" can be equipped and used for combat.

Resource (resources.json)

{
  "id": "copper_ore",
  "name": "Copper Ore",
  "type": "resource",
  "stackable": false,
  "maxStackSize": 100,
  "value": 5,
  "weight": 2,
  "description": "Copper ore that can be smelted into a bronze bar",
  "examine": "Ore containing copper. Can be combined with tin to make bronze.",
  "tradeable": true,
  "rarity": "common",
  "modelPath": null,
  "iconPath": "asset://icons/ore-copper.png"
}

Consumable (food.json)

Consumables can include inventoryActions to define OSRS-style context menu actions:
{
  "id": "shrimp",
  "name": "Shrimp",
  "type": "consumable",
  "stackable": false,
  "value": 10,
  "weight": 0.2,
  "description": "Some nicely cooked shrimp",
  "examine": "Some nicely cooked shrimp.",
  "tradeable": true,
  "rarity": "common",
  "modelPath": null,
  "iconPath": "asset://icons/shrimp.png",
  "healAmount": 3,
  "inventoryActions": ["Eat", "Use", "Drop", "Examine"]
}
The first action in inventoryActions becomes the left-click default. If not specified, the system uses heuristic detection based on item properties (food → Eat, potions → Drink, etc.).

Context Menu Color Coding

Context menus use OSRS-accurate color coding for entity names:
// From GameConstants.ts
export const CONTEXT_MENU_COLORS = {
  ITEM: "#ff9040",    // Orange for item names
  NPC: "#ffff00",     // Yellow for NPC names
  OBJECT: "#00ffff",  // Cyan for scenery/objects
  PLAYER: "#ffffff",  // White for player names
} as const;
Examples:
  • “Eat Shrimp” (orange)
  • “Attack Goblin” (yellow)
  • “Mine Copper rocks” (cyan)
  • “Trade Shop keeper” (yellow)

Currency (misc.json)

{
  "id": "coins",
  "name": "Coins",
  "type": "currency",
  "stackable": true,
  "maxStackSize": 2147483647,
  "value": 1,
  "weight": 0,
  "description": "The universal currency of Hyperia",
  "examine": "Gold coins used as currency throughout the realm",
  "tradeable": true,
  "rarity": "always",
  "modelPath": null,
  "iconPath": "asset://icons/coins.png"
}

Adding New Items

1

Choose the right manifest file

  • Weapons → items/weapons.json
  • Tools → items/tools.json
  • Resources (ores, logs, bars, raw fish) → items/resources.json
  • Food → items/food.json
  • Currency, junk → items/misc.json
2

Add entry with proper structure

Follow the examples above. For tiered equipment, specify the tier field instead of hardcoding requirements.
3

Create 3D Model (optional)

Generate model in 3D Asset Forge if needed
4

Restart Server

Server must restart to reload manifests
DO NOT add item data directly to items.ts. Keep all content in JSON manifests for data-driven design.

Tool Priority System

Tools use a priority system to determine which tool to use when multiple are available:
{
  "tool": {
    "skill": "woodcutting",
    "priority": 1  // Higher = better tool
  }
}
For example:
  • Bronze hatchet: priority 1
  • Iron hatchet: priority 2
  • Steel hatchet: priority 3
  • Rune hatchet: priority 6
The system automatically selects the highest priority tool the player has equipped or in inventory.

Inventory Actions System

Items can define explicit context menu actions using the inventoryActions array. This is the OSRS-accurate approach where actions are stored per-item in the manifest.
{
  "id": "bronze_sword",
  "inventoryActions": ["Wield", "Use", "Drop", "Examine"]
}
Key Features:
  • First action becomes the left-click default
  • Actions appear in context menu in the order specified
  • “Cancel” is always added automatically as the last option
  • Manifest-defined actions take priority over heuristic detection

Common Action Patterns

Item TypeActions
Food["Eat", "Use", "Drop", "Examine"]
Potions["Drink", "Use", "Drop", "Examine"]
Weapons["Wield", "Use", "Drop", "Examine"]
Armor["Wear", "Use", "Drop", "Examine"]
Bones["Bury", "Use", "Drop", "Examine"]
Generic["Use", "Drop", "Examine"]

Action Handlers

The following actions have built-in handlers in InventoryActionDispatcher:
  • Eat: Sends useItem packet → server validates eat delay (3 ticks) → consumes food → heals player
  • Drink: Sends useItem packet → server validates → applies potion effects
  • Wield: Sends equipItem network message (weapons/shields)
  • Wear: Sends equipItem network message (armor)
  • Bury: Sends buryBones network message
  • Use: Enters targeting mode for item-on-item/item-on-object interactions
  • Drop: Calls world.network.dropItem()
  • Examine: Shows examine text in chat and toast
  • Cancel: Closes context menu (always added automatically)

Heuristic Fallback

If inventoryActions is not specified, the system uses type detection helpers from item-helpers.ts:
// From packages/shared/src/utils/item-helpers.ts
export function getPrimaryAction(item: Item | null, isNoted: boolean): PrimaryActionType {
  if (isNoted) return "use";
  
  const manifestAction = getPrimaryActionFromManifest(item);
  if (manifestAction) return manifestAction;
  
  // Fallback to heuristic detection
  if (isFood(item)) return "eat";
  if (isPotion(item)) return "drink";
  if (isBone(item)) return "bury";
  if (usesWield(item)) return "wield";
  if (usesWear(item)) return "wear";
  
  return "use";
}
Detection Rules:
  • Food: type: "consumable" + healAmount > 0 + not potion
  • Potions: type: "consumable" + id.includes("potion")
  • Weapons: equipSlot: "weapon" or equipSlot: "2h" or weaponType defined
  • Shields: equipSlot: "shield"
  • Armor: equipable: true + not weapon/shield
  • Bones: id === "bones" or id.endsWith("_bones")
  • Noted Items: Always use “use” action (cannot eat/equip noted items)
Manifest-defined inventoryActions take priority over heuristic detection. This allows custom actions for special items.

Equipment Slots

Items equip to specific slots:
SlotItem Types
weaponSwords, axes, bows, staffs
shieldShields, defenders
helmetHelmets, hats, hoods
bodyPlatebodies, chainbodies, robes
legsPlatelegs, chainlegs, skirts
bootsBoots
glovesGloves, bracers
capeCapes, cloaks
ringRings
amuletAmulets, necklaces
ammoArrows, bolts, runes


Ground Item Display

Items dropped on the ground can be configured for visual appearance using modelScale and groundOffset properties.

Model Scale

Controls the size of the 3D model when displayed as a ground item:
{
  "id": "logs",
  "modelScale": 0.4,  // 40% of original model size
  // Default: 0.3 (30% scale)
}

Ground Offset

Controls vertical positioning and animation behavior:
/** Ground item Y positioning:
 *  - When <= 0: model bottom is bbox-snapped to terrain with this as offset (0 = flush)
 *  - When > 0: item floats at this height with bobbing animation
 *  - When undefined: defaults to 0.5 (standard floating ground item)
 */
groundOffset?: number;
Examples:
// Grounded item (sits on terrain, no animation)
{
  "id": "logs",
  "groundOffset": 0,  // Model bottom flush with terrain
  "modelScale": 0.4
}

// Grounded with slight offset
{
  "id": "fire",
  "groundOffset": -0.05,  // Model bottom 0.05 units below terrain
  "modelScale": 0.35
}

// Floating item (default behavior)
{
  "id": "coins",
  "groundOffset": 0.5,  // Floats 0.5 units above terrain with bobbing
  "modelScale": 0.3
}

// Floating at custom height
{
  "id": "rare_item",
  "groundOffset": 0.8,  // Floats higher than normal
  "modelScale": 0.35
}

Bounding Box Snapping

When groundOffset <= 0, the system automatically calculates the model’s bounding box and positions it flush with the terrain:
// From ItemEntity.ts
// Grounded items: compute bounding box and snap bottom to terrain level
const gOffset = this.config.groundOffset;
if (gOffset !== undefined && gOffset <= 0) {
  const bbox = new THREE.Box3().setFromObject(this.mesh);
  // 0.2 matches the yOffset passed to groundToTerrain() in GroundItemSystem
  const GROUND_ITEM_TERRAIN_OFFSET = 0.2;
  this.mesh.position.y = -bbox.min.y - GROUND_ITEM_TERRAIN_OFFSET + gOffset;
}
Benefits:
  • Items sit naturally on terrain regardless of model pivot point
  • No manual Y-offset tuning required per item
  • Works correctly on slopes and uneven terrain
  • Fire models and other objects use the same system
Animation Behavior:
// Grounded items (groundOffset <= 0): no animation
if (offset !== undefined && offset <= 0) {
  // Y position set by createMesh bbox snap, no animation
} else {
  // Regular items: float and spin
  const floatHeight = offset ?? 0.5;
  this.mesh.position.y = floatHeight + Math.sin(time * 2) * 0.1;
  this.mesh.rotation.y += deltaTime * 0.5;
}
Grounded items (groundOffset <= 0) are static and do not float or spin. This is ideal for fires, logs, and other objects that should sit naturally on the ground.