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

# Smithing System

> OSRS-accurate smelting and smithing with anvils, furnaces, and manifest-driven recipes

# Smithing System

The smithing system implements OSRS-accurate smelting and smithing mechanics with manifest-driven recipes, tick-based timing, and auto-smithing support.

<Info>
  Smithing code lives in `packages/shared/src/systems/shared/interaction/SmithingSystem.ts` and uses recipes from `ProcessingDataProvider`.
</Info>

## Overview

Smithing is a **two-step process**:

1. **Smelting** - Combine ores at a furnace to create bars
2. **Smithing** - Use bars at an anvil to create equipment

Both steps:

* Use **tick-based timing** (4 ticks = 2.4 seconds per action)
* Support **"Make X"** functionality (auto-craft multiple items)
* Are **100% success rate** (except iron ore smelting)
* Grant **Smithing XP** per item created

***

## Smelting (Furnaces)

### How to Smelt

1. Have ores in your inventory
2. Click a furnace
3. Select bar type from the interface
4. Choose quantity (1, 5, 10, X, All)
5. System auto-smelts until out of materials

### Smelting Requirements

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From ProcessingDataProvider.ts
interface SmeltingRecipe {
  barItemId: string;        // Output bar ID (e.g., "bronze_bar")
  barName: string;          // Display name
  oreRequirements: {        // Input ores
    [oreId: string]: number;
  };
  levelRequired: number;    // Smithing level needed
  xp: number;               // XP per bar
  successRate: number;      // 1.0 = 100%, 0.5 = 50%
  ticks: number;            // Ticks per smelt (default 4)
}
```

### Smelting Recipes

| Bar     | Level | Ores Required             | XP   | Success Rate | Ticks |
| ------- | ----- | ------------------------- | ---- | ------------ | ----- |
| Bronze  | 1     | 1 Copper + 1 Tin          | 6.25 | 100%         | 4     |
| Iron    | 15    | 1 Iron Ore                | 12.5 | 50%          | 4     |
| Steel   | 30    | 1 Iron Ore + 2 Coal       | 17.5 | 100%         | 4     |
| Mithril | 50    | 1 Mithril Ore + 4 Coal    | 30   | 100%         | 4     |
| Adamant | 70    | 1 Adamantite Ore + 6 Coal | 37.5 | 100%         | 4     |
| Rune    | 85    | 1 Runite Ore + 8 Coal     | 50   | 100%         | 4     |

<Warning>
  Iron ore has a **50% failure rate** when smelting. Failed attempts consume the ore but grant no bar or XP. This matches OSRS mechanics.
</Warning>

### Iron Smelting Failure

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SmeltingSystem.ts
if (recipe.successRate < 1.0) {
  const roll = Math.random();
  if (roll >= recipe.successRate) {
    // Failed smelt - consume ore, no bar, no XP
    this.emitTypedEvent(EventType.UI_MESSAGE, {
      playerId,
      message: SMITHING_CONSTANTS.MESSAGES.IRON_SMELT_FAIL,
      type: "error",
    });
    
    // Still consume the ore
    this.consumeOres(playerId, recipe.oreRequirements);
    
    // Schedule next smelt
    this.scheduleNextSmelt(playerId);
    return;
  }
}
```

***

## Smithing (Anvils)

### How to Smith

1. Have bars in your inventory
2. **Have a hammer in your inventory** (required, not consumed)
3. Click an anvil
4. Select item from the interface
5. Choose quantity (1, 5, 10, X, All)
6. System auto-smiths until out of bars

### Smithing Requirements

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
interface SmithingRecipe {
  itemId: string;           // Output item ID (e.g., "bronze_sword")
  name: string;             // Display name
  barType: string;          // Input bar ID (e.g., "bronze_bar")
  barsRequired: number;     // Bars consumed per item
  levelRequired: number;    // Smithing level needed
  xp: number;               // XP per item
  category: string;         // UI category (sword, hatchet, pickaxe, etc.)
  ticks: number;            // Ticks per smith (default 4)
}
```

### Smithing Recipes

**Weapons:**

| Item            | Level | Bars      | XP   | Category |
| --------------- | ----- | --------- | ---- | -------- |
| Bronze Dagger   | 1     | 1 Bronze  | 12.5 | dagger   |
| Bronze Sword    | 4     | 1 Bronze  | 12.5 | sword    |
| Bronze Scimitar | 5     | 2 Bronze  | 25   | scimitar |
| Iron Sword      | 19    | 1 Iron    | 25   | sword    |
| Steel Sword     | 34    | 1 Steel   | 37.5 | sword    |
| Mithril Sword   | 54    | 1 Mithril | 50   | sword    |
| Adamant Sword   | 74    | 1 Adamant | 62.5 | sword    |
| Rune Sword      | 89    | 1 Rune    | 75   | sword    |

**Armor:**

| Item              | Level | Bars      | XP    | Category  |
| ----------------- | ----- | --------- | ----- | --------- |
| Bronze Platebody  | 18    | 5 Bronze  | 62.5  | platebody |
| Iron Platebody    | 33    | 5 Iron    | 125   | platebody |
| Steel Platebody   | 48    | 5 Steel   | 187.5 | platebody |
| Mithril Platebody | 68    | 5 Mithril | 250   | platebody |

**Tools:**

| Item           | Level | Bars     | XP   | Category |
| -------------- | ----- | -------- | ---- | -------- |
| Bronze Hatchet | 1     | 1 Bronze | 12.5 | hatchet  |
| Bronze Pickaxe | 1     | 1 Bronze | 12.5 | pickaxe  |
| Iron Hatchet   | 16    | 1 Iron   | 25   | hatchet  |
| Iron Pickaxe   | 16    | 1 Iron   | 25   | pickaxe  |
| Steel Hatchet  | 31    | 1 Steel  | 37.5 | hatchet  |
| Steel Pickaxe  | 31    | 1 Steel  | 37.5 | pickaxe  |

***

## Tick-Based Timing

Both smelting and smithing use **4-tick actions** (2.4 seconds):

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SmithingConstants.ts
export const SMITHING_CONSTANTS = {
  DEFAULT_SMELTING_TICKS: 4,  // 4 ticks = 2.4s
  DEFAULT_SMITHING_TICKS: 4,  // 4 ticks = 2.4s
  TICK_DURATION_MS: 600,      // 600ms per tick
};
```

### Session Processing

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SmithingSystem.ts
update(_dt: number): void {
  const currentTick = this.world.currentTick ?? 0;

  // Only process once per tick
  if (currentTick === this.lastProcessedTick) return;
  this.lastProcessedTick = currentTick;

  // Process all active sessions
  for (const [playerId, session] of this.activeSessions) {
    if (currentTick >= session.completionTick) {
      this.completeSmith(playerId);
    }
  }
}
```

***

## Auto-Smithing

The system supports **auto-smithing** - once started, it continues until:

* Target quantity reached
* Out of bars
* Player moves
* Player disconnects

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
/**
 * Schedule the next smith action for a session.
 * Called after each successful smith to queue the next one.
 */
private scheduleNextSmith(playerId: string): void {
  const session = this.activeSessions.get(playerId);
  if (!session) return;

  // Check if we've reached the target quantity
  if (session.smithed >= session.quantity) {
    this.completeSmithing(playerId);
    return;
  }

  // Check materials (bars)
  if (!this.hasRequiredBars(playerId, recipe.barType, recipe.barsRequired)) {
    this.emitTypedEvent(EventType.UI_MESSAGE, {
      playerId,
      message: "You have run out of bars.",
      type: "info",
    });
    this.completeSmithing(playerId);
    return;
  }

  // Set completion tick for next smith action
  const currentTick = this.world.currentTick ?? 0;
  session.completionTick = currentTick + recipe.ticks;
}
```

***

## Hammer Requirement

Smithing at anvils **requires a hammer** in your inventory:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
/**
 * Check if player has a hammer in inventory
 */
private hasHammer(playerId: string): boolean {
  const inventory = this.world.getInventory?.(playerId);
  if (!inventory || !Array.isArray(inventory)) return false;

  return inventory.some(
    (item) => isLooseInventoryItem(item) && item.itemId === HAMMER_ITEM_ID
  );
}
```

The hammer is **not consumed** - it's a permanent tool.

***

## Manifest Integration

### Smelting Recipes

Defined in `items.json` with `smeltingRecipe` property:

```json theme={"theme":{"light":"github-light","dark":"css-variables"}}
{
  "id": "bronze_bar",
  "name": "Bronze bar",
  "type": "resource",
  "smeltingRecipe": {
    "oreRequirements": {
      "copper_ore": 1,
      "tin_ore": 1
    },
    "levelRequired": 1,
    "xp": 6.25,
    "successRate": 1.0,
    "ticks": 4
  }
}
```

### Smithing Recipes

Defined in `items.json` with `smithingRecipe` property:

```json theme={"theme":{"light":"github-light","dark":"css-variables"}}
{
  "id": "bronze_sword",
  "name": "Bronze sword",
  "type": "weapon",
  "smithingRecipe": {
    "barType": "bronze_bar",
    "barsRequired": 1,
    "levelRequired": 4,
    "xp": 12.5,
    "category": "sword",
    "ticks": 4
  }
}
```

***

## Station 3D Models

Anvils and furnaces now use **manifest-driven 3D models**:

```json theme={"theme":{"light":"github-light","dark":"css-variables"}}
{
  "id": "anvil",
  "name": "Anvil",
  "type": "station",
  "modelPath": "/assets/models/stations/anvil.glb",
  "scale": 1.0,
  "interactionType": "smithing"
}
```

This allows easy customization of station appearances without code changes.

***

## Events

| Event                         | Data                                              | Description              |
| ----------------------------- | ------------------------------------------------- | ------------------------ |
| `SMITHING_INTERACT`           | `playerId`, `anvilId`                             | Player clicked anvil     |
| `SMITHING_INTERFACE_OPEN`     | `playerId`, `anvilId`, `availableRecipes`         | Show smithing UI         |
| `PROCESSING_SMITHING_REQUEST` | `playerId`, `recipeId`, `anvilId`, `quantity`     | Start smithing           |
| `SMITHING_START`              | `playerId`, `recipeId`, `anvilId`                 | Smithing session started |
| `SMITHING_COMPLETE`           | `playerId`, `recipeId`, `totalSmithed`, `totalXp` | Session finished         |
| `INVENTORY_ITEM_REMOVED`      | `playerId`, `itemId`, `quantity`                  | Bars consumed            |
| `INVENTORY_ITEM_ADDED`        | `playerId`, `item`                                | Smithed item added       |
| `SKILLS_XP_GAINED`            | `playerId`, `skill`, `amount`                     | XP granted               |

***

## API Reference

### SmithingSystem

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
class SmithingSystem extends SystemBase {
  /**
   * Check if player is currently smithing
   */
  isPlayerSmithing(playerId: string): boolean;

  /**
   * Update method - processes tick-based smithing sessions
   * Called each frame, but only processes once per game tick
   */
  update(_dt: number): void;
}
```

### ProcessingDataProvider

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
class ProcessingDataProvider {
  /**
   * Get smithing recipe by output item ID
   */
  getSmithingRecipe(itemId: string): SmithingRecipe | null;

  /**
   * Get all smithable items with availability info
   * Checks player's bars and level
   */
  getSmithableItemsWithAvailability(
    inventory: Array<{ itemId: string; quantity: number }>,
    smithingLevel: number
  ): Array<SmithingRecipe & { meetsLevel: boolean; hasBars: boolean }>;

  /**
   * Get smelting recipe by bar item ID
   */
  getSmeltingRecipe(barItemId: string): SmeltingRecipe | null;
}
```

***

## Configuration

### Constants

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SmithingConstants.ts
export const SMITHING_CONSTANTS = {
  HAMMER_ITEM_ID: "hammer",
  COAL_ITEM_ID: "coal",
  DEFAULT_SMELTING_TICKS: 4,
  DEFAULT_SMITHING_TICKS: 4,
  TICK_DURATION_MS: 600,
  MAX_QUANTITY: 10000,
  MIN_QUANTITY: 1,
};
```

### Messages

All user-facing messages are centralized:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
MESSAGES: {
  ALREADY_SMITHING: "You are already smithing.",
  NO_HAMMER: "You need a hammer to work the metal on this anvil.",
  NO_BARS: "You don't have the bars to smith anything.",
  LEVEL_TOO_LOW_SMITH: "You need level {level} Smithing to make that.",
  SMITHING_START: "You begin smithing {item}s.",
  OUT_OF_BARS: "You have run out of bars.",
  SMITH_SUCCESS: "You hammer the {metal} and make a {item}.",
}
```

***

## Security Features

### Input Validation

All inputs are validated server-side:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
/**
 * Validate and clamp quantity to safe bounds
 */
export function clampQuantity(quantity: unknown): number {
  if (typeof quantity !== "number" || !Number.isFinite(quantity)) {
    return SMITHING_CONSTANTS.MIN_QUANTITY;
  }
  return Math.floor(
    Math.max(
      SMITHING_CONSTANTS.MIN_QUANTITY,
      Math.min(quantity, SMITHING_CONSTANTS.MAX_QUANTITY),
    ),
  );
}

/**
 * Validate a string ID (barItemId, furnaceId, recipeId, anvilId)
 */
export function isValidItemId(id: unknown): id is string {
  return (
    typeof id === "string" &&
    id.length > 0 &&
    id.length <= SMITHING_CONSTANTS.MAX_ITEM_ID_LENGTH
  );
}
```

### Server-Authoritative

All smithing logic runs server-side:

1. **Recipe validation** - Server checks recipe exists
2. **Level checks** - Server validates smithing level
3. **Material checks** - Server verifies bars in inventory
4. **Hammer check** - Server confirms hammer present
5. **Consumption** - Server removes bars and adds items

***

## Type Safety

The smithing system uses **strong typing** with type guards:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
/**
 * Loose inventory item type - matches items from inventory lookups
 */
export interface LooseInventoryItem {
  itemId: string;
  quantity?: number;
  slot?: number;
  metadata?: Record<string, unknown> | null;
}

/**
 * Type guard to validate an object is a valid inventory item
 */
export function isLooseInventoryItem(
  item: unknown,
): item is LooseInventoryItem {
  if (typeof item !== "object" || item === null) return false;
  if (!("itemId" in item)) return false;
  if (typeof (item as LooseInventoryItem).itemId !== "string") return false;

  const qty = (item as LooseInventoryItem).quantity;
  if (qty !== undefined && typeof qty !== "number") return false;

  return true;
}

/**
 * Get quantity from an inventory item, defaulting to 1 if not present
 */
export function getItemQuantity(item: LooseInventoryItem): number {
  return item.quantity ?? 1;
}
```

***

## Testing

Smithing has comprehensive test coverage:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SmithingSystem.test.ts
describe("SmithingSystem", () => {
  it("requires hammer in inventory", async () => {
    const { world, player, anvil } = await setupSmithingTest();
    
    // Try to smith without hammer
    world.emit(EventType.SMITHING_INTERACT, {
      playerId: player.id,
      anvilId: anvil.id,
    });
    
    // Should show error message
    expect(lastMessage).toContain("need a hammer");
  });

  it("auto-smiths multiple items", async () => {
    const { world, player } = await setupSmithingTest();
    
    // Give player 10 bronze bars and a hammer
    giveItem(player.id, "bronze_bar", 10);
    giveItem(player.id, "hammer", 1);
    
    // Start smithing 5 swords
    world.emit(EventType.PROCESSING_SMITHING_REQUEST, {
      playerId: player.id,
      recipeId: "bronze_sword",
      anvilId: "anvil_1",
      quantity: 5,
    });
    
    // Process ticks until complete
    for (let i = 0; i < 25; i++) {
      world.tick();
    }
    
    // Should have 5 swords, 5 bars remaining
    expect(countItem(player.id, "bronze_sword")).toBe(5);
    expect(countItem(player.id, "bronze_bar")).toBe(5);
  });

  it("stops when out of bars", async () => {
    const { world, player } = await setupSmithingTest();
    
    // Give player 3 bars (can only make 3 swords)
    giveItem(player.id, "bronze_bar", 3);
    giveItem(player.id, "hammer", 1);
    
    // Try to smith 10 swords
    world.emit(EventType.PROCESSING_SMITHING_REQUEST, {
      playerId: player.id,
      recipeId: "bronze_sword",
      quantity: 10,
    });
    
    // Process until complete
    for (let i = 0; i < 50; i++) {
      world.tick();
    }
    
    // Should only have 3 swords (ran out of bars)
    expect(countItem(player.id, "bronze_sword")).toBe(3);
    expect(lastMessage).toContain("run out of bars");
  });
});
```

***

## Related Documentation

* [Mining System](/wiki/game-systems/mining) - Gathering ores
* [Skills System](/concepts/skills) - XP and leveling
* [Item Manifests](/concepts/manifests) - Defining recipes
* [Processing Systems](/wiki/game-systems/processing) - General processing mechanics
