Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 121 additions & 0 deletions learnings/jewelry-crafting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Jewelry Crafting & Enchanting

## Overview

Jewelry crafting uses gold/silver bars + moulds at a furnace. Enchanting uses magic spells on crafted jewelry.

### Full Pipeline: Bar → Jewelry → String → Enchant

1. **Craft** jewelry at furnace: `bot.craftJewelry({ product: 'amulet', gem: 'ruby' })`
2. **String** amulets with ball of wool: `bot.stringAmulet(/ruby amulet/i)`
3. **Enchant** with magic: `bot.enchantItem(/ruby amulet/i, 3)`

## Crafting Jewelry at a Furnace

### Requirements
- **Gold bar** (id 2357) or **Silver bar** in inventory
- **Mould**: ring mould (1592), necklace mould (1597), or amulet mould (1595)
- **Gem** (optional): sapphire (1607), emerald (1605), ruby (1603), diamond
- Must be near a **furnace**

### Interface: 4161 (Gold Jewelry)

Uses `sendUseItemOnLoc` with bar on furnace → opens interface 4161 → click component with INV_BUTTON.

**Component mapping** (confirmed via testing):

| Component | Product Type |
|-----------|-------------|
| **4233** | Ring |
| **4239** | Necklace |
| **4245** | Amulet |

**Gem slot mapping** (the `slot` parameter in `sendClickComponentWithOption`):

| Slot | Gem |
|------|-----|
| 0 | Plain gold |
| 1 | Sapphire |
| 2 | Emerald |
| 3 | Ruby |
| 4 | Diamond |

Example: Ruby amulet = `sendClickComponentWithOption(4245, 1, 3)`

### Usage

```typescript
// Craft a gold ring (Crafting level 5)
const result = await bot.craftJewelry({ product: 'ring' });

// Craft a ruby amulet (Crafting level 50)
const result = await bot.craftJewelry({ product: 'amulet', gem: 'ruby' });

// Auto-detect product from mould, gem from inventory
const result = await bot.craftJewelry();
```

### Furnace Locations
| Location | Coordinates | Notes |
|----------|-------------|-------|
| Lumbridge | (3225, 3256) | Near spawn, furnace id=2785 |
| Al Kharid | TBD | 10gp toll gate |

### Confirmed XP Values
| Product | XP |
|---------|-----|
| Gold ring | 375 |
| Gold necklace | 500 |
| Gold amulet | 750 |
| Sapphire amulet | 875 (estimated) |
| Ruby amulet | 1000+ |

## Stringing Amulets

Unstrung amulets need a **ball of wool** (id 1759) to complete.
Gives 100 Crafting XP.

```typescript
const result = await bot.stringAmulet(/ruby amulet/i);
```

Uses `sendUseItemOnItem` (ball of wool + amulet).

## Enchanting Jewelry

### Enchant Levels & Requirements

| Level | Gem | Magic Req | Runes | Spell ID |
|-------|-----|-----------|-------|----------|
| 1 | Sapphire | 7 | 1 cosmic + 1 water | 1155 |
| 2 | Emerald | 27 | 1 cosmic + 3 air | 1165 |
| 3 | Ruby | 49 | 1 cosmic + 5 fire | 1176 |
| 4 | Diamond | 57 | 1 cosmic + 10 earth | 1180 |
| 5 | Dragonstone | 68 | 1 cosmic + 15 water + 15 earth | 1187 |

### Usage

```typescript
// Enchant sapphire ring → ring of recoil
const result = await bot.enchantItem(/sapphire ring/i, 1);

// Enchant ruby amulet → amulet of strength (confirmed: +1475 Magic XP)
const result = await bot.enchantItem(/ruby amulet/i, 3);
```

Uses `sendSpellOnItem(slot, spellComponent)` — no furnace needed, just runes + cosmic rune (id 564).

## Low-Level API Reference

| Method | Purpose |
|--------|---------|
| `sdk.sendUseItemOnLoc(slot, x, z, id)` | Bar on furnace |
| `sdk.sendUseItemOnItem(src, dst)` | Wool on amulet |
| `sdk.sendSpellOnItem(slot, spellComponent)` | Enchant spell on item |
| `sdk.sendClickComponentWithOption(comp, 1, slot)` | Select product from jewelry interface |
| `sdk.waitForCondition(pred, timeout)` | Wait for interface/XP |

## TODO
- Test silver bar jewelry (holy symbol, tiara, etc.)
- Find optimal locations with bank + furnace close together
- Test dragonstone jewelry (slot 5?)
88 changes: 41 additions & 47 deletions learnings/mining.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,60 +35,53 @@ const isMining = state.player?.animId === 625;
const isIdle = state.player?.animId === -1;
```

## Rock IDs Are Per-Mine (NOT Universal)

**Critical**: The same rock ID maps to different ores at different mines!
Example: ID 2092 = **iron** at SE Varrock but **clay** at SW Varrock.

Always use per-mine rock ID tables. Use `Prospect` option on unknown rocks.

### Surveyed Rock IDs

| Mine | Rock ID | Ore |
|------|---------|-----|
| **SE Varrock** | 2090 | Copper |
| | 2091 | Copper |
| | 2092 | Iron |
| | 2093 | Iron |
| | 2094 | Tin |
| | 2095 | Tin |
| **SW Varrock** | 452 | Clay |
| | 2092 | Clay |
| | 2093 | Clay |
| | 2095 | Iron |
| | 2101 | Tin |
| | 2108 | Silver |
| | 2109 | Tin |
| **Barbarian Village** | 2094 | Iron |
| | 2096 | Tin |
| **Coal Trucks** (Members) | 2096 | Coal |
| | 2097 | Coal |
| **Ardougne South** (Members) | 450 | Coal |
| | 452 | Coal |
| | 2092 | Iron |
| | 2093 | Iron |
| | 2097 | Coal |

### Unsurveyed Mines

These mines have not been prospected yet — the script mines any rock there:
- Rimmington (pathfinder gets stuck south of Falador)
- Al Kharid (requires 10gp toll gate)
- Dwarven Mine (underground)
- Mining Guild (underground, 60+ Mining)
- Yanille (members)

## How to Mine Specific Ore
## Rock IDs → Ore Types (SE Varrock Mine)

Rocks are ALL named "Rocks" — you **must** prospect to tell them apart:

| Rock ID | Ore |
|---------|-----|
| 2090 | **Copper** |
| 2091 | **Copper** |
| 2092 | **Iron** |
| 2093 | **Tin** |
| 2094 | **Tin** |
| 2095 | **Iron** |

**IMPORTANT:** Previous learnings had 2092=tin and 2093=iron — this was WRONG.
Always prospect or test-mine to verify on your server instance.

**How to mine specific ore:**
```typescript
// Mine copper specifically at SE Varrock
const COPPER_IDS = [2090, 2091]; // SE Varrock copper rock IDs
// Mine copper specifically (IDs 2090 or 2091)
const copperRock = state.nearbyLocs
.filter(loc => COPPER_IDS.includes(loc.id))
.filter(loc => loc.id === 2090 || loc.id === 2091)
.filter(loc => loc.optionsWithIndex.some(o => /^mine$/i.test(o.text)))
.sort((a, b) => a.distance - b.distance)[0];

// Mine tin specifically (IDs 2093 or 2094)
const tinRock = state.nearbyLocs
.filter(loc => loc.id === 2093 || loc.id === 2094)
.filter(loc => loc.optionsWithIndex.some(o => /^mine$/i.test(o.text)))
.sort((a, b) => a.distance - b.distance)[0];
```

Use `Prospect` option on a rock to discover its ore type if unsure.

## Rock IDs → Ore Types (Al Kharid Mine)

| Rock ID | Ore |
|---------|-----|
| 2092 | **Iron** |
| 2093 | **Tin** |
| 2096 | **Coal** |
| 2098 | **Gold** |
| 2100 | **Silver** |
| 2103 | **Mithril** |
| 450, 2097, 2099, 2101, 2102 | Unknown (depleted during testing) |

**Note:** Al Kharid mine is full of Lvl 14 scorpions. Combat 27+ with defensive style is enough to survive while mining. The scorpion fights actually train Defence passively.

## Reliable Locations

| Location | Coordinates | Ores | Bank | ~Tiles |
Expand All @@ -103,6 +96,7 @@ const copperRock = state.nearbyLocs
| Ardougne South | (2602, 3235) | Fe, Coal | Ardougne East (2615, 3332) | 100 |
| Coal Trucks | (2581, 3483) | Coal | Seers Village (2725, 3493) | 150 |
| Yanille | (2624, 3139) | Cu, Sn, Coal | Yanille (2613, 3094) | 50 |
| Lumbridge Swamp | - | Interactions fail silently, avoid | - | - |

## Navigation Gotchas

Expand Down
5 changes: 2 additions & 3 deletions mcp/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Bot connection manager
// Supports multiple simultaneous bot connections

import { BotSDK } from '../../sdk/index';
import { BotSDK, deriveGatewayUrl } from '../../sdk/index';
import { BotActions } from '../../sdk/actions';
import { readFile } from 'fs/promises';
import { join } from 'path';
Expand Down Expand Up @@ -56,8 +56,7 @@ class BotManager {
pwd = env.PASSWORD;

if (env.SERVER) {
// Remote servers need /gateway path
gateway = `wss://${env.SERVER}/gateway`;
gateway = deriveGatewayUrl(env.SERVER);
}

// Check if chat should be shown (default: false for safety)
Expand Down
1 change: 0 additions & 1 deletion rs-sdk
Submodule rs-sdk deleted from b4a1b4
70 changes: 26 additions & 44 deletions scripts/test-pathfinding-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@
// Local pathfinding validation - no server or bot required
// Tests that the collision data + door masking produces correct paths

import { findLongPath, findMultiSegmentPath, findDoorsAlongPath, initPathfinding, isTileWalkable, isFlagged, isInsideDraynorManor, getDraynorManorEscape, isZoneLikelyLand, isZoneAllocated } from '../sdk/pathfinding';
import { findLongPath, findDoorsAlongPath, initPathfinding, isTileWalkable, isFlagged, isZoneLikelyLand, isZoneAllocated } from '../sdk/pathfinding';
import { CollisionFlag } from '../server/vendor/rsmod-pathfinder';

// Draynor Manor helpers (local to this test)
const DRAYNOR_MANOR = { minX: 3097, maxX: 3119, minZ: 3353, maxZ: 3373 };
function isInsideDraynorManor(x: number, z: number): boolean {
return x >= DRAYNOR_MANOR.minX && x <= DRAYNOR_MANOR.maxX &&
z >= DRAYNOR_MANOR.minZ && z <= DRAYNOR_MANOR.maxZ;
}
function getDraynorManorEscape(): { x: number; z: number } {
return { x: 3125, z: 3370 };
}

console.log('Initializing pathfinding...');
initPathfinding();

Expand Down Expand Up @@ -115,7 +125,7 @@ const faladorWallTiles = [
let wallsBlocked = 0;
for (const [x, z] of faladorWallTiles) {
// Check if any wall flag is set on this tile
const hasWall = isFlagged(x, z, 0,
const hasWall = isFlagged(x!, z!, 0,
CollisionFlag.WALL_NORTH | CollisionFlag.WALL_EAST |
CollisionFlag.WALL_SOUTH | CollisionFlag.WALL_WEST);
if (hasWall) wallsBlocked++;
Expand All @@ -131,7 +141,7 @@ if (wallsBlocked > 0) {

// Check Lumbridge castle door tiles are walkable (walls removed by door mask)
const lumbridgeDoorTile = [3217, 3218]; // known door position
const doorWalkable = isTileWalkable(0, lumbridgeDoorTile[0], lumbridgeDoorTile[1]);
const doorWalkable = isTileWalkable(0, lumbridgeDoorTile[0]!, lumbridgeDoorTile[1]!);
if (doorWalkable) {
console.log(` PASS: Lumbridge castle door tile is walkable (door mask working)`);
passed++;
Expand All @@ -142,52 +152,24 @@ if (doorWalkable) {
passed++; // still a pass - the path test above validates routing works
}

console.log('\n--- Multi-Segment Long Distance Tests ---');

// These destinations are >256 tiles from source, requiring multi-segment routing
console.log('\n--- Long Distance Tests ---');

function testMultiSegment(
label: string,
srcX: number, srcZ: number,
destX: number, destZ: number,
opts: { maxDist?: number } = {}
) {
const maxDist = opts.maxDist ?? 15;
const path = findMultiSegmentPath(0, srcX, srcZ, destX, destZ);

if (path.length === 0) {
console.log(` FAIL: ${label} (no path found)`);
failed++;
return;
}

const last = path[path.length - 1]!;
const dist = Math.sqrt(Math.pow(last.x - destX, 2) + Math.pow(last.z - destZ, 2));

if (dist > maxDist) {
console.log(` FAIL: ${label} (path ends at (${last.x}, ${last.z}), ${dist.toFixed(0)} tiles away)`);
failed++;
return;
}

console.log(` PASS: ${label} (${path.length} waypoints, ends at (${last.x}, ${last.z}), dist: ${dist.toFixed(0)})`);
passed++;
}
// These destinations are far from source, testing the 2048x2048 BFS grid

// Melzar's Maze to Yanille (~340 tiles)
testMultiSegment("Melzar's Maze to Yanille",
test("Melzar's Maze to Yanille",
2923, 3206, 2605, 3090, { maxDist: 20 });

// Lumbridge to Ardougne (~560 tiles)
testMultiSegment('Lumbridge to Ardougne',
test('Lumbridge to Ardougne',
3222, 3218, 2662, 3305, { maxDist: 20 });

// Varrock to Falador (~250 tiles, borderline single-segment)
testMultiSegment('Varrock to Falador',
// Varrock to Falador (~250 tiles)
test('Varrock to Falador',
3210, 3428, 2964, 3378, { maxDist: 15 });

// Lumbridge to Draynor (short distance, should still work via multi-segment)
testMultiSegment('Lumbridge to Draynor (short, via multi-segment)',
// Lumbridge to Draynor
test('Lumbridge to Draynor',
3222, 3218, 3092, 3243, { maxDist: 10 });

console.log('\n--- Door Detection Along Path Tests ---');
Expand Down Expand Up @@ -262,8 +244,8 @@ if (manorEscapePath.length > 0) {
failed++;
}

// Test multi-segment path from escape exit to Lumbridge
testMultiSegment('Escape exit to Lumbridge (via multi-segment)',
// Test path from escape exit to Lumbridge
test('Escape exit to Lumbridge',
3125, 3370, 3222, 3218, { maxDist: 15 });

console.log('\n--- Water / Ocean Avoidance Tests ---');
Expand Down Expand Up @@ -317,14 +299,14 @@ for (const { label, x, z } of landTestTiles) {
}
}

console.log('\n--- Cross-Continent Multi-Segment Tests ---');
console.log('\n--- Cross-Continent Tests ---');

// Wizards Tower to Tree Gnome Stronghold — the original failing route (~700 tiles)
testMultiSegment('Wizards Tower to Tree Gnome Stronghold',
test('Wizards Tower to Tree Gnome Stronghold',
3109, 3162, 2450, 3420, { maxDist: 35 });

// Lumbridge to Yanille (southwest, must avoid ocean)
testMultiSegment('Lumbridge to Yanille',
test('Lumbridge to Yanille',
3222, 3218, 2605, 3090, { maxDist: 25 });

console.log(`\n========== RESULTS ==========`);
Expand Down
Loading