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
| Tier | Attack | Defence |
|---|
| bronze/iron | 1 | 1 |
| steel | 5 | 5 |
| black | 10 | 10 |
| mithril | 20 | 20 |
| adamant | 30 | 30 |
| rune | 40 | 40 |
| dragon | 60 | 60 |
| Tier | Attack | Woodcutting | Mining |
|---|
| bronze/iron | 1 | 1 | 1 |
| steel | 5 | 6 | 6 |
| mithril | 20 | 21 | 21 |
| adamant | 30 | 31 | 31 |
| rune | 40 | 41 | 41 |
| dragon | 60 | 61 | 61 |
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 Type | Speed (ms) | Speed (ticks) |
|---|
| Dagger | 1800 | 3 |
| Scimitar | 1800 | 3 |
| Sword | 2400 | 4 |
| Longsword | 2400 | 4 |
| Battleaxe | 3000 | 5 |
| 2H Sword | 3600 | 6 |
| Shortbow | 1800 | 3 |
| Longbow | 3000 | 5 |
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
}
}
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 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
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
Add entry with proper structure
Follow the examples above. For tiered equipment, specify the tier field instead of hardcoding requirements.
Create 3D Model (optional)
Generate model in 3D Asset Forge if needed
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.
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 Type | Actions |
|---|
| 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:
| Slot | Item Types |
|---|
weapon | Swords, axes, bows, staffs |
shield | Shields, defenders |
helmet | Helmets, hats, hoods |
body | Platebodies, chainbodies, robes |
legs | Platelegs, chainlegs, skirts |
boots | Boots |
gloves | Gloves, bracers |
cape | Capes, cloaks |
ring | Rings |
amulet | Amulets, necklaces |
ammo | Arrows, 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.