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

# Context Menus

> OSRS-style right-click menus with colored entity names and manifest-driven actions

# Context Menu System

Hyperscape features OSRS-accurate right-click context menus for inventory items, world entities, NPCs, and players with colored entity names and manifest-driven action ordering.

<Info>
  Context menu code lives in `packages/client/src/game/systems/InventoryActionDispatcher.ts` and `packages/shared/src/utils/item-helpers.ts`.
</Info>

## Overview

Context menus provide:

* **Manifest-driven actions** - Actions defined in item/entity manifests
* **OSRS-accurate ordering** - Primary action first, then secondary actions
* **Colored entity names** - Yellow NPCs, cyan scenery, orange items
* **Cancel option** - Always last in the menu
* **Left-click primary action** - First action in the list

***

## Inventory Context Menus

### Item Actions

Right-click items in your inventory to see available actions:

```typescript theme={null}
// Example: Bronze sword
inventoryActions: ["Wield", "Drop", "Examine"]

// Example: Cooked shrimp
inventoryActions: ["Eat", "Drop", "Examine"]

// Example: Tinderbox
inventoryActions: ["Use", "Drop", "Examine"]
```

### Action Ordering

Actions are ordered by **priority** (lower = higher priority):

```typescript theme={null}
// From item-helpers.ts
export const ACTION_PRIORITY = {
  eat: 1,      // Food primary action
  drink: 1,    // Potion primary action
  wield: 1,    // Weapon/shield primary action
  wear: 1,     // Armor primary action
  bury: 1,     // Bones primary action
  use: 2,      // Generic use action
  drop: 9,     // Always near bottom
  examine: 10, // Always last (before Cancel)
  cancel: 11,  // Always absolute last
};
```

### Manifest-Driven Actions

Items define their actions in `items.json`:

```json theme={null}
{
  "id": "bronze_sword",
  "name": "Bronze sword",
  "type": "weapon",
  "equipSlot": "weapon",
  "inventoryActions": ["Wield", "Drop", "Examine"],
  "examine": "A basic bronze sword."
}
```

The first action becomes the left-click default.

### Heuristic Detection

If `inventoryActions` is not specified, the system detects actions based on item properties:

| Item Type            | Detection                                                          | Actions                   |
| -------------------- | ------------------------------------------------------------------ | ------------------------- |
| Food                 | `type: "consumable"` + `healAmount > 0` + not potion               | Eat, Use, Drop, Examine   |
| Potions              | `type: "consumable"` + `id.includes("potion")`                     | Drink, Use, Drop, Examine |
| Weapons              | `equipSlot: "weapon"` or `equipSlot: "2h"` or `weaponType` defined | Wield, Use, Drop, Examine |
| Armor                | `equipable: true` + not weapon/shield                              | Wear, Use, Drop, Examine  |
| Shields              | `equipSlot: "shield"`                                              | Wield, Use, Drop, Examine |
| Bones                | `id === "bones"` or `id.endsWith("_bones")`                        | Bury, Use, Drop, Examine  |
| Noted Items          | `isNoted: true` or `id.endsWith("_noted")`                         | Use, Drop, Examine        |
| Tools with equipSlot | `type: "tool"` + `equipSlot: "weapon"`                             | Wield, Use, Drop, Examine |

<Info>
  Tools like hatchets and pickaxes can be equipped as weapons. The system checks `equipSlot` first before falling back to type-based detection.
</Info>

### Action Handlers

The `InventoryActionDispatcher` provides centralized handling for all inventory actions:

```typescript theme={null}
export const HANDLED_INVENTORY_ACTIONS = new Set<string>([
  "eat",
  "drink",
  "bury",
  "wield",
  "wear",
  "drop",
  "examine",
  "use",
]);
```

Supported actions:

* **eat**: Sends `useItem` network packet → server validates eat delay → consumes food → heals player
* **drink**: Sends `useItem` network packet → server validates → applies potion effects
* **bury**: Sends `buryBones` network message
* **wield**: Sends `equipItem` network message (weapons/shields)
* **wear**: Sends `equipItem` network message (armor)
* **drop**: Calls `world.network.dropItem()`
* **examine**: Shows examine text in chat and toast
* **use**: Enters targeting mode for item-on-item/item-on-object interactions
* **cancel**: No-op, menu already closed

<Info>
  **Food Consumption**: The eat action is server-authoritative with 3-tick (1.8s) eat delay and combat attack delay integration. See [Inventory System](/wiki/game-systems/inventory#item-consumption) for details.
</Info>

***

## Entity Context Menus

### Colored Entity Names

Entity names are colored by type (OSRS-accurate):

```typescript theme={null}
// From CONTEXT_MENU_COLORS constants
export const CONTEXT_MENU_COLORS = {
  NPC: "#ffff00",      // Yellow - NPCs (shopkeepers, quest givers)
  SCENERY: "#00ffff",  // Cyan - Interactive objects (trees, rocks, anvils)
  ITEM: "#ff9040",     // Orange - Ground items
  PLAYER: "#ffffff",   // White - Other players
  MOB: "#ff0000",      // Red - Hostile mobs (goblins, etc.)
};
```

### Entity Actions

Entities define actions in their manifests:

```json theme={null}
{
  "id": "goblin",
  "name": "Goblin",
  "type": "mob",
  "entityActions": ["Attack", "Examine"],
  "examine": "An ugly green creature."
}
```

### Context Menu Example

Right-clicking a goblin shows:

```
Goblin (level 2)
  Attack
  Examine
  Cancel
```

The name "Goblin" is colored **red** (#ff0000) because it's a mob.

***

## InventoryActionDispatcher

The `InventoryActionDispatcher` is the **single source of truth** for handling inventory actions. Both context menu selections and left-click primary actions route through this dispatcher.

### Dispatcher Flow

```typescript theme={null}
export function dispatchInventoryAction(
  action: string,
  ctx: InventoryActionContext,
): ActionResult {
  const { world, itemId, slot, quantity = 1 } = ctx;

  switch (action) {
    case "eat":
    case "drink":
      // Server-authoritative consumption
      world.network?.send("useItem", { itemId, slot });
      return { success: true };

    case "wield":
    case "wear":
      world.network?.send("equipItem", {
        playerId: localPlayer.id,
        itemId,
        inventorySlot: slot,
      });
      return { success: true };

    case "drop":
      world.network?.send("dropItem", { itemId, slot, quantity });
      return { success: true };

    case "examine":
      const examineText = itemData?.examine || `It's a ${itemId}.`;
      world.emit(EventType.UI_TOAST, { message: examineText, type: "info" });
      return { success: true };

    case "use":
      // Enter targeting mode for "Use X on Y" interactions
      world.emit(EventType.ITEM_ACTION_SELECTED, {
        playerId: localPlayer.id,
        actionId: "use",
        itemId,
        slot,
      });
      return { success: true };

    case "cancel":
      // Intentional no-op - menu already closed
      return { success: true };

    default:
      console.warn(`Unhandled action: "${action}" for item "${itemId}"`);
      return { success: false, message: `Unhandled action: ${action}` };
  }
}
```

***

## Primary Action Detection

### Manifest-First Approach

The system uses a **manifest-first** approach with heuristic fallback:

```typescript theme={null}
/**
 * Get primary action using manifest-first approach.
 * OSRS-accurate: reads from inventoryActions if available.
 */
export function getPrimaryAction(
  item: Item | null,
  isNoted: boolean,
): PrimaryActionType {
  if (isNoted) return "use";

  // Try 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";
}
```

### Item Type Detection

Helper functions determine item types:

```typescript theme={null}
/** Food items - have healAmount and are consumable */
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")
  );
}

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

/** Equipment that uses "Wield" (weapons + shields) */
export function usesWield(item: Item | null): boolean {
  return isWeapon(item) || isShield(item);
}

/** Equipment that uses "Wear" (armor: head, body, legs, etc.) */
export function usesWear(item: Item | null): boolean {
  if (!item) return false;
  if (!item.equipable && !item.equipSlot) return false;
  return !usesWield(item);
}
```

***

## Cancel Option

All context menus include a **Cancel** option at the bottom (OSRS-accurate):

```typescript theme={null}
// From InventoryPanel.tsx
const actions = [
  ...itemActions,
  { id: "cancel", label: "Cancel", color: "#ffffff" }
];
```

The Cancel action is a **silent no-op** - it just closes the menu:

```typescript theme={null}
// From InventoryActionDispatcher.ts
case "cancel":
  // Intentional no-op - menu already closed by EntityContextMenu
  return { success: true };
```

***

## Context Menu Colors

### Color Constants

```typescript theme={null}
// From packages/shared/src/utils/item-helpers.ts
import { 
  isFood, 
  isPotion, 
  isBone, 
  usesWield, 
  usesWear, 
  isNotedItem,
  getPrimaryAction,
  getPrimaryActionFromManifest,
  HANDLED_INVENTORY_ACTIONS
} from '@hyperscape/shared';

// Detect item types
const canEat = isFood(item);           // true for food items (healAmount > 0, not potion)
const canDrink = isPotion(item);       // true for potions (id contains "potion")
const canBury = isBone(item);          // true for bones (id is "bones" or ends with "_bones")
const shouldWield = usesWield(item);   // true for weapons/shields
const shouldWear = usesWear(item);     // true for armor (not weapons/shields)
const isNoted = isNotedItem(item);     // true for bank notes (isNoted flag or "_noted" suffix)

// Get primary action (manifest-first with heuristic fallback)
const action = getPrimaryAction(item, isNoted);
// Returns: "eat" | "drink" | "bury" | "wield" | "wear" | "use"

// Get action from manifest only (returns null if not defined)
const manifestAction = getPrimaryActionFromManifest(item);

// Check if action has a handler
const isHandled = HANDLED_INVENTORY_ACTIONS.has(action);
```

### Detection Logic

The `getPrimaryAction` function uses a manifest-first approach:

1. **Noted items**: Always return "use" (cannot eat/equip notes)
2. **Manifest actions**: Check `item.inventoryActions[0]` if defined
3. **Heuristic fallback**: Detect based on item properties
   * Food: `type === "consumable"` + `healAmount > 0` + not potion
   * Potions: `type === "consumable"` + `id.includes("potion")`
   * Bones: `id === "bones"` or `id.endsWith("_bones")`
   * Weapons/Shields: `usesWield()` checks `equipSlot` and `weaponType`
   * Armor: `usesWear()` checks `equipable` and `equipSlot`
4. **Final fallback**: Return "use"

***

## Adding Custom Actions

```typescript theme={null}
// EntityContextMenu.tsx
const entityColor = CONTEXT_MENU_COLORS[entityType] || "#ffffff";

<div style={{ color: entityColor }}>
  {entityName} {level && `(level ${level})`}
</div>
```

***

## Testing

Context menus have unit test coverage:

```typescript theme={null}
// From item-helpers.test.ts
describe("item-helpers", () => {
  it("detects food items correctly", () => {
    const shrimp = { id: "cooked_shrimp", type: "consumable", healAmount: 3 };
    expect(isFood(shrimp)).toBe(true);
    
    const potion = { id: "strength_potion", type: "consumable", healAmount: 0 };
    expect(isFood(potion)).toBe(false);
  });

  it("determines primary action from manifest", () => {
    const sword = { inventoryActions: ["Wield", "Drop", "Examine"] };
    expect(getPrimaryActionFromManifest(sword)).toBe("wield");
    
    const food = { inventoryActions: ["Eat", "Drop", "Examine"] };
    expect(getPrimaryActionFromManifest(food)).toBe("eat");
  });
});
```

Integration tests verify the full dispatcher flow:

```typescript theme={null}
// From InventoryActionDispatcher.test.ts
it("dispatches eat action to server", () => {
  const mockSend = vi.fn();
  const world = { network: { send: mockSend } };
  
  dispatchInventoryAction("eat", {
    world,
    itemId: "cooked_shrimp",
    slot: 5,
  });
  
  expect(mockSend).toHaveBeenCalledWith("useItem", {
    itemId: "cooked_shrimp",
    slot: 5,
  });
});
```

***

## Related Documentation

* [Inventory System](/wiki/game-systems/inventory) - Item storage
* [Item Manifests](/concepts/manifests) - Defining item actions
* [Eating System](/wiki/game-systems/eating) - Food consumption
* [Equipment System](/wiki/game-systems/equipment) - Wield/Wear actions
