Skip to main content

Context Menu API

The context menu system provides OSRS-accurate right-click menus with colored entity names, manifest-driven actions, and centralized dispatching.

InventoryActionDispatcher

Centralized inventory action dispatching. Location: packages/client/src/game/systems/InventoryActionDispatcher.ts

dispatchInventoryAction()

Dispatch an inventory action to the appropriate handler.
dispatchInventoryAction(
  action: string,
  ctx: InventoryActionContext
): ActionResult
Parameters:
  • action - The action ID (e.g., “eat”, “wield”, “drop”)
  • ctx - Context containing world, itemId, slot, and optional quantity
Returns: ActionResult indicating success/failure Example:
import { dispatchInventoryAction } from '../systems/InventoryActionDispatcher';

// Eat food
const result = dispatchInventoryAction("eat", {
  world,
  itemId: "shrimp",
  slot: 5,
});

// Wield weapon
const result = dispatchInventoryAction("wield", {
  world,
  itemId: "bronze_sword",
  slot: 10,
});

// Drop item
const result = dispatchInventoryAction("drop", {
  world,
  itemId: "logs",
  slot: 3,
  quantity: 10,
});

Supported Actions

ActionHandlerDescription
eatuseItem network messageConsumes food, heals HP
drinkITEM_ACTION_SELECTED eventConsumes potion, applies effects
buryburyBones network messageBuries bones for Prayer XP
wieldequipItem network messageEquips weapons/shields
wearequipItem network messageEquips armor
dropdropItem network messageDrops item on ground
examineUI_TOAST + chat messageShows examine text
useITEM_ACTION_SELECTED eventEnters targeting mode
cancelNo-opCloses menu

Item Helpers

Type detection utilities for inventory actions. Location: packages/shared/src/utils/item-helpers.ts

Type Detection Functions

isFood()

Check if item is food (can be eaten).
isFood(item: Item | null): boolean
Detection: type === "consumable" + healAmount > 0 + not a potion Example:
import { isFood } from '@hyperscape/shared';

const item = getItem("shrimp");
if (isFood(item)) {
  // Show "Eat" action
}

isPotion()

Check if item is a potion (can be drunk).
isPotion(item: Item | null): boolean
Detection: type === "consumable" + id.includes("potion")

isBone()

Check if item is bones (can be buried).
isBone(item: Item | null): boolean
Detection: id === "bones" or id.endsWith("_bones")

isWeapon()

Check if item is a weapon.
isWeapon(item: Item | null): boolean
Detection: equipSlot === "weapon" or equipSlot === "2h" or is2h === true or weaponType != null

isShield()

Check if item is a shield.
isShield(item: Item | null): boolean
Detection: equipSlot === "shield"

usesWield()

Check if item uses “Wield” action (weapons + shields).
usesWield(item: Item | null): boolean
Returns: isWeapon(item) || isShield(item)

usesWear()

Check if item uses “Wear” action (armor, not weapons/shields).
usesWear(item: Item | null): boolean
Detection: equipable === true and not a weapon/shield

isNotedItem()

Check if item is a bank note.
isNotedItem(item: Item | null): boolean
Detection: isNoted === true or id.endsWith("_noted")

Primary Action Detection

getPrimaryAction()

Get primary action using manifest-first approach with heuristic fallback.
getPrimaryAction(
  item: Item | null,
  isNoted: boolean
): PrimaryActionType
Parameters:
  • item - Item to check
  • isNoted - Whether item is a bank note
Returns: "eat" | "drink" | "bury" | "wield" | "wear" | "use" Logic:
  1. If noted → return “use”
  2. Check item.inventoryActions[0] if defined
  3. Heuristic fallback based on item properties
  4. Final fallback → “use”
Example:
import { getPrimaryAction, isNotedItem } from '@hyperscape/shared';

const item = getItem("shrimp");
const isNoted = isNotedItem(item);
const action = getPrimaryAction(item, isNoted);
// Returns: "eat"

// On left-click
dispatchInventoryAction(action, { world, itemId: item.id, slot });

getPrimaryActionFromManifest()

Get primary action from manifest’s inventoryActions only.
getPrimaryActionFromManifest(item: Item | null): PrimaryActionType | null
Returns: First action from inventoryActions array, or null if not defined Example:
const item = getItem("bronze_sword");
// item.inventoryActions = ["Wield", "Use", "Drop", "Examine"]

const action = getPrimaryActionFromManifest(item);
// Returns: "wield"

Context Menu Colors

Location: packages/shared/src/constants/GameConstants.ts

CONTEXT_MENU_COLORS

OSRS-accurate color constants for context menu entity names.
export const CONTEXT_MENU_COLORS = {
  ITEM: "#ff9040",      // Orange - for items
  NPC: "#ffff00",       // Yellow - for NPCs and mobs
  OBJECT: "#00ffff",    // Cyan - for scenery/objects
  PLAYER: "#ffffff",    // White - for players
};
Usage:
import { CONTEXT_MENU_COLORS } from '@hyperscape/shared';

// Styled label for context menu
const styledLabel = [
  { text: "Take " },
  { text: "Bones", color: CONTEXT_MENU_COLORS.ITEM }
];

Context Menu Action Structure

ContextMenuAction Interface

interface ContextMenuAction {
  id: string;                    // Unique action ID
  label: string;                 // Plain text label
  styledLabel?: LabelSegment[];  // Colored segments
  enabled: boolean;              // Can be executed
  priority: number;              // Sort order (lower = higher)
  handler?: () => void;          // Action callback
}

interface LabelSegment {
  text: string;
  color?: string;  // Hex color code
}
Example:
const action: ContextMenuAction = {
  id: "attack",
  label: "Attack Goblin (Level: 5)",
  styledLabel: [
    { text: "Attack " },
    { text: "Goblin", color: CONTEXT_MENU_COLORS.NPC },
    { text: " (Level: " },
    { text: "5", color: "#ff0000" },  // Red for dangerous
    { text: ")" }
  ],
  enabled: true,
  priority: 1,
  handler: () => attackMob(target)
};

Interaction Handlers

Base class for all interaction handlers. Location: packages/shared/src/systems/client/interaction/handlers/BaseInteractionHandler.ts

BaseInteractionHandler

All handlers extend this base class:
abstract class BaseInteractionHandler {
  abstract handleLeftClick(target: RaycastTarget): void;
  abstract getContextMenuActions(target: RaycastTarget): ContextMenuAction[];
  
  // Helper methods
  protected createWalkHereAction(target: RaycastTarget): ContextMenuAction;
  protected showExamineMessage(text: string): void;
  protected queueInteraction(params: InteractionParams): void;
}

Handler Registry

HandlerEntity TypesPrimary Action
ItemInteractionHandlerGround itemsTake
NPCInteractionHandlerNPCs (bankers, shopkeepers)Talk-to
MobInteractionHandlerHostile mobsAttack
PlayerInteractionHandlerOther playersTrade
ResourceInteractionHandlerTrees, rocks, fishing spotsChop/Mine/Fish
BankInteractionHandlerBank booths/chestsBank
CookingSourceInteractionHandlerFires, rangesCook
SmeltingSourceInteractionHandlerFurnacesSmelt
SmithingSourceInteractionHandlerAnvilsSmith
CorpseInteractionHandlerGravestones, corpsesLoot

Manifest Integration

Item Manifest

Items can define explicit inventoryActions:
{
  "id": "shrimp",
  "name": "Shrimp",
  "type": "consumable",
  "healAmount": 3,
  "inventoryActions": ["Eat", "Use", "Drop", "Examine"]
}
Rules:
  • First action becomes left-click default
  • Actions are case-insensitive when dispatched
  • If not specified, system uses heuristic detection
  • Noted items always use [“Use”, “Drop”, “Examine”]

Supported Manifest Actions

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

Testing

InventoryActionDispatcher Tests

File: packages/client/src/game/systems/__tests__/InventoryActionDispatcher.test.ts Coverage: 333 lines, 100% of all actions Example:
it("emits ITEM_ACTION_SELECTED event for eat action", () => {
  const result = dispatchInventoryAction("eat", {
    world: mockWorld,
    itemId: "shrimp",
    slot: 0,
  });

  expect(result.success).toBe(true);
  expect(mockWorld.emit).toHaveBeenCalledWith(
    EventType.ITEM_ACTION_SELECTED,
    {
      playerId: "player1",
      actionId: "eat",
      itemId: "shrimp",
      slot: 0,
    }
  );
});

Item Helpers Tests

File: packages/shared/src/utils/__tests__/item-helpers.test.ts Coverage: 510 lines, all edge cases Example:
it("returns eat for food without manifest", () => {
  const food = createItem({
    id: "shrimp",
    type: "consumable",
    healAmount: 10,
  });
  
  expect(getPrimaryAction(food, false)).toBe("eat");
});

it("uses manifest action when available", () => {
  const item = createItem({
    inventoryActions: ["Wield", "Use", "Drop"],
  });
  
  expect(getPrimaryAction(item, false)).toBe("wield");
});