Repo cleanup: remove ~3.3GB of cruft, dead code, and stale docs

Disk/data:
- Delete scripts/ entirely: 3.2GB of gitignored Jan-Feb run telemetry
  plus the legacy per-skill script/lab_log layout superseded by bots/
  (tracked files remain recoverable from git history)
- Prune bots/ to the 10 most-used bots (claude46, Codex7, max, codexpl8,
  runepl8, maxbittker, codex53, aaravA, codex_53, multibot) + _template;
  all removed dirs archived to ~/rs-sdk-cleanup-archive-2026-07-13.tgz
  along with stream/ (held live Twitch/bot/Anthropic credentials)
- Remove orphaned sdk/node_modules + sdk/bun.lock (pathfinder is
  vendored in server/vendor; sdk has no deps of its own)

Git hygiene:
- Untrack sdk/collision-data.json (22MB blob per regen; kept on disk,
  regen curl documented in DEVELOPERS.md and .gitignore)
- gitignore backups/, .obsidian/, and engine public/client/*.js (the
  gap that let stale client.js shadow fresh webclient builds return)
- Delete the untracked stale public/client/client.js shadow copy

Dead code/config:
- Drop unused deps: tmi.js, @types/tmi.js, ts-morph
- Drop dead tsconfig globs (stream/, bot_arcs/)
- Remove unused nix toolchain (flake.nix, flake.lock, .envrc)

Docs:
- GEMINI.md/agents.md were byte-identical 4-month-stale copies of
  claude.md; replace with symlinks so all tools read one file
- DEVELOPERS.md: remove abolished copy-client.js-to-engine step and
  nonexistent agent//javaclient sections; document collision regen
- sdk/README.md: fix reference to nonexistent scripts/example-remote.ts
- Remove stale sdk/SDK_DESIGN.md and misleading sdk/test/test_status.md

Also commit the forgotten sdk/test/click-dialog-by-text.test.ts (tests
a shipped API, was never git-added).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Fable 5 committed 2026-07-13 11:48:15 -04:00
1 parent b30931f040
commit 716a9375f9
56 files changed
+199 -13854

No files matched your search

-1
View File
@@ -1 +0,0 @@
use flake
+5 -3
View File
@@ -10,9 +10,11 @@ public-client/
server.json
.env
# Nix
result
.direnv/
backups/
.obsidian/
# Regenerable: curl https://rs-sdk-demo.fly.dev/api/exportCollision > sdk/collision-data.json
sdk/collision-data.json
# Bot folders (except template)
bots/*/
+10 -26
View File
@@ -30,9 +30,9 @@ The game server handling world simulation, player logic, and network protocol.
```
engine/
├── src/ # Server source code
├── public/ # Static files served to clients
│ ├── client/ # Standard web client build
│ └── bot/ # Bot client build (with BotSDK)
├── public/ # Static assets (soundfont, wasm). Built clients are served
│ # directly from webclient/out — do NOT copy client.js here,
│ # it would shadow fresh builds (see commit 6569364d2)
├── view/ # EJS templates (bot.ejs for bot interface)
├── data/ # Runtime game data
├── prisma/ # Database schema and migrations
@@ -111,32 +111,16 @@ The interactive menu provides options to:
| `bun run build` | Build client bundles (standard + bot) |
| `bun run build:dev` | Build in development mode |
After building, copy to engine:
No copy step is needed after building — the engine serves the built bundles
directly from `webclient/out`.
### Collision data (`sdk/collision-data.json`)
Used by `sdk/pathfinding.ts` and not tracked in git. Regenerate it from a running server:
```sh
cp out/standard/client.js ../engine/public/client/client.js
cp out/bot/client.js ../engine/public/bot/client.js
curl https://rs-sdk-demo.fly.dev/api/exportCollision > sdk/collision-data.json
```
### Agent (`cd agent`)
| Command | Description |
|---------|-------------|
| `bun run gateway` | Start gateway (unified sync + controller) |
| `bun run gateway:dev` | Gateway with hot-reload |
| `bun run agent` | Start Claude Agent service |
| `bun run agent:dev` | Agent service with hot-reload |
| `bun run cli` | Run agent CLI |
| `bun cli.ts launch <bot> "goal"` | Launch browser + start agent |
| `bun cli.ts status` | View connected bots |
| `bun run login` | Automated login helper |
### Java Client (`cd javaclient`)
| Command | Description |
|---------|-------------|
| `./gradlew build` | Build the Java client |
| `./gradlew run --args="10 0 highmem members 32"` | Run the Java client |
---
## Workflow
-252
View File
@@ -1,252 +0,0 @@
# RS-Agent Bot Guide
You're here to play the mmo game through the progressive development of botting scripts, starting small then adapting to your desires and ideas.
It is strongly recommended to get started and make the first step towards your goals, then researching and learning as you go.
## First Time Setup
**Create a new bot using the setup script:**
Ask the user for a bot name (max 12 chars, alphanumeric). If they skip, use the command without a username to auto-generate a random 9-character name.
```bash
# With custom username
bun bots/create-bot.ts {username}
# Auto-generate random username
bun bots/create-bot.ts
# Use local server (sets SERVER=localhost in bot.env)
bun bots/create-bot.ts {username} --local
# Use a custom server
bun bots/create-bot.ts {username} --server=myserver.example.com
```
This automatically creates:
- `bots/{username}/bot.env` - Credentials with auto-generated password
- `bots/{username}/lab_log.md` - Session notes template
- `bots/{username}/script.ts` - Ready-to-run starter script
### Quick Start
1. Install dependencies: `bun install` (from project root)
2. Open project in your AI coding tool — the MCP server will be available automatically
3. Control your bot with suggestions.
### Tools
| Tool | Description |
|------|-------------|
| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. |
| `list_bots()` | List connected bots |
| `disconnect_bot(name)` | Disconnect a bot |
### Example
```typescript
// Just execute - auto-connects on first use
execute_code({
bot_name: "mybot",
code: `
const state = sdk.getState();
console.log('Position:', state.player.worldX, state.player.worldZ);
// Chop trees for 1 minute
const endTime = Date.now() + 60_000;
while (Date.now() < endTime) {
const tree = sdk.findNearbyLoc(/^tree$/i);
if (tree) await bot.chopTree(tree);
}
return sdk.getInventory();
`
})
```
See `mcp/README.md` for detailed API reference.
Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals.
## Session Workflow
This is a **persistent character** - you don't restart fresh each time. The workflow is:
### 1. Check World State First
Before writing any script, check where the bot is and what it has:
```bash
bun sdk/cli.ts {username}
```
This shows: position, inventory, skills, nearby NPCs/objects, and more.
**Exception**: Skip this if you just created the character and know it's at spawn.
**Tutorial Check**: If the character is in the tutorial area, call `await bot.skipTutorial()` before running any other scripts. The tutorial blocks normal gameplay.
### 2. Write Your Script
Edit `bots/{username}/script_name.ts` with your goal. Keep scripts focused on one task. you may write multiple scripts for different tasks and switch between them.
### 3. Run the Script
```bash
bun bots/{username}/script_name.ts
```
### 4. Observe and Iterate
Watch the output. After the script finishes (or fails), check state again:
```bash
bun sdk/cli.ts {username}
```
Record observations in `lab_log.md`, then improve the script.
## Script Duration Guidelines
**Start short, extend as you gain confidence:**
| Duration | Use When |
|----------|----------|
| **10s** | New script, single actions, untested logic, debugging |
| **30s-1 min** | Validated approach, building confidence |
| **5+ min** | Proven strategy, grinding runs. USE SPARINGLY |
A failed 5-minute run wastes more time than five 30 second diagnostic runs. **Fail fast and start simple.**
Look out for "I can't reach" messages - the solution is often to open closed gates or that the item isn't accessible.
Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item and npc locations, and shop information.
## SDK API Reference
For the complete method reference, see **[sdk/API.md](sdk/API.md)** (auto-generated from source).
**Quick overview:**
- `bot.*` - High-level actions that wait for effects to complete (chopTree, walkTo, attackNpc, etc.)
- `sdk.*` - Low-level methods that resolve on server acknowledgment (sendWalk, getState, findNearbyNpc, etc.)
### bot.* Quick Reference
| Method | What it does |
|--------|-------------|
| `walkTo(x, z, tolerance?)` | Pathfind to coords, opens doors along the way |
| `talkTo(target)` | Walk to NPC, start dialog |
| `interactNpc(target, option?)` | Walk to NPC, interact with any option (e.g. `'trade'`, `'fish'`) |
| `interactLoc(target, option?)` | Walk to loc, interact with any option (e.g. `'mine'`, `'smelt'`) |
| `attackNpc(target)` | Walk to NPC, start combat |
| `pickpocketNpc(target)` | Pickpocket NPC, detects XP gain vs stun |
| `castSpellOnNpc(target, spell)` | Cast combat spell on NPC |
| `chopTree(target?)` | Chop tree, wait for logs |
| `pickupItem(target)` | Pick up ground item |
| `openDoor(target?)` | Open a door or gate |
| `openBank()` | Open nearest bank |
| `depositItem(target, amount?)` | Deposit item to bank |
| `withdrawItem(slot, amount?)` | Withdraw item from bank |
| `openShop(target?)` | Open shop via shopkeeper NPC |
| `buyFromShop(target, amount?)` | Buy item from open shop |
| `sellToShop(target, amount?)` | Sell item to open shop |
| `equipItem(target)` | Equip from inventory |
| `unequipItem(target)` | Unequip to inventory |
| `eatFood(target)` | Eat food, returns HP gained |
| `useItemOnLoc(item, loc)` | Use inventory item on loc (e.g. fish on range) |
| `useItemOnNpc(item, npc)` | Use inventory item on NPC |
| `burnLogs(target?)` | Light logs with tinderbox |
| `fletchLogs(product?)` | Fletch logs with knife |
| `craftLeather(product?)` | Craft leather with needle |
| `smithAtAnvil(product)` | Smith bars at anvil |
| `dismissBlockingUI()` | Dismiss level-up dialogs (called automatically by all actions) |
| `navigateDialog(choices)` | Auto-click through dialog options |
| `skipTutorial()` | Skip the tutorial island |
### sdk.* Commonly Used Directly
| Method | What it does |
|--------|-------------|
| `getState()` | Full world state snapshot |
| `getSkill(name)` / `getSkillXp(name)` | Skill info |
| `getInventory()` / `findInventoryItem(pattern)` | Inventory queries |
| `findNearbyNpc(pattern)` / `findNearbyLoc(pattern)` | Find nearby entities |
| `findGroundItem(pattern)` | Find ground items |
| `getDialog()` | Current dialog state |
| `sendClickDialog(option)` | Click dialog option |
| `sendClickComponent(id)` | Click interface button |
| `sendDropItem(slot)` | Drop inventory item |
| `sendUseItem(slot)` | Use inventory item (bury, etc.) |
| `sendUseItemOnItem(src, dst)` | Combine two items |
| `sendSay(message)` | Send chat message |
| `waitForCondition(pred)` | Wait for state predicate |
| `waitForTicks(n)` | Wait n game ticks |
| `scanNearbyLocs(radius?)` | Extended-range loc scan |
---
### Dismiss Level-Up Dialogs
All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.) before executing. You don't need to call this manually in loops.
```typescript
// Only needed if using low-level sdk methods directly:
await bot.dismissBlockingUI();
// Or manually check
if (sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
}
```
### Error Handling
```typescript
const result = await bot.chopTree();
if (!result.success) {
console.log(`Failed: ${result.message}`);
// Handle failure - maybe walk somewhere else
}
```
## Project Structure
```
bots/
└── {username}/
├── bot.env # Credentials (BOT_USERNAME, PASSWORD, SERVER)
├── lab_log.md # Session notes and observations
└── script.ts # Current script
sdk/
├── index.ts # BotSDK (low-level)
├── actions.ts # BotActions (high-level)
├── cli.ts # CLI for checking state
└── types.ts # Type definitions
learnings/
├── banking.md
└── ...etc
wiki/
├── npcs/
├── items/
├── skills/
└── shops/
```
## Troubleshooting
**"No state received"** - Bot isn't connected to game. Open browser first or use `autoLaunchBrowser: true`.
**Script stalls** - Check for open dialogs (`state.dialog.isOpen`). Level-ups block everything.
**"Can't reach"** - Path is blocked. Try walking closer first, or find a different target.
**Wrong target** - Use more specific regex patterns: `/^tree$/i` not `/tree/i` (which matches "tree stump").
Start small and build up!
Symlink
+1
View File
@@ -0,0 +1 @@
claude.md
-252
View File
@@ -1,252 +0,0 @@
# RS-Agent Bot Guide
You're here to play the mmo game through the progressive development of botting scripts, starting small then adapting to your desires and ideas.
It is strongly recommended to get started and make the first step towards your goals, then researching and learning as you go.
## First Time Setup
**Create a new bot using the setup script:**
Ask the user for a bot name (max 12 chars, alphanumeric). If they skip, use the command without a username to auto-generate a random 9-character name.
```bash
# With custom username
bun bots/create-bot.ts {username}
# Auto-generate random username
bun bots/create-bot.ts
# Use local server (sets SERVER=localhost in bot.env)
bun bots/create-bot.ts {username} --local
# Use a custom server
bun bots/create-bot.ts {username} --server=myserver.example.com
```
This automatically creates:
- `bots/{username}/bot.env` - Credentials with auto-generated password
- `bots/{username}/lab_log.md` - Session notes template
- `bots/{username}/script.ts` - Ready-to-run starter script
### Quick Start
1. Install dependencies: `bun install` (from project root)
2. Open project in your AI coding tool — the MCP server will be available automatically
3. Control your bot with suggestions.
### Tools
| Tool | Description |
|------|-------------|
| `execute_code(bot_name, code)` | Run code on a bot. Auto-connects on first use. |
| `list_bots()` | List connected bots |
| `disconnect_bot(name)` | Disconnect a bot |
### Example
```typescript
// Just execute - auto-connects on first use
execute_code({
bot_name: "mybot",
code: `
const state = sdk.getState();
console.log('Position:', state.player.worldX, state.player.worldZ);
// Chop trees for 1 minute
const endTime = Date.now() + 60_000;
while (Date.now() < endTime) {
const tree = sdk.findNearbyLoc(/^tree$/i);
if (tree) await bot.chopTree(tree);
}
return sdk.getInventory();
`
})
```
See `mcp/README.md` for detailed API reference.
Code runs in an async context with `bot` (BotActions) and `sdk` (BotSDK) available as globals.
## Session Workflow
This is a **persistent character** - you don't restart fresh each time. The workflow is:
### 1. Check World State First
Before writing any script, check where the bot is and what it has:
```bash
bun sdk/cli.ts {username}
```
This shows: position, inventory, skills, nearby NPCs/objects, and more.
**Exception**: Skip this if you just created the character and know it's at spawn.
**Tutorial Check**: If the character is in the tutorial area, call `await bot.skipTutorial()` before running any other scripts. The tutorial blocks normal gameplay.
### 2. Write Your Script
Edit `bots/{username}/script_name.ts` with your goal. Keep scripts focused on one task. you may write multiple scripts for different tasks and switch between them.
### 3. Run the Script
```bash
bun bots/{username}/script_name.ts
```
### 4. Observe and Iterate
Watch the output. After the script finishes (or fails), check state again:
```bash
bun sdk/cli.ts {username}
```
Record observations in `lab_log.md`, then improve the script.
## Script Duration Guidelines
**Start short, extend as you gain confidence:**
| Duration | Use When |
|----------|----------|
| **10s** | New script, single actions, untested logic, debugging |
| **30s-1 min** | Validated approach, building confidence |
| **5+ min** | Proven strategy, grinding runs. USE SPARINGLY |
A failed 5-minute run wastes more time than five 30 second diagnostic runs. **Fail fast and start simple.**
Look out for "I can't reach" messages - the solution is often to open closed gates or that the item isn't accessible.
Read and grep in the learnings/ and wiki/ folder for tips, skill guides, item and npc locations, and shop information.
## SDK API Reference
For the complete method reference, see **[sdk/API.md](sdk/API.md)** (auto-generated from source).
**Quick overview:**
- `bot.*` - High-level actions that wait for effects to complete (chopTree, walkTo, attackNpc, etc.)
- `sdk.*` - Low-level methods that resolve on server acknowledgment (sendWalk, getState, findNearbyNpc, etc.)
### bot.* Quick Reference
| Method | What it does |
|--------|-------------|
| `walkTo(x, z, tolerance?)` | Pathfind to coords, opens doors along the way |
| `talkTo(target)` | Walk to NPC, start dialog |
| `interactNpc(target, option?)` | Walk to NPC, interact with any option (e.g. `'trade'`, `'fish'`) |
| `interactLoc(target, option?)` | Walk to loc, interact with any option (e.g. `'mine'`, `'smelt'`) |
| `attackNpc(target)` | Walk to NPC, start combat |
| `pickpocketNpc(target)` | Pickpocket NPC, detects XP gain vs stun |
| `castSpellOnNpc(target, spell)` | Cast combat spell on NPC |
| `chopTree(target?)` | Chop tree, wait for logs |
| `pickupItem(target)` | Pick up ground item |
| `openDoor(target?)` | Open a door or gate |
| `openBank()` | Open nearest bank |
| `depositItem(target, amount?)` | Deposit item to bank |
| `withdrawItem(slot, amount?)` | Withdraw item from bank |
| `openShop(target?)` | Open shop via shopkeeper NPC |
| `buyFromShop(target, amount?)` | Buy item from open shop |
| `sellToShop(target, amount?)` | Sell item to open shop |
| `equipItem(target)` | Equip from inventory |
| `unequipItem(target)` | Unequip to inventory |
| `eatFood(target)` | Eat food, returns HP gained |
| `useItemOnLoc(item, loc)` | Use inventory item on loc (e.g. fish on range) |
| `useItemOnNpc(item, npc)` | Use inventory item on NPC |
| `burnLogs(target?)` | Light logs with tinderbox |
| `fletchLogs(product?)` | Fletch logs with knife |
| `craftLeather(product?)` | Craft leather with needle |
| `smithAtAnvil(product)` | Smith bars at anvil |
| `dismissBlockingUI()` | Dismiss level-up dialogs (called automatically by all actions) |
| `navigateDialog(choices)` | Auto-click through dialog options |
| `skipTutorial()` | Skip the tutorial island |
### sdk.* Commonly Used Directly
| Method | What it does |
|--------|-------------|
| `getState()` | Full world state snapshot |
| `getSkill(name)` / `getSkillXp(name)` | Skill info |
| `getInventory()` / `findInventoryItem(pattern)` | Inventory queries |
| `findNearbyNpc(pattern)` / `findNearbyLoc(pattern)` | Find nearby entities |
| `findGroundItem(pattern)` | Find ground items |
| `getDialog()` | Current dialog state |
| `sendClickDialog(option)` | Click dialog option |
| `sendClickComponent(id)` | Click interface button |
| `sendDropItem(slot)` | Drop inventory item |
| `sendUseItem(slot)` | Use inventory item (bury, etc.) |
| `sendUseItemOnItem(src, dst)` | Combine two items |
| `sendSay(message)` | Send chat message |
| `waitForCondition(pred)` | Wait for state predicate |
| `waitForTicks(n)` | Wait n game ticks |
| `scanNearbyLocs(radius?)` | Extended-range loc scan |
---
### Dismiss Level-Up Dialogs
All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.) before executing. You don't need to call this manually in loops.
```typescript
// Only needed if using low-level sdk methods directly:
await bot.dismissBlockingUI();
// Or manually check
if (sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
}
```
### Error Handling
```typescript
const result = await bot.chopTree();
if (!result.success) {
console.log(`Failed: ${result.message}`);
// Handle failure - maybe walk somewhere else
}
```
## Project Structure
```
bots/
└── {username}/
├── bot.env # Credentials (BOT_USERNAME, PASSWORD, SERVER)
├── lab_log.md # Session notes and observations
└── script.ts # Current script
sdk/
├── index.ts # BotSDK (low-level)
├── actions.ts # BotActions (high-level)
├── cli.ts # CLI for checking state
└── types.ts # Type definitions
learnings/
├── banking.md
└── ...etc
wiki/
├── npcs/
├── items/
├── skills/
└── shops/
```
## Troubleshooting
**"No state received"** - Bot isn't connected to game. Open browser first or use `autoLaunchBrowser: true`.
**Script stalls** - Check for open dialogs (`state.dialog.isOpen`). Level-ups block everything.
**"Can't reach"** - Path is blocked. Try walking closer first, or find a different target.
**Wrong target** - Use more specific regex patterns: `/^tree$/i` not `/tree/i` (which matches "tree stump").
Start small and build up!
Symlink
+1
View File
@@ -0,0 +1 @@
claude.md
+1 -37
View File
@@ -1,17 +1,15 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "server",
"dependencies": {
"@inquirer/prompts": "^7.5.3",
"open": "^11.0.0",
"tmi.js": "^1.8.5",
"ts-morph": "^27.0.2",
},
"devDependencies": {
"@types/bun": "latest",
"@types/tmi.js": "^1.8.6",
},
"peerDependencies": {
"typescript": "^5",
@@ -47,24 +45,16 @@
"@inquirer/type": ["@inquirer/type@3.0.7", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA=="],
"@ts-morph/common": ["@ts-morph/common@0.28.1", "", { "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", "tinyglobby": "^0.2.14" } }, "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="],
"@types/tmi.js": ["@types/tmi.js@1.8.6", "", {}, "sha512-LVzNK7AxTMyh9qHLanAQR1o0I9XzfbIcXk85cx85igmCJHHO1Sm71sdhQ8Mj1WmRGzynPIoCXx6mVaFynWbsQw=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@@ -73,8 +63,6 @@
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
"code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -89,8 +77,6 @@
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
@@ -103,20 +89,12 @@
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
@@ -129,30 +107,16 @@
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tmi.js": ["tmi.js@1.8.5", "", { "dependencies": { "node-fetch": "^2.6.1", "ws": "^8.2.0" } }, "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"ts-morph": ["ts-morph@27.0.2", "", { "dependencies": { "@ts-morph/common": "~0.28.1", "code-block-writer": "^13.0.3" } }, "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w=="],
"type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
Generated
-27
View File
@@ -1,27 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1770169770,
"narHash": "sha256-awR8qIwJxJJiOmcEGgP2KUqYmHG4v/z8XpL9z8FnT1A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "aa290c9891fa4ebe88f8889e59633d20cc06a5f2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
-22
View File
@@ -1,22 +0,0 @@
{
description = "rs-sdk dev shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};
outputs = { nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
in
{
devShells = forAllSystems (pkgs: {
default = pkgs.mkShell {
packages = with pkgs; [
bun
];
};
});
};
}
+2 -5
View File
@@ -2,16 +2,13 @@
"name": "server",
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@types/tmi.js": "^1.8.6"
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@inquirer/prompts": "^7.5.3",
"open": "^11.0.0",
"tmi.js": "^1.8.5",
"ts-morph": "^27.0.2"
"open": "^11.0.0"
}
}
-433
View File
@@ -1,433 +0,0 @@
# Lab Log: Agility Training
## Overview
**Goal**: Train Agility from level 1 to 10+ using the Gnome Stronghold Agility Course
**Starting Preset**: `TestPresets.LUMBRIDGE_SPAWN` (fresh account at Lumbridge)
**Challenge**: The Gnome Stronghold is located very far northwest (~400+ tiles from Lumbridge). This requires a long travel phase before training can begin.
## Course Information
### Gnome Stronghold Agility Course
- **Location**: (2474, 3436) - Northwest of Ardougne
- **Requirements**: Level 1 Agility (starter course)
- **Members-only**: Yes (may be blocked on some servers)
### Course Layout (7 obstacles in order)
| # | Obstacle | Action | XP |
|---|----------|--------|-----|
| 1 | Log balance | Walk-across | 7.5 |
| 2 | Obstacle net | Climb-over | 7.5 |
| 3 | Tree branch | Climb | 5 |
| 4 | Balancing rope | Walk-on | 7.5 |
| 5 | Tree branch | Climb-down | 5 |
| 6 | Obstacle net | Climb-over | 7.5 |
| 7 | Obstacle pipe | Squeeze-through | 7.5 |
| - | Lap bonus | - | 39 |
| **Total** | | | **~86.5** |
### XP Requirements
- Level 10 = 1,154 XP
- Laps needed = ~14
---
## Run 001 - Initial Test
**Date**: 2026-01-27
**Timeout**: 10 min
**Stall timeout**: 90s
**Hypothesis**: The script should travel from Lumbridge to Gnome Stronghold and begin training.
### What Happened
Travel progressed well through the southern route:
1. Lumbridge (3222, 3218) -> West of Lumbridge ✓
2. Near Draynor Manor road ✓
3. Draynor area ✓ (took some HP damage from wandering monsters)
4. West of Draynor ✓
5. Port Sarim area ✓
6. Rimmington area ✓
7. **BLOCKED at (2908, 3319)** - South of Falador
### Outcome
**FAILED** - Path blocked south of Falador
The pathfinder returned "No path found" for all waypoints north of position (2908, 3319). This suggests:
- The Falador walls block direct entry
- The gate/entrance may not be pathable
- Or the area north is members-only content
### Analysis
Position (2908, 3319) is approximately:
- South of Falador's western walls
- Near the crafting guild area
- The city of Falador has walls that require using gates
The Gnome Stronghold is in a members-only area. The path north from Rimmington goes through:
- Falador (need to enter through a gate)
- Taverley (members-only)
- Catherby area (members-only)
- Gnome Stronghold (members-only)
### Root Cause
**Members-only content is not accessible from a F2P spawn point.**
The Gnome Stronghold Agility Course is the only level 1 agility course, but it's in a members-only area. Without members access, there is no way to train Agility from level 1 using LUMBRIDGE_SPAWN preset.
---
## Known Limitations
### 1. Members-Only Area
The Gnome Stronghold is in a members-only region. If the server doesn't support members content, travel will fail.
**Detection**: Script will log position when it gets stuck and throw an error.
### 2. Long Travel Time
The journey from Lumbridge to Gnome Stronghold is ~400+ tiles:
- Lumbridge (3222, 3218)
- Gnome Stronghold (2474, 3436)
- Requires passing through Draynor, Falador, Taverley
**Mitigation**: 90s stall timeout, 10 min total time limit.
### 3. Obstacles Have Directional Requirements
Some obstacles can only be approached from certain directions:
- `gnome_obstacle_net_1`: Must approach from south (z > loc z)
- `gnome_obstacle_net_2`: Must approach from north (z < loc z)
- `gnome_obstacle_pipe`: Must enter from south
**Mitigation**: Course is designed to flow in one direction. Script follows natural order.
---
## Obstacle Interaction Patterns
From `content/scripts/skill_agility/scripts/gnome_course.rs2`:
```rs2
[oploc1,gnome_log_balance]
~agility_force_move(75, human_walk_logbalance, movecoord(coord, 0, 0, -7));
[oploc1,gnome_obstacle_net_1]
if(coordz(coord) <= coordz(loc_coord)) {
mes("You can not do that from here.");
return;
}
~agility_climb_up(75, movecoord(coord, 0, 1, -2));
[oploc1,gnome_obstacle_pipe]
if(coordz(coord) > coordz(loc_coord)) {
mes("You can't enter the pipe from this side.");
return;
}
```
Key observations:
- Each obstacle awards 5-7.5 XP individually
- Completing all 7 obstacles in sequence awards 39 XP bonus
- Course progress tracked via `%gnome_course_progress` varp (1-7, resets to 0)
---
## Alternative Approaches Considered
### 1. Spawn at Gnome Stronghold
Could modify preset to spawn at the course, but this violates the methodology:
> "Preset is a constraint, not a variable"
### 2. Barbarian Outpost Course
Another option at level 35, but requires higher level and is also far from Lumbridge.
### 3. Wilderness Agility Course
Level 52 requirement, dangerous area. Not suitable for level 1.
---
## Conclusion
### STATUS: BLOCKED - MEMBERS-ONLY CONTENT
**Agility cannot be trained from level 1 using `TestPresets.LUMBRIDGE_SPAWN`.**
The only level 1 Agility course (Gnome Stronghold) is in a members-only area that is inaccessible from Lumbridge.
### Travel Test Results
| Waypoint | Coordinates | Status |
|----------|-------------|--------|
| Lumbridge | (3222, 3218) | Start |
| West of Lumbridge | (3200, 3210) | ✓ |
| Near Draynor Manor | (3170, 3220) | ✓ |
| Draynor area | (3100, 3250) | ✓ |
| West of Draynor | (3050, 3260) | ✓ |
| Port Sarim | (2980, 3270) | ✓ |
| Rimmington | (2920, 3280) | ✓ |
| South of Falador | (2920, 3350) | **BLOCKED** |
| Gnome Stronghold | (2474, 3436) | Unreachable |
**Maximum distance traveled**: ~315 tiles (Lumbridge to Rimmington)
**Remaining distance**: ~450 tiles to Gnome Stronghold
### What Would Be Needed
To train Agility from level 1, one of these would be required:
1. **Members-only preset** that spawns at or near Gnome Stronghold
2. **Server-side change** to make Gnome area accessible
3. **Alternative course** that doesn't exist in classic RS (e.g., a F2P agility course)
### Learnings
1. **Members-only content** - Agility is fundamentally a members-only skill in classic RS
2. **Pathfinding works** - The `bot.walkTo()` successfully navigated ~315 tiles
3. **Long-distance travel** - The waypoint system worked well for multi-leg journeys
4. **HP regeneration** - Bot took some combat damage but HP naturally regenerated during travel
---
## Run 002 - Fixed Waypoints (Falador Gate Route)
**Date**: 2026-01-27
**Change**: Fixed waypoints to route through Falador's south gate instead of walking straight north into walls
### Problem
Previous waypoints tried to go straight north from Rimmington (2920, 3280) to "South of Falador" (2920, 3350), which walked directly into Falador's walls and got stuck at (2908, 3319).
### Fix Applied
Changed waypoints to:
1. From Rimmington, go **EAST** first toward Falador south gate (~2965, 3305)
2. Enter through south gate entrance (~2965, 3340)
3. Navigate through Falador city center
4. Exit through west/north side toward Taverley
### What Happened
Travel progression:
1. Lumbridge (3222, 3218) -> West of Lumbridge ✓
2. Near Draynor Manor road ✓
3. Draynor area ✓
4. West of Draynor ✓
5. Port Sarim ✓
6. Rimmington (2921, 3271) ✓
7. **Approach Falador south gate (2961, 3302) ✓** ← NEW WAYPOINT
8. South gate entrance - struggled but got in via east side (3001, 3356) ✓
9. **Inside Falador south (2948, 3376) ✓** ← MADE IT INSIDE!
10. **Falador center (2951, 3395) ✓** ← NEW PROGRESS!
11. **BLOCKED at (2946, 3407)** - Cannot exit north to Taverley
### Outcome
**PARTIAL SUCCESS** - Waypoint fix worked, now blocked at members-only boundary
| Metric | Run 001 | Run 002 | Change |
|--------|---------|---------|--------|
| Distance traveled | ~315 tiles | ~550 tiles | +235 tiles |
| Stuck position | (2908, 3319) | (2946, 3407) | Inside Falador now |
| Progress | South of Falador walls | Inside Falador center | Much better |
| Remaining distance | ~450 tiles | ~473 tiles | Similar |
### Analysis
1. **Waypoint fix successful** - Bot no longer gets stuck at Falador walls
2. **Got inside Falador** - Bot reached (2946, 3407) which is inside the city
3. **Members-only boundary confirmed** - No path found to any waypoint north/west of Falador
4. **Taverley is blocked** - All paths to Taverley (2880, 3440) return "No path found"
### Root Cause Confirmed
**The Gnome Stronghold is in members-only territory.** The boundary is at the north edge of Falador. Everything beyond (Taverley, Catherby, Gnome Stronghold) is inaccessible without members access.
### Updated Travel Map
```
Lumbridge (3222, 3218)
↓ ~22 tiles
West of Lumbridge (3200, 3210) ✓
↓ ~30 tiles
Draynor area (3100, 3250) ✓
↓ ~50 tiles
Port Sarim (2980, 3270) ✓
↓ ~60 tiles
Rimmington (2920, 3280) ✓
↓ ~45 tiles (go EAST first!)
Falador South Gate (2965, 3305) ✓
↓ ~35 tiles
Inside Falador (2948, 3376) ✓
↓ ~25 tiles
Falador Center (2946, 3407) ✓
↓ BLOCKED - Members-only
Taverley (2880, 3440) ✗
↓
Gnome Stronghold (2474, 3436) ✗
```
**Total F2P traversable distance**: ~550 tiles (Lumbridge to Falador center)
---
## Run 003 - Manual Gate Navigation for Falador-Taverley
**Date**: 2026-01-27
**Change**: Implemented manual gate handling since pathfinder can't traverse the Falador-Taverley passage
### Problem from Run 002
Bot got stuck at Falador center (2946, 3407) because the pathfinder couldn't handle the gate/passage to Taverley. However, the gates ARE clickable and Taverley IS accessible on this server.
### Fix Applied
Added manual gate navigation:
1. Detect when bot is in "Falador gate zone" (x: 2920-2960, z: 3400-3450)
2. Walk northwest toward gate area with smaller waypoints
3. Use `getNearbyLocs()` to find gates/doors/passages
4. Use `sendInteractLoc()` to click/open gates
5. Handle any dialogs that appear
6. Walk through after clicking
Key new functions:
- `isPastTaverleyGate()` - Check if x < 2900 (past gate area)
- `isInFaladorGateZone()` - Detect when manual navigation needed
- `findAndUseGate()` - Search for and interact with gates
- `navigateFaladorToTaverley()` - Orchestrate gate passage
### What Happened
**MAJOR SUCCESS** - Bot passed through Falador-Taverley gate!
Travel progression:
1. Lumbridge -> Falador center ✓ (same as Run 002)
2. Detected "gate zone" at (2952, 3395) ✓
3. **Switched to manual gate navigation** ✓
4. Found doors at (2949, 3450) and gates at (2935, 3450) ✓
5. **Clicked Door, walked through** ✓ - Position changed (2949, 3450) -> (2944, 3455)
6. Found more gates at (2935, 3450) ✓
7. **Clicked Gate, walked through** ✓ - Position changed (2936, 3451) -> (2932, 3453)
8. **Exited to Taverley area** - reached (2880, 3434) ✓
9. Continued west toward Catherby - reached (2704, 3440) ✓
10. **NEW BLOCKER at (2641, 3448)** - Near White Wolf Mountain
### Gate Detection Output
```
Found 4 potential gates/doors:
- Door at (2949, 3450) dist=5.0 opts=[Open]
- Door at (2952, 3453) dist=8.0 opts=[Open]
- Gate at (2935, 3450) dist=10.0 opts=[Open]
- Gate at (2935, 3451) dist=10.0 opts=[Open]
```
The `findAndUseGate()` function successfully:
1. Found gates using `/gate|door|passage|entrance|exit|barrier/i` regex
2. Identified "Open" option on the gates
3. Walked closer when needed
4. Clicked the gate with `sendInteractLoc()`
5. Walked through after clicking
### Outcome
| Metric | Run 002 | Run 003 | Change |
|--------|---------|---------|--------|
| Distance traveled | ~550 tiles | ~600+ tiles | **+50+ tiles** |
| Stuck position | (2946, 3407) | (2625, 3398) | **Past Taverley!** |
| Progress | Falador center | West of Catherby | **Major progress** |
| Remaining distance | ~473 tiles | ~156 tiles | **-317 tiles!** |
### Analysis
1. **Gate handling worked!** - `findAndUseGate()` successfully found and clicked through gates
2. **Multiple gates needed** - Had to pass through at least 2 doors/gates in the area
3. **Taverley accessible** - This server DOES have Taverley accessible
4. **New blocker: White Wolf Mountain** - Around (2641, 3448) there's another obstacle
### New Blocker: White Wolf Mountain Area
After passing Taverley, the bot got stuck around (2641, 3448) / (2625, 3398). This area is:
- Between Taverley and Catherby
- Near White Wolf Mountain
- May have wolves that attack or terrain obstacles
The pathfinder returned "No path found" suggesting:
- Mountain terrain blocks direct path
- May need to go around (north or south)
- Or there's another gate/passage needed
### Updated Travel Map
```
Lumbridge (3222, 3218)
↓ ~22 tiles
West of Lumbridge (3200, 3210) ✓
↓ ~30 tiles
Draynor area (3100, 3250) ✓
↓ ~50 tiles
Port Sarim (2980, 3270) ✓
↓ ~60 tiles
Rimmington (2920, 3280) ✓
↓ ~45 tiles
Falador South Gate (2965, 3305) ✓
↓ ~35 tiles
Inside Falador (2948, 3376) ✓
↓ ~25 tiles
Falador Center (2952, 3395) ✓
↓ MANUAL GATE NAVIGATION
North Falador (2947, 3419) ✓
↓ Gate/Door at (2949, 3450) - CLICKED THROUGH
Near Taverley gate (2945, 3445) ✓
↓ Gate at (2935, 3450) - CLICKED THROUGH
Taverley area (2880, 3434) ✓ ← NEW PROGRESS!
↓ ~75 tiles
West of Taverley (2779, 3442) ✓ ← NEW PROGRESS!
↓ ~75 tiles
Toward Catherby (2704, 3440) ✓ ← NEW PROGRESS!
↓ BLOCKED
White Wolf Mountain area (2641, 3448) ✗
↓
Gnome Stronghold (2474, 3436) ✗
```
### Key Learnings
1. **Manual gate handling works** - When pathfinder fails, use `getNearbyLocs()` + `sendInteractLoc()`
2. **Position change detection** - Check if position changed significantly after clicking gate
3. **Walk after click** - Need to explicitly walk through after clicking gate open
4. **Multiple gates** - Some passages have multiple gates/doors in sequence
5. **Progressive discovery** - Each run reveals new blockers further along the path
### Next Steps
Need to handle White Wolf Mountain area:
1. Investigate what's at (2641, 3448) - wolves? terrain? gate?
2. May need to go around the mountain (north via Burthorpe or south)
3. Or find another gate/passage to click through
### Code Changes Summary
```typescript
// New helper to detect gate zone
function isInFaladorGateZone(ctx): boolean {
// x: 2920-2960, z: 3400-3450
}
// New helper to check if past gate
function isPastTaverleyGate(ctx): boolean {
return state.player.worldX < 2900;
}
// Search and use gates
async function findAndUseGate(ctx): Promise<boolean> {
const locs = ctx.sdk.getNearbyLocs();
const gateKeywords = /gate|door|passage|entrance|exit|barrier/i;
// Find gates, click Open option, walk through
}
// Orchestrate gate passage
async function navigateFaladorToTaverley(ctx): Promise<boolean> {
// Walk to gate area waypoints
// Use findAndUseGate() when blocked
}
```
-291
View File
@@ -1,291 +0,0 @@
/**
* Agility Training Script
*
* Goal: Train Agility from level 1 to 10+ using the Gnome Stronghold Agility Course
*
* Uses GNOME_AGILITY preset to start directly at the course.
*
* Course obstacles (in order):
* 1. Log balance (Walk-across) - 7.5 XP
* 2. Obstacle net (Climb-over) - 7.5 XP
* 3. Tree branch (Climb) - 5 XP
* 4. Balancing rope (Walk-on) - 7.5 XP
* 5. Tree branch (Climb-down) - 5 XP
* 6. Obstacle net (Climb-over) - 7.5 XP
* 7. Obstacle pipe (Squeeze-through) - 7.5 XP + 39 XP bonus for completing course
*
* Total per lap: ~86.5 XP
* XP needed for level 10: 1,154 XP = ~14 laps
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Gnome Stronghold Agility Course start location
const GNOME_AGILITY_START = { x: 2474, z: 3436 };
// XP requirements
const TARGET_LEVEL = 10;
const XP_FOR_LEVEL_10 = 1154;
// Course obstacle definitions (in order around the course)
const COURSE_OBSTACLES = [
{ name: /log balance/i, option: /walk/i, description: 'Log balance' },
{ name: /obstacle net/i, option: /climb/i, description: 'First net' },
{ name: /tree branch/i, option: /^climb$/i, description: 'Tree branch up' },
{ name: /balancing rope/i, option: /walk/i, description: 'Balancing rope' },
{ name: /tree branch/i, option: /climb-down/i, description: 'Tree branch down' },
{ name: /obstacle net/i, option: /climb/i, description: 'Second net' },
{ name: /obstacle pipe/i, option: /squeeze/i, description: 'Obstacle pipe' },
];
/**
* Calculate distance to a point
*/
function distanceTo(ctx: ScriptContext, x: number, z: number): number {
const state = ctx.sdk.getState();
if (!state?.player) return Infinity;
const dx = x - state.player.worldX;
const dz = z - state.player.worldZ;
return Math.sqrt(dx * dx + dz * dz);
}
/**
* Find the next agility obstacle to interact with
*/
function findNextObstacle(ctx: ScriptContext, courseIndex: number) {
const locs = ctx.sdk.getNearbyLocs();
const target = COURSE_OBSTACLES[courseIndex % COURSE_OBSTACLES.length];
if (!target) return null;
// Find obstacle matching this course position
const obstacle = locs.find(loc =>
target.name.test(loc.name) &&
loc.optionsWithIndex.some(o => target.option.test(o.text))
);
// Debug: if not found, log nearby agility-like obstacles
if (!obstacle && courseIndex === 0) {
const agilityLocs = locs.filter(loc =>
loc.optionsWithIndex.some(o =>
/walk|climb|squeeze|balance|cross/i.test(o.text)
)
);
if (agilityLocs.length > 0) {
console.log(`Nearby agility objects: ${agilityLocs.map(l => `${l.name}@(${l.x},${l.z})`).join(', ')}`);
}
}
return obstacle;
}
/**
* Complete one obstacle
*/
async function completeObstacle(ctx: ScriptContext, courseIndex: number): Promise<boolean> {
const xpBefore = ctx.sdk.getSkill('Agility')?.experience ?? 0;
const target = COURSE_OBSTACLES[courseIndex % COURSE_OBSTACLES.length];
const startTick = ctx.sdk.getState()?.tick ?? 0;
const obstacle = findNextObstacle(ctx, courseIndex);
if (!obstacle) {
console.warn(`Could not find obstacle: ${target?.description}`);
return false;
}
const opt = obstacle.optionsWithIndex.find(o => target!.option.test(o.text));
if (!opt) {
console.warn(`No matching option on ${obstacle.name}`);
return false;
}
console.log(`Attempting: ${obstacle.name} (${opt.text})`);
// Walk closer if needed
if (obstacle.distance > 3) {
await ctx.bot.walkTo(obstacle.x, obstacle.z, 2);
}
// Interact with obstacle
await ctx.sdk.sendInteractLoc(obstacle.x, obstacle.z, obstacle.id, opt.opIndex);
// Wait for XP gain or significant position change
const startX = ctx.sdk.getState()?.player?.worldX ?? 0;
const startZ = ctx.sdk.getState()?.player?.worldZ ?? 0;
try {
await ctx.sdk.waitForCondition(state => {
// Dismiss dialogs (level-up messages)
if (state.dialog.isOpen) {
ctx.sdk.sendClickDialog(0).catch(() => {});
return false;
}
// XP gain
const xpNow = state.skills.find(s => s.name === 'Agility')?.experience ?? 0;
if (xpNow > xpBefore) return true;
// Significant position change (obstacle completed)
const dx = Math.abs((state.player?.worldX ?? 0) - startX);
const dz = Math.abs((state.player?.worldZ ?? 0) - startZ);
if (dx > 4 || dz > 4) return true;
// Check for "can not do that from here" message
for (const msg of state.gameMessages) {
if (msg.tick > startTick && msg.text.toLowerCase().includes('can not do that')) {
return true;
}
}
return false;
}, 20000);
const xpAfter = ctx.sdk.getSkill('Agility')?.experience ?? 0;
if (xpAfter > xpBefore) {
console.log(` XP gained: +${xpAfter - xpBefore}`);
return true;
}
// Position changed but no XP - might need to re-attempt
const endX = ctx.sdk.getState()?.player?.worldX ?? 0;
const endZ = ctx.sdk.getState()?.player?.worldZ ?? 0;
if (Math.abs(endX - startX) > 4 || Math.abs(endZ - startZ) > 4) {
console.log(` Position changed - obstacle may be complete`);
return true;
}
console.warn(` Obstacle did not complete`);
return false;
} catch {
console.warn(` Timeout waiting for obstacle completion`);
return false;
}
}
/**
* Complete one full lap of the course
*/
async function completeLap(ctx: ScriptContext): Promise<boolean> {
console.log('Starting new lap...');
for (let i = 0; i < COURSE_OBSTACLES.length; i++) {
const success = await completeObstacle(ctx, i);
if (!success) {
// Try to find any nearby obstacle and continue
const locs = ctx.sdk.getNearbyLocs();
const anyObstacle = locs.find(loc =>
loc.optionsWithIndex.some(o =>
/walk|climb|squeeze|balance/i.test(o.text)
)
);
if (anyObstacle) {
console.log(`Found alternate obstacle: ${anyObstacle.name}`);
const opt = anyObstacle.optionsWithIndex.find(o =>
/walk|climb|squeeze|balance/i.test(o.text)
);
if (opt) {
await ctx.sdk.sendInteractLoc(anyObstacle.x, anyObstacle.z, anyObstacle.id, opt.opIndex);
try {
await ctx.sdk.waitForStateChange(5000);
} catch { /* ignore */ }
}
}
}
}
return true;
}
/**
* Main training function
*/
async function trainAgility(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state?.player) throw new Error('No initial state');
const startXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
const startLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
console.log(`=== Agility Training ===`);
console.log(`Starting Level: ${startLevel}`);
console.log(`Starting XP: ${startXp}`);
console.log(`Target: Level ${TARGET_LEVEL} (${XP_FOR_LEVEL_10} XP)`);
console.log(`Position: (${state.player.worldX}, ${state.player.worldZ})`);
// Train at the agility course
let lapsCompleted = 0;
const MAX_LAPS = 20; // Safety limit
while (lapsCompleted < MAX_LAPS) {
const currentXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
const currentLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
// Check if we've reached our goal
if (currentLevel >= TARGET_LEVEL) {
console.log(`\nGoal reached! Level ${currentLevel}`);
break;
}
console.log(`\n--- Lap ${lapsCompleted + 1} ---`);
console.log(`Level: ${currentLevel}, XP: ${currentXp}/${XP_FOR_LEVEL_10}`);
// Walk to the course START (Log balance location) before each lap
const LOG_BALANCE_LOCATION = { x: 2474, z: 3438 };
let distToStart = distanceTo(ctx, LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z);
if (distToStart > 10) {
console.log(`Walking to course start (Log balance)...`);
await ctx.bot.walkTo(LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z, 5);
distToStart = distanceTo(ctx, LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z);
// If still far, use raw walk
if (distToStart > 10) {
console.log(` Using raw walk to course start...`);
await ctx.sdk.sendWalk(LOG_BALANCE_LOCATION.x, LOG_BALANCE_LOCATION.z, true);
await new Promise(r => setTimeout(r, 3000));
}
}
await completeLap(ctx);
lapsCompleted++;
// Dismiss any level-up dialogs
await ctx.bot.dismissBlockingUI();
}
// Final summary
const finalXp = ctx.sdk.getSkill('Agility')?.experience ?? 0;
const finalLevel = ctx.sdk.getSkill('Agility')?.baseLevel ?? 1;
console.log('\n=== Training Complete ===');
console.log(`Level: ${startLevel} -> ${finalLevel}`);
console.log(`XP: ${startXp} -> ${finalXp} (+${finalXp - startXp})`);
console.log(`Laps completed: ${lapsCompleted}`);
if (finalLevel < TARGET_LEVEL) {
console.warn(`Did not reach target level ${TARGET_LEVEL}`);
}
}
async function main() {
const username = `ag${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.GNOME_AGILITY);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
await trainAgility(ctx);
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000, // 10 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-73
View File
@@ -1,73 +0,0 @@
# Lab Log: al-kharid-travel
Goal: Reach Al Kharid as fast as possible from Lumbridge (starting with 0gp).
## Key Finding
**There is NO free route** - the toll gate is the only entrance to Al Kharid. You must pay 10gp.
## Final Solution
**Time: ~33-41 seconds** (from 0gp start)
1. Walk to Lumbridge general store
2. Sell bronze sword (gives exactly 10gp)
3. Walk to toll gate
4. Pay toll and pass through
---
## Phase 1: Toll Route Only (with 10gp)
### Run 001-010
- Tested toll gate mechanics
- Found that `openDoor()` doesn't work for toll gates
- Need direct `sendInteractLoc` + dialog handling
- After paying, need retry loop for walking through
- **Result: ~17-18s** with 10gp start
---
## Phase 2: Long Route Testing
### Run 011-017
- Tested going around the toll gate
- All routes blocked by fences/walls
- Confirmed: Al Kharid is fully enclosed, toll is mandatory
---
## Phase 3: Coin Sourcing
### Run 018 - Selling Items
- Used `bot.openShop()` and `bot.sellToShop()`
- Bronze sword sells for 10gp (exactly what we need!)
- **Result: 32-41s** from 0gp start
### Breakdown
| Step | Time |
|------|------|
| Walk to shop | ~8s |
| Open shop + sell sword | ~7s |
| Walk to gate | ~8s |
| Pay toll + walk through | ~10s |
| **Total** | **~33s** |
---
## Learnings
### 1. Strategic Findings
- **No free route** - toll gate is the only entrance
- Bronze sword sells for exactly 10gp at general store
- Selling is faster than combat for sourcing coins
- Gate requires: click → dialog → pay → retry walk
### 2. Process & Tooling Reflections
- `bot.openShop()` and `bot.sellToShop()` work well
- Shop sells are instant, combat takes 4-5s per kill
- Dialog handling needs click-through for "continue" prompts
### 3. SDK Issues & Gaps
- `openDoor()` doesn't work for toll-style gates
- `walkTo` can fail on recently-opened gates (needs retry)
-136
View File
@@ -1,136 +0,0 @@
/**
* Al Kharid Travel Script
*
* Simple approach: sell bow at general store, pay gate toll
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Helper to walk using raw sendWalk (bypasses broken pathfinding)
async function rawWalkTo(ctx: ScriptContext, targetX: number, targetZ: number, timeout = 10000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const pos = ctx.sdk.getState()?.player;
if (!pos) break;
const dist = Math.sqrt(Math.pow(targetX - pos.worldX, 2) + Math.pow(targetZ - pos.worldZ, 2));
if (dist <= 3) {
console.log(`Arrived at (${pos.worldX}, ${pos.worldZ})`);
return true;
}
await ctx.sdk.sendWalk(targetX, targetZ, true);
await new Promise(r => setTimeout(r, 600));
}
const finalPos = ctx.sdk.getState()?.player;
console.log(`Walk ended at (${finalPos?.worldX}, ${finalPos?.worldZ})`);
return false;
}
async function main() {
const username = `ak${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
const pos = ctx.sdk.getState()?.player;
console.log(`Starting at (${pos?.worldX}, ${pos?.worldZ})`);
if (pos && pos.worldX >= 3270) {
console.log('Already in Al Kharid!');
return;
}
// 1. Walk to general store
console.log('Walking to general store...');
await rawWalkTo(ctx, 3212, 3246);
// Find and open shop
const npcs = ctx.sdk.getState()?.nearbyNpcs;
console.log(`Nearby: ${npcs?.slice(0, 5).map((n: any) => n.name).join(', ')}`);
const openResult = await ctx.bot.openShop(/shop.*keeper/i);
if (!openResult.success) {
throw new Error(`Shop failed: ${openResult.message}`);
}
// Sell bow
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 'all');
console.log(sellResult.message);
await ctx.bot.closeShop();
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
console.log(`Have ${coins?.count ?? 0}gp`);
// 2. Walk to gate
console.log('Walking to gate...');
await rawWalkTo(ctx, 3267, 3228);
// 3. Talk to border guard
console.log('Talking to guard...');
const talkResult = await ctx.bot.talkTo(/border guard/i);
if (!talkResult.success) {
// Try generic guard
const talkResult2 = await ctx.bot.talkTo(/guard/i);
if (!talkResult2.success) {
throw new Error(`Guard not found`);
}
}
// 4. Pay toll via dialog
let paid = false;
for (let i = 0; i < 20; i++) {
const state = ctx.sdk.getState();
if (!state?.dialog.isOpen) {
if (paid) break; // Dialog closed after paying
await new Promise(r => setTimeout(r, 300));
continue;
}
const opts = state.dialog.options;
console.log(`Dialog: ${opts.map((o: any) => o.text).join(' | ') || '(continue)'}`);
const yesOpt = opts.find((o: any) => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
paid = true;
await new Promise(r => setTimeout(r, 500));
continue; // Keep clearing dialogs
}
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 400));
}
// Make sure dialog is closed
for (let i = 0; i < 5 && ctx.sdk.getState()?.dialog.isOpen; i++) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
// 5. Walk through gate into Al Kharid
console.log('Walking through gate...');
await rawWalkTo(ctx, 3275, 3227);
const finalPos = ctx.sdk.getState()?.player;
if (finalPos && finalPos.worldX >= 3270) {
console.log(`Success! At (${finalPos.worldX}, ${finalPos.worldZ})`);
} else {
throw new Error(`Failed at (${finalPos?.worldX}, ${finalPos?.worldZ})`);
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 2 * 60 * 1000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-528
View File
@@ -1,528 +0,0 @@
# Lab Log: combat-trainer
**Goal**: Maximize Attack + Strength + Defence + Hitpoints levels over 5 minutes.
**Strategy**: Kill goblins near Lumbridge while cycling combat styles for balanced XP gain.
---
## Run 001 - (pending)
**Setup**: LUMBRIDGE_SPAWN preset - standard tutorial-complete items (bronze sword, shield, dagger, axe, arrows, etc.)
### Hypotheses
1. The new combat SDK features (`player.combat`, `npc.inCombat`, `combatEvents`) will help us track combat state effectively
2. Cycling combat styles every 3 kills will provide balanced training across Attack/Strength/Defence
3. 45 second stall timeout should accommodate natural combat lulls
### Testing
- `player.combat.inCombat` - does this accurately reflect when we're fighting?
- `npc.targetIndex` - can we detect if an NPC is already fighting someone else?
- `combatEvents` array - is damage_dealt/damage_taken tracked reliably?
- Combat style switching - does `sendSetCombatStyle()` work?
### Results
**Outcome**: TIMEOUT (ran full 5 minutes)
**Final Stats**: Attack 12, Hitpoints 12, Combat Level 7+ (from level 3!)
### Observations
**GOOD NEWS**: Training actually worked great! XP was gained steadily despite our broken combat tracking.
**SDK Issues Found**:
1. **`npc.hp` and `npc.maxHp` always 0** - NPC health tracking isn't populating. All goblins show `HP: 0/0`. This makes it impossible to detect NPC death by health.
2. **`player.combat.inCombat` unreliable** - Our `waitForCombatEnd()` exits immediately with "lost_target" even while combat is ongoing (proven by XP gains in state snapshots).
3. **`npc.inCombat` false positives** - Getting "already in combat" for goblins that we should be able to attack. May need to check if NPC is targeting US specifically vs someone else.
4. **`combatEvents` not visible in state snapshots** - Can't confirm if damage_dealt/damage_taken events are firing. Need to log these explicitly.
### Root Cause
Our `waitForCombatEnd()` checks `player.combat.inCombat` which returns false almost immediately, causing us to think combat ended when it hasn't. Meanwhile the actual combat continues in the background and we gain XP.
### Fix Ideas
1. Don't rely on `player.combat.inCombat` - instead wait for XP gains or NPC disappearance
2. Add timeout-based combat wait (e.g., wait 5-10 seconds after attack)
3. Track the NPC index and wait for it to disappear from nearbyNpcs
4. Use `combatEvents` array to detect damage being dealt
---
## SDK Feedback
### Combat Status Fields Being Tested
1. **PlayerCombatState** (`player.combat`)
- `inCombat: boolean` - player has a target
- `targetIndex: number` - who we're targeting
- `lastDamageTick: number` - when we last took damage
2. **NearbyNpc combat fields**
- `hp / maxHp / healthPercent` - NPC health tracking
- `targetIndex: number` - who the NPC is targeting
- `inCombat: boolean` - is NPC in combat with anyone
3. **CombatEvent array** (`state.combatEvents`)
- `type: 'damage_taken' | 'damage_dealt' | 'kill'`
- `damage: number`
- `sourceIndex / targetIndex` - who hit who
### Questions Answered
- [x] Is `player.combat.inCombat` reliable? **NO** - returns false even during active combat
- [x] Does `npc.inCombat` correctly indicate when NPCs are fighting? **UNCLEAR** - may have false positives
- [ ] Are combat events being logged correctly? **NEEDS TESTING** - not visible in state snapshots
- [x] Is kill detection working (NPC disappears or HP=0)? **PARTIAL** - NPC disappearance works, but hp/maxHp always 0
---
## SDK Fixes Applied (by Claude)
**Date**: After Run 001 feedback
### Issues Fixed
1. **`player.combat.inCombat` now reliable** ✅
- **Root cause**: Only checked `targetId !== -1`, which resets between attack animations
- **Fix**: Now also checks `combatCycle > loopCycle` (400-tick window after any hit)
- **Test result**: `player.combat.inCombat: true` during combat (was false before)
2. **`npc.inCombat` improved** ✅
- Same fix applied - uses `combatCycle` in addition to `targetId`
3. **`npc.hp/maxHp` documented** ✅
- This is expected behavior - server only sends HP data when NPC takes damage
- Added JSDoc comments explaining: "NOTE: 0 until NPC takes damage"
- **Test result**: After first hit, health shows correctly (e.g., 7/7 → 6/7 → 5/7)
4. **`npc.combatCycle` field added** ✅
- New field exposed for scripts to do custom timing logic
- Value is `tick + 400` when NPC takes damage
5. **`combatEvents` ARE working** ✅
- Test showed `damage_dealt` events being tracked
- Note: Damage value may show 0 for misses/blocks
### Code Changes
- `webclient/src/bot/BotSDK.ts:354-358` - Player `inCombat` fix
- `webclient/src/bot/BotSDK.ts:611-619` - NPC `inCombat` fix
- `agent/types.ts` - Added `combatCycle` field and JSDoc docs
### Test Commands
```bash
# Run combat state test
bun test/combat-state.ts
# Run combat events test (more thorough)
bun test/combat-events.ts
```
### Recommended Script Changes
Your `waitForCombatEnd()` should now work better since `player.combat.inCombat` stays TRUE during combat. However, consider also checking:
```typescript
// More robust combat detection
const isInCombat = (): boolean => {
const state = sdk.getState();
const pc = state?.player?.combat;
if (!pc) return false;
// inCombat is now reliable (uses combatCycle internally)
return pc.inCombat;
};
// For NPC-specific checks, use combatCycle directly
const npcRecentlyHit = (npc: NearbyNpc, currentTick: number): boolean => {
return npc.combatCycle > currentTick;
};
```
**Please test and confirm if these fixes resolve your issues!**
---
## Run 002 - 2026-01-25 (after SDK fixes)
**Outcome**: TIMEOUT (ran full 5 minutes) - 8+ kills, **17,490 XP gained!**
### Results
| Metric | Run 001 | Run 002 | Improvement |
|--------|---------|---------|-------------|
| Kills | 1 | 8+ | 8x |
| Total XP | ~1600 | 17,490 | 10x |
| Attack | 0→? | +8,800 | Working |
| Strength | 0 | +4,400 | Working |
| Defence | 0 | 0 | (switched style too late) |
| Hitpoints | +390 | +4,290 | 10x |
| Damage dealt | 0 | 20 | Now tracked |
| Damage taken | 0 | 10 | Now tracked |
| Food eaten | 0 | 1 | Working |
| Looted | 0 | 5 | Working |
### SDK Fixes Verified
1. **`player.combat.inCombat` - FIXED** - Stays true during combat now
2. **`npc.combatCycle` - WORKING** - Reliable combat detection via tick comparison
3. **`combatEvents` - WORKING** - Damage tracking functional
4. **Kill detection - WORKING** - NPC disappearance detected correctly
### Script Fixes Applied
1. Fixed `waitForCombatEnd()` to use `combatCycle` comparison
2. Fixed food regex to not match "fishing net" (`/^(bread|shrimps?|...)$/i`)
### Remaining Issues
1. **HP: 0/0 still showing** - Expected behavior (0 until first damage)
2. **"Timeout waiting to attack"** - Pathing issues, need investigation
3. **"already in combat"** - May be competing with other players/NPCs
4. **Defence XP = 0** - Style switching happened too late in the run
### Next Steps
- Start with Defensive style to get Defence XP
- Better NPC selection (avoid ones being fought by others)
- Log HP values after first hit to confirm tracking
---
## Run 003 - 2026-01-24 (10 minute run with weapon upgrade)
**Setup**: Extended run with phased strategy and weapon upgrade path
### Hypotheses
1. **10 minutes allows for weapon upgrade trip** - Time to farm coins (~140gp), travel to Al Kharid, buy iron scimitar, return
2. **Iron scimitar major DPS boost** - +9 slash attack, +10 strength bonus vs bronze sword's +4/+1 - should significantly increase kill speed
3. **Starting with Defensive style** - Will fix the Defence XP = 0 problem from Run 002
4. **Phased approach**:
- Phase 1 (farming): Kill goblins with Defensive style, prioritize coin looting
- Phase 2 (upgrading): Walk to Al Kharid, buy iron scimitar, return
- Phase 3 (training): Continue with better weapon, cycle styles
### Script Changes
1. **Time extended to 10 minutes** (from 5)
2. **Stall timeout increased to 60s** (from 45s) to accommodate shop trip
3. **Combat style logic**:
- Phase 1: Always Defensive (for Defence XP)
- Phase 3: Cycle Def → Atk → Str → Def... every 3 kills
4. **Weapon upgrade system**:
- Triggers when coins >= 142 (112 scimitar + 10 toll + 20 buffer)
- Walks to Al Kharid gate (3268, 3227)
- Handles toll gate dialog
- Walks to Zeke's Scimitar Shop (3287, 3186)
- Buys and equips iron scimitar
- Returns to goblin area
5. **Improved looting**: Prioritizes coins over bones, larger pickup radius (5 tiles)
### Testing
- [ ] Does the phase system work correctly?
- [ ] Can we successfully buy from Al Kharid shop?
- [ ] Does the gate toll dialog handling work?
- [ ] Is Defence XP > 0 now?
- [ ] Is the iron scimitar significantly faster at killing?
### Results
**Outcome**: TIMEOUT (ran full 10 minutes) - 15+ kills before server issues
| Metric | Run 002 | Run 003 (partial) | Notes |
|--------|---------|-------------------|-------|
| Time limit | 5 min | 10 min | Extended |
| Kills | 8+ | 15+ | More time = more kills |
| Total XP | 17,490 | 44,020 | 2.5x improvement |
| Attack | +8,800 | +0 | (Defensive style used) |
| Strength | +4,400 | +33,200 | **BUG: Wrong style!** |
| Defence | 0 | 0 | Style 3 not working |
| Hitpoints | +4,290 | +10,820 | 2.5x |
| Coins collected | ? | 1 | Goblins drop very few |
### Observations
**CRITICAL BUG FOUND**: Combat style switching still not working properly!
- Set `COMBAT_STYLES.DEFENSIVE = 3` (was 2) because swords have 4 styles not 3
- But XP still going to Strength instead of Defence
- Need to verify sword combat style indices in-game
**Weapon upgrade not viable**: Only collected 1 coin in 10 minutes from goblins.
Goblins have poor coin drops - need different funding strategy (maybe sell bones?).
### Fixes Applied After Run 003
1. **Combat style indices corrected for swords**:
```typescript
COMBAT_STYLES = {
ACCURATE: 0, // Stab - Attack XP
AGGRESSIVE: 1, // Lunge - Strength XP
CONTROLLED: 2, // Slash - Shared XP
DEFENSIVE: 3, // Block - Defence XP
}
```
2. **Phase transition timeout**: Skip weapon upgrade after 15 kills if insufficient coins
3. **Training phase now uses Controlled style** (index 2) for balanced XP instead of cycling
### Server Issues
After initial run, game server became unresponsive to new logins.
- Multiple Java processes running (potential conflict)
- All tests failing with "Timeout waiting for login"
### Next Steps
- [x] Verify combat style indices work correctly (check in-game) - **FIXED in Run 003b!**
- [ ] Consider alternative funding: sell bones to general store
- [x] Test when server is restored
- [ ] May need to start with coins in preset for testing upgrade path
---
## Run 003b - 2026-01-25 (after combat style fix)
**Setup**: Same as Run 003 but with corrected combat style indices
### Results
**Outcome**: TIMEOUT (ran full 10 minutes) - 6 kills
| Metric | Run 003 | Run 003b | Notes |
|--------|---------|----------|-------|
| Kills | 15+ | 6 | More combat interruptions |
| Total XP | 44,020 | 29,680 | Lower due to fewer kills |
| Attack | +0 | +0 | Correct (using Defensive) |
| Strength | +33,200 | +0 | **FIXED!** |
| Defence | 0 | **+22,400** | **COMBAT STYLE FIX WORKS!** |
| Hitpoints | +10,820 | +7,280 | Proportional to kills |
| Coins | 1 | 2 | Still very few |
### Key Finding: COMBAT STYLE FIX VERIFIED!
**Defence XP is now being gained!** The fix to use style index 3 for Defensive (Block) was correct.
```
Switching to Block (Defence) style
...
XP Gained: Atk +0, Str +0, Def +22400, HP +7280
```
### Issues Observed
1. **Many "lost_target" results** - NPCs disappearing mid-combat
2. **"already in combat" errors** - Other players/NPCs competing for goblins
3. **"Timeout waiting to attack"** - Pathing issues reaching NPCs
4. **Only 6 kills in 10 min** vs 15+ before - need to investigate combat reliability
### Analysis
The combat style system now works correctly:
- Phase 1 (farming): Uses Block (style 3) → Defence XP ✓
- Phase 3 (training): Will use Slash (style 2) → Shared XP
The lower kill count suggests combat tracking/targeting needs improvement, but the core style switching is validated.
---
## Run 004 - 2026-01-25 (improved combat detection)
**Setup**: Added XP-based combat detection for more reliable kill tracking
### Combat Detection Improvements
1. Track XP at start of combat wait
2. Check XP gains during loop - if XP increased, combat is happening
3. Count NPC disappearance as kill if XP was gained
4. Reduced "lost_target" false negatives
### Results
**Outcome**: ERROR (browser disconnected after ~5 minutes)
| Metric | Run 003b | Run 004 | Notes |
|--------|----------|---------|-------|
| Kills counted | 6 | 4 | Still undercounting |
| Defence XP | 22,400 | **14,000** | Great progress! |
| Defence Level | ~20 | **30** | Level 30 in 5 min! |
| Hitpoints XP | 7,280 | 5,704 | Proportional |
| Hitpoints Level | ~15 | **22** | |
| Coins | 0 | 2+ | Picking up coins now |
### Key Findings
1. **Combat style confirmed working** - All XP going to Defence (0 Attack, 0 Strength)
2. **XP-based detection helps** - Combat starts being detected properly
3. **Kill counting still inconsistent** - 4 counted vs ~hundreds actual (based on XP)
4. **Browser stability issues** - Connection keeps dropping mid-run
### Analysis
The 14,000 Defence XP with only 4 "counted" kills suggests we're actually getting many more kills than detected. At ~40 XP per goblin kill (Defence + HP), that's ~350+ actual kills in ~5 minutes.
XP rate: 14,000 + 5,704 = ~19,700 XP in ~5 min = **~237k XP/hour potential!**
### Browser Issues
The puppeteer connection keeps closing unexpectedly:
- "ConnectionClosedError: Connection closed"
- Happens after variable time (5-10 minutes)
- Not related to script logic - infrastructure issue
---
## Run 006 - 2026-01-25 (30-minute runs with improvements)
**Goal**: Extended 30-minute runs with improved reliability and metrics.
### Improvements Made
1. **Fixed cowhide counting** - Now properly counts all unstacked items in inventory:
```typescript
function countCowhides(ctx): number {
return ctx.sdk.getInventory()
.filter(i => /^cow\s?hide$/i.test(i.name))
.reduce((sum, i) => sum + (i.count ?? 1), 0);
}
```
2. **Extended time limit**: 10 min → 30 min
3. **Increased stall timeout**: 90s → 120s
4. **Inventory management** - Drops bones when inventory gets full:
- Prioritizes hides over bones
- Drops up to 5 bones when ≤2 free slots
5. **Improved pickup reliability**:
- Reduced pickup distance: 5 tiles → 3 tiles (avoids long-distance timeouts)
- Added failed pickup tracking - won't retry same item for 30 seconds
- Skip bones entirely during training phase
6. **Better stats logging**:
- XP/hour calculation
- Time-based logging every 5 minutes (in addition to kill-based)
### Test Results
**Run 006a**: 5 minutes before disconnect (tick freeze - server issue)
| Metric | Value |
|--------|-------|
| Kills | 16 |
| Hides collected | 10/50 |
| XP gained | 45,110 |
| XP/hour | ~740,000 |
| Combat style rotation | Working |
**Run 006b**: 5 minutes before disconnect (tick freeze - server issue)
| Metric | Value |
|--------|-------|
| Kills | 15 |
| Hides collected | 9/50 |
| XP gained | 48,820 |
| XP/hour | ~800,966 |
| Bone dropping | Working! |
**Observations**:
- Cowhide counting now works correctly! Shows "1/50", "2/50", etc.
- Pickup efficiency improved (fewer repeated timeout attempts)
- **Bone dropping triggered**: "Dropping 1 bones to make room for hides..."
- XP rate extrapolates to ~50 hides in ~25-30 minutes
- Server tick freezes consistently at ~5 minutes (infrastructure issue)
### Issues Encountered
1. **Server disconnects** - Game tick freezes causing disconnections
- Not a script issue, infrastructure/server problem
- "Game tick frozen for 17s (last tick: 21405)"
2. **Some attack timeouts** still occurring
- "Timeout waiting to attack Cow"
- May be pathfinding or NPC selection issue
### Next Steps
- [ ] Investigate server tick freeze issues
- [ ] Potentially reduce attack timeout for faster fallback
- [ ] Test full 30-minute run when server is stable
---
## Run 005 - 2026-01-25 (Al Kharid Journey!)
**Goal**: Train at Lumbridge until Combat Level 15, then travel to Al Kharid for better training.
### New Strategy
1. **Balanced training** - Cycle combat styles every 2 kills (Attack → Strength → Defence)
2. **Combat level trigger** - Travel to Al Kharid at Combat Level 15
3. **Gate handling** - Sell bones for 10gp toll if needed
4. **Al Kharid training** - Buy kebabs for food, train on warriors/men
### Results
**Outcome**: TIMEOUT (ran full 10 minutes) - Successfully reached Al Kharid!
| Metric | Run 004 | Run 005 | Notes |
|--------|---------|---------|-------|
| Time limit | 5 min | 10 min | Extended |
| Combat Level | 16 | 17 | Similar |
| Kills counted | 4 | 8 | Includes Al Kharid training |
| Location | Lumbridge | **Al Kharid** | **Made it!** |
| Training style | Defence only | Balanced | All 3 melee stats |
| Gate passed | N/A | **Yes** | Toll paid successfully |
| Scimitar bought | N/A | No | Not enough coins (5/112) |
| Kebabs bought | N/A | No | Wrong NPC found (Zeke) |
### What Worked
1. **Gate passage** - Successfully navigated Al Kharid toll gate on attempt 4-5
2. **Combat style cycling** - XP distributed across Attack, Strength, and Defence
3. **Phase transitions** - Lumbridge → Upgrading → Al Kharid worked correctly
4. **Fallback targeting** - When warriors too strong, switched to Men
### Issues Found
1. **Kebab shop location** - Script found Zeke (scimitar shop) instead of Karim
- Fixed: Updated coordinates to (3273, 3181)
2. **Not enough coins for scimitar** - Only had 5gp after toll (needed 112)
- Goblins/bones don't provide enough gold for purchases
- Consider: Start with coins in preset, or raise combat level trigger
3. **Warriors hit hard** - Ran out of food quickly, had to fight Men instead
- Without kebabs, survival is difficult against level 9 warriors
### XP Distribution Example
From one run:
- Attack: Level 22-26 (6000-8800 XP)
- Strength: Level 1-19 (0-4400 XP)
- Defence: Level 1-13 (0-2000 XP)
- Hitpoints: Level 19-21 (4000-5200 XP)
### Combat Style Rotation
Working correctly:
```
Kill 0-1: Stab (Attack)
Kill 2-3: Lunge (Strength)
Kill 4-5: Block (Defence)
Kill 6-7: Stab (Attack)
...
```
### Next Steps
- [ ] Fix kebab shop NPC detection
- [ ] Consider raising combat level trigger to 20 (more coins/bones)
- [ ] Add coin collection priority over bones
- [ ] Test with preset that includes starting coins
---
-795
View File
@@ -1,795 +0,0 @@
/**
* Combat Trainer Script
*
* Goal: Maximize Attack + Strength + Defence + Hitpoints levels over 10 minutes.
*
* Strategy:
* - Phase 1: Kill cows at Lumbridge cow field, collect hides (~50 needed)
* - Phase 2: Sell hides at Lumbridge general store for ~130gp
* - Phase 3: Pass through Al Kharid gate (10gp toll), buy iron scimitar (112gp)
* - Phase 4: Return to cow field and train with iron scimitar
* - Cycle combat styles for balanced Attack/Strength/Defence training
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Combat style indices for swords (4 styles: Stab, Lunge, Slash, Block)
// See: https://oldschool.runescape.wiki/w/Combat_Options
const COMBAT_STYLES = {
ACCURATE: 0, // Stab - Trains Attack
AGGRESSIVE: 1, // Lunge - Trains Strength
CONTROLLED: 2, // Slash - Trains Attack+Strength+Defence evenly
DEFENSIVE: 3, // Block - Trains Defence only
};
// Training locations
const LOCATIONS = {
COW_FIELD: { x: 3253, z: 3269 }, // Lumbridge cow field (east of castle)
LUMBRIDGE_GENERAL_STORE: { x: 3211, z: 3247 }, // To sell hides
ALKHARID_GATE: { x: 3268, z: 3228 }, // Gate to Al Kharid
ALKHARID_SCIMITAR_SHOP: { x: 3287, z: 3186 }, // Zeke's shop
};
// Upgrade config
const IRON_SCIMITAR_PRICE = 112;
const GATE_TOLL = 10;
const COINS_NEEDED = IRON_SCIMITAR_PRICE + GATE_TOLL + 10; // 132gp buffer
const HIDES_NEEDED = 50; // ~2-3gp each = 100-150gp
/**
* Count total cowhides in inventory (they don't stack, so count items)
*/
function countCowhides(ctx: ScriptContext): number {
const inventory = ctx.sdk.getInventory();
return inventory.filter(i => /^cow\s?hide$/i.test(i.name)).reduce((sum, i) => sum + (i.count ?? 1), 0);
}
/**
* Count total coins in inventory
*/
function countCoins(ctx: ScriptContext): number {
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
return coins?.count ?? 0;
}
/**
* Get free inventory slots
*/
function getFreeSlots(ctx: ScriptContext): number {
const inventory = ctx.sdk.getInventory();
return 28 - inventory.length;
}
/**
* Drop bones to make room for hides (bones are worth less than hides)
*/
async function dropBonesIfNeeded(ctx: ScriptContext): Promise<void> {
const freeSlots = getFreeSlots(ctx);
// If inventory has 2 or fewer free slots, drop bones to make room
if (freeSlots <= 2) {
const inventory = ctx.sdk.getInventory();
const bones = inventory.filter(i => /^bones$/i.test(i.name));
if (bones.length > 0) {
console.log(`Dropping ${bones.length} bones to make room for hides...`);
for (const bone of bones.slice(0, 5)) { // Drop up to 5 bones at a time
await ctx.sdk.sendDropItem(bone.slot);
await new Promise(r => setTimeout(r, 300));
}
}
}
}
// Track combat statistics
interface CombatStats {
kills: number;
damageDealt: number;
damageTaken: number;
startXp: { atk: number; str: number; def: number; hp: number };
foodEaten: number;
looted: number;
hidesCollected: number;
coinsCollected: number;
weaponUpgraded: boolean;
phase: 'farming' | 'selling' | 'buying' | 'training'; // Cow-based phases
lastStatsLog: number;
startTime: number; // For timing
lastTimeBasedLog: number; // Last time-based log timestamp
}
/**
* Find the best cow to attack.
* Cows are level 2, easy targets that drop valuable hides.
*/
function findBestTarget(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const targets = state.nearbyNpcs
.filter(npc => /^cow$/i.test(npc.name)) // Only cows (not "cow calf")
.filter(npc => npc.options.some(o => /attack/i.test(o)))
// Filter out NPCs we know are busy (recently returned "already in combat")
.filter(npc => canAttackNpc(npc.index))
// Filter out NPCs already fighting someone else
.filter(npc => {
if (npc.targetIndex === -1) return true;
return !npc.inCombat;
})
.sort((a, b) => {
// Prefer NPCs not in combat
if (a.inCombat !== b.inCombat) {
return a.inCombat ? 1 : -1;
}
// Then by distance
return a.distance - b.distance;
});
return targets[0] ?? null;
}
/**
* Check if we should eat food based on HP
*/
function shouldEat(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state) return false;
const hp = state.skills.find(s => s.name === 'Hitpoints');
if (!hp) return false;
// Eat if below 50% HP
return hp.level < hp.baseLevel * 0.5;
}
// Style rotation for 2:3:2 ratio (Attack:Strength:Defence)
// Strength-focused training for faster kills
const STYLE_ROTATION = [
{ style: COMBAT_STYLES.ACCURATE, name: 'Stab (Attack)' },
{ style: COMBAT_STYLES.ACCURATE, name: 'Stab (Attack)' },
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
{ style: COMBAT_STYLES.AGGRESSIVE, name: 'Lunge (Strength)' },
{ style: COMBAT_STYLES.DEFENSIVE, name: 'Block (Defence)' },
{ style: COMBAT_STYLES.DEFENSIVE, name: 'Block (Defence)' },
];
// Track style cycling - initialized in resetStyleCycling()
let lastStyleChange = 0;
let currentStyleIndex = 0;
const STYLE_CYCLE_MS = 20_000; // Change every 20 seconds (full rotation = 140s)
// Track which style we last SET (not what the game reports)
let lastSetStyle = -1;
// Track failed pickups to avoid retrying (key: "x,z,id", value: timestamp)
const failedPickups = new Map<string, number>();
const FAILED_PICKUP_COOLDOWN = 30_000; // Don't retry failed pickups for 30 seconds
function canAttemptPickup(item: { x: number; z: number; id: number }): boolean {
const key = `${item.x},${item.z},${item.id}`;
const lastFailed = failedPickups.get(key);
if (lastFailed && Date.now() - lastFailed < FAILED_PICKUP_COOLDOWN) {
return false; // Still on cooldown
}
return true;
}
function markPickupFailed(item: { x: number; z: number; id: number }): void {
const key = `${item.x},${item.z},${item.id}`;
failedPickups.set(key, Date.now());
// Clean up old entries
const now = Date.now();
for (const [k, v] of failedPickups.entries()) {
if (now - v > FAILED_PICKUP_COOLDOWN * 2) {
failedPickups.delete(k);
}
}
}
// Track NPCs that are "already in combat" to avoid attacking them repeatedly
const busyNpcs = new Map<number, number>(); // NPC index -> timestamp
const BUSY_NPC_COOLDOWN = 15_000; // Don't retry NPCs that were busy for 15 seconds
function canAttackNpc(npcIndex: number): boolean {
const lastBusy = busyNpcs.get(npcIndex);
if (lastBusy && Date.now() - lastBusy < BUSY_NPC_COOLDOWN) {
return false;
}
return true;
}
function markNpcBusy(npcIndex: number): void {
busyNpcs.set(npcIndex, Date.now());
// Clean up old entries
const now = Date.now();
for (const [k, v] of busyNpcs.entries()) {
if (now - v > BUSY_NPC_COOLDOWN * 2) {
busyNpcs.delete(k);
}
}
}
/**
* Reset style cycling state - call at start of training
*/
function resetStyleCycling(): void {
lastStyleChange = Date.now();
currentStyleIndex = 0;
lastSetStyle = -1;
}
/**
* Cycle combat style for 2:3:2 Attack:Strength:Defence ratio.
* Time-based cycling since kill counting is unreliable.
* IMPORTANT: Always set the style, don't rely on game state reporting correctly.
*/
async function cycleCombatStyle(ctx: ScriptContext): Promise<void> {
// Check if it's time to switch styles
const now = Date.now();
if (now - lastStyleChange >= STYLE_CYCLE_MS) {
currentStyleIndex = (currentStyleIndex + 1) % STYLE_ROTATION.length;
lastStyleChange = now;
}
const target = STYLE_ROTATION[currentStyleIndex]!;
// Always set style if it differs from what we last set (don't trust game state)
if (lastSetStyle !== target.style) {
console.log(`Setting ${target.name} style (slot ${currentStyleIndex}/7)`);
await ctx.sdk.sendSetCombatStyle(target.style);
lastSetStyle = target.style;
}
}
/**
* Wait for current combat to complete (NPC dies or we need to heal)
* Uses multiple detection methods: XP gains, combatCycle, and NPC disappearance.
*/
async function waitForCombatEnd(
ctx: ScriptContext,
targetNpc: NearbyNpc,
stats: CombatStats
): Promise<'kill' | 'fled' | 'lost_target' | 'need_heal'> {
let lastSeenTick = ctx.sdk.getState()?.tick ?? 0;
let combatStarted = false;
let ticksSinceCombatEnded = 0;
let loopCount = 0;
// Track starting XP to detect combat via XP gains
const startState = ctx.sdk.getState();
const startXp = {
def: startState?.skills.find(s => s.name === 'Defence')?.experience ?? 0,
hp: startState?.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
};
// Wait up to 30 seconds for combat to resolve
const maxWaitMs = 30000;
const startTime = Date.now();
// Initial delay to let combat actually start (attack animation takes time)
await new Promise(r => setTimeout(r, 800));
while (Date.now() - startTime < maxWaitMs) {
await new Promise(r => setTimeout(r, 400));
loopCount++;
const state = ctx.sdk.getState();
if (!state) return 'lost_target';
const currentTick = state.tick;
// Check if we need to heal
if (shouldEat(ctx)) {
return 'need_heal';
}
// Check XP gains as combat indicator (most reliable!)
const currentXp = {
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
};
const xpGained = (currentXp.def - startXp.def) + (currentXp.hp - startXp.hp);
if (xpGained > 0) {
combatStarted = true; // XP gain = definitely in combat
}
// Find our target NPC
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
if (!target) {
// NPC disappeared - count as kill if we gained XP or waited a bit
if (combatStarted || xpGained > 0 || loopCount >= 2) {
stats.kills++;
return 'kill';
}
return 'lost_target';
}
// Check NPC health - if 0, it died (only valid once maxHp > 0)
if (target.maxHp > 0 && target.hp === 0) {
stats.kills++;
return 'kill';
}
// Track combat events
for (const event of state.combatEvents) {
if (event.tick > lastSeenTick) {
if (event.type === 'damage_dealt' && event.targetIndex === targetNpc.index) {
stats.damageDealt += event.damage;
combatStarted = true;
}
if (event.type === 'damage_taken') {
stats.damageTaken += event.damage;
combatStarted = true;
}
}
}
lastSeenTick = currentTick;
// Check combat status via combatCycle
const npcInCombat = target.combatCycle > currentTick;
const playerInCombat = state.player?.combat?.inCombat ?? false;
const inActiveCombat = playerInCombat || npcInCombat || xpGained > 0;
if (inActiveCombat) {
combatStarted = true;
ticksSinceCombatEnded = 0;
} else if (combatStarted) {
ticksSinceCombatEnded++;
if (ticksSinceCombatEnded >= 4) {
return 'fled';
}
} else if (loopCount >= 8) {
// Combat never started after ~4 seconds
return 'lost_target';
}
}
return 'lost_target';
}
/**
* Sell cow hides at Lumbridge general store.
*/
async function sellHides(ctx: ScriptContext, stats: CombatStats): Promise<boolean> {
console.log('=== Selling Cow Hides ===');
stats.phase = 'selling';
const hideCount = countCowhides(ctx);
if (hideCount === 0) {
console.warn('No cow hides to sell!');
stats.phase = 'farming';
return false;
}
console.log(`Walking to Lumbridge general store with ${hideCount} hides...`);
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_GENERAL_STORE.x, LOCATIONS.LUMBRIDGE_GENERAL_STORE.z);
const shopResult = await ctx.bot.openShop(/shop.?keeper/i);
if (!shopResult.success) {
console.warn('Failed to open general store');
stats.phase = 'farming';
return false;
}
// Sell all hides (one at a time since they don't stack)
const inventory = ctx.sdk.getInventory();
const allHides = inventory.filter(i => /^cow\s?hide$/i.test(i.name));
let soldCount = 0;
for (const hide of allHides) {
const sellResult = await ctx.bot.sellToShop(/^cow\s?hide$/i, 1);
if (sellResult.success) {
soldCount++;
} else {
console.warn(`Failed to sell hide: ${sellResult.message}`);
break;
}
}
console.log(`Sold ${soldCount} cow hides!`);
await ctx.bot.closeShop();
// Check coins
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
const coinCount = coins?.count ?? 0;
console.log(`Total coins: ${coinCount}`);
return true;
}
/**
* Buy iron scimitar from Al Kharid.
* 1. Pass through toll gate (10gp)
* 2. Buy iron scimitar from Zeke (112gp)
* 3. Return to cow field
*/
async function buyIronScimitar(ctx: ScriptContext, stats: CombatStats): Promise<boolean> {
console.log('=== Buying Iron Scimitar ===');
stats.phase = 'buying';
// Check we have enough coins
let coins = ctx.sdk.findInventoryItem(/^coins$/i);
let coinCount = coins?.count ?? 0;
if (coinCount < COINS_NEEDED) {
console.warn(`Not enough coins (${coinCount}/${COINS_NEEDED})`);
stats.phase = 'farming';
return false;
}
// Walk to Al Kharid gate
console.log('Walking to Al Kharid gate...');
await ctx.bot.walkTo(LOCATIONS.ALKHARID_GATE.x, LOCATIONS.ALKHARID_GATE.z);
// Handle toll gate
console.log('Opening toll gate...');
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name));
if (gate) {
const openOpt = gate.optionsWithIndex.find(o => /open/i.test(o.text));
if (openOpt) {
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 800));
}
}
// Handle gate dialog
for (let i = 0; i < 20; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) {
await new Promise(r => setTimeout(r, 150));
continue;
}
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying 10gp toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
break;
}
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
// Walk through gate
await new Promise(r => setTimeout(r, 1000));
for (let i = 0; i < 10; i++) {
const state = ctx.sdk.getState();
if ((state?.player?.worldX ?? 0) >= 3270) {
console.log('Entered Al Kharid!');
break;
}
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
continue;
}
await ctx.bot.walkTo(3277, 3227);
await new Promise(r => setTimeout(r, 800));
}
// Verify in Al Kharid
if ((ctx.sdk.getState()?.player?.worldX ?? 0) < 3270) {
console.warn('Failed to enter Al Kharid');
stats.phase = 'farming';
return false;
}
// Walk to Zeke's shop
console.log('Walking to Zeke\'s Scimitar Shop...');
await ctx.bot.walkTo(LOCATIONS.ALKHARID_SCIMITAR_SHOP.x, LOCATIONS.ALKHARID_SCIMITAR_SHOP.z);
// Buy iron scimitar
const scimitarShop = await ctx.bot.openShop(/zeke/i);
if (!scimitarShop.success) {
console.warn('Failed to open scimitar shop');
stats.phase = 'training';
return false;
}
const buyScim = await ctx.bot.buyFromShop(/iron scimitar/i, 1);
if (buyScim.success) {
console.log('*** IRON SCIMITAR PURCHASED! ***');
stats.weaponUpgraded = true;
} else {
console.warn(`Failed to buy scimitar: ${buyScim.message}`);
}
await ctx.bot.closeShop();
// Equip the scimitar
const scimitar = ctx.sdk.findInventoryItem(/iron scimitar/i);
if (scimitar) {
console.log('Equipping iron scimitar...');
await ctx.bot.equipItem(scimitar);
}
// Return to cow field
console.log('Returning to cow field...');
// Walk back through gate (no toll to exit)
await ctx.bot.walkTo(3267, 3227); // Outside gate
await new Promise(r => setTimeout(r, 500));
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
stats.phase = 'training';
console.log('=== Ready to train with Iron Scimitar! ===');
return true;
}
/**
* Main combat training loop - Cow Hide Strategy
*/
async function combatTrainingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
// Initialize stats tracking
const now = Date.now();
const stats: CombatStats = {
kills: 0,
damageDealt: 0,
damageTaken: 0,
startXp: {
atk: state.skills.find(s => s.name === 'Attack')?.experience ?? 0,
str: state.skills.find(s => s.name === 'Strength')?.experience ?? 0,
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
},
foodEaten: 0,
looted: 0,
hidesCollected: 0,
coinsCollected: 0,
weaponUpgraded: false,
phase: 'farming', // Start farming cow hides
lastStatsLog: 0,
startTime: now,
lastTimeBasedLog: now,
};
console.log('=== Combat Trainer - Cow Hide Strategy ===');
console.log(`Goal: Collect ${HIDES_NEEDED} cow hides → Sell → Buy Iron Scimitar → Train`);
console.log(`Starting XP - Atk: ${stats.startXp.atk}, Str: ${stats.startXp.str}, Def: ${stats.startXp.def}, HP: ${stats.startXp.hp}`);
// Initialize style cycling timer now (not at module load)
resetStyleCycling();
// Equip starting gear
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
if (sword) {
console.log(`Equipping ${sword.name}...`);
await ctx.bot.equipItem(sword);
}
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
if (shield) {
console.log(`Equipping ${shield.name}...`);
await ctx.bot.equipItem(shield);
}
// Walk to cow field
console.log('Walking to cow field...');
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
// Main training loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Dismiss any blocking dialogs
if (currentState.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
continue;
}
// Log periodic stats (every 10 kills or every 5 minutes)
const timeSinceLastLog = Date.now() - stats.lastTimeBasedLog;
const shouldLogByKills = stats.kills > 0 && stats.kills % 10 === 0 && stats.kills !== stats.lastStatsLog;
const shouldLogByTime = timeSinceLastLog >= 5 * 60_000; // 5 minutes
if (shouldLogByKills || shouldLogByTime) {
stats.lastStatsLog = stats.kills;
stats.lastTimeBasedLog = Date.now();
logStats(ctx, stats);
}
// Cycle combat style for balanced training (time-based)
await cycleCombatStyle(ctx);
// Check current resources using proper counting
const hideCount = countCowhides(ctx);
const coinCount = countCoins(ctx);
// Phase logic: farming → selling → buying → training
if (stats.phase === 'farming' && !stats.weaponUpgraded) {
// Check if we have enough hides to sell
if (hideCount >= HIDES_NEEDED) {
console.log(`Collected ${hideCount} hides! Time to sell and buy scimitar!`);
await sellHides(ctx, stats);
continue;
}
// Also check if we already have enough coins (from previous attempts or loot)
if (coinCount >= COINS_NEEDED) {
console.log(`Already have ${coinCount} coins! Skipping to buy scimitar!`);
await buyIronScimitar(ctx, stats);
continue;
}
// Drop bones if inventory is getting full (prioritize hides over bones)
await dropBonesIfNeeded(ctx);
}
if (stats.phase === 'selling') {
// After selling, try to buy
const newCoinCount = countCoins(ctx);
if (newCoinCount >= COINS_NEEDED) {
await buyIronScimitar(ctx, stats);
} else {
console.log(`Only ${newCoinCount} coins, need ${COINS_NEEDED}. Collecting more hides...`);
stats.phase = 'farming';
}
continue;
}
// Pick up loot - prioritize cowhides!
// Skip bones if: inventory tight, or in training phase (don't need money anymore)
const skipBones = getFreeSlots(ctx) <= 4 || stats.phase === 'training';
// In training phase, only pick up hides if we're still working toward iron scimitar
const shouldLoot = stats.phase === 'farming' || !stats.weaponUpgraded;
const loot = shouldLoot ? ctx.sdk.getGroundItems()
.filter(i => /cow\s?hide|cowhide|bones|coins/i.test(i.name))
.filter(i => !skipBones || !/^bones$/i.test(i.name)) // Skip bones if inventory tight
.filter(i => i.distance <= 3) // Reduced from 5 to avoid timeouts
.filter(i => canAttemptPickup(i)) // Skip items we recently failed to pick up
.sort((a, b) => {
// Priority: hides > coins > bones
const priority = (name: string) => {
if (/cow\s?hide|cowhide/i.test(name)) return 0;
if (/coins/i.test(name)) return 1;
return 2;
};
return priority(a.name) - priority(b.name) || a.distance - b.distance;
}) : [];
if (loot.length > 0) {
const item = loot[0]!;
const result = await ctx.bot.pickupItem(item);
if (result.success) {
stats.looted++;
if (/cow\s?hide|cowhide/i.test(item.name)) {
stats.hidesCollected += item.count ?? 1;
const totalHides = countCowhides(ctx);
console.log(`Picked up cowhide (${totalHides}/${HIDES_NEEDED})`);
} else if (/coins/i.test(item.name)) {
stats.coinsCollected += item.count ?? 1;
}
} else {
// Mark this item as failed so we don't retry immediately
markPickupFailed(item);
}
}
// Find a cow to attack
const target = findBestTarget(ctx);
if (!target) {
console.log('No cows nearby - walking to cow field...');
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
continue;
}
// Check if already fighting
const playerCombat = currentState.player?.combat;
if (playerCombat?.inCombat && playerCombat.targetIndex === target.index) {
const result = await waitForCombatEnd(ctx, target, stats);
continue;
}
// Attack the cow
const attackResult = await ctx.bot.attackNpc(target);
if (!attackResult.success) {
console.warn(`Attack failed: ${attackResult.message}`);
// Mark NPC as busy if already in combat (won't retry for 15s)
if (attackResult.reason === 'already_in_combat') {
markNpcBusy(target.index);
}
// Try opening gate if blocked
if (attackResult.reason === 'out_of_reach') {
await ctx.bot.openDoor(/gate/i);
}
continue;
}
// Wait for combat to complete
const combatResult = await waitForCombatEnd(ctx, target, stats);
if (combatResult === 'kill') {
console.log(`Kill #${stats.kills}! (Hides: ${hideCount}/${HIDES_NEEDED})`);
}
}
}
/**
* Log current training statistics
*/
function logStats(ctx: ScriptContext, stats: CombatStats): void {
const state = ctx.sdk.getState();
if (!state) return;
const currentXp = {
atk: state.skills.find(s => s.name === 'Attack')?.experience ?? 0,
str: state.skills.find(s => s.name === 'Strength')?.experience ?? 0,
def: state.skills.find(s => s.name === 'Defence')?.experience ?? 0,
hp: state.skills.find(s => s.name === 'Hitpoints')?.experience ?? 0,
};
const xpGained = {
atk: currentXp.atk - stats.startXp.atk,
str: currentXp.str - stats.startXp.str,
def: currentXp.def - stats.startXp.def,
hp: currentXp.hp - stats.startXp.hp,
};
const totalXp = xpGained.atk + xpGained.str + xpGained.def + xpGained.hp;
const hides = countCowhides(ctx);
// Calculate XP/hour
const elapsedMs = Date.now() - stats.startTime;
const elapsedMinutes = Math.round(elapsedMs / 60_000);
const xpPerHour = elapsedMs > 60_000 ? Math.round(totalXp / (elapsedMs / 3_600_000)) : 0;
console.log(`--- Stats after ${elapsedMinutes}m / ${stats.kills} kills (Phase: ${stats.phase}) ---`);
console.log(`XP: Atk +${xpGained.atk}, Str +${xpGained.str}, Def +${xpGained.def}, HP +${xpGained.hp} (Total: +${totalXp})`);
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
console.log(`Hides: ${hides}/${HIDES_NEEDED}, Coins: ${stats.coinsCollected}`);
console.log(`Weapon: ${stats.weaponUpgraded ? 'IRON SCIMITAR!' : 'Bronze Sword'}`);
}
async function main() {
// Create fresh account
const username = `CT${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await combatTrainingLoop(ctx);
} finally {
// Log final stats
const state = ctx.sdk.getState();
if (state) {
const skills = state.skills;
const atk = skills.find(s => s.name === 'Attack');
const str = skills.find(s => s.name === 'Strength');
const def = skills.find(s => s.name === 'Defence');
const hp = skills.find(s => s.name === 'Hitpoints');
const scimitar = ctx.sdk.findInventoryItem(/iron scimitar/i) ||
state.equipment?.find(e => /iron scimitar/i.test(e.name));
console.log('=== Final Results ===');
console.log(`Combat Level: ${state.player?.combatLevel ?? '?'}`);
console.log(`Attack: Level ${atk?.baseLevel} (${atk?.experience} XP)`);
console.log(`Strength: Level ${str?.baseLevel} (${str?.experience} XP)`);
console.log(`Defence: Level ${def?.baseLevel} (${def?.experience} XP)`);
console.log(`Hitpoints: Level ${hp?.baseLevel} (${hp?.experience} XP)`);
console.log(`Iron Scimitar: ${scimitar ? 'YES!' : 'No'}`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 30 * 60 * 1000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-117
View File
@@ -1,117 +0,0 @@
# Lab Log: cooking
Training Cooking from level 1 to 10+ starting from Lumbridge spawn.
## Strategy Evolution
### v1 - Draynor + Lumbridge Range (Failed)
- Walk to Draynor fishing spot (~3087, 3230)
- Fish until inventory full
- Walk back to Lumbridge Castle range and cook
- **Issue**: Dark Wizards attack while walking back, causing deaths
### v2 - Draynor + Fire-based (Abandoned)
- Fish at Draynor
- Cut log and make fire to cook
- **Issue**: Still had Dark Wizard risk, complex fire-making logic
### v3 - Al-Kharid (Current)
- Sell shortbow at Lumbridge shop for 20gp
- Pay toll gate (10gp) to enter Al-Kharid
- Fish at Al-Kharid fishing spot
- Cook at Al-Kharid range
- **Issue**: Scorpions attack while fishing, causing deaths
## XP Requirements
- Cooking level 10 requires 1,154 XP
- Raw shrimps give 30 XP when cooked
- Need ~39 successful cooks to hit level 10
- Burns are likely at level 1-2 (reduced XP rate)
---
## Run History
### Run 001 - v1 Initial
**Outcome**: Disconnected
**Duration**: 118s
- Got to Draynor, caught 10 fish
- Disconnected while walking back to Lumbridge (tick frozen)
- Dark wizard damage observed
### Run 002 - v3 Al-Kharid
**Outcome**: Died from scorpion
**Duration**: ~120s
- Successfully passed toll gate
- Caught 23 fish at Al-Kharid
- Died from scorpion attacks (ran out of food)
### Run 003 - v3 with HP monitoring
**Outcome**: Died from scorpion
**Duration**: ~156s
- HP monitoring worked - ate Shrimps and Bread when low
- Caught 23 fish
- Ran out of food and died
### Run 004 - Safer location
**Outcome**: Bot disconnected
**Duration**: 65s
- Connection dropped during fishing
### Run 005 - SUCCESS!
**Outcome**: Goal achieved
**Duration**: 223s
- Caught 24 fish (inventory full)
- Cooked all 24 fish at Al-Kharid range
- Gained 31,500 XP
- **Reached Cooking level 38!** (far exceeding goal of 10)
- No scorpion deaths (luck/better position)
---
## Key Findings
### Connection Stability
- "Bot not connected" errors happen frequently (~30% of runs)
- "Game tick frozen" errors indicate server-side issues
- These are beyond script control
### Al-Kharid Fishing
- Position (3267, 3148) has scorpions within attack range (dist 1-4)
- Position (3265, 3153) may be slightly safer
- Scorpions deal ~2 damage per hit
- With 10 HP and only 2 food items, survival is limited
### HP Monitoring
- Hitpoints skill level reflects current HP
- Eating food works with `sendUseItem(slot, 1)`
- 2 food items (Shrimps, Bread) not enough for extended fishing
## Suggested Improvements
1. **Keep more food**: Don't drop bread/shrimps, or buy more food
2. **Safer position**: Test positions farther north from scorpions
3. **Run away logic**: Move to safe area when HP critical
4. **Retry on disconnect**: Add reconnection handling in sdk/runner
---
## Learnings
### 1. Strategic Findings
- Al-Kharid is safer from Dark Wizards but has Scorpion threat
- Draynor has Dark Wizard threat on return path
- Need food for survival in both areas
- Toll gate payment (10gp) works via dialog options
### 2. Process & Tooling Reflections
- Connection stability is major blocker for long runs
- State delta logging very helpful for debugging
- HP tracking via skills array works correctly
### 3. SDK Issues & Gaps
- `sendClickInventory` doesn't exist - use `sendUseItem` instead
- Connection drops seem related to WebSocket stability
- No automatic reconnection handling
-707
View File
@@ -1,707 +0,0 @@
/**
* Cooking Trainer Script
*
* Goal: Train Cooking from level 1 to 10+
*
* Strategy (v3 - Al-Kharid with toll gate):
* - Start at Lumbridge Castle with fishing net, tinderbox, axe, shortbow
* - Sell shortbow at Lumbridge general store for 20gp
* - Walk to Al-Kharid toll gate and pay 10gp
* - Fish at Al-Kharid fishing spot (safe area)
* - Cook at Al-Kharid range
* - Repeat until level 10
*
* This avoids the dangerous Dark Wizard area near Draynor.
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc, NearbyLoc, InventoryItem } from '../../sdk/types';
// Key locations
const LOCATIONS = {
LUMBRIDGE_SHOP: { x: 3212, z: 3247 }, // General store
TOLL_GATE: { x: 3268, z: 3228 }, // Al-Kharid toll gate
ALKHARID_FISHING: { x: 3265, z: 3153 }, // Shrimp fishing - moved north away from scorpions
ALKHARID_RANGE: { x: 3273, z: 3180 }, // Range for cooking
};
interface Stats {
fishCaught: number;
fishCooked: number;
fishBurnt: number;
cycles: number;
startCookingXp: number;
startTime: number;
lastProgressTime: number;
}
// ============ Helper Functions ============
function getCookingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.experience ?? 0;
}
function getCookingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.baseLevel ?? 1;
}
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
const state = ctx.sdk.getState();
if (!state?.player) return null;
return { x: state.player.worldX, z: state.player.worldZ };
}
function distanceTo(ctx: ScriptContext, target: { x: number; z: number }): number {
const pos = getPlayerPos(ctx);
if (!pos) return 999;
return Math.abs(pos.x - target.x) + Math.abs(pos.z - target.z);
}
/**
* Find the nearest fishing spot with Net option
*/
function findFishingSpot(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
// Find any fishing spot with "Net" option
const spots = state.nearbyNpcs
.filter(npc => /fishing\s*spot/i.test(npc.name))
.filter(npc => npc.options.some(opt => /^net$/i.test(opt)))
.sort((a, b) => a.distance - b.distance);
return spots[0] ?? null;
}
/**
* Count raw fish in inventory
*/
function countRawFish(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
return state.inventory
.filter(item => /^raw\s/i.test(item.name))
.reduce((sum, item) => sum + item.count, 0);
}
/**
* Get all raw fish items in inventory
*/
function getRawFishItems(ctx: ScriptContext): InventoryItem[] {
const state = ctx.sdk.getState();
if (!state) return [];
return state.inventory.filter(item => /^raw\s/i.test(item.name));
}
/**
* Get available slots (keeping essential items: net, tinderbox, axe)
*/
function getAvailableSlots(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
return 28 - state.inventory.length;
}
/**
* Get current HP
*/
function getCurrentHp(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 10;
return state.skills.find(s => s.name === 'Hitpoints')?.level ?? 10;
}
/**
* Eat food if HP is low, or run away if no food
*/
async function eatIfLowHp(ctx: ScriptContext, stats: Stats): Promise<boolean> {
const state = ctx.sdk.getState();
if (!state) return false;
// Get current HP from Hitpoints skill level (it reflects current HP, not just max)
const hitpointsSkill = state.skills.find(s => s.name === 'Hitpoints');
const hp = hitpointsSkill?.level ?? 10;
if (hp > 5) return false; // HP is fine
// Find food to eat (cooked shrimps, bread, or cooked anchovies)
const food = state.inventory.find(i =>
(/shrimps/i.test(i.name) && !/^raw\s/i.test(i.name)) ||
/bread/i.test(i.name) ||
(/anchov/i.test(i.name) && !/^raw\s/i.test(i.name))
);
if (!food) {
// No food - run away from combat!
if (hp <= 3) {
console.log(`HP critical (${hp}), running away!`);
// Run north towards bank (safer area)
await ctx.sdk.sendWalk(LOCATIONS.ALKHARID_RANGE.x, LOCATIONS.ALKHARID_RANGE.z, true);
await new Promise(r => setTimeout(r, 2000));
}
return false;
}
console.log(`HP low (${hp}), eating ${food.name}`);
await ctx.sdk.sendUseItem(food.slot, 1); // Option 1 = "Eat"
await new Promise(r => setTimeout(r, 500));
return true;
}
/**
* Dismiss dialogs (level-up notifications, etc.)
*/
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
let dismissed = 0;
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
dismissed++;
}
return dismissed;
}
/**
* Walk to a position with pathfinding (with retries)
*/
async function walkToPosition(ctx: ScriptContext, stats: Stats, target: { x: number; z: number }, name: string): Promise<boolean> {
const startDist = distanceTo(ctx, target);
console.log(`Walking to ${name} (${target.x}, ${target.z}), dist: ${startDist}...`);
// Use pathfinding for long distances - retry up to 3 times
for (let attempt = 0; attempt < 3; attempt++) {
const currentDist = distanceTo(ctx, target);
if (currentDist <= 10) {
console.log(`Arrived at ${name}`);
return true;
}
if (currentDist > 20) {
console.log(`Pathfinding attempt ${attempt + 1}, dist: ${currentDist}`);
try {
await ctx.bot.walkTo(target.x, target.z);
} catch (e) {
console.warn(`Pathfinding error: ${e}`);
}
}
}
// Final check
let finalDist = distanceTo(ctx, target);
if (finalDist <= 10) {
console.log(`Arrived at ${name}`);
return true;
}
// Direct walking for remaining distance
console.log(`Direct walking to finish, dist: ${finalDist}`);
await ctx.sdk.sendWalk(target.x, target.z, true);
// Wait to arrive
for (let i = 0; i < 100; i++) {
await new Promise(r => setTimeout(r, 400));
await dismissDialogs(ctx, stats, 1);
const dist = distanceTo(ctx, target);
if (dist <= 5) {
console.log(`Arrived at ${name}`);
return true;
}
// Re-send walk periodically
if (i > 0 && i % 10 === 0) {
ctx.sdk.sendWalk(target.x, target.z, true);
}
}
console.warn(`Failed to reach ${name} (dist: ${distanceTo(ctx, target)})`);
return false;
}
// ============ Main Phases ============
/**
* Phase 0a: Sell shortbow at Lumbridge general store for coins
*/
async function sellShortbowForCoins(ctx: ScriptContext, stats: Stats): Promise<boolean> {
// Check if we already have coins
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
if (coins && coins.count >= 10) {
console.log(`Already have ${coins.count} coins`);
return true;
}
// Check if we have shortbow to sell
const shortbow = ctx.sdk.getState()?.inventory.find(i => /shortbow/i.test(i.name));
if (!shortbow) {
console.warn('No shortbow to sell');
return false;
}
console.log('Walking to Lumbridge general store...');
if (!await walkToPosition(ctx, stats, LOCATIONS.LUMBRIDGE_SHOP, 'Lumbridge shop')) {
console.warn('Could not reach shop');
return false;
}
// Open shop
console.log('Opening shop...');
const result = await ctx.bot.openShop(/shop\s*keeper/i);
if (!result.success) {
console.warn(`Failed to open shop: ${result.message}`);
return false;
}
// Sell shortbow
console.log('Selling shortbow...');
const sellResult = await ctx.bot.sellToShop(/shortbow/i);
if (!sellResult.success) {
console.warn(`Failed to sell shortbow: ${sellResult.message}`);
}
// Close shop
await ctx.bot.closeShop();
await new Promise(r => setTimeout(r, 500));
const newCoins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
console.log(`Now have ${newCoins?.count ?? 0} coins`);
return (newCoins?.count ?? 0) >= 10;
}
/**
* Phase 0b: Pass through Al-Kharid toll gate
*/
async function passThruTollGate(ctx: ScriptContext, stats: Stats): Promise<boolean> {
// Check if already in Al-Kharid
const pos = getPlayerPos(ctx);
if (pos && pos.x >= 3270) {
console.log('Already in Al-Kharid');
return true;
}
console.log('Walking to toll gate...');
if (!await walkToPosition(ctx, stats, LOCATIONS.TOLL_GATE, 'toll gate')) {
console.warn('Could not reach toll gate');
return false;
}
// Find and interact with gate
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name));
if (!gate) {
console.warn('Gate not found');
return false;
}
const openOpt = gate.optionsWithIndex.find(o => /open|pay/i.test(o.text));
if (!openOpt) {
console.warn('No open option on gate');
return false;
}
console.log('Opening toll gate...');
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 1000));
// Handle dialog - click through until "Yes" option appears
for (let i = 0; i < 20; i++) {
const state = ctx.sdk.getState();
if (!state?.dialog.isOpen) {
// Check if we're already through
const pos = getPlayerPos(ctx);
if (pos && pos.x >= 3270) {
console.log('Passed through toll gate!');
return true;
}
await new Promise(r => setTimeout(r, 200));
continue;
}
const yesOpt = state.dialog.options.find(o => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
await new Promise(r => setTimeout(r, 500));
continue; // Keep checking dialog/position
}
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
// Wait and walk through repeatedly
await new Promise(r => setTimeout(r, 500));
for (let i = 0; i < 10; i++) {
// Check if already through
const pos = getPlayerPos(ctx);
if (pos && pos.x >= 3270) {
console.log('Passed through toll gate!');
return true;
}
// Dismiss any remaining dialogs
if (ctx.sdk.getState()?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
// Try to walk through
await ctx.sdk.sendWalk(3277, 3227, true); // Inside Al-Kharid
await new Promise(r => setTimeout(r, 1500));
}
console.warn('Failed to pass through toll gate');
return false;
}
/**
* Phase 0c: Drop non-essential items to make room for fish
*/
async function dropNonEssentials(ctx: ScriptContext, stats: Stats): Promise<void> {
const state = ctx.sdk.getState();
if (!state) return;
// Keep: fishing net, coins (for kebabs if needed)
// Keep: net, coins, food items (for emergency healing)
// Note: only keep NON-raw shrimps (the cooked one in starting inventory)
const essentialPatterns = [/fishing\s*net/i, /coins/i, /bread/i, /^shrimps$/i];
const itemsToDrop = state.inventory.filter(item =>
!essentialPatterns.some(p => p.test(item.name))
);
if (itemsToDrop.length === 0) {
console.log('No non-essential items to drop');
return;
}
console.log(`Dropping ${itemsToDrop.length} non-essential items...`);
for (const item of itemsToDrop) {
await ctx.sdk.sendDropItem(item.slot);
await new Promise(r => setTimeout(r, 100));
}
console.log(`Inventory now has ${ctx.sdk.getState()?.inventory.length ?? 0} items, ${getAvailableSlots(ctx)} slots free`);
}
/**
* Phase 1: Fish until inventory full at Al-Kharid
*/
async function fishUntilFull(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log(`Phase 1: Fishing at Al-Kharid until inventory full...`);
let lastFishCount = countRawFish(ctx);
let noSpotCount = 0;
// Fish until inventory full
while (getAvailableSlots(ctx) > 0) {
// Check HP and eat if needed
await eatIfLowHp(ctx, stats);
// Dismiss dialogs first
if (await dismissDialogs(ctx, stats) > 0) continue;
// Check for new fish
const currentFish = countRawFish(ctx);
if (currentFish > lastFishCount) {
stats.fishCaught += currentFish - lastFishCount;
if (currentFish % 5 === 0) {
console.log(`Fish caught: ${stats.fishCaught} (${getAvailableSlots(ctx)} slots free)`);
}
noSpotCount = 0;
}
lastFishCount = currentFish;
// Find and use fishing spot
const spot = findFishingSpot(ctx);
if (!spot) {
noSpotCount++;
if (noSpotCount % 50 === 0) console.log('Waiting for fishing spot...');
// Check if we drifted too far (e.g., respawned in Lumbridge)
if (noSpotCount > 50) {
const dist = distanceTo(ctx, LOCATIONS.ALKHARID_FISHING);
const pos = getPlayerPos(ctx);
if (dist > 30) {
console.log(`Drifted too far (${dist} tiles at ${pos?.x},${pos?.z}), walking back...`);
// Check if we're in Lumbridge (x < 3260) and need to pass toll gate
if (pos && pos.x < 3260) {
console.log('Respawned in Lumbridge, need to handle re-entry');
// If we still have fish, we already made progress - just go to cooking
if (countRawFish(ctx) > 5) {
console.log('Have fish to cook, skipping re-entry to Al-Kharid');
return; // Exit fishing phase, will proceed to cooking
}
// Check if we have coins for toll
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
if (!coins || coins.count < 10) {
console.log('No coins for toll, cannot re-enter Al-Kharid');
throw new Error('Respawned without coins, cannot continue');
}
// Pass through toll gate
const passedToll = await passThruTollGate(ctx, stats);
if (!passedToll) {
throw new Error('Could not re-enter Al-Kharid after respawn');
}
}
await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing');
noSpotCount = 0;
continue;
}
}
// Walk around if no spot for too long
if (noSpotCount > 100) {
console.log('No spot visible, walking around...');
await ctx.sdk.sendWalk(
LOCATIONS.ALKHARID_FISHING.x + (Math.random() > 0.5 ? 3 : -3),
LOCATIONS.ALKHARID_FISHING.z,
true
);
await new Promise(r => setTimeout(r, 1500));
noSpotCount = 0;
}
await new Promise(r => setTimeout(r, 100));
continue;
}
noSpotCount = 0;
const netOpt = spot.optionsWithIndex.find(o => /^net$/i.test(o.text));
if (!netOpt) {
await new Promise(r => setTimeout(r, 300));
continue;
}
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
await new Promise(r => setTimeout(r, 200));
}
console.log(`Inventory full: ${countRawFish(ctx)} raw fish`);
}
/**
* Phase 2: Cook all fish at Al-Kharid range
*/
async function cookAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log('Phase 2: Walking to Al-Kharid range...');
if (!await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_RANGE, 'Al-Kharid range')) {
console.warn('Could not reach range');
return;
}
console.log('Cooking fish at range...');
// Find the range
const range = ctx.sdk.getState()?.nearbyLocs.find(loc => /range|stove/i.test(loc.name));
if (!range) {
console.warn('No range found nearby');
const locs = ctx.sdk.getState()?.nearbyLocs.slice(0, 10) ?? [];
console.log(`Nearby locs: ${locs.map(l => l.name).join(', ')}`);
return;
}
console.log(`Found: ${range.name} at (${range.x}, ${range.z})`);
const cookingXpBefore = getCookingXp(ctx);
const rawFishBefore = countRawFish(ctx);
// Cook each raw fish
while (countRawFish(ctx) > 0) {
await dismissDialogs(ctx, stats, 1);
const rawFish = getRawFishItems(ctx)[0];
if (!rawFish) break;
// Use fish on range
await ctx.sdk.sendUseItemOnLoc(rawFish.slot, range.x, range.z, range.id);
// Wait for cooking interface or direct cooking
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 300));
const state = ctx.sdk.getState();
// Handle cooking interface if it appears
const firstInterfaceOpt = state?.interface?.options[0];
if (state?.interface?.isOpen && firstInterfaceOpt) {
console.log('Clicking cook option...');
await ctx.sdk.sendClickInterfaceOption(0);
// Wait for batch cooking to complete
let noChange = 0;
while (noChange < 15 && countRawFish(ctx) > 0) {
await new Promise(r => setTimeout(r, 400));
await dismissDialogs(ctx, stats, 1);
const prev = countRawFish(ctx);
await new Promise(r => setTimeout(r, 200));
if (countRawFish(ctx) === prev) noChange++;
else noChange = 0;
}
break;
}
// Handle dialog
if (state?.dialog?.isOpen) {
await ctx.sdk.sendClickDialog(0);
}
// Check if this fish was cooked
const currentRaw = countRawFish(ctx);
if (currentRaw < rawFishBefore) {
break;
}
}
}
// Calculate results
const xpGained = getCookingXp(ctx) - cookingXpBefore;
const fishCooked = rawFishBefore - countRawFish(ctx);
stats.fishCooked += fishCooked;
console.log(`Cooked ${fishCooked} fish (XP: +${xpGained})`);
}
/**
* Phase 3: Drop all cooked and burnt fish
*/
async function dropAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
const state = ctx.sdk.getState();
if (!state) return;
// Drop everything except fishing net
const itemsToDrop = state.inventory.filter(item =>
/shrimp|anchov/i.test(item.name) ||
/^burnt\s/i.test(item.name)
);
if (itemsToDrop.length === 0) {
console.log('Nothing to drop');
return;
}
console.log(`Dropping ${itemsToDrop.length} items...`);
for (const item of itemsToDrop) {
await ctx.sdk.sendDropItem(item.slot);
await new Promise(r => setTimeout(r, 100));
}
}
/**
* Log final statistics
*/
function logFinalStats(ctx: ScriptContext, stats: Stats) {
const state = ctx.sdk.getState();
const cooking = state?.skills.find(s => s.name === 'Cooking');
const cookingXpGained = (cooking?.experience ?? 0) - stats.startCookingXp;
const duration = (Date.now() - stats.startTime) / 1000;
console.log('');
console.log('=== Final Results ===');
console.log(`Duration: ${Math.round(duration)}s`);
console.log(`Cycles: ${stats.cycles}`);
console.log(`--- Cooking ---`);
console.log(` Level: ${cooking?.baseLevel ?? '?'}`);
console.log(` XP Gained: ${cookingXpGained}`);
console.log(` Fish Caught: ${stats.fishCaught}`);
console.log(` Fish Cooked: ${stats.fishCooked}`);
}
/**
* Main loop
*/
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
const GOAL_LEVEL = 10;
console.log('=== Cooking Trainer (v3 - Al-Kharid) ===');
console.log(`Goal: Reach Cooking level ${GOAL_LEVEL}`);
console.log(`Starting level: ${getCookingLevel(ctx)}`);
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
console.log(`Inventory: ${ctx.sdk.getState()?.inventory.length ?? 0} items`);
await ctx.bot.dismissBlockingUI();
// Phase 0a: Sell shortbow for coins
const gotCoins = await sellShortbowForCoins(ctx, stats);
if (!gotCoins) {
throw new Error('Could not get coins for toll');
}
// Phase 0b: Pass through toll gate to Al-Kharid
const enteredAlKharid = await passThruTollGate(ctx, stats);
if (!enteredAlKharid) {
throw new Error('Could not enter Al-Kharid');
}
// Phase 0c: Drop non-essential items
await dropNonEssentials(ctx, stats);
// Walk to Al-Kharid fishing spot
console.log('Walking to Al-Kharid fishing spot...');
if (!await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing')) {
throw new Error('Could not reach Al-Kharid fishing spot');
}
while (getCookingLevel(ctx) < GOAL_LEVEL) {
stats.cycles++;
console.log(`\n--- Cycle ${stats.cycles} (Cooking level ${getCookingLevel(ctx)}) ---`);
// Phase 1: Fish until inventory full
await fishUntilFull(ctx, stats);
// Phase 2: Cook at range
await cookAllFish(ctx, stats);
// Phase 3: Drop cooked fish
await dropAllFish(ctx, stats);
// Walk back to fishing spot
console.log('Returning to fishing spot...');
await walkToPosition(ctx, stats, LOCATIONS.ALKHARID_FISHING, 'Al-Kharid fishing');
}
console.log(`\nGoal achieved! Cooking level ${getCookingLevel(ctx)}`);
}
async function main() {
// Create fresh account
const username = `CK${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
const stats: Stats = {
fishCaught: 0,
fishCooked: 0,
fishBurnt: 0,
cycles: 0,
startCookingXp: getCookingXp(ctx),
startTime: Date.now(),
lastProgressTime: Date.now(),
};
try {
await mainLoop(ctx, stats);
} catch (e) {
console.error(`Script aborted: ${e}`);
throw e;
} finally {
logFinalStats(ctx, stats);
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 15 * 60 * 1000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-190
View File
@@ -1,190 +0,0 @@
# Lab Log: cowhide-banking
**Goal**: Collect and bank as many cow hides as possible in 15 minutes.
**Preset**: LUMBRIDGE_SPAWN (standard post-tutorial inventory)
**Strategy**:
1. Equip bronze sword + wooden shield for combat
2. Walk to Lumbridge cow field (3253, 3270)
3. Kill cows, pick up cow hides
4. When inventory is full (~26 items), bank at Lumbridge Castle (2nd floor)
5. Return to cow field and repeat
**Key Metrics**:
- Hides banked (primary metric)
- Hides per minute rate
- Bank trips completed
- Total kills
---
## Run 001 - 2026-01-25 06:15
**Outcome**: error (Bot disconnected)
**Duration**: ~2 min
### What Happened
- Script started correctly, equipped gear, walked to cow field
- Killed 3 cows, collected 4 hides
- HP dropped to 3-4/23 - no food eating logic
- Bot disconnected with "Bot not connected" error
### Root Cause
- No HP checking - bot fought at critically low HP
- Eventually died or got disconnected
### Fix Applied
- Added HP checking in main loop
- Bot now eats food when HP < 50%
---
## Run 002 - 2026-01-25 06:22
**Outcome**: error (Bot disconnected)
**Duration**: ~3.5 min
### What Happened
- HP eating logic worked! Ate shrimp (1) and bread (1)
- But only 2 food items in starting inventory
- Ran out of food at HP 8/26
- Continued fighting with low HP (6-9 HP)
- Collected 9 hides, made 4 kills
- Bot eventually disconnected
### Observations
1. **Food shortage** - Starting preset only has 1 shrimp + 1 bread
2. **Kill counter low** - Only 4 kills registered despite many attacks. Combat detection may be unreliable.
3. **Never reached banking** - Threshold is 20 hides or 26 items, never got there
4. **Attack XP gain is fast** - Attack went from 1 to 38 in ~3 min (cows give good XP)
### Problems Identified
1. Not enough food to survive extended combat
2. Banking threshold too high for quick testing
3. Bot dies before collecting enough hides
### Next Steps
1. Lower banking threshold to 8 hides (faster iteration)
2. Consider: pick up bones and bury for prayer XP? No - waste time
3. Consider: use Al Kharid bank (closer to cow field east side)?
4. Accept that first trip may be short due to food
---
## Run 003 - 2026-01-25 06:34
**Outcome**: error (Bot disconnected during banking walk)
**Duration**: ~2.5 min
### What Happened
- Lowered banking threshold to 8 hides
- Killed 7 cows, collected 8 hides
- **Banking triggered successfully!** ("=== Banking Trip ===" appeared)
- Started walking to castle stairs
- Position reached (3245, 3296) - north of cow field, heading to castle
- Bot disconnected - likely died with 6 HP while walking (no food, goblins along path)
### Observations
1. Banking threshold works correctly at 8 hides
2. Script detects floor level correctly (level: 0)
3. Bot dies during banking walk - 6 HP is too low to survive
4. Need to either bank earlier or avoid combat on the way
### Next Steps
1. Lower HP threshold for eating to avoid critical HP
2. Or: trigger banking BEFORE running out of food
3. Or: flee/escape when HP too low for banking
---
## Run 004-007 - 2026-01-25 06:47-07:06
**Outcome**: Multiple runs with varying results
### Key Findings
1. **Banking loop bug fixed**: Added verification that deposits actually worked before counting
2. **Stair location was wrong**: (3206, 3228) was the graveyard, not castle interior
3. **Castle coordinates updated**: Added castle entrance (3210, 3217), stairs (3206, 3208)
4. **Bank interface never opened**: Script aborted banking correctly after fix
5. **Connection instability**: SDK shows reconnect attempts, suggests server-side issues
### Run 005 Details (best run)
- 7 kills, 8 hides collected
- Multiple successful banking trips attempted (but failed due to wrong coordinates)
- Emergency banking logic triggered correctly
- Script properly aborted when bank didn't open
### Remaining Issues
1. Need to verify stair climbing works with new coordinates
2. Connection stability is external/server issue
3. Kill counter undercounts (5 kills tracked but more cows killed)
### Fixes Applied
- Added deposit verification (check hides left inventory)
- Abort banking if interface doesn't open (prevents infinite loop)
- Better logging for stair/loc detection
- Updated castle coordinates
---
## Run 010 - 2026-01-25 07:28 (BEST RUN!)
**Outcome**: Tick freeze → disconnect after ~10 minutes
**Duration**: ~10 minutes
### Final Stats
- **22 kills**
- **Strength level 46** (from 1!)
- **Hitpoints level 36** (from 10!)
- **Combat level 24**
- **9 hides dropped** multiple times to make space
### What Happened
- Script ran successfully for ~10 minutes
- Dropped hides when inventory full (simplified banking)
- Strength training working (aggressive combat style)
- Eventually tick froze (36988 repeated twice)
- Then "Bot not connected"
### Fixes That Helped
1. **Page crash handlers** - Added `page.on('crash')` and error handlers to browser.ts
2. **Simplified to dropping** - Removed banking attempts, just drop hides
3. **Combat style set** - `sendSetCombatStyle(1)` for strength XP
### Observations
- Tick freeze pattern still occurs but after MUCH longer runtime
- Console showed one 404 error but script continued fine
- Dialogs being dismissed during combat properly
### Pattern Analysis
The disconnects seem correlated with:
1. Game tick freezing (same tick repeated in consecutive state snapshots)
2. This happens eventually regardless of script actions
3. Might be a server-side or game client memory issue
---
## Notes
### Cow Field Location
- Lumbridge cow field is east of the castle at approximately (3253, 3270)
- Contains regular cows (level 2)
- Has a gate that may need to be opened
### Banking Route
- Lumbridge Castle bank is on the 2nd floor (level 2)
- Requires climbing two flights of stairs
- Route: Cow field -> Castle entrance -> Stairs up x2 -> Bank -> Stairs down x2 -> Cow field
### Potential Optimizations
1. Minimize pathing time by staying near gate
2. Only pick up cow hides (ignore bones, beef to maximize hide rate)
3. Consider banking threshold - full inventory vs. partial trips
4. Could potentially use Al Kharid bank if we get 10gp for toll (closer from east side of field)
### Known Issues
- [ ] Need to verify Lumbridge Castle bank access works
- [ ] Need to verify stair climbing works correctly
- [ ] Gate handling at cow field
-578
View File
@@ -1,578 +0,0 @@
/**
* Cow Hide Banking Script
*
* Goal: Collect and bank as many cow hides as possible in 15 minutes.
*
* Strategy:
* - Start at Lumbridge (standard post-tutorial spawn)
* - Equip combat gear (bronze sword + wooden shield)
* - Walk to Lumbridge cow field
* - Kill cows, pick up cow hides
* - When inventory is full, walk to Lumbridge Castle bank and deposit
* - Track: kills, hides collected, hides banked, bank trips
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Locations
const LOCATIONS = {
COW_FIELD: { x: 3253, z: 3270 }, // Lumbridge cow field (east of castle)
LUMBRIDGE_BANK: { x: 3208, z: 3220 }, // Lumbridge Castle bank (top floor, level 2)
LUMBRIDGE_CASTLE_ENTRANCE: { x: 3210, z: 3217 }, // Castle main entrance
LUMBRIDGE_STAIRS_GROUND: { x: 3206, z: 3208 }, // Stairs inside castle ground floor
};
// Configuration
const INVENTORY_THRESHOLD = 24; // Bank when inventory has this many items
const MAX_HIDES_BEFORE_BANK = 8; // Force bank trip after collecting this many hides (lowered for faster iteration)
// Statistics tracking
interface HideStats {
kills: number;
hidesCollected: number;
hidesBanked: number;
bankTrips: number;
startTime: number;
lastProgressTime: number;
}
/**
* Find the best cow to attack
* - Filters out cows already in combat with someone else
* - Prefers idle cows over cows in combat
* - Sorts by distance
*/
function findBestCow(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const cows = state.nearbyNpcs
.filter(npc => /^cow$/i.test(npc.name))
.filter(npc => npc.options.some(o => /attack/i.test(o)))
// Filter out cows already fighting someone else
.filter(npc => {
if (npc.targetIndex === -1) return true;
return !npc.inCombat;
})
.sort((a, b) => {
// Prefer cows not in combat
if (a.inCombat !== b.inCombat) {
return a.inCombat ? 1 : -1;
}
// Then by distance
return a.distance - b.distance;
});
return cows[0] ?? null;
}
/**
* Wait for combat to complete
*/
async function waitForCombatEnd(
ctx: ScriptContext,
targetNpc: NearbyNpc,
stats: HideStats
): Promise<'kill' | 'fled' | 'lost_target'> {
let combatStarted = false;
let ticksSinceCombatEnded = 0;
let loopCount = 0;
const maxWaitMs = 30000;
const startTime = Date.now();
// Initial delay for attack animation
await new Promise(r => setTimeout(r, 800));
while (Date.now() - startTime < maxWaitMs) {
await new Promise(r => setTimeout(r, 400));
loopCount++;
const state = ctx.sdk.getState();
if (!state) return 'lost_target';
// CRITICAL: Dismiss any dialogs during combat (level-up messages)
// Without this, dialogs block game responses and cause server timeouts
if (state.dialog.isOpen) {
console.log('Dismissing dialog during combat...');
await ctx.sdk.sendClickDialog(0);
continue;
}
const currentTick = state.tick;
// Find our target cow
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
if (!target) {
// Cow disappeared - likely died
if (combatStarted || loopCount >= 2) {
stats.kills++;
return 'kill';
}
return 'lost_target';
}
// Check cow health
if (target.maxHp > 0 && target.hp === 0) {
stats.kills++;
return 'kill';
}
// Check combat status
const npcInCombat = target.combatCycle > currentTick;
const playerInCombat = state.player?.combat?.inCombat ?? false;
const inActiveCombat = playerInCombat || npcInCombat;
if (inActiveCombat) {
combatStarted = true;
ticksSinceCombatEnded = 0;
} else if (combatStarted) {
ticksSinceCombatEnded++;
if (ticksSinceCombatEnded >= 4) {
return 'fled';
}
} else if (loopCount >= 8) {
return 'lost_target';
}
}
return 'lost_target';
}
/**
* Pick up cow hides from the ground
*/
async function pickupHides(ctx: ScriptContext, stats: HideStats): Promise<number> {
let pickedUp = 0;
const state = ctx.sdk.getState();
if (!state) return 0;
// Check inventory space
if (state.inventory.length >= 28) {
return 0;
}
const groundItems = ctx.sdk.getGroundItems()
.filter(i => /cow\s*hide/i.test(i.name))
.filter(i => i.distance <= 8)
.sort((a, b) => a.distance - b.distance);
for (const item of groundItems.slice(0, 3)) {
if (ctx.sdk.getState()!.inventory.length >= 28) break;
console.log(`Picking up ${item.name}...`);
const result = await ctx.bot.pickupItem(item);
if (result.success) {
pickedUp++;
stats.hidesCollected++;
}
await new Promise(r => setTimeout(r, 300));
}
return pickedUp;
}
/**
* Bank the collected hides at Lumbridge Castle bank
*/
async function bankHides(ctx: ScriptContext, stats: HideStats): Promise<boolean> {
console.log('=== Banking Trip ===');
stats.bankTrips++;
const currentState = ctx.sdk.getState();
const currentLevel = currentState?.player?.level ?? 0;
const hidesBeforeBank = currentState?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
console.log(`Current floor level: ${currentLevel}, hides in inventory: ${hidesBeforeBank}`);
// If we're at ground level (0), we need to go up stairs
if (currentLevel === 0) {
console.log('Walking to castle entrance...');
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_CASTLE_ENTRANCE.x, LOCATIONS.LUMBRIDGE_CASTLE_ENTRANCE.z);
console.log('Walking to castle stairs...');
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_STAIRS_GROUND.x, LOCATIONS.LUMBRIDGE_STAIRS_GROUND.z);
// Climb up stairs to level 1
console.log('Climbing to first floor...');
const nearbyLocs = ctx.sdk.getState()?.nearbyLocs ?? [];
console.log(`Nearby locs: ${nearbyLocs.slice(0, 5).map(l => l.name).join(', ')}`);
const stairs1 = nearbyLocs.find(l => /staircase/i.test(l.name));
if (stairs1) {
console.log(`Found stairs: ${stairs1.name} at (${stairs1.x}, ${stairs1.z})`);
console.log(`Options: ${stairs1.optionsWithIndex.map(o => o.text).join(', ')}`);
const climbOpt = stairs1.optionsWithIndex.find(o => /climb.?up/i.test(o.text));
if (climbOpt) {
console.log(`Using climb option: ${climbOpt.text}`);
await ctx.sdk.sendInteractLoc(stairs1.x, stairs1.z, stairs1.id, climbOpt.opIndex);
await new Promise(r => setTimeout(r, 2000));
} else {
console.warn('No climb-up option found on stairs');
}
} else {
console.warn('No staircase found nearby!');
}
}
// Check if we're on first floor (level 1), need to go to level 2
const midState = ctx.sdk.getState();
const midLevel = midState?.player?.level ?? 0;
console.log(`After first climb, floor level: ${midLevel}`);
if (midLevel === 1) {
// Find stairs to climb to level 2
console.log('Climbing to second floor (bank floor)...');
const stairs2 = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
if (stairs2) {
const climbOpt = stairs2.optionsWithIndex.find(o => /climb.?up/i.test(o.text));
if (climbOpt) {
await ctx.sdk.sendInteractLoc(stairs2.x, stairs2.z, stairs2.id, climbOpt.opIndex);
await new Promise(r => setTimeout(r, 1500));
}
}
}
// Now we should be at level 2, walk to bank area
console.log('Walking to bank...');
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_BANK.x, LOCATIONS.LUMBRIDGE_BANK.z);
// Open bank - find bank booth or banker
console.log('Opening bank...');
let bankOpened = false;
// Try bank booth first
const bankBooth = ctx.sdk.getState()?.nearbyLocs.find(l => /bank booth|bank chest/i.test(l.name));
if (bankBooth) {
const bankOpt = bankBooth.optionsWithIndex.find(o => /^bank$/i.test(o.text)) ||
bankBooth.optionsWithIndex.find(o => /use/i.test(o.text)) ||
bankBooth.optionsWithIndex[0];
if (bankOpt) {
console.log(`Using bank booth option: ${bankOpt.text}`);
await ctx.sdk.sendInteractLoc(bankBooth.x, bankBooth.z, bankBooth.id, bankOpt.opIndex);
await new Promise(r => setTimeout(r, 1000));
// Wait for interface to open
for (let i = 0; i < 10; i++) {
const state = ctx.sdk.getState();
if (state?.interface?.isOpen) {
bankOpened = true;
console.log('Bank interface opened!');
break;
}
if (state?.dialog?.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 500));
} else {
await new Promise(r => setTimeout(r, 300));
}
}
}
}
// If no bank booth, try banker NPC
if (!bankOpened) {
const banker = ctx.sdk.findNearbyNpc(/banker/i);
if (banker) {
const bankOpt = banker.optionsWithIndex.find(o => /bank/i.test(o.text));
if (bankOpt) {
console.log(`Using banker: ${banker.name}`);
await ctx.sdk.sendInteractNpc(banker.index, bankOpt.opIndex);
await new Promise(r => setTimeout(r, 1000));
// Wait for interface to open
for (let i = 0; i < 10; i++) {
const state = ctx.sdk.getState();
if (state?.interface?.isOpen) {
bankOpened = true;
console.log('Bank interface opened!');
break;
}
await new Promise(r => setTimeout(r, 300));
}
}
}
}
if (!bankOpened) {
console.warn('Failed to open bank interface - aborting banking trip');
// Return to cow field without depositing
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
console.log('=== Banking failed, back at cow field ===');
return false;
}
// Count hides before depositing
const hidesBefore = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
// Deposit all cow hides
const hides = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)) ?? [];
for (const hide of hides) {
console.log(`Depositing ${hide.name} from slot ${hide.slot}...`);
await ctx.sdk.sendBankDeposit(hide.slot, hide.count);
await new Promise(r => setTimeout(r, 200));
}
// Wait for items to leave inventory
await new Promise(r => setTimeout(r, 800));
// Verify deposits worked by checking inventory
const hidesAfter = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)).length ?? 0;
const actualDeposited = hidesBefore - hidesAfter;
if (actualDeposited > 0) {
stats.hidesBanked += actualDeposited;
console.log(`Deposited ${actualDeposited} hides. Total banked: ${stats.hidesBanked}`);
} else {
console.warn(`Deposit failed - hides still in inventory (${hidesAfter})`);
}
// Close bank interface by pressing escape or clicking close
// The interface will close when we walk away anyway
console.log('Returning to cow field...');
// Climb down to level 1
const stairs = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
if (stairs) {
const downOpt = stairs.optionsWithIndex.find(o => /climb.?down/i.test(o.text));
if (downOpt) {
await ctx.sdk.sendInteractLoc(stairs.x, stairs.z, stairs.id, downOpt.opIndex);
await new Promise(r => setTimeout(r, 1500));
}
}
// Climb down to level 0
const stairs2 = ctx.sdk.getState()?.nearbyLocs.find(l => /staircase/i.test(l.name));
if (stairs2) {
const downOpt = stairs2.optionsWithIndex.find(o => /climb.?down/i.test(o.text));
if (downOpt) {
await ctx.sdk.sendInteractLoc(stairs2.x, stairs2.z, stairs2.id, downOpt.opIndex);
await new Promise(r => setTimeout(r, 1500));
}
}
// Walk back to cow field
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
console.log('=== Back at cow field ===');
return true;
}
/**
* Log current statistics
*/
function logStats(ctx: ScriptContext, stats: HideStats): void {
const elapsed = Math.floor((Date.now() - stats.startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
const hidesPerMinute = elapsed > 0 ? (stats.hidesBanked / (elapsed / 60)).toFixed(1) : '0';
console.log(`--- Stats (${minutes}m ${seconds}s) ---`);
console.log(`Kills: ${stats.kills}, Hides collected: ${stats.hidesCollected}`);
console.log(`Hides banked: ${stats.hidesBanked}, Bank trips: ${stats.bankTrips}`);
console.log(`Rate: ${hidesPerMinute} hides/min`);
}
/**
* Check if we should clear inventory (drop hides to make space)
*/
function shouldDropHides(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state) return false;
// Drop hides if inventory is getting full
return state.inventory.length >= INVENTORY_THRESHOLD;
}
/**
* Drop cow hides to make inventory space
*/
async function dropHides(ctx: ScriptContext, stats: HideStats): Promise<void> {
console.log('=== Dropping hides to make space ===');
const hides = ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name)) ?? [];
for (const hide of hides) {
console.log(`Dropping ${hide.name}...`);
await ctx.sdk.sendDropItem(hide.slot);
stats.hidesCollected++; // Count as collected even though dropped
await new Promise(r => setTimeout(r, 150));
}
console.log(`Dropped ${hides.length} hides. Continuing training...`);
}
/**
* Main cow hide collecting loop
*/
async function cowhideLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
const stats: HideStats = {
kills: 0,
hidesCollected: 0,
hidesBanked: 0,
bankTrips: 0,
startTime: Date.now(),
lastProgressTime: Date.now(),
};
console.log('=== Cow Hide Banking Script Started ===');
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
// Equip combat gear
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
if (sword) {
console.log('Equipping bronze sword...');
await ctx.bot.equipItem(sword);
}
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
if (shield) {
console.log('Equipping wooden shield...');
await ctx.bot.equipItem(shield);
}
// Set combat style to Aggressive for Strength XP
console.log('Setting combat style to Aggressive (Strength training)...');
await ctx.sdk.sendSetCombatStyle(1); // 0=accurate, 1=aggressive, 2=defensive, 3=controlled
// Walk to cow field
console.log('Walking to cow field...');
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
let lastStatsLog = 0;
// Main loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Dismiss any blocking dialogs (level-up messages)
if (currentState.dialog.isOpen) {
console.log('Dismissing dialog...');
await ctx.sdk.sendClickDialog(0);
continue;
}
// Check HP and eat food if needed
const hp = currentState.skills.find(s => s.name === 'Hitpoints');
if (hp && hp.level < hp.baseLevel * 0.5) {
// Try to eat available food
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|kebab)$/i);
if (food) {
console.log(`HP low (${hp.level}/${hp.baseLevel}) - eating ${food.name}`);
await ctx.bot.eatFood(food);
continue;
} else {
console.warn(`HP low (${hp.level}/${hp.baseLevel}) but no food!`);
}
}
// Log stats periodically (every 10 kills)
if (stats.kills > 0 && stats.kills % 10 === 0 && stats.kills !== lastStatsLog) {
lastStatsLog = stats.kills;
logStats(ctx, stats);
}
// Check if we need to drop hides to make space (simplified - no banking for now)
if (shouldDropHides(ctx)) {
await dropHides(ctx, stats);
continue;
}
// Try to pick up any nearby hides first
await pickupHides(ctx, stats);
// Find a cow to attack
const cow = findBestCow(ctx);
if (!cow) {
// No cows nearby, walk to cow field center
console.log('No cows nearby, walking to cow field...');
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
continue;
}
// Check if already in combat with this cow
const playerCombat = currentState.player?.combat;
if (playerCombat?.inCombat && playerCombat.targetIndex === cow.index) {
const result = await waitForCombatEnd(ctx, cow, stats);
console.log(`Combat ended: ${result}`);
continue;
}
// Attack the cow
console.log(`Attacking ${cow.name} (dist: ${cow.distance})`);
const attackResult = await ctx.bot.attackNpc(cow);
if (!attackResult.success) {
console.warn(`Attack failed: ${attackResult.message}`);
// Try to open gate if blocked
if (attackResult.reason === 'out_of_reach') {
console.log('Trying to open gate...');
const gateResult = await ctx.bot.openDoor(/gate/i);
if (gateResult.success) {
console.log('Gate opened!');
}
}
continue;
}
// Wait for combat to complete
const combatResult = await waitForCombatEnd(ctx, cow, stats);
if (combatResult === 'kill') {
console.log(`Kill #${stats.kills}!`);
// Immediately try to pick up the hide
await new Promise(r => setTimeout(r, 600));
await pickupHides(ctx, stats);
}
}
}
// Main script
async function main() {
// Create fresh account
const username = `ch${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await cowhideLoop(ctx);
} finally {
// Log final stats
const state = ctx.sdk.getState();
if (state) {
console.log('=== Final Results ===');
const hides = state.inventory.filter(i => /cow\s*hide/i.test(i.name));
console.log(`Hides in inventory: ${hides.length}`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 15 * 60 * 1000, // 15 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-228
View File
@@ -1,228 +0,0 @@
# Lab Log: Crafting Trainer
Goal: Train Crafting from level 1 to level 10+ starting from LUMBRIDGE_SPAWN (no special inventory).
## Strategy Analysis
### XP Requirements
- Level 10 requires 4,470 XP
- Starting from level 1 (0 XP)
### Crafting Options from Lumbridge
1. **Spinning Wool** (2.5 XP each)
- Sheep near Lumbridge
- Spinning wheel in Lumbridge Castle 2nd floor
- Need ~1,788 balls - too slow
2. **Leather Crafting** (13.8+ XP each)
- Cows near Lumbridge for hides
- Need to get to Al Kharid (10gp toll)
- Buy needle + thread from craft shop
- Tan hides at Ellis the tanner
- Craft leather gloves (13.8 XP, level 1 req)
- Need ~324 items for level 10
3. **Pottery** (6.3-15 XP depending on item)
- Need clay (mine at Barbarian Village or Rimmington)
- Need water (bucket from spawn)
- Need potter's wheel + pottery oven
- No pottery facilities near Lumbridge
**Chosen approach: Leather crafting** - Best XP rate accessible from Lumbridge
---
## Run 001-016: Development & Testing
**Duration**: Multiple runs, 1-5 minutes each
**Outcome**: Server stability issues (tick freezes), but major progress on script logic
### Phase Completion Status
| Phase | Status | Notes |
|-------|--------|-------|
| 1. Sell shortbow for coins | ✅ Working | Gets 20gp |
| 2. Collect cowhides | ✅ Working | 3 hides per trip (reduced from 10 due to tick freezes) |
| 3. Travel to Al Kharid | ✅ Working | Toll gate dialog handled |
| 4. Buy crafting supplies | ✅ Working | Dommik's Crafting Store found after search |
| 5. Tan hides | ✅ Working | Tanning complete: Leather: 3, Hides: 0 |
| 6. Craft leather items | ❓ Partial | Interface opens but clicking doesn't craft |
### Key Discoveries
#### 1. Tanner Uses Dialog, Not Shop
The Al Kharid tanner uses `Talk-to` (opnpc1), not `Trade`. The dialog flow is:
1. Talk to tanner → "Greetings friend... I see you have brought me some hides."
2. Select "Yes please." when asked about tanning
3. Select "Soft leather" from 4-option menu
4. Hides converted to leather automatically (1gp each)
```typescript
// Talk to tanner, then handle dialog
const talkOpt = tanner.optionsWithIndex.find(o => /talk/i.test(o.text));
await ctx.sdk.sendInteractNpc(tanner.index, talkOpt.opIndex);
// Click "Yes please" then "Soft leather" in dialog
```
#### 2. Dommik's Crafting Store Location
The crafting store is NOT at the Al Kharid general store. Search required:
- General store is at (3315, 3178)
- Dommik found near (3318, 3183) or (3325, 3180)
- Search multiple spots and check for NPC `/^dommik$/i`
- Shop: "Dommiks Crafting Store" - sells needle (1gp) and thread
#### 3. Crafting Interface Issue (Interface ID 2311)
The leather crafting interface has a problem:
- Opens correctly with `sendUseItemOnItem(needle.slot, leather.slot)`
- Shows 15 buttons all labeled "Ok" with no item names
- Options are: `1:Ok, 2:Ok, 3:Ok, 4:Ok, 5:Ok...`
- `sendClickInterface(1)` doesn't trigger crafting
- Interface stays open, items not crafted
**Hypothesis**: May need `sendClickInterfaceComponent` instead of `sendClickInterface`
#### 4. Server Stability (Known Issue)
Consistent tick freezes during combat:
- "Game tick frozen for 16s (last tick: XXXX)"
- Causes disconnect errors
- Reduced batch size from 10 to 3 hides to minimize exposure
- Not a script bug - infrastructure issue
---
## Learnings
### 1. Strategic Findings
**Working approaches:**
- Leather crafting path is viable with these phases
- Tanning through dialog (not shop) - key discovery
- Dommik's shop location requires NPC search, not fixed coords
**Optimization ideas:**
- Could try higher-XP leather items after level 7 (boots)
- Banking crafted items would allow longer runs
### 2. Process & Tooling Reflections
**What made debugging easier:**
- Extensive logging of dialog options
- Logging interface IDs and option indices
- Position logging during navigation
**What would help:**
- More interface debug info (component types, button types)
- Server stability improvements
- Interface click verification
### 3. SDK Issues & Gaps
**Confirmed working:**
- `sendInteractNpc` for dialog-based NPCs
- `sendClickDialog` for NPC conversations
- Shop buying/selling via `buyFromShop`, `sellToShop`
- Walking via `walkTo`
**Issues found:**
- `sendClickInterface` doesn't work for crafting menu (ID 2311)
- Interface options lack descriptive text (all "Ok")
- Need to investigate `sendClickInterfaceComponent` for crafting
---
---
## Run 017 - 2026-01-27 22:17 - SUCCESS
**Outcome**: success
**Duration**: 182s
**Level**: 1 → 11 (exceeded goal!)
### What Happened
- Phase 1-5 all worked (coins, hides, toll gate, shop, tanning)
- Crafting interface (ID 2311) successfully clicked with index 2
- 2 leather gloves crafted, each giving 690 XP
### Key Fixes Applied
#### 1. Cow Field Exit Logic
Added `isInCowField()` check and `exitCowField()` function to handle the fenced cow field:
```typescript
if (isInCowField(ctx)) {
const exited = await exitCowField(ctx, stats);
if (!exited) { ctx.warn('Could not exit cow field'); return false; }
}
```
#### 2. New Porcelain Function: `craftLeather()`
Created `bot.craftLeather()` in `agent/bot-actions.ts` that:
- Validates needle, leather, thread in inventory
- Checks crafting level requirements
- Opens interface 2311 with needle-on-leather
- Clicks the correct button (index 2 = gloves, 3 = boots, etc.)
- Returns success/failure with detailed reason codes
```typescript
const result = await ctx.bot.craftLeather('gloves');
if (result.success) {
ctx.log(`${result.message}`); // "Crafted leather gloves (+690 XP)"
} else {
ctx.warn(`Failed: ${result.message} (reason: ${result.reason})`);
// reason: 'no_needle' | 'no_leather' | 'no_thread' | 'level_too_low' | etc.
}
```
#### 3. Reduced Batch Size
Changed `HIDES_PER_TRIP` from 3 to 2 for faster cycles.
### XP Analysis
- Expected: ~13.8 XP per leather gloves
- Actual: 690 XP per craft (50x higher than expected!)
- This might be tutorial/starter bonus XP or a server configuration
---
## Learnings
### 1. Strategic Findings
**Working approaches:**
- Leather crafting path is fully viable
- Interface 2311 button mapping: index 2 = leather gloves (confirmed)
- Tanning through dialog works (Talk-to, not Trade)
- Small batches (2 hides) work well for avoiding server freezes
**Key discoveries:**
- Cow field requires explicit gate exit logic
- The crafting interface buttons are indexed starting at 2 (not 1)
- Porcelain SDK functions dramatically simplify script logic
### 2. Process & Tooling Reflections
**What made debugging easier:**
- Detailed failure reasons from porcelain functions
- State delta logging shows exactly what changed
- Small batch sizes for faster iteration cycles
**Improvements made:**
- Added `craftLeather()` porcelain function with failure reason codes
- This pattern (returning `{ success, message, reason }`) should be used for all SDK actions
### 3. SDK Issues & Gaps
**Confirmed working:**
- `sendClickInterface(2)` for leather gloves
- Interface ID 2311 for leather crafting
**Issues found:**
- Interface options only show "Ok" text, no item names
- Index 1 doesn't work (maybe reserved/close button?)
**New SDK function added:**
- `bot.craftLeather(product, timeout)` - handles the full leather crafting flow
## Code Location
- Script: `scripts/crafting/script.ts`
- Porcelain function: `agent/bot-actions.ts:craftLeather()`
- Key functions: `tanHides()`, `craftLeatherItems()`, `buyCraftingSupplies()`
-869
View File
@@ -1,869 +0,0 @@
/**
* Crafting Training Script
*
* Goal: Train Crafting from level 1 to level 10+ starting from Lumbridge spawn.
*
* Strategy:
* 1. Sell shortbow at general store to get coins
* 2. Collect cowhides from cow field
* 3. Travel to Al Kharid (pay toll)
* 4. Buy needle + thread from craft shop
* 5. Tan hides at Ellis
* 6. Craft leather items
* 7. Repeat as needed
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Locations
const LOCATIONS = {
LUMBRIDGE_GENERAL_STORE: { x: 3211, z: 3247 },
COW_FIELD: { x: 3253, z: 3270 },
TOLL_GATE: { x: 3268, z: 3228 },
INSIDE_AL_KHARID: { x: 3277, z: 3227 },
// Dommik's Crafting Store - trying multiple locations
// Standard OSRS location should be around (3321, 3179) - east of general store
AL_KHARID_CRAFT_SHOP: { x: 3321, z: 3179 },
AL_KHARID_TANNER: { x: 3274, z: 3192 },
AL_KHARID_BANK: { x: 3269, z: 3167 },
};
// Configuration
// Note: Server has tick freeze issues during extended combat (known bug)
// Using small batches (2 hides) for speed
const HIDES_PER_TRIP = 2; // Collect this many hides before crafting
const TARGET_LEVEL = 10;
// Stats tracking
interface CraftingStats {
hidesCollected: number;
hidesTanned: number;
itemsCrafted: number;
xpGained: number;
startTime: number;
}
// ============ Helper Functions ============
function getCoins(ctx: ScriptContext): number {
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
return coins?.count ?? 0;
}
function getHideCount(ctx: ScriptContext): number {
return ctx.sdk.getState()?.inventory.filter(i => /cow\s*hide/i.test(i.name))
.reduce((sum, i) => sum + i.count, 0) ?? 0;
}
function getLeatherCount(ctx: ScriptContext): number {
return ctx.sdk.getState()?.inventory.filter(i => /^leather$/i.test(i.name))
.reduce((sum, i) => sum + i.count, 0) ?? 0;
}
function getCraftingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Crafting')?.baseLevel ?? 1;
}
function getCraftingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Crafting')?.experience ?? 0;
}
function isInsideAlKharid(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state?.player) return false;
return state.player.worldX >= 3270;
}
function hasNeedle(ctx: ScriptContext): boolean {
return ctx.sdk.findInventoryItem(/needle/i) !== null;
}
function hasThread(ctx: ScriptContext): boolean {
return ctx.sdk.findInventoryItem(/thread/i) !== null;
}
async function dismissDialogs(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
while (ctx.sdk.getState()?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
}
// ============ Phase 1: Get Initial Coins ============
async function getInitialCoins(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
if (getCoins(ctx) >= 30) {
console.log(`Already have ${getCoins(ctx)}gp`);
return;
}
console.log('Phase 1: Getting coins from general store...');
// Walk to general store
await ctx.bot.walkTo(LOCATIONS.LUMBRIDGE_GENERAL_STORE.x, LOCATIONS.LUMBRIDGE_GENERAL_STORE.z);
// Open shop
const openResult = await ctx.bot.openShop(/shop keeper/i);
if (!openResult.success) {
console.warn(`Failed to open shop: ${openResult.message}`);
return;
}
console.log('Shop opened');
// Sell shortbow (worth ~20gp)
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 'all');
if (sellResult.success) {
console.log(sellResult.message);
}
// Close shop
await ctx.bot.closeShop();
console.log(`Have ${getCoins(ctx)}gp after selling`);
}
// ============ Phase 2: Collect Cowhides ============
function findBestCow(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const cows = state.nearbyNpcs
.filter(npc => /^cow$/i.test(npc.name))
.filter(npc => npc.options.some(o => /attack/i.test(o)))
.filter(npc => !npc.inCombat || npc.targetIndex === -1)
.sort((a, b) => a.distance - b.distance);
return cows[0] ?? null;
}
async function waitForCombatEnd(
ctx: ScriptContext,
targetNpc: NearbyNpc,
stats: CraftingStats
): Promise<'kill' | 'fled' | 'lost_target'> {
let combatStarted = false;
let ticksSinceCombatEnded = 0;
let loopCount = 0;
const maxWaitMs = 30000;
const startTime = Date.now();
await new Promise(r => setTimeout(r, 800));
while (Date.now() - startTime < maxWaitMs) {
await new Promise(r => setTimeout(r, 400));
loopCount++;
const state = ctx.sdk.getState();
if (!state) return 'lost_target';
// Dismiss level-up dialogs
if (state.dialog.isOpen) {
console.log('Dismissing dialog during combat...');
await ctx.sdk.sendClickDialog(0);
continue;
}
const currentTick = state.tick;
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
if (!target) {
if (combatStarted || loopCount >= 2) {
return 'kill';
}
return 'lost_target';
}
if (target.maxHp > 0 && target.hp === 0) {
return 'kill';
}
const npcInCombat = target.combatCycle > currentTick;
const playerInCombat = state.player?.combat?.inCombat ?? false;
const inActiveCombat = playerInCombat || npcInCombat;
if (inActiveCombat) {
combatStarted = true;
ticksSinceCombatEnded = 0;
} else if (combatStarted) {
ticksSinceCombatEnded++;
if (ticksSinceCombatEnded >= 4) {
return 'fled';
}
} else if (loopCount >= 8) {
return 'lost_target';
}
}
return 'lost_target';
}
async function collectCowhides(ctx: ScriptContext, stats: CraftingStats, targetHides: number): Promise<void> {
console.log(`Phase 2: Collecting ${targetHides} cowhides...`);
// Equip combat gear
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
if (sword) {
await ctx.bot.equipItem(sword);
}
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
if (shield) {
await ctx.bot.equipItem(shield);
}
// Walk to cow field
await ctx.bot.walkTo(LOCATIONS.COW_FIELD.x, LOCATIONS.COW_FIELD.z);
while (getHideCount(ctx) < targetHides) {
const state = ctx.sdk.getState();
if (!state) break;
// Check inventory space
if (state.inventory.length >= 28) {
console.log('Inventory full, stopping collection');
break;
}
// Dismiss dialogs
await dismissDialogs(ctx, stats);
// Pick up nearby hides
const groundHides = ctx.sdk.getGroundItems()
.filter(i => /cow\s*hide/i.test(i.name))
.filter(i => i.distance <= 8)
.sort((a, b) => a.distance - b.distance);
for (const hide of groundHides.slice(0, 2)) {
if (ctx.sdk.getState()!.inventory.length >= 28) break;
const result = await ctx.bot.pickupItem(hide);
if (result.success) {
stats.hidesCollected++;
console.log(`Picked up hide (total: ${getHideCount(ctx)})`);
}
}
// Find cow to kill
const cow = findBestCow(ctx);
if (!cow) {
console.log('No cows nearby, walking around...');
await ctx.bot.walkTo(
LOCATIONS.COW_FIELD.x + (Math.random() - 0.5) * 20,
LOCATIONS.COW_FIELD.z + (Math.random() - 0.5) * 20
);
continue;
}
// Check if already in combat
const playerCombat = state.player?.combat;
if (playerCombat?.inCombat) {
await waitForCombatEnd(ctx, cow, stats);
continue;
}
// Attack cow
const attackResult = await ctx.bot.attackNpc(cow);
if (!attackResult.success) {
if (attackResult.reason === 'out_of_reach') {
const gateResult = await ctx.bot.openDoor(/gate/i);
if (gateResult.success) {
console.log('Opened gate');
}
}
continue;
}
// Wait for kill
const combatResult = await waitForCombatEnd(ctx, cow, stats);
if (combatResult === 'kill') {
console.log(`Kill! Hides: ${getHideCount(ctx)}`);
await new Promise(r => setTimeout(r, 600));
}
}
console.log(`Collected ${getHideCount(ctx)} hides`);
}
// ============ Phase 3: Travel to Al Kharid ============
// Check if player is in the cow field area (fenced area)
function isInCowField(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state?.player) return false;
const x = state.player.worldX;
const z = state.player.worldZ;
// Cow field is roughly (3242-3265, 3255-3298) - inside the fenced area
return x >= 3242 && x <= 3265 && z >= 3255 && z <= 3298;
}
async function exitCowField(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
console.log('Exiting cow field...');
// The cow field gate is on the south side around (3253, 3255)
// Try to find and open any nearby gate
for (let attempt = 0; attempt < 5; attempt++) {
// Look for a gate
const gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 10);
if (gate) {
const openOpt = gate.optionsWithIndex.find(o => /open/i.test(o.text));
if (openOpt) {
console.log(`Opening cow field gate at (${gate.x}, ${gate.z})`);
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 800));
}
}
// Walk towards the exit point just south of the cow field
const result = await ctx.bot.walkTo(3253, 3250);
if (result.success && !isInCowField(ctx)) {
console.log('Exited cow field');
return true;
}
// If we're not making progress, try opening door
if (!result.success) {
const doorResult = await ctx.bot.openDoor(/gate/i);
if (doorResult.success) {
console.log('Opened gate via openDoor');
}
}
}
return !isInCowField(ctx);
}
async function travelToAlKharid(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
if (isInsideAlKharid(ctx)) {
console.log('Already in Al Kharid');
return true;
}
console.log('Phase 3: Traveling to Al Kharid...');
if (getCoins(ctx) < 10) {
console.warn(`Not enough coins for toll (have ${getCoins(ctx)})`);
return false;
}
// First, exit the cow field if we're in it
if (isInCowField(ctx)) {
const exited = await exitCowField(ctx, stats);
if (!exited) {
console.warn('Could not exit cow field');
return false;
}
}
// Walk to gate - make sure we're at the correct position
console.log(`Walking to toll gate from (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
await ctx.bot.walkTo(LOCATIONS.TOLL_GATE.x, LOCATIONS.TOLL_GATE.z);
await new Promise(r => setTimeout(r, 1000)); // Wait for pathfinding
// Find and interact with gate
let gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 15);
// If gate not found, try walking closer to known toll gate position
if (!gate) {
console.log('Gate not found, walking to exact toll gate position...');
await ctx.bot.walkTo(3267, 3228); // Exact toll gate position
await new Promise(r => setTimeout(r, 1000));
gate = ctx.sdk.getState()?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 15);
}
if (!gate) {
console.warn('No gate found');
return false;
}
const openOpt = gate.optionsWithIndex.find(o => /pay|open/i.test(o.text));
if (!openOpt) {
console.warn('No open/pay option on gate');
return false;
}
// Click gate to trigger dialog
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 800));
// Handle toll dialog
let paidToll = false;
for (let i = 0; i < 20 && !paidToll; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) {
await new Promise(r => setTimeout(r, 150));
continue;
}
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
paidToll = true;
} else {
await ctx.sdk.sendClickDialog(0);
}
await new Promise(r => setTimeout(r, 200));
}
// Wait and dismiss remaining dialogs
await new Promise(r => setTimeout(r, 500));
await dismissDialogs(ctx, stats);
// Walk through
for (let i = 0; i < 3; i++) {
await ctx.bot.walkTo(LOCATIONS.INSIDE_AL_KHARID.x, LOCATIONS.INSIDE_AL_KHARID.z);
await new Promise(r => setTimeout(r, 500));
if (isInsideAlKharid(ctx)) break;
}
const success = isInsideAlKharid(ctx);
console.log(success ? 'Arrived in Al Kharid!' : 'Failed to enter Al Kharid');
return success;
}
// ============ Phase 4: Buy Crafting Supplies ============
async function buyCraftingSupplies(ctx: ScriptContext, stats: CraftingStats): Promise<boolean> {
if (hasNeedle(ctx) && hasThread(ctx)) {
console.log('Already have needle and thread');
return true;
}
console.log('Phase 4: Buying crafting supplies...');
// Walk to craft shop
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_CRAFT_SHOP.x, LOCATIONS.AL_KHARID_CRAFT_SHOP.z);
// Log player position for debugging
const pos = ctx.sdk.getState()?.player;
console.log(`Player at: (${pos?.worldX}, ${pos?.worldZ})`);
// Log all nearby NPCs to find Dommik
const allNpcs = ctx.sdk.getState()?.nearbyNpcs || [];
console.log(`All NPCs nearby (${allNpcs.length}): ${allNpcs.slice(0, 15).map(n => `${n.name}(${n.distance})`).join(', ')}`);
// Find Dommik the crafting shop owner
let shopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
if (!shopkeeper) {
// Try broader search
shopkeeper = ctx.sdk.findNearbyNpc(/craft/i);
}
if (!shopkeeper) {
// Dommik might not be loaded yet - try walking around to find more NPCs
console.log('Dommik not found, searching nearby...');
// Try walking to different spots in the area
const searchSpots = [
{ x: 3321, z: 3179 },
{ x: 3316, z: 3175 },
{ x: 3325, z: 3180 },
{ x: 3318, z: 3183 },
];
for (const spot of searchSpots) {
await ctx.bot.walkTo(spot.x, spot.z);
await new Promise(r => setTimeout(r, 500));
shopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
if (shopkeeper) {
console.log(`Found Dommik at spot (${spot.x}, ${spot.z})`);
break;
}
const npcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 10).map(n => `${n.name}(${n.distance})`).join(', ') ?? 'none';
console.log(`At (${spot.x}, ${spot.z}): NPCs = ${npcs}`);
}
}
if (!shopkeeper) {
console.warn(`Could not find Dommik after searching`);
return false;
}
console.log(`Found shopkeeper: ${shopkeeper.name} at distance ${shopkeeper.distance}`);
// Open shop using Trade option
const tradeOpt = shopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
if (!tradeOpt) {
console.warn(`No trade option on Dommik. Options: ${shopkeeper.options.join(', ')}`);
return false;
}
console.log(`Trading with Dommik (distance: ${shopkeeper.distance})...`);
await ctx.sdk.sendInteractNpc(shopkeeper.index, tradeOpt.opIndex);
// Wait for shop to open (longer wait - interaction will walk us closer)
for (let i = 0; i < 50; i++) {
await new Promise(r => setTimeout(r, 200));
// Check for shop opening
if (ctx.sdk.getState()?.shop.isOpen) break;
// If dialog opened instead (Talk-to triggered), dismiss it
if (ctx.sdk.getState()?.dialog.isOpen) {
console.log('Dialog opened, clicking through...');
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
}
if (!ctx.sdk.getState()?.shop.isOpen) {
// Maybe we need to open a door first?
const door = ctx.sdk.getState()?.nearbyLocs.find(l => /door/i.test(l.name) && l.distance < 5);
if (door) {
console.log('Trying to open door...');
const openOpt = door.optionsWithIndex.find(o => /open/i.test(o.text));
if (openOpt) {
await ctx.sdk.sendInteractLoc(door.x, door.z, door.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 1000));
// Try trading again
const newShopkeeper = ctx.sdk.findNearbyNpc(/^dommik$/i);
if (newShopkeeper) {
const newTradeOpt = newShopkeeper.optionsWithIndex.find(o => /trade/i.test(o.text));
if (newTradeOpt) {
await ctx.sdk.sendInteractNpc(newShopkeeper.index, newTradeOpt.opIndex);
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 200));
if (ctx.sdk.getState()?.shop.isOpen) break;
}
}
}
}
}
}
if (!ctx.sdk.getState()?.shop.isOpen) {
console.warn('Shop still did not open');
return false;
}
console.log(`Shop opened: ${ctx.sdk.getState()?.shop.title}`);
// Buy needle if needed
if (!hasNeedle(ctx)) {
const buyResult = await ctx.bot.buyFromShop(/needle/i, 1);
console.log(buyResult.success ? 'Bought needle' : `Failed to buy needle: ${buyResult.message}`);
}
// Buy thread if needed
if (!hasThread(ctx)) {
const buyResult = await ctx.bot.buyFromShop(/thread/i, 5);
console.log(buyResult.success ? 'Bought thread' : `Failed to buy thread: ${buyResult.message}`);
}
await ctx.bot.closeShop();
return hasNeedle(ctx) && hasThread(ctx);
}
// ============ Phase 5: Tan Hides ============
async function tanHides(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
const hideCount = getHideCount(ctx);
if (hideCount === 0) {
console.log('No hides to tan');
return;
}
console.log(`Phase 5: Tanning ${hideCount} hides...`);
// Walk to tanner
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_TANNER.x, LOCATIONS.AL_KHARID_TANNER.z);
// Find the tanner
const tanner = ctx.sdk.findNearbyNpc(/^tanner$/i);
if (!tanner) {
const npcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 5).map(n => `${n.name}(${n.options.join('/')})`).join(', ') ?? 'none';
console.warn(`No tanner found. Nearby NPCs: ${npcs}`);
return;
}
console.log(`Found tanner: ${tanner.name} at distance ${tanner.distance}, options: ${tanner.options.join(', ')}`);
// Talk to the tanner (uses Talk-to, not Trade)
const talkOpt = tanner.optionsWithIndex.find(o => /talk/i.test(o.text));
if (!talkOpt) {
console.warn('No talk option on tanner');
return;
}
console.log('Talking to tanner...');
await ctx.sdk.sendInteractNpc(tanner.index, talkOpt.opIndex);
// Wait for dialog - game will auto-walk if needed
for (let i = 0; i < 50; i++) {
await new Promise(r => setTimeout(r, 200));
if (ctx.sdk.getState()?.dialog?.isOpen) break;
}
if (!ctx.sdk.getState()?.dialog?.isOpen) {
console.warn('Dialog did not open with tanner');
// Log current state
const player = ctx.sdk.getState()?.player;
console.log(`Player at (${player?.worldX}, ${player?.worldZ})`);
return;
}
console.log('Tanner dialog opened!');
// Handle dialog - need to:
// 1. Click through initial greeting
// 2. Say "Yes please" when asked about tanning hides
// 3. Select "Soft leather" option
for (let i = 0; i < 50; i++) {
await new Promise(r => setTimeout(r, 400));
const s = ctx.sdk.getState();
if (!s?.dialog?.isOpen) {
// Dialog closed - tanning should be complete
console.log('Dialog closed');
break;
}
const options = s.dialog.options;
const optText = options.map(o => o.text).join(' | ');
console.log(`Dialog options: ${optText}`);
// Look for "Soft leather" option first (this is what we want)
const softLeatherOpt = options.find(o => /soft leather/i.test(o.text));
if (softLeatherOpt) {
console.log('Selecting "Soft leather"');
await ctx.sdk.sendClickDialog(softLeatherOpt.index);
await new Promise(r => setTimeout(r, 500));
continue;
}
// Look for "Yes please" to confirm tanning
const yesOpt = options.find(o => /yes.*please/i.test(o.text));
if (yesOpt) {
console.log('Clicking "Yes please"');
await ctx.sdk.sendClickDialog(yesOpt.index);
await new Promise(r => setTimeout(r, 500));
continue;
}
// If just "Click here to continue", click it
if (options.length === 1 && /continue/i.test(options[0]!.text)) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Default - click option 0
await ctx.sdk.sendClickDialog(0);
}
// Wait for tanning to complete
await new Promise(r => setTimeout(r, 500));
const leather = getLeatherCount(ctx);
const hidesLeft = getHideCount(ctx);
stats.hidesTanned += leather;
console.log(`Tanning complete. Leather: ${leather}, Hides remaining: ${hidesLeft}`);
}
// ============ Phase 6: Craft Leather Items ============
async function craftLeatherItems(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
const leatherCount = getLeatherCount(ctx);
if (leatherCount === 0) {
console.log('No leather to craft');
return;
}
console.log(`Phase 6: Crafting with ${leatherCount} leather...`);
const xpBefore = getCraftingXp(ctx);
// Craft all leather using the porcelain function
while (getLeatherCount(ctx) > 0) {
const result = await ctx.bot.craftLeather('gloves');
if (result.success) {
console.log(`${result.message}`);
stats.itemsCrafted += result.itemsCrafted ?? 1;
stats.xpGained += result.xpGained ?? 0;
} else {
console.warn(`Craft failed: ${result.message} (reason: ${result.reason})`);
// If level too low, we can't craft anything
if (result.reason === 'level_too_low') {
break;
}
// If interface didn't open, the game might be stuck - try again
if (result.reason === 'interface_not_opened') {
await dismissDialogs(ctx, stats);
continue;
}
// If no materials, stop
if (result.reason === 'no_leather' || result.reason === 'no_needle' || result.reason === 'no_thread') {
break;
}
// Timeout or no XP gained - try again once
if (result.reason === 'timeout' || result.reason === 'no_xp_gained') {
await dismissDialogs(ctx, stats);
// Try one more time then give up
const retry = await ctx.bot.craftLeather('gloves');
if (!retry.success) {
console.warn(`Retry also failed: ${retry.message}`);
break;
}
console.log(`Retry succeeded: ${retry.message}`);
stats.itemsCrafted += retry.itemsCrafted ?? 1;
stats.xpGained += retry.xpGained ?? 0;
}
}
// Dismiss any level-up dialogs between crafts
await dismissDialogs(ctx, stats);
}
const totalXpGained = getCraftingXp(ctx) - xpBefore;
console.log(`Crafting complete. Total: +${totalXpGained} XP. Level: ${getCraftingLevel(ctx)}`);
}
// ============ Phase 7: Bank and Repeat ============
async function bankItems(ctx: ScriptContext, stats: CraftingStats): Promise<void> {
console.log('Banking items...');
await ctx.bot.walkTo(LOCATIONS.AL_KHARID_BANK.x, LOCATIONS.AL_KHARID_BANK.z);
const openResult = await ctx.bot.openBank();
if (!openResult.success) {
console.warn(`Failed to open bank: ${openResult.message}`);
return;
}
// Deposit crafted items (leather gloves, etc.)
const craftedItems = ctx.sdk.getState()?.inventory.filter(i =>
/gloves|boots|vamb|chaps|body|cowl/i.test(i.name) && /leather/i.test(i.name)
) ?? [];
for (const item of craftedItems) {
await ctx.sdk.sendBankDeposit(item.slot, item.count);
await new Promise(r => setTimeout(r, 150));
}
await ctx.bot.closeBank();
}
// ============ Main Loop ============
async function craftingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
const stats: CraftingStats = {
hidesCollected: 0,
hidesTanned: 0,
itemsCrafted: 0,
xpGained: 0,
startTime: Date.now(),
};
console.log('=== Crafting Training Script ===');
console.log(`Starting level: ${getCraftingLevel(ctx)}`);
console.log(`Target level: ${TARGET_LEVEL}`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
// Get initial coins
await getInitialCoins(ctx, stats);
let cycle = 0;
while (getCraftingLevel(ctx) < TARGET_LEVEL) {
cycle++;
console.log(`\n=== Cycle ${cycle} ===`);
console.log(`Level: ${getCraftingLevel(ctx)}, XP: ${getCraftingXp(ctx)}`);
// Collect hides if we don't have enough
if (getHideCount(ctx) < HIDES_PER_TRIP && !isInsideAlKharid(ctx)) {
await collectCowhides(ctx, stats, HIDES_PER_TRIP);
}
// Travel to Al Kharid if not there
if (!isInsideAlKharid(ctx)) {
const success = await travelToAlKharid(ctx, stats);
if (!success) {
throw new Error('Could not travel to Al Kharid');
}
}
// Buy supplies if needed
if (!hasNeedle(ctx) || !hasThread(ctx)) {
const success = await buyCraftingSupplies(ctx, stats);
if (!success) {
throw new Error('Could not buy crafting supplies');
}
}
// Tan hides
if (getHideCount(ctx) > 0) {
await tanHides(ctx, stats);
}
// Craft items
if (getLeatherCount(ctx) > 0) {
await craftLeatherItems(ctx, stats);
}
// Bank crafted items if inventory is getting full
if (ctx.sdk.getState()!.inventory.length >= 25) {
await bankItems(ctx, stats);
}
// If we've used all materials and still need more levels, go get more hides
if (getHideCount(ctx) === 0 && getLeatherCount(ctx) === 0 && getCraftingLevel(ctx) < TARGET_LEVEL) {
console.log('Need more hides, returning to Lumbridge...');
// Walk back towards Lumbridge (toll gate is free to exit)
await ctx.bot.walkTo(LOCATIONS.TOLL_GATE.x, LOCATIONS.TOLL_GATE.z);
// Walk to Lumbridge side
await ctx.bot.walkTo(3260, 3228);
}
}
// Final stats
const duration = (Date.now() - stats.startTime) / 1000;
console.log('\n=== Final Results ===');
console.log(`Duration: ${Math.round(duration)}s`);
console.log(`Hides collected: ${stats.hidesCollected}`);
console.log(`Hides tanned: ${stats.hidesTanned}`);
console.log(`Items crafted: ${stats.itemsCrafted}`);
console.log(`XP gained: ${stats.xpGained}`);
console.log(`Final level: ${getCraftingLevel(ctx)}`);
}
// Main script
async function main() {
// Create fresh account
const username = `cr${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await craftingLoop(ctx);
} catch (e) {
console.error(`Script aborted: ${e instanceof Error ? e.message : String(e)}`);
throw e;
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 30 * 60 * 1000, // 30 minutes (leather crafting takes time)
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-55
View File
@@ -1,55 +0,0 @@
# Lab Log: firemaking
Training Firemaking from level 1 to level 10+ starting from Lumbridge.
## Strategy
1. Use LUMBRIDGE_SPAWN preset (includes tinderbox + bronze axe)
2. Chop trees near Lumbridge castle to get logs
3. Burn logs with tinderbox
4. Repeat until Firemaking level 10
## Run Log
---
## Run 001 - 2026-01-27 17:06
**Outcome**: SUCCESS
**Duration**: 122s
### What Happened
- Script started at Lumbridge (3222, 3218)
- Chopped 5 logs from trees near Lumbridge castle
- Woodcutting leveled from 1 to 22 (XP is very fast!)
- Burned just 1 log
- Firemaking jumped from level 1 to level 13 with 2000 XP from a single log
### Observations
- XP rates are very generous - skills level extremely fast
- Only needed to burn 1 log to exceed level 10 target
- `bot.chopTree()` and `bot.burnLogs()` worked reliably
- Level-up dialogs were handled automatically by `dismissDialogs()` helper
### What Worked
- LUMBRIDGE_SPAWN preset includes tinderbox + bronze axe (perfect for this task)
- Trees near spawn are easily accessible
- The BotActions methods handle the mechanics well
---
## Learnings
### 1. Strategic Findings
- XP rates in this server are very fast - single actions give substantial XP
- For firemaking training, the bottleneck is chopping logs (takes longer than burning)
- Burning logs is instant (one `burnLogs()` call completes quickly)
- Trees respawn quickly near Lumbridge
### 2. Process & Tooling Reflections
- The sdk/runner framework works well out of the box
- Final state output is helpful for understanding what happened
- Bot actions like `chopTree()` and `burnLogs()` abstract away complexity nicely
### 3. SDK Issues & Gaps
- None encountered - the SDK worked as expected for this simple task
-314
View File
@@ -1,314 +0,0 @@
/**
* Firemaking Training Script
*
* Goal: Train Firemaking from level 1 to level 10+ starting from Lumbridge.
*
* Strategy:
* 1. Start at Lumbridge (preset has tinderbox)
* 2. Chop trees to get logs
* 3. Burn logs using tinderbox
* 4. Repeat until level 10
*
* Notes:
* - LUMBRIDGE_SPAWN preset includes a tinderbox
* - Trees respawn quickly in Lumbridge
* - Fire lighting can fail if standing on a fire or blocked tile
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyLoc, InventoryItem } from '../../sdk/types';
// Lumbridge tree locations (near castle)
const LUMBRIDGE_TREES_AREA = { x: 3200, z: 3230 };
interface Stats {
logsCut: number;
logsBurned: number;
startFiremakingXp: number;
startWoodcuttingXp: number;
startTime: number;
lastProgressTime: number;
}
// ============ Helper Functions ============
// Note: ctx.progress() removed - bot actions and movement auto-reset stall timer
function getFiremakingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Firemaking')?.experience ?? 0;
}
function getFiremakingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Firemaking')?.baseLevel ?? 1;
}
function getWoodcuttingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.experience ?? 0;
}
function getWoodcuttingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
}
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
const state = ctx.sdk.getState();
if (!state?.player) return null;
return { x: state.player.worldX, z: state.player.worldZ };
}
function countLogs(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
return state.inventory.filter(item => /^logs$/i.test(item.name)).reduce((sum, i) => sum + i.count, 0);
}
function hasTinderbox(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state) return false;
return state.inventory.some(item => /tinderbox/i.test(item.name));
}
function hasAxe(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state) return false;
// Check inventory and equipment for any axe
const inInv = state.inventory.some(item => /axe/i.test(item.name));
const equipped = state.equipment.some(item => item && /axe/i.test(item.name));
return inInv || equipped;
}
function findTree(ctx: ScriptContext): NearbyLoc | null {
const state = ctx.sdk.getState();
if (!state) return null;
// Find regular trees (not oak, willow, etc. - those require higher levels)
const trees = state.nearbyLocs
.filter(loc => /^tree$/i.test(loc.name))
.filter(loc => loc.options.some(opt => /^chop/i.test(opt)))
.sort((a, b) => a.distance - b.distance);
return trees[0] ?? null;
}
function getInventoryCount(ctx: ScriptContext): number {
return ctx.sdk.getState()?.inventory.length ?? 0;
}
/**
* Dismiss dialogs (level-up dialogs, etc.)
*/
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
let dismissed = 0;
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
dismissed++;
// progress auto-tracked
}
return dismissed;
}
// ============ Main Phases ============
/**
* Phase 1: Chop trees until we have some logs
*/
async function chopTrees(ctx: ScriptContext, stats: Stats, targetLogs: number = 5): Promise<void> {
console.log(`Chopping trees until we have ${targetLogs} logs...`);
let attempts = 0;
let noTreeCount = 0;
while (countLogs(ctx) < targetLogs && getInventoryCount(ctx) < 28) {
attempts++;
if (attempts % 10 === 0) // progress auto-tracked
// Dismiss any dialogs (level-up, etc.)
if (await dismissDialogs(ctx, stats) > 0) continue;
// Find a tree
const tree = findTree(ctx);
if (!tree) {
noTreeCount++;
if (noTreeCount % 20 === 0) {
console.log('Waiting for tree to respawn...');
// Walk around a bit to find more trees
const pos = getPlayerPos(ctx);
if (pos) {
const offsetX = (Math.random() - 0.5) * 10;
const offsetZ = (Math.random() - 0.5) * 10;
await ctx.sdk.sendWalk(pos.x + offsetX, pos.z + offsetZ, true);
}
}
await new Promise(r => setTimeout(r, 200));
continue;
}
noTreeCount = 0;
// Chop the tree
const logsBefore = countLogs(ctx);
const result = await ctx.bot.chopTree(tree);
// progress auto-tracked
if (result.success) {
const logsAfter = countLogs(ctx);
if (logsAfter > logsBefore) {
stats.logsCut += logsAfter - logsBefore;
console.log(`Got logs! Total: ${stats.logsCut} (inventory: ${logsAfter})`);
}
}
await new Promise(r => setTimeout(r, 200));
}
console.log(`Finished chopping: ${countLogs(ctx)} logs in inventory`);
}
/**
* Phase 2: Burn all logs
*/
async function burnLogs(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log('Burning logs...');
while (countLogs(ctx) > 0) {
// Dismiss any dialogs
if (await dismissDialogs(ctx, stats) > 0) continue;
const logsBefore = countLogs(ctx);
const fmXpBefore = getFiremakingXp(ctx);
const result = await ctx.bot.burnLogs();
// progress auto-tracked
if (result.success) {
stats.logsBurned++;
console.log(`Burned log! FM XP: +${result.xpGained}, Level: ${getFiremakingLevel(ctx)}`);
} else {
console.warn(`Failed to burn log: ${result.message}`);
// If we failed, try moving to a different spot
const pos = getPlayerPos(ctx);
if (pos && result.message.includes('location')) {
console.log('Moving to find a better spot...');
const offsetX = (Math.random() - 0.5) * 6;
const offsetZ = (Math.random() - 0.5) * 6;
await ctx.sdk.sendWalk(pos.x + offsetX, pos.z + offsetZ, true);
await new Promise(r => setTimeout(r, 1000));
}
}
// Check if we're already at target level
if (getFiremakingLevel(ctx) >= 10) {
console.log('Reached level 10!');
return;
}
await new Promise(r => setTimeout(r, 200));
}
}
/**
* Log final statistics
*/
function logFinalStats(ctx: ScriptContext, stats: Stats) {
const state = ctx.sdk.getState();
const firemaking = state?.skills.find(s => s.name === 'Firemaking');
const woodcutting = state?.skills.find(s => s.name === 'Woodcutting');
const fmXpGained = (firemaking?.experience ?? 0) - stats.startFiremakingXp;
const wcXpGained = (woodcutting?.experience ?? 0) - stats.startWoodcuttingXp;
const duration = (Date.now() - stats.startTime) / 1000;
console.log('');
console.log('=== Final Results ===');
console.log(`Duration: ${Math.round(duration)}s`);
console.log(`--- Woodcutting ---`);
console.log(` Level: ${woodcutting?.baseLevel ?? '?'}`);
console.log(` XP Gained: ${wcXpGained}`);
console.log(` Logs Cut: ${stats.logsCut}`);
console.log(`--- Firemaking ---`);
console.log(` Level: ${firemaking?.baseLevel ?? '?'}`);
console.log(` XP Gained: ${fmXpGained}`);
console.log(` Logs Burned: ${stats.logsBurned}`);
}
/**
* Main loop: chop → burn → repeat until level 10
*/
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log('=== Firemaking Training Script ===');
console.log(`Starting levels: Firemaking ${getFiremakingLevel(ctx)}, Woodcutting ${getWoodcuttingLevel(ctx)}`);
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
// Verify we have the required items
if (!hasTinderbox(ctx)) {
throw new Error('No tinderbox in inventory!');
}
if (!hasAxe(ctx)) {
throw new Error('No axe in inventory or equipped!');
}
console.log('Tinderbox and axe confirmed.');
await ctx.bot.dismissBlockingUI();
// progress auto-tracked
let cycle = 0;
while (getFiremakingLevel(ctx) < 10) {
cycle++;
console.log(`\n--- Cycle ${cycle} ---`);
console.log(`Current FM Level: ${getFiremakingLevel(ctx)}`);
// Phase 1: Chop trees to get logs
await chopTrees(ctx, stats, 5);
// Phase 2: Burn all logs
await burnLogs(ctx, stats);
// progress auto-tracked
}
console.log('\n*** GOAL ACHIEVED: Firemaking Level 10+ ***');
}
// Main entry point
async function main() {
// Create fresh account
const username = `FM${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
const stats: Stats = {
logsCut: 0,
logsBurned: 0,
startFiremakingXp: getFiremakingXp(ctx),
startWoodcuttingXp: getWoodcuttingXp(ctx),
startTime: Date.now(),
lastProgressTime: Date.now(),
};
try {
await mainLoop(ctx, stats);
} catch (e) {
if (e instanceof Error) {
console.error(`Script aborted: ${e.message}`);
} else {
throw e;
}
} finally {
logFinalStats(ctx, stats);
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-166
View File
@@ -1,166 +0,0 @@
# Lab Log: fishing-speedrun
Goal: Maximize combined Fishing+Cooking level in 10 minutes.
## Current Status
**Best Result**: Combined Level 112 (Fishing 56 + Cooking 56) in 10 min
## Known Issues
1. **Fire lighting unreliable**: `sendUseItemOnItem` consumes logs but fire not detected
2. **Connection drops**: Browser disconnects intermittently
3. **Draynor dangerous**: Dark wizards kill player - use Al-Kharid instead
## Current Strategy (v3 - Al-Kharid)
**Preset**: FISHER_COOK_AT_DRAYNOR (now at Al-Kharid position)
- Small fishing net
- Tinderbox
- 15 logs
- Position: Al-Kharid (3267, 3148) - SAFE
**Cycle**:
1. Fish until 18 raw fish
2. Light fire with tinderbox + logs
3. Cook all fish on fire
4. Drop cooked/burned fish
5. Repeat (or just fish after logs run out)
---
## Run History
### Run 007 - Banking Fixed!
**Date**: 2026-01-25
**Duration**: ~280s (connection dropped mid-run)
**Results** (best stable cycle):
- Fishing: Level 49 (95,000 XP)
- Cooking: Level 47 (81,000 XP)
- **COMBINED: 96** (single full cycle with banking)
- Fish Caught: 46
- Fish Cooked: 27
- **Fish Banked: 24** ✓
**Fix Applied**:
- Bank booth "Use" option doesn't work
- **"Use-quickly" (option 2) DOES open the bank interface!**
- Deposits now work correctly
**Remaining Issue**:
- Server connection drops intermittently
- When disconnected, player respawns at Lumbridge (~117 tiles away)
- Pathfinding fails with "Not connected" error
- Script unable to recover from Lumbridge spawn
---
### Run 006 - v4 Range + Drop (before banking fix)
**Date**: 2026-01-25
**Duration**: 600s (full 10 min)
**Results**:
- Fishing: Level 56 (198,000 XP)
- Cooking: Level 56 (198,000 XP)
- **COMBINED: 112**
- Cycles: 3 complete fish→cook→drop cycles
- Fish Caught: 78
- Fish Cooked: 54
**Strategy**:
- Fish at Al-Kharid until 27 raw fish
- Walk to range, cook all at once
- Bank interface didn't open (14 booths found but "Use" option failed)
- Fallback: Drop cooked fish
- Return to fishing spot
**Notes**:
- Cooking works perfectly - 27 fish cooked per batch
- Drop fallback ensures continuous progress when banking fails
- 3 cycles per 10 min with current walking distances
---
### Run 005 - Previous Best (Draynor with cooking)
**Date**: 2026-01-25
**Duration**: 520s (~8.7 min) - connection lost early
**Results**:
- Fishing: Level 60 (282,000 XP)
- Cooking: Level 39 (36,000 XP)
- **COMBINED: 99**
- Fires Lit: 1
- Fish Cooked: ~12
### Run 004 - Fishing Only (Draynor)
**Date**: 2026-01-25
**Duration**: 600s (full 10 min)
**Results**:
- Fishing: Level 64 (414,000 XP)
- Cooking: Level 1
- **COMBINED: 65**
### Run 002 - Initial Fishing Success
**Date**: 2026-01-25
**Duration**: 221s
**Results**:
- Fishing: Level 53
- XP/Hour: ~2.3M
---
## Technical Notes
### XP Rates (100x Server)
- Shrimp: 1,000 XP fishing, 3,000 XP cooking
- Anchovies: 4,000 XP fishing, 3,400 XP cooking
### Safe Locations
- **Al-Kharid (3267, 3148)**: Safe shrimp fishing
- Draynor (3086, 3230): DANGEROUS - dark wizards!
### Environment
- `NODE_RANDOM_EVENTS=false`: Disable random events
## Next Steps
1. ~~Fix fire lighting detection (XP check timing)~~ - Switching to permanent range
2. ~~Consider using cooking range instead of fires~~ - **Implemented in v4**
3. Handle connection drops with retries
---
## Strategy v4 - Range & Bank (Current)
**Change**: No more portable fires. Use permanent cooking range + bank cycle.
**Locations**:
- Fishing spot: (3267, 3148)
- Range: (3273, 3180)
- Bank: (3269, 3167)
**Cycle**:
1. Fish until inventory full (28 slots)
2. Walk north to Al-Kharid range
3. Cook all fish on range
4. Walk to Al-Kharid bank
5. Deposit all cooked fish
6. Walk back to fishing spot
7. Repeat
**Advantages**:
- Permanent range never disappears (unlike fires)
- No logs/tinderbox needed (more inventory space)
- Banking allows unlimited cooking without dropping
**Disadvantages**:
- More walking time per cycle
- Distance: ~32 tiles fishing→range, ~13 tiles range→bank, ~19 tiles bank→fishing
-689
View File
@@ -1,689 +0,0 @@
/**
* Fishing + Cooking Speedrun Script (v4 - Range & Bank)
*
* Goal: Maximize combined Fishing+Cooking level in 10 minutes at Al-Kharid.
*
* Strategy:
* - Fish at Al-Kharid fishing spots until inventory full
* - Walk to Al-Kharid range and cook all fish
* - Walk to Al-Kharid bank and deposit cooked fish
* - Return to fishing spot and repeat
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc, NearbyLoc, InventoryItem } from '../../sdk/types';
// Al-Kharid locations
const LOCATIONS = {
FISHING_SPOT: { x: 3267, z: 3148 }, // Safe shrimp fishing
RANGE: { x: 3273, z: 3180 }, // Al-Kharid range
BANK: { x: 3269, z: 3167 }, // Al-Kharid bank
};
interface Stats {
fishCaught: number;
fishCooked: number;
fishBanked: number;
cycles: number;
startFishingXp: number;
startCookingXp: number;
startTime: number;
lastProgressTime: number;
}
// ============ Helper Functions ============
function getFishingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fishing')?.experience ?? 0;
}
function getFishingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fishing')?.baseLevel ?? 1;
}
function getCookingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.experience ?? 0;
}
function getCookingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Cooking')?.baseLevel ?? 1;
}
function getCombinedLevel(ctx: ScriptContext): number {
return getFishingLevel(ctx) + getCookingLevel(ctx);
}
function getPlayerPos(ctx: ScriptContext): { x: number; z: number } | null {
const state = ctx.sdk.getState();
if (!state?.player) return null;
return { x: state.player.worldX, z: state.player.worldZ };
}
function distanceTo(ctx: ScriptContext, target: { x: number; z: number }): number {
const pos = getPlayerPos(ctx);
if (!pos) return 999;
return Math.abs(pos.x - target.x) + Math.abs(pos.z - target.z);
}
/**
* Find the nearest fishing spot (Net option)
*/
function findFishingSpot(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
// Find any fishing spot with "Net" option
const spots = state.nearbyNpcs
.filter(npc => /fishing\s*spot/i.test(npc.name))
.filter(npc => npc.options.some(opt => /^net$/i.test(opt)))
.sort((a, b) => a.distance - b.distance);
return spots[0] ?? null;
}
/**
* Find any fishing spot nearby (for debugging)
*/
function findAnyFishingSpot(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
return state.nearbyNpcs
.filter(npc => /fishing\s*spot/i.test(npc.name))
.sort((a, b) => a.distance - b.distance)[0] ?? null;
}
/**
* Count raw fish in inventory
*/
function countRawFish(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
return state.inventory
.filter(item => /^raw\s/i.test(item.name))
.reduce((sum, item) => sum + item.count, 0);
}
/**
* Get all raw fish items in inventory
*/
function getRawFishItems(ctx: ScriptContext): InventoryItem[] {
const state = ctx.sdk.getState();
if (!state) return [];
return state.inventory.filter(item => /^raw\s/i.test(item.name));
}
/**
* Count cooked fish in inventory (shrimp/anchovies without "raw" prefix)
*/
function countCookedFish(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
return state.inventory
.filter(item =>
/shrimp|anchov/i.test(item.name) &&
!/^raw\s/i.test(item.name) &&
!/^burnt\s/i.test(item.name))
.reduce((sum, item) => sum + item.count, 0);
}
/**
* Get cooked fish items in inventory
*/
function getCookedFishItems(ctx: ScriptContext): InventoryItem[] {
const state = ctx.sdk.getState();
if (!state) return [];
return state.inventory.filter(item =>
/shrimp|anchov/i.test(item.name) &&
!/^raw\s/i.test(item.name) &&
!/^burnt\s/i.test(item.name));
}
/**
* Get inventory count (used slots)
*/
function getInventoryCount(ctx: ScriptContext): number {
return ctx.sdk.getState()?.inventory.length ?? 0;
}
/**
* Get count of non-fishing-net items (to check for fish capacity)
*/
function getAvailableFishSlots(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
// Count items that aren't fishing net
const nonNetItems = state.inventory.filter(i => !/fishing\s*net/i.test(i.name)).length;
return 27 - nonNetItems; // 27 fish slots (28 total - 1 net)
}
/**
* Dismiss dialogs (max count to avoid loops)
*/
async function dismissDialogs(ctx: ScriptContext, stats: Stats, maxCount: number = 3): Promise<number> {
let dismissed = 0;
while (ctx.sdk.getState()?.dialog.isOpen && dismissed < maxCount) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
dismissed++;
}
return dismissed;
}
/**
* Walk to a position and wait to arrive (handles long distances)
*/
async function walkToPosition(ctx: ScriptContext, stats: Stats, target: { x: number; z: number }, name: string): Promise<boolean> {
const startDist = distanceTo(ctx, target);
console.log(`Walking to ${name} (${target.x}, ${target.z}), dist: ${startDist}...`);
// For long distances, use bot.walkTo which handles pathfinding
if (startDist > 30) {
console.log('Long distance walk, using pathfinding...');
const posBefore = getPlayerPos(ctx);
try {
await ctx.bot.walkTo(target.x, target.z);
} catch (e) {
console.warn(`Pathfinding error: ${e}`);
}
const posAfter = getPlayerPos(ctx);
const finalDist = distanceTo(ctx, target);
console.log(`Moved from (${posBefore?.x},${posBefore?.z}) to (${posAfter?.x},${posAfter?.z}), dist: ${finalDist}`);
if (finalDist <= 10) {
console.log(`Arrived at ${name}`);
return true;
}
// If we didn't move at all, try dismissing dialogs and waiting
if (posBefore?.x === posAfter?.x && posBefore?.z === posAfter?.z) {
console.log('Position unchanged - checking for blocking UI...');
await ctx.bot.dismissBlockingUI();
await new Promise(r => setTimeout(r, 1000));
}
}
// Direct walking for short distances
await ctx.sdk.sendWalk(target.x, target.z, true);
// Wait to arrive (within 5 tiles)
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 400));
// Dismiss any dialogs
await dismissDialogs(ctx, stats, 1);
const dist = distanceTo(ctx, target);
if (dist <= 5) {
console.log(`Arrived at ${name}`);
return true;
}
// Re-send walk periodically
if (i > 0 && i % 5 === 0) {
await ctx.sdk.sendWalk(target.x, target.z, true);
}
}
console.warn(`Failed to reach ${name} (dist: ${distanceTo(ctx, target)})`);
return false;
}
// ============ Main Phases ============
/**
* Phase 1: Fish until inventory is full
*/
async function fishUntilFull(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log('Phase 1: Fishing until inventory full...');
let lastFishCount = countRawFish(ctx);
let attempts = 0;
let noSpotCount = 0;
// Fish until no available slots for raw fish
while (getAvailableFishSlots(ctx) > 0) {
attempts++;
// Dismiss dialogs
if (await dismissDialogs(ctx, stats) > 0) continue;
// Check for new fish
const currentFish = countRawFish(ctx);
if (currentFish > lastFishCount) {
stats.fishCaught += currentFish - lastFishCount;
if (currentFish % 5 === 0 || currentFish - lastFishCount > 1) {
console.log(`Fish caught: ${stats.fishCaught} (slots: ${getAvailableFishSlots(ctx)} free)`);
}
noSpotCount = 0; // Reset counter when we catch fish
}
lastFishCount = currentFish;
// Find and use fishing spot
const spot = findFishingSpot(ctx);
if (!spot) {
noSpotCount++;
if (noSpotCount % 50 === 0) console.log('Waiting for fishing spot...');
// If no spot found for too long, try to find any spot and walk to it
if (noSpotCount > 100) { // ~10 seconds
const playerPos = getPlayerPos(ctx);
const distToSpot = distanceTo(ctx, LOCATIONS.FISHING_SPOT);
console.log(`Position: (${playerPos?.x}, ${playerPos?.z}), dist to fishing: ${distToSpot}`);
// If we're far from the fishing area (e.g. teleported to Lumbridge), walk back
if (distToSpot > 20) {
console.log(`Too far from fishing spot, walking back...`);
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
} else {
// We're close but no spot - check if there's any fishing spot
const anySpot = findAnyFishingSpot(ctx);
if (anySpot) {
console.log(`Found fishing spot at (${anySpot.x}, ${anySpot.z}), options: ${anySpot.options.join(', ')}`);
await ctx.sdk.sendWalk(anySpot.x, anySpot.z, true);
await new Promise(r => setTimeout(r, 2000));
} else {
const nearbyNpcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 5) ?? [];
console.log(`No spot visible. NPCs: ${nearbyNpcs.map(n => n.name).join(', ') || 'none'}`);
// Walk around a bit to find the spot
await ctx.sdk.sendWalk(LOCATIONS.FISHING_SPOT.x + (Math.random() > 0.5 ? 3 : -3), LOCATIONS.FISHING_SPOT.z, true);
await new Promise(r => setTimeout(r, 1500));
}
}
noSpotCount = 0;
}
await new Promise(r => setTimeout(r, 100));
continue;
}
noSpotCount = 0;
const netOpt = spot.optionsWithIndex.find(o => /^net$/i.test(o.text));
if (!netOpt) {
await new Promise(r => setTimeout(r, 300));
continue;
}
await ctx.sdk.sendInteractNpc(spot.index, netOpt.opIndex);
await new Promise(r => setTimeout(r, 200));
}
console.log(`Inventory full: ${countRawFish(ctx)} raw fish`);
}
/**
* Phase 2: Cook all fish at the range
*/
async function cookAllFish(ctx: ScriptContext, stats: Stats): Promise<void> {
// Walk to range
if (!await walkToPosition(ctx, stats, LOCATIONS.RANGE, 'range')) {
console.warn('Could not reach range');
return;
}
console.log('Phase 2: Cooking fish at range...');
// Find the range
const range = ctx.sdk.getState()?.nearbyLocs.find(loc => /range|stove/i.test(loc.name));
if (!range) {
console.warn('No range found nearby');
const locs = ctx.sdk.getState()?.nearbyLocs.slice(0, 10) ?? [];
console.log(`Nearby locs: ${locs.map(l => l.name).join(', ')}`);
return;
}
console.log(`Found: ${range.name} at (${range.x}, ${range.z})`);
const cookingXpBefore = getCookingXp(ctx);
const rawFishBefore = countRawFish(ctx);
// Cook each raw fish one at a time (game may not have batch cooking interface)
while (countRawFish(ctx) > 0) {
const rawFish = getRawFishItems(ctx)[0];
if (!rawFish) break;
// Use fish on range
await ctx.sdk.sendUseItemOnLoc(rawFish.slot, range.x, range.z, range.id);
// Wait for cooking to happen (XP gain or raw fish count decrease)
// Also handle any interface/dialog that appears
for (let i = 0; i < 15; i++) {
await new Promise(r => setTimeout(r, 300));
const state = ctx.sdk.getState();
// Handle cooking interface if it appears
const firstInterfaceOpt = state?.interface?.options[0];
if (state?.interface?.isOpen && firstInterfaceOpt) {
console.log('Clicking cook option...');
await ctx.sdk.sendClickInterfaceOption(0);
// After clicking, wait for batch cooking to complete
let noChange = 0;
while (noChange < 10 && countRawFish(ctx) > 0) {
await new Promise(r => setTimeout(r, 400));
await dismissDialogs(ctx, stats, 1);
const prev = countRawFish(ctx);
await new Promise(r => setTimeout(r, 200));
if (countRawFish(ctx) === prev) noChange++;
else noChange = 0;
}
break;
}
// Handle dialog
if (state?.dialog?.isOpen) {
await ctx.sdk.sendClickDialog(0);
}
// Check if this fish was cooked (raw count decreased)
if (countRawFish(ctx) < rawFishBefore - stats.fishCooked) {
break;
}
}
}
// Calculate results
const xpGained = getCookingXp(ctx) - cookingXpBefore;
const fishCooked = rawFishBefore - countRawFish(ctx);
stats.fishCooked += Math.max(0, fishCooked);
console.log(`Cooked ${fishCooked} fish (XP: +${xpGained})`);
console.log(`Done cooking. Cooked fish in inventory: ${countCookedFish(ctx)}`);
}
/**
* Phase 3: Clear cooked fish (bank or drop)
*/
async function clearCookedFish(ctx: ScriptContext, stats: Stats): Promise<void> {
const cookedBefore = countCookedFish(ctx);
if (cookedBefore === 0) {
console.log('No cooked fish to clear');
return;
}
console.log(`Phase 3: Clearing ${cookedBefore} cooked fish...`);
// Try banking first
const bankSuccess = await tryBanking(ctx, stats, cookedBefore);
if (!bankSuccess) {
// Banking failed - drop cooked fish instead
console.log('Banking failed, dropping cooked fish...');
await dropCookedFish(ctx, stats);
}
}
/**
* Try to bank cooked fish
*/
async function tryBanking(ctx: ScriptContext, stats: Stats, cookedBefore: number): Promise<boolean> {
// Walk to bank
if (!await walkToPosition(ctx, stats, LOCATIONS.BANK, 'bank')) {
return false;
}
// Debug: log what we see at the bank
const state = ctx.sdk.getState();
const bankBooths = state?.nearbyLocs.filter(l => /bank/i.test(l.name)) ?? [];
const bankers = state?.nearbyNpcs.filter(n => /banker/i.test(n.name)) ?? [];
console.log(`At bank: ${bankBooths.length} booths, ${bankers.length} bankers`);
// Try banker NPC first (more reliable)
const banker = bankers.find(n => n.optionsWithIndex.some(o => /bank/i.test(o.text)));
if (banker) {
const bankOpt = banker.optionsWithIndex.find(o => /^bank$/i.test(o.text));
if (bankOpt) {
console.log(`Using banker: ${banker.name}, option ${bankOpt.opIndex}: ${bankOpt.text}`);
await ctx.sdk.sendInteractNpc(banker.index, bankOpt.opIndex);
// Wait for interface or dialog
const opened = await waitForBankInterface(ctx, stats);
if (opened) {
return await depositCookedFish(ctx, stats, cookedBefore);
}
}
}
// Try bank booth with different options
const bankBooth = bankBooths[0];
if (bankBooth) {
console.log(` Booth: ${bankBooth.name} at (${bankBooth.x}, ${bankBooth.z}), options: ${bankBooth.optionsWithIndex.map(o => `${o.opIndex}:${o.text}`).join(', ')}`);
// Try "Bank" option first, then "Use-quickly", then "Use"
const options = [
bankBooth.optionsWithIndex.find(o => /^bank$/i.test(o.text)),
bankBooth.optionsWithIndex.find(o => /use-quickly/i.test(o.text)),
bankBooth.optionsWithIndex.find(o => /^use$/i.test(o.text)),
].filter(Boolean);
for (const opt of options) {
if (!opt) continue;
console.log(`Trying bank booth option ${opt.opIndex}: ${opt.text}`);
await ctx.sdk.sendInteractLoc(bankBooth.x, bankBooth.z, bankBooth.id, opt.opIndex);
const opened = await waitForBankInterface(ctx, stats);
if (opened) {
return await depositCookedFish(ctx, stats, cookedBefore);
}
}
}
console.warn('Bank interface did not open');
return false;
}
/**
* Wait for bank interface to open (handling dialogs)
*/
async function waitForBankInterface(ctx: ScriptContext, stats: Stats): Promise<boolean> {
// Wait for interface OR dialog to appear
for (let i = 0; i < 40; i++) { // 8 seconds total
await new Promise(r => setTimeout(r, 200));
const currentState = ctx.sdk.getState();
// Bank interface opened!
if (currentState?.interface?.isOpen) {
console.log(`Bank interface opened! (id: ${currentState.interface.interfaceId})`);
return true;
}
// Click through dialogs
if (currentState?.dialog?.isOpen) {
const text = currentState.dialog.text || '';
const options = currentState.dialog.options;
console.log(`Dialog: "${text.substring(0, 50)}..." options: ${options.map(o => o.text).join(', ')}`);
// Look for bank-related option or just click first option
const bankOption = options.find(o => /bank/i.test(o.text));
const firstOption = options[0];
if (bankOption) {
await ctx.sdk.sendClickDialog(bankOption.index);
} else if (firstOption) {
await ctx.sdk.sendClickDialog(firstOption.index);
} else {
await ctx.sdk.sendClickDialog(0);
}
await new Promise(r => setTimeout(r, 300));
}
}
return false;
}
/**
* Deposit cooked fish when bank interface is open
*/
async function depositCookedFish(ctx: ScriptContext, stats: Stats, cookedBefore: number): Promise<boolean> {
const currentState = ctx.sdk.getState();
if (!currentState?.interface?.isOpen) {
console.warn('Bank interface not open');
return false;
}
// Deposit all cooked and burnt fish
const itemsToDeposit = currentState.inventory.filter(item =>
(/shrimp|anchov/i.test(item.name) && !/^raw\s/i.test(item.name)) ||
/^burnt\s/i.test(item.name));
console.log(`Depositing ${itemsToDeposit.length} items...`);
for (const item of itemsToDeposit) {
console.log(` ${item.name} x${item.count} from slot ${item.slot}`);
await ctx.sdk.sendBankDeposit(item.slot, item.count);
await new Promise(r => setTimeout(r, 150));
}
await new Promise(r => setTimeout(r, 300));
const cookedAfter = countCookedFish(ctx);
const deposited = cookedBefore - cookedAfter;
stats.fishBanked += Math.max(0, deposited);
// Bank interface closes automatically when we walk away
console.log(`Banked ${deposited} fish (total: ${stats.fishBanked})`);
return deposited > 0;
}
/**
* Drop all cooked and burnt fish
*/
async function dropCookedFish(ctx: ScriptContext, stats: Stats): Promise<void> {
const state = ctx.sdk.getState();
if (!state) return;
const itemsToDrop = state.inventory.filter(item =>
(/shrimp|anchov/i.test(item.name) && !/^raw\s/i.test(item.name)) ||
/^burnt\s/i.test(item.name));
let dropped = 0;
for (const item of itemsToDrop) {
await ctx.sdk.sendDropItem(item.slot);
dropped += item.count;
await new Promise(r => setTimeout(r, 100));
}
console.log(`Dropped ${dropped} cooked/burnt fish`);
}
/**
* Log final statistics
*/
function logFinalStats(ctx: ScriptContext, stats: Stats) {
const state = ctx.sdk.getState();
const fishing = state?.skills.find(s => s.name === 'Fishing');
const cooking = state?.skills.find(s => s.name === 'Cooking');
const fishingXpGained = (fishing?.experience ?? 0) - stats.startFishingXp;
const cookingXpGained = (cooking?.experience ?? 0) - stats.startCookingXp;
const duration = (Date.now() - stats.startTime) / 1000;
console.log('');
console.log('=== Final Results ===');
console.log(`Duration: ${Math.round(duration)}s`);
console.log(`Cycles: ${stats.cycles}`);
console.log(`--- Fishing ---`);
console.log(` Level: ${fishing?.baseLevel ?? '?'}`);
console.log(` XP Gained: ${fishingXpGained}`);
console.log(` Fish Caught: ${stats.fishCaught}`);
console.log(`--- Cooking ---`);
console.log(` Level: ${cooking?.baseLevel ?? '?'}`);
console.log(` XP Gained: ${cookingXpGained}`);
console.log(` Fish Cooked: ${stats.fishCooked}`);
console.log(` Fish Banked: ${stats.fishBanked}`);
console.log(`--- Combined ---`);
console.log(` COMBINED LEVEL: ${(fishing?.baseLevel ?? 1) + (cooking?.baseLevel ?? 1)}`);
}
/**
* Ensure we're at the fishing spot
*/
async function ensureAtFishingSpot(ctx: ScriptContext, stats: Stats): Promise<void> {
const pos = getPlayerPos(ctx);
const dist = distanceTo(ctx, LOCATIONS.FISHING_SPOT);
console.log(`Current position: (${pos?.x}, ${pos?.z}), distance to fishing spot: ${dist}`);
if (dist > 10) {
console.log('Not at fishing spot, walking there...');
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
}
}
/**
* Main loop: fish → cook at range → bank → repeat
*/
async function mainLoop(ctx: ScriptContext, stats: Stats): Promise<void> {
console.log('=== Fishing + Cooking Speedrun (v4 - Range & Bank) ===');
console.log(`Starting levels: Fishing ${getFishingLevel(ctx)}, Cooking ${getCookingLevel(ctx)}`);
console.log(`Combined: ${getCombinedLevel(ctx)}`);
console.log(`Position: (${ctx.sdk.getState()?.player?.worldX}, ${ctx.sdk.getState()?.player?.worldZ})`);
await ctx.bot.dismissBlockingUI();
// Ensure we start at the fishing spot
await ensureAtFishingSpot(ctx, stats);
while (true) {
stats.cycles++;
console.log(`\n--- Cycle ${stats.cycles} ---`);
console.log(`Current levels: Fishing ${getFishingLevel(ctx)}, Cooking ${getCookingLevel(ctx)} = ${getCombinedLevel(ctx)}`);
// Phase 1: Fish until inventory full
await fishUntilFull(ctx, stats);
// Phase 2: Walk to range and cook
await cookAllFish(ctx, stats);
// Phase 3: Bank or drop cooked fish
await clearCookedFish(ctx, stats);
// Phase 4: Walk back to fishing spot
console.log('Phase 4: Returning to fishing spot...');
await walkToPosition(ctx, stats, LOCATIONS.FISHING_SPOT, 'fishing spot');
}
}
async function main() {
// Create fresh account
const username = `FS${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.FISHER_COOK_AT_DRAYNOR);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
const stats: Stats = {
fishCaught: 0,
fishCooked: 0,
fishBanked: 0,
cycles: 0,
startFishingXp: getFishingXp(ctx),
startCookingXp: getCookingXp(ctx),
startTime: Date.now(),
lastProgressTime: Date.now(),
};
try {
await mainLoop(ctx, stats);
} catch (e) {
if (e instanceof Error) {
console.error(`Script aborted: ${e.message}`);
} else {
throw e;
}
} finally {
logFinalStats(ctx, stats);
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-514
View File
@@ -1,514 +0,0 @@
# Lab Log: fletching-gp
Goal: Maximize GP earned from fletching unstrung bows and selling them (now 10 minutes).
## Strategy Overview
**Approach**: Chop -> Fletch -> Sell cycle
- Chop normal trees near Lumbridge for logs
- Fletch logs into best available product based on Fletching level:
- Level 1-4: Arrow shafts (15 per log, 5 XP)
- Level 5+: Shortbow (u) (1 per log, 5 XP)
- Sell products at Lumbridge general store
- Track total GP earned
**Starting conditions**: Bronze axe, knife, position near Lumbridge trees
**Key metrics**:
- Total GP earned (primary reward signal)
- Logs chopped
- Items fletched
- Items sold
- Final Fletching level
---
## Run 014 - 2026-01-25 07:15 (died to giant spider again)
**Outcome**: died, lost all items
**GP Earned**: 93 GP earned, then lost on death
### What Happened
1. Knife pickup worked, cycle 1 completed (93 GP)
2. Switched to oak trees near general store
3. **Giant spider attacked while chopping oaks**
4. HP: 9→7→6→3→1→DEAD
5. Respawned at Lumbridge, lost knife + axe + 93 GP
### Death Timeline (from events.jsonl)
```
tick 13791: HP=9, Giant spider dist=0 (attacking!)
tick 14527: HP=7
tick 15213: HP=6
tick 15966: HP=3
tick 16654: HP=1
tick 18122: HP=10, respawned - inventory wiped
```
### Problem
The oak trees near Lumbridge general store (3203, 3243) are in a dangerous area with Giant spiders (level 2). They're aggressive and will attack low-level players.
### Potential Solutions
1. **Kill threatening spiders** - fight back when attacked
2. Add food and eat when HP low
3. Use different oak tree location (e.g., near Lumbridge church)
4. Run away when attacked
---
## Run 013 - 2026-01-25 06:56 (legit start - knife from ground)
**Outcome**: timeout (93 GP in 1 cycle)
**Duration**: 10m 0s
**GP Earned**: 93 GP
### Configuration
- Start: Level 1 Fletching, bronze axe only
- **Pick up knife from ground** (SE of Lumbridge at 3224, 3202)
- No cheats!
### Results
- Found and picked up knife successfully
- Cycle 1: 6 normal logs → 4 longbow + 1 shortbow + 15 shafts = **93 GP**
- Leveled 1→21 Fletching
- Died to spider in oak phase (see Run 014)
### Key Finding
**Legit start works!** Knife pickup from ground is viable.
---
## Run 012 - 2026-01-25 06:10 (oak progression success!)
**Outcome**: timeout (successful - 243 GP!)
**Duration**: 10m 0s
**GP Earned**: 243 GP (2 cycles)
### Configuration
- Start: Level 1 Fletching, bronze axe + knife + 5 bread
- Strategy: Normal trees until level 20, then oaks
### Results
| Cycle | Logs | Products | GP | Fletching Level |
|-------|------|----------|-----|-----------------|
| 1 | 6 normal | 4 longbow, 1 shortbow, 15 shafts | 93 | 1→21 |
| 2 | 6 oak | 1 oak longbow, 5 oak shortbow | 150 | 21→33 |
**Total: 243 GP** - more than 2x previous best!
### Key Finding: Oak bows are VERY profitable!
- **Oak longbow: 112 GP** (vs 21 GP for normal longbow)
- **Oak shortbow: ~8 GP each** (38 GP for 5)
- Progression strategy works: normal logs → level 20 → oaks
### Observations
- Bot switched to oaks automatically at level 21
- Fletching dialog has some reliability issues ("Dialog closed without fletching")
- Only 2 cycles in 10 min - lots of time lost to dialog errors and recovery
- No death this time (food not needed, or no random events)
### Next Steps
- Optimize to get more cycles per 10 min
- Maybe stay with oaks longer once level 20+
- Fix fletching dialog reliability
---
## Run 011 - 2026-01-25 05:21 (oak experiment - knife lost)
**Outcome**: timeout (stuck in loop)
**Duration**: 10m 0s
**GP Earned**: 93 GP (1 cycle only)
### Configuration
- Start: Level 1 Fletching, Level 1 Woodcutting
- Bronze axe + Knife
- Try normal trees first, switch to oaks at WC 15
### What Happened
1. **First cycle was AMAZING**:
- Chopped 6 normal logs
- Leveled from 1→21 Fletching in one batch (boosted XP)
- Made: 1 arrow shaft stack, 1 shortbow, 4 longbows
- Sold for **93 GP** (84 longbow, 9 shortbow, 0 arrow shafts)
2. **Then knife disappeared**:
- Continued chopping successfully (WC leveled to 39!)
- Walked to oak trees, chopped 10 oak logs
- Tried to fletch oak → "No knife in inventory"
- Script stuck in infinite fletch-fail loop
### Key Finding: Longbows are VERY profitable!
- Previous runs at level 5: 42 GP per 6 shortbows (first cycle)
- This run at level 10+: 84 GP per 4 longbows!
- **Longbows sell for ~21 GP each vs ~7 GP for shortbows**
### Bug Investigation - SOLVED
The bot **DIED from a Swarm random event!**
- HP: 10→7→6→2→0 over ~30 seconds
- Respawned at Lumbridge castle, lost knife + 93 GP
- Then stuck trying to fletch without knife
Event log showed:
- tick 9471: HP 7, Swarm attacking
- tick 11632: HP 0 (dead!)
- tick 12325: Respawned at (3220, 3208) without knife/coins
### Implications
1. **Normal logs + longbows** might be optimal - no need for oaks!
2. Need food in inventory to survive random events
3. Starting at level 10 Fletching (to make longbows immediately) could be worth testing
---
## Run 010 - 2026-01-25 04:49 (shopSell timeout)
**Outcome**: error
**Duration**: ~3m
**GP Earned**: 12 GP (1 cycle before crash)
### What Happened
The script was running smoothly but crashed with "Action timed out: shopSell" during the second sell cycle. This appears to be a server/browser stability issue.
### Stability Notes
Observed crashes across runs:
- Run 006: "Bot not connected" at ~9 min
- Run 007: Completed but with fletch failures
- Run 008: ✅ Full 10 min success
- Run 009: ✅ Full 10 min success
- Run 010: "shopSell timeout" at ~3 min
The script logic is solid; stability issues are external (server/browser).
---
## Run 009 - 2026-01-25 04:37 (saturated shop)
**Outcome**: timeout (successful completion)
**Duration**: 10m 0s
**GP Earned**: 61 GP
### Key Finding: Shop Persistence
The Lumbridge General Store was **already saturated from Run 008**. Shop stock persists across game sessions!
### GP Breakdown
| Cycle | GP Earned | Notes |
|-------|-----------|-------|
| 1 | 13 | Already saturated! |
| 2 | 12 | Stable low prices |
| 3 | 12 | Stable low prices |
| 4 | 12 | Stable low prices |
| 5 | 12 | Stable low prices |
**Total: 61 GP** (vs 108 GP on fresh shop)
### Comparison: Fresh vs Saturated Shop
| Metric | Fresh Shop (Run 008) | Saturated (Run 009) |
|--------|---------------------|---------------------|
| First cycle GP | 42 | 13 |
| Total GP | 108 | 61 |
| GP/cycle (avg) | 27 | 12 |
### Implications
1. **Shop rotation is critical** - a fresh shop gives 2x more GP
2. The shop price "floor" is ~2 GP per bow (12 GP per 6 bows)
3. Subsequent runs without shop reset will earn ~60 GP baseline
---
## Run 008 - 2026-01-25 04:26 (10-minute success) ⭐
**Outcome**: timeout (successful completion)
**Duration**: 10m 0s
**GP Earned**: 108 GP
### Configuration
- Time limit: 10 minutes
- Start: Level 5 Fletching
- Batch size: 6 logs
- Added GP/cycle tracking
### Results
- **4 complete sell cycles**
- **Fletching level 29**
- 30 logs chopped, 24 fletched, 24 sold
### GP Breakdown by Cycle
| Cycle | GP Earned | Cumulative | GP/Bow | Shop State |
|-------|-----------|------------|--------|------------|
| 1 | 42 | 42 | ~7 | Fresh |
| 2 | 32 | 74 | ~5.3 | Filling |
| 3 | 21 | 95 | ~3.5 | Saturating |
| 4 | 13 | 108 | ~2.2 | Saturated |
### Key Success Factors
1. **Full 10-minute run without crash** - server stability improved
2. **Cycle tracking working** - clear visibility into saturation curve
3. **No fletching failures** (unlike Run 007)
4. **108 GP baseline established** for 10-minute runs
### Saturation Analysis
The GP/cycle drops ~50% every 2 cycles due to shop accumulation. Extrapolating:
- Cycles 5-6 would earn ~6-8 GP each (diminishing returns)
- **Shop rotation could significantly improve earnings** - walking to Varrock after cycle 2-3 when Lumbridge saturates
### Next Steps
1. Consider shop rotation (walk to Varrock General Store after Lumbridge saturates)
2. Or accept saturation and optimize for speed (more cycles = more total GP even at lower rates)
---
## Run 007 - 2026-01-25 04:16 (fletch failures)
**Outcome**: timeout
**Duration**: 10m 0s
**GP Earned**: 8 GP (issues)
### What Happened
- Multiple "Fletching dialog did not open" errors
- Shop opening failure at one point
- GP tracking showed incorrect values due to sell cycle interruptions
### Root Cause
Intermittent fletching dialog issues - the knife+log interaction sometimes fails to open the dialog. Need better retry logic or dialog detection.
---
## Run 006 - 2026-01-25 03:57 (10-minute extension)
**Outcome**: error (connection dropped ~9 min in)
**Duration**: ~9m 4s
**GP Earned**: 105 GP (97 recorded + 8 in final partial sell)
### Configuration
- Time limit extended to **10 minutes**
- Start: Level 5 Fletching
- Batch size: 6 logs
- Sell threshold: 6 items
### Results
- 5 complete sell cycles before crash
- Reached Fletching level 31
- 30 logs chopped, 30 items fletched, 28+ items sold
### GP Breakdown by Cycle
| Cycle | GP Earned | Cumulative | Shop State |
|-------|-----------|------------|------------|
| 1 | 42 | 42 | Fresh (9,8,7,7,6,5 GP) |
| 2 | 28 | 70 | Filling up |
| 3 | 15 | 85 | Saturating |
| 4 | 12 | 97 | More saturated |
| 5 | 8 | 105 | Nearly saturated |
### Key Observations
1. **Shop saturation accelerates**: GP per cycle dropped from 42→28→15→12→8 as stock accumulated
2. **Script executed flawlessly**: No logic errors, all cycles completed cleanly
3. **External crash**: "Bot not connected" error during 5th sell cycle - browser/server issue, not script bug
4. **Chopping rate**: ~3.3 logs/minute with bronze axe (30 logs in ~9 min)
5. **Projected 10-min earnings**: ~110-120 GP at current saturation rate
### Issue: Connection Dropped
The bot disconnected during `sellToShop()` after 9 minutes of stable operation. This appears to be a browser/server stability issue rather than a script problem. The final state snapshot shows:
- Shop was open
- 3 shortbows remaining in inventory
- 97 GP before crash, 8 GP sold in final transaction
### Ideas for Improvement
1. **Shop rotation**: Could walk to Varrock General Store after Lumbridge saturates (~3 cycles)
2. **Better axe**: Steel/mithril axe would increase chop rate significantly
3. **Oak logs**: At higher levels, oak logs → oak longbows sell for more GP
4. **Connection resilience**: Add reconnection logic if disconnected mid-operation
---
## Run 005 - 2026-01-25 02:35
**Outcome**: timeout (but successful cycles)
**Duration**: 5m 0s
**GP Earned**: 25 GP (shop saturated from previous runs)
### Configuration
- Start: Level 5 Fletching
- Batch size: 6 logs
- Sell threshold: 6 items
### Results
- 2 complete sell cycles
- Reached Fletching level 23
- 12 shortbows sold
### Key Finding: Shop Saturation
Shop prices depend on current stock:
- **Fresh shop**: 9,8,7,7,6,6 GP = 43 GP per 6 bows
- **Saturated shop**: 2,2,2,2,2,2 GP = 12 GP per 6 bows
The Lumbridge General Store retains stock between runs, causing prices to drop over time. Best results occur when the shop has no shortbows in stock.
### What Works Well
1. Starting at level 5 skips worthless arrow shafts
2. 6-bow batches balance efficiency and frequency
3. GP-based success detection in sell loop is reliable
4. Walking away from stuck shop recovers gracefully
---
## Run 001 - 2026-01-25 00:43
**Outcome**: timeout
**Duration**: 5m 0s
**GP Earned**: 0
### What Happened
- Chopped 16 logs successfully
- Fletched 16 times, reached Fletching level 28
- Sold 240 arrow shafts total
- Completed 2 full chop->fletch->sell cycles
### Metrics
- Logs chopped: 16
- Items fletched: 16
- Items sold: 240
- Final Fletching level: 28
### Issues Found
1. **fletchLogs() product selection bug**: Even when calling `fletchLogs("short bow")`, the function creates Arrow shafts. The dialog option selection isn't working correctly.
- Evidence: Events show `"method":"fletchLogs","args":["short bow"]` returning `"product":{"name":"Arrow shaft"}`
- Root cause: The product pattern matching or dialog click sequence isn't selecting the right option
2. **Arrow shafts sell for 0 GP**: The Lumbridge general store gives 0 coins for arrow shafts.
- Evidence: Sold 240 arrow shafts, GP earned stayed at 0
- Need to sell shortbows or longbows (which are worth more) to earn GP
3. **Shop closing stuck**: `closeShop()` consistently times out and requires walking away to force close.
- Workaround: Added retry limit and walk-away fallback
4. **Level-up dialogs blocking**: Frequent level-up dialogs (from rapid XP gain) interfere with fletching and need to be dismissed.
- Workaround: Added dialog dismissal loops before key operations
### Root Cause Analysis
The primary failure is that **we cannot make shortbows** due to the `fletchLogs()` product selection bug. Since arrow shafts are worthless at the general store, we earn 0 GP regardless of how many we sell.
### Fixes Needed
1. **Fix fletchLogs() in bot-actions.ts**: The dialog option selection logic needs to properly click the shortbow/longbow option before clicking Ok. The current implementation falls through to making arrow shafts.
2. **Alternative shop**: Try selling at a different shop that buys arrow shafts for GP, or fix the bow creation to sell bows instead.
### What Worked Well
- Script structure and cycle logic is correct
- Chopping logs works reliably (~6-8 logs per minute with bronze axe)
- Fletching arrow shafts works (just not bows)
- Selling to shop works (just for 0 GP)
- Walking away to force-close stuck shops is effective
- Reached level 28 Fletching in 5 minutes (good XP rate!)
---
## Next Steps
1. Investigate and fix the `fletchLogs()` product selection bug in bot-actions.ts
2. Once fixed, verify shortbows sell for GP at general store
3. Consider starting with oak logs near a different location for higher-value products
4. Optimize batch sizes based on travel time vs. crafting time
---
## Technical Notes
### Inventory Structure
- Logs don't stack - each is a separate inventory item with count=1
- Arrow shafts DO stack - one item with count=15 per log
- Shortbow (u) doesn't stack - one item per bow
### XP Rates (observed)
- Arrow shafts: ~750 XP per fletch action (server has boosted rates?)
- Reached level 24 from 1 in first 8 fletches
### Shop Behavior
- General store at (3212, 3246) buys most items
- Arrow shafts sell for 0 GP (possibly server-specific)
- Shop interface sometimes gets stuck open
---
## Learnings (Updated after 10-minute expansion)
### 1. Strategic Findings
**Key Metric: GP Earnings**
- **Fresh shop**: 108 GP in 10 minutes (4 cycles: 42, 32, 21, 13 GP)
- **Saturated shop**: 61 GP in 10 minutes (5 cycles: ~12 GP each)
- **Shop stock persists** across game sessions - this is the biggest factor!
**What Worked:**
- **Batch size of 6 logs** is optimal - small enough for frequent sell cycles, large enough to minimize travel overhead
- **Starting at Fletching level 5** skips worthless arrow shafts and goes straight to shortbows
- **GP-based success detection** in sell loop is more reliable than trusting `sellToShop()` return values
- **Walking away from stuck shop** is an effective recovery mechanism
- **Dismissing dialogs before operations** prevents blocking
- **GP/cycle tracking** - lets us see saturation in real-time
**What Didn't Work:**
- **Arrow shafts as a product** - worthless at general stores (0 GP)
- **Relying on sellToShop success field** - reports false failures even when sales succeed
- **Running without checking shop state** - subsequent runs earn less due to saturation
**Game Mechanics Insights:**
- Shop prices decrease as stock increases: 9→8→7→7→6→5 GP for first 6 bows
- Shop "floor price" is ~2 GP per bow when saturated
- Shop stock persists between game sessions/runs - not reset per character
- Logs don't stack in inventory (count=1 each), shortbows don't stack (count=1 each)
**Saturation Curve (GP per 6 bows):**
| Cycle | Fresh Shop | Saturated |
|-------|-----------|-----------|
| 1 | 42 | 12 |
| 2 | 32 | 12 |
| 3 | 21 | 12 |
| 4 | 13 | 12 |
| 5 | - | 12 |
**Future Improvements:**
- Shop rotation to Varrock when Lumbridge saturates (would require ~2 min travel)
- Better axe (steel/mithril) for faster chopping
- Oak logs for higher-value products at higher levels
### 2. Process & Tooling Reflections
**What Made Debugging Easier:**
- The `events.jsonl` with action results was invaluable for tracing issues
- State snapshots showing inventory contents helped verify operations
- **GP/cycle tracking** made saturation visible immediately
- Adding inline `ctx.log()` for decision points
**What Made Debugging Harder:**
- State snapshots don't include shop data (shop.isOpen, shop.playerItems)
- Hard to distinguish "script bug" vs "SDK bug" vs "game server" when things fail
- Server/browser stability issues cause intermittent crashes
**Stability Observations:**
- ~50% of 10-minute runs complete without crashes
- Common errors: "Bot not connected", "shopSell timeout", "Fletching dialog did not open"
- These appear to be external server/browser issues, not script bugs
### 3. SDK Issues & Gaps
**Functions That Don't Work As Expected:**
- `sellToShop()` returns `success: false` even when GP increases
- `closeShop()` times out frequently - requires walk-away workaround
- `fletchLogs()` occasionally fails with "dialog did not open"
**Missing Functionality:**
- No `getShopStock()` or way to check current shop prices
- No way to check if shop will buy an item without attempting sale
- No `waitForShopClose()` with force-close option
- Missing shop locations (Varrock, etc.) in Locations constants
**Workarounds Still In Use:**
- Checking GP before/after sell to detect success
- Walking away to force-close stuck shops
- Manual dialog dismissal loops before operations
- Retry counters to escape stuck states
-459
View File
@@ -1,459 +0,0 @@
/**
* Fletching GP Maximizer
*
* Strategy: Normal logs until level 20, then oaks
* - Chop normal trees -> fletch longbows -> sell
* - At level 20+, switch to oak trees -> oak shortbows/longbows
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets, Items, Locations } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Locations
const NORMAL_TREES_AREA = { x: 3200, z: 3230 };
const OAK_TREES_AREA = { x: 3203, z: 3243 };
const GENERAL_STORE = Locations.LUMBRIDGE_SHOP;
const LOGS_BEFORE_FLETCH = 6;
const MIN_LOGS_TO_FLETCH = 4;
type FletchProduct = 'arrow shafts' | 'short bow' | 'long bow' | 'oak short bow' | 'oak long bow';
function getBestProduct(fletchingLevel: number, logType: 'normal' | 'oak'): FletchProduct | null {
if (logType === 'oak') {
if (fletchingLevel >= 25) return 'oak long bow';
if (fletchingLevel >= 20) return 'oak short bow';
return null; // Can't fletch oaks below 20
}
if (fletchingLevel >= 10) return 'long bow';
if (fletchingLevel >= 5) return 'short bow';
return 'arrow shafts';
}
// Track GP earned
interface GPTracker {
startingGP: number;
currentGP: number;
itemsSold: number;
logsChopped: number;
productsFletched: number;
sellCycles: number;
gpPerCycle: number[]; // Track GP earned each sell cycle
}
function getFletchingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.baseLevel ?? 1;
}
function getWoodcuttingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
}
function getGP(ctx: ScriptContext): number {
const coins = ctx.sdk.findInventoryItem(/coins/i);
return coins?.count ?? 0;
}
function countLogs(ctx: ScriptContext): { normal: number; oak: number; total: number } {
// Logs don't stack - count all log items in inventory
const inv = ctx.sdk.getState()?.inventory ?? [];
const normal = inv.filter(i => /^logs$/i.test(i.name)).length;
const oak = inv.filter(i => /^oak logs$/i.test(i.name)).length;
return { normal, oak, total: normal + oak };
}
function countSellableItems(ctx: ScriptContext): number {
const inv = ctx.sdk.getState()?.inventory ?? [];
// Count arrow shafts (15 per log) and unstrung bows (1 per log)
// Include oak shortbow and oak longbow
return inv.filter(i =>
/arrow shaft|shortbow|longbow/i.test(i.name)
).reduce((sum, i) => sum + i.count, 0);
}
function getInventoryFreeSlots(ctx: ScriptContext): number {
const inv = ctx.sdk.getState()?.inventory ?? [];
return 28 - inv.length;
}
function logStats(ctx: ScriptContext, tracker: GPTracker, label: string): void {
const level = getFletchingLevel(ctx);
const gpEarned = tracker.currentGP - tracker.startingGP;
const cyclesStr = tracker.gpPerCycle.length > 0
? ` | Cycles: [${tracker.gpPerCycle.join(', ')}]`
: '';
console.log(`[${label}] GP: ${gpEarned} | Fletch: ${level} | Logs: ${tracker.logsChopped} | Fletched: ${tracker.productsFletched} | Sold: ${tracker.itemsSold}${cyclesStr}`);
}
async function fletchingGP(ctx: ScriptContext) {
const { bot, sdk } = ctx;
// Initialize tracking
const tracker: GPTracker = {
startingGP: getGP(ctx),
currentGP: getGP(ctx),
itemsSold: 0,
logsChopped: 0,
productsFletched: 0,
sellCycles: 0,
gpPerCycle: [],
};
logStats(ctx, tracker, 'START');
// First: Find and pick up a knife from the ground
const hasKnife = () => !!sdk.findInventoryItem(/knife/i);
if (!hasKnife()) {
console.log('Looking for knife on ground...');
// Try to find and pickup knife
for (let attempt = 0; attempt < 5 && !hasKnife(); attempt++) {
const knife = sdk.findGroundItem(/knife/i);
if (knife) {
console.log(`Found knife at (${knife.x}, ${knife.z}), picking up...`);
const result = await bot.pickupItem(knife);
if (result.success) {
console.log('Got knife!');
break;
} else {
console.log(`Pickup failed: ${result.message}`);
}
} else {
console.log(`No knife visible (attempt ${attempt + 1})`);
}
await sleep(1000);
}
if (!hasKnife()) {
console.log('WARNING: No knife found - will try to continue anyway');
}
}
// Main loop: Chop -> Fletch -> Sell
while (true) {
const state = ctx.sdk.getState();
if (!state?.player) {
await sleep(500);
continue;
}
// Eat if HP is low (survive random events)
const hpSkill = sdk.getSkill('Hitpoints');
const currentHp = hpSkill?.level ?? 10;
const maxHp = hpSkill?.baseLevel ?? 10;
if (currentHp < maxHp - 3) {
const food = sdk.findInventoryItem(/bread|shrimp|fish/i);
if (food) {
console.log(`HP low (${currentHp}/${maxHp}), eating ${food.name}...`);
await bot.eatFood(food);
}
}
// Close shop first if open (before checking dialogs)
if (state.shop.isOpen) {
// First dismiss any dialogs that might be blocking
if (state.dialog.isOpen) {
await sdk.sendClickDialog(0);
await sleep(300);
}
// Try closing shop up to 5 times, then walk away to force close
for (let attempt = 0; attempt < 5; attempt++) {
console.log(`Closing shop (attempt ${attempt + 1})...`);
await sdk.sendCloseShop();
await sleep(600);
if (!ctx.sdk.getState()?.shop.isOpen) {
console.log('Shop closed successfully');
break;
}
}
// If STILL open after 5 attempts, walk away to force close
if (ctx.sdk.getState()?.shop.isOpen) {
console.log('Shop stuck, walking away to force close...');
await bot.walkTo(NORMAL_TREES_AREA.x, NORMAL_TREES_AREA.z);
}
continue;
}
// Dismiss level-up dialogs (NOT shop interface)
if (state.dialog.isOpen) {
await sdk.sendClickDialog(0);
await sleep(250);
continue;
}
const freeSlots = getInventoryFreeSlots(ctx);
const logs = countLogs(ctx);
const sellableCount = countSellableItems(ctx);
const fletchLevel = getFletchingLevel(ctx);
// Log state every time we have logs (for debugging)
if (logs.total >= MIN_LOGS_TO_FLETCH) {
console.log(`Decision: ${logs.oak} oak + ${logs.normal} normal logs, ${sellableCount} sellable, ${freeSlots} free slots`);
}
// Decision tree:
// 1. If we have sellable items and inventory is getting full, go sell
// 2. If we have enough logs, fletch them
// 3. Otherwise, chop more trees
if (sellableCount > 0 && (freeSlots < 8 || sellableCount >= 6)) {
// === SELLING PHASE ===
console.log(`Selling ${sellableCount} items...`);
// Walk to general store
await bot.walkTo(GENERAL_STORE.x, GENERAL_STORE.z);
// Open shop
const shopResult = await bot.openShop(/shop.*keeper|general/i);
if (!shopResult.success) {
console.log(`Failed to open shop: ${shopResult.message}`);
await sleep(1000);
continue;
}
// Sell all fletched products - oak longbows first (most valuable)
const sellPatterns = [/oak longbow/i, /oak shortbow/i, /longbow/i, /shortbow/i, /arrow shaft/i];
const gpBefore = getGP(ctx);
let totalItemsSold = 0;
for (const pattern of sellPatterns) {
let attempts = 0;
while (attempts < 20) { // Max 20 attempts per pattern
attempts++;
const currentState = ctx.sdk.getState();
if (!currentState?.shop.isOpen) break;
const item = currentState.shop.playerItems.find(i => pattern.test(i.name));
if (!item || item.count === 0) break;
const gpBeforeSell = getGP(ctx);
const sellResult = await bot.sellToShop(item, 'all');
// Check if GP increased (actual success) even if sellResult reports failure
const gpAfterSell = getGP(ctx);
if (gpAfterSell > gpBeforeSell || sellResult.success) {
const gpGained = gpAfterSell - gpBeforeSell;
const sold = sellResult.amountSold ?? 1;
totalItemsSold += sold;
tracker.itemsSold += sold;
console.log(`Sold ${item.name} (+${gpGained} GP)`);
} else if (sellResult.rejected) {
console.log(`Shop won't buy ${item.name}`);
break;
} else {
// No GP gain and not successful, move to next pattern
break;
}
await sleep(100);
}
}
const gpAfter = getGP(ctx);
const cycleGP = gpAfter - gpBefore;
tracker.sellCycles++;
tracker.gpPerCycle.push(cycleGP);
console.log(`Sell cycle #${tracker.sellCycles} complete: +${cycleGP} GP, ${totalItemsSold} items`);
// Log saturation warning if GP per cycle is dropping significantly
if (tracker.gpPerCycle.length >= 2) {
const lastCycleGP = tracker.gpPerCycle[tracker.gpPerCycle.length - 2];
if (lastCycleGP !== undefined && cycleGP < lastCycleGP * 0.6) {
console.log(`WARNING: Shop saturation detected: GP dropped from ${lastCycleGP} to ${cycleGP}`);
}
}
// Dismiss any level-up dialogs that appeared during selling
for (let i = 0; i < 5; i++) {
const currentState = ctx.sdk.getState();
if (currentState?.dialog.isOpen) {
console.log('Dismissing dialog before closing shop...');
await sdk.sendClickDialog(0);
await sleep(300);
} else {
break;
}
}
// Close shop before checking GP
await bot.closeShop();
await sleep(500);
// If shop still open, force close
if (ctx.sdk.getState()?.shop.isOpen) {
console.log('Shop still open after closeShop, forcing close...');
await sdk.sendCloseShop();
await sleep(500);
}
// Update GP tracking
tracker.currentGP = getGP(ctx);
const gpEarned = tracker.currentGP - tracker.startingGP;
console.log(`Total GP earned so far: ${gpEarned}`);
logStats(ctx, tracker, 'AFTER SELL');
} else if (logs.total >= MIN_LOGS_TO_FLETCH && (logs.total >= LOGS_BEFORE_FLETCH || freeSlots <= 3)) {
// === FLETCHING PHASE ===
// First dismiss any dialogs
for (let i = 0; i < 3; i++) {
if (ctx.sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
await sleep(300);
} else {
break;
}
}
// Use oak logs only if we can fletch them (level 20+)
const useOakLogs = fletchLevel >= 20 && logs.oak > 0;
const logType = useOakLogs ? 'oak' : 'normal';
// Skip if we only have oak logs but can't fletch them
if (logs.normal === 0 && !useOakLogs) {
console.log(`Can't fletch oak logs at level ${fletchLevel}, need 20+`);
await sleep(500);
continue;
}
const product = getBestProduct(fletchLevel, logType)!;
console.log(`Fletching ${logType} logs into ${product} (level ${fletchLevel})...`);
// Fletch all logs with timeout protection
let fletchedThisBatch = 0;
let consecutiveFailures = 0;
const fletchStart = Date.now();
const maxFletchTime = 60_000; // 60 seconds max for fletching
while (countLogs(ctx).total > 0 && Date.now() - fletchStart < maxFletchTime) {
// Dismiss any dialogs first
if (ctx.sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
await sleep(200);
continue;
}
const currentLogs = countLogs(ctx);
const currentLevel = getFletchingLevel(ctx);
const currentUseOak = currentLevel >= 20 && currentLogs.oak > 0;
const currentLogType = currentUseOak ? 'oak' : 'normal';
// Stop if we only have oaks but can't fletch them
if (currentLogs.normal === 0 && !currentUseOak) break;
const currentProduct = getBestProduct(currentLevel, currentLogType)!;
const fletchResult = await bot.fletchLogs(currentProduct);
if (fletchResult.success) {
fletchedThisBatch++;
tracker.productsFletched++;
consecutiveFailures = 0;
// Check if we leveled up
const newLevel = getFletchingLevel(ctx);
if (newLevel !== fletchLevel) {
console.log(`Fletching level up! Now level ${newLevel}`);
}
} else {
consecutiveFailures++;
console.log(`Fletch failed (${consecutiveFailures}/3): ${fletchResult.message}`);
if (consecutiveFailures >= 3) {
console.log(`Too many fletch failures, moving on...`);
break;
}
// Wait a bit before retry
await sleep(500);
}
await sleep(100);
}
if (fletchedThisBatch > 0) {
console.log(`Fletched ${fletchedThisBatch} items`);
}
} else {
// === CHOPPING PHASE ===
// Normal trees until level 20, then oaks
const useOaks = fletchLevel >= 20;
const tree = useOaks
? sdk.findNearbyLoc(/^oak$/i) ?? sdk.findNearbyLoc(/^tree$/i)
: sdk.findNearbyLoc(/^tree$/i);
if (!tree) {
const targetArea = useOaks ? OAK_TREES_AREA : NORMAL_TREES_AREA;
console.log(`No trees nearby, walking to ${useOaks ? 'oak' : 'normal'} trees...`);
await bot.walkTo(targetArea.x, targetArea.z);
await sleep(500);
continue;
}
// Chop the tree
const chopResult = await bot.chopTree(tree);
if (chopResult.success) {
tracker.logsChopped++;
// Log progress occasionally
if (tracker.logsChopped % 10 === 0) {
logStats(ctx, tracker, `CHOP #${tracker.logsChopped}`);
}
} else {
// Tree might be gone, wait and retry
await sleep(500);
}
}
await sleep(100);
}
}
// Main script
async function main() {
// Create fresh account
const username = `fg${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, {
position: { x: 3224, z: 3205 }, // Near knife spawn SE of castle
inventory: [
{ id: Items.BRONZE_AXE, count: 1 },
],
});
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
console.log('=== Fletching GP Maximizer ===');
console.log('Goal: Maximize GP from fletching and selling longbows in 10 minutes');
// Wait for state to initialize
await new Promise(r => setTimeout(r, 2000));
const state = ctx.sdk.getState();
if (!state?.player) {
console.error('No player state');
return;
}
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
// Dismiss any startup dialogs
await ctx.bot.dismissBlockingUI();
// Run the fletching loop
await fletchingGP(ctx);
console.log('=== Script Complete ===');
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000, // 10 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
// Helper
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
-107
View File
@@ -1,107 +0,0 @@
# Lab Log: fletching
## Goal
Train Fletching from level 1 to 10+ starting from a fresh Lumbridge spawn.
## Strategy
1. Pick up knife from ground spawn SE of Lumbridge castle (around 3224, 3202)
2. Chop trees for logs using bronze axe (from preset)
3. Use knife on logs to make arrow shafts (level 1-4)
4. At level 5+, can make shortbows for 5 XP each
5. At level 10+, can make longbows for 10 XP each
6. Drop fletched items when inventory fills
## XP Requirements
- Level 10 requires 1,154 XP
- Arrow shafts: 5 XP per log (but bonus XP from game speeds this up significantly)
- Shortbow: 5 XP per log (level 5)
- Longbow: 10 XP per log (level 10)
---
## Run 001 - 2026-01-27 19:06
**Outcome**: Failed - no knife
**Duration**: ~40s
### What Happened
- Walked to expected knife spawn location (3224, 3205)
- Bot ended up at (3217, 3237) instead - wrong direction!
- No knife found on ground after 10 attempts
- Tried general store but they don't sell knives
- Script aborted
### Root Cause
1. The walkTo function got stuck and went wrong direction
2. Knife spawn location was slightly incorrect (3205 vs actual 3202)
3. Needed more wait time for knife to potentially respawn
### Fix
- Added more debugging output
- Extended wait time for knife respawn
- Check for knife from starting position first
---
## Run 002 - 2026-01-27 19:07
**Outcome**: Connection error
**Duration**: ~10s
### What Happened
Bot disconnected during walk to knife spawn.
### Root Cause
Transient connection issue - not script related.
---
## Run 003 - 2026-01-27 19:07 (SUCCESS)
**Outcome**: Success
**Duration**: ~60s
### What Happened
1. Started at Lumbridge castle (3222, 3218)
2. Walked toward knife spawn but got stuck at (3228, 3233)
3. Waited 8 attempts (~16 seconds) for knife to appear
4. Found knife at (3224, 3202) - slightly different than expected coords
5. Picked up knife successfully
6. Walked to trees at (3200, 3230)
7. Chopped 4 logs - Woodcutting went 1->20 (bonus XP)
8. Fletched all 4 logs into arrow shafts
9. Each log gave massive XP: Fletching jumped 1->4->7->9->11
### Key Observations
- Knife spawns at (3224, 3202), not (3224, 3205)
- Game gives significant bonus XP, so only 4 logs needed to reach level 11
- Arrow shafts give 375 XP per log in this game version (vs standard 5 XP)
- Woodcutting levels up extremely fast too
- Level-up dialogs handled correctly
### Final Stats
- Fletching: Level 11 (from level 1)
- Woodcutting: Level 20 (from level 1)
- Logs chopped: 4
- Arrow shafts made: 60 (4 x 15)
- Total fletching XP: ~1,500
---
## Learnings
### 1. Strategic Findings
- Knife spawns at (3224, 3202) SE of Lumbridge castle
- Only need 4 logs to reach level 10+ due to bonus XP system
- Arrow shafts are efficient - 15 per log at 5 XP base each
- Don't need to switch to shortbows/longbows - arrow shafts get you to 10+ fast
### 2. Process & Tooling Reflections
- Good debugging output (position, ground items) helped identify knife location
- WalkTo can get stuck - need tolerance for imperfect pathing
- Waiting for item respawns (2s intervals, 15 attempts) worked well
### 3. SDK Issues & Gaps
- walkTo() sometimes fails to reach exact destination but gets close enough
- Ground item visibility range seems limited - knife only appeared after waiting
- No issue with fletchLogs() - worked reliably
-332
View File
@@ -1,332 +0,0 @@
/**
* Fletching Trainer Script
*
* Goal: Train Fletching from level 1 to 10+ starting from Lumbridge spawn.
*
* Strategy:
* 1. Pick up knife from ground spawn near Lumbridge castle (SE of castle)
* 2. Chop trees for logs using bronze axe (included in preset)
* 3. Use knife on logs to make arrow shafts (level 1-4)
* 4. At level 5+, can make shortbows for more XP per log
* 5. Drop arrow shafts/bows when inventory fills
*
* XP per action:
* - Arrow shafts: 5 XP (15 shafts per log)
* - Shortbow: 5 XP (level 5 required)
* - Longbow: 10 XP (level 10 required)
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Locations
const KNIFE_SPAWN = { x: 3224, z: 3202 }; // SE of Lumbridge castle (actual spawn location)
const LUMBRIDGE_TREES = { x: 3200, z: 3230 }; // Trees west of Lumbridge castle
// Configuration
const TARGET_LEVEL = 10;
function getFletchingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.baseLevel ?? 1;
}
function getFletchingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Fletching')?.experience ?? 0;
}
function countLogs(ctx: ScriptContext): number {
const inv = ctx.sdk.getState()?.inventory ?? [];
return inv.filter(i => /^logs$/i.test(i.name)).length;
}
function hasKnife(ctx: ScriptContext): boolean {
return !!ctx.sdk.findInventoryItem(/knife/i);
}
function hasAxe(ctx: ScriptContext): boolean {
const inv = ctx.sdk.getState()?.inventory ?? [];
const equip = ctx.sdk.getState()?.equipment ?? [];
return inv.some(i => /axe/i.test(i.name)) || equip.some(i => /axe/i.test(i.name));
}
function getInventoryFreeSlots(ctx: ScriptContext): number {
const inv = ctx.sdk.getState()?.inventory ?? [];
return 28 - inv.length;
}
async function dropFletchedItems(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) return;
// Drop arrow shafts and bows (keep knife, axe, logs)
const itemsToDrop = state.inventory.filter(item =>
/arrow shaft|bow/i.test(item.name) && !/shortbow/i.test(item.name) === false
);
// Also drop unstrung bows and arrow shafts
const allDroppable = state.inventory.filter(item =>
/arrow shaft|shortbow|longbow/i.test(item.name)
);
if (allDroppable.length === 0) return;
console.log(`Dropping ${allDroppable.length} fletched items...`);
for (const item of allDroppable) {
await ctx.sdk.sendDropItem(item.slot);
await sleep(150);
}
}
async function acquireKnife(ctx: ScriptContext): Promise<boolean> {
const { bot, sdk } = ctx;
// First check if knife is already visible from starting position
let knife = sdk.findGroundItem(/knife/i);
if (knife) {
console.log(`Found knife at (${knife.x}, ${knife.z}) from starting position!`);
const result = await bot.pickupItem(knife);
if (result.success) {
console.log('Got knife!');
return true;
}
}
// Walk to knife spawn area (SE of Lumbridge castle)
console.log(`Walking to knife spawn at (${KNIFE_SPAWN.x}, ${KNIFE_SPAWN.z})...`);
const walkResult = await bot.walkTo(KNIFE_SPAWN.x, KNIFE_SPAWN.z);
console.log(`Walk result: ${walkResult.message}`);
// Log current position for debugging
const pos = ctx.sdk.getState()?.player;
console.log(`Current position after walk: (${pos?.worldX}, ${pos?.worldZ})`);
// Try to pick up knife from ground - wait for respawn if needed
for (let attempt = 0; attempt < 15 && !hasKnife(ctx); attempt++) {
knife = sdk.findGroundItem(/knife/i);
if (knife) {
console.log(`Found knife at (${knife.x}, ${knife.z}), picking up...`);
const result = await bot.pickupItem(knife);
if (result.success) {
console.log('Got knife!');
return true;
} else {
console.log(`Pickup failed: ${result.message}`);
}
} else {
// Log nearby ground items for debugging
const groundItems = ctx.sdk.getState()?.groundItems ?? [];
if (groundItems.length > 0) {
console.log(`Nearby ground items: ${groundItems.map(i => `${i.name} at (${i.x},${i.z})`).join(', ')}`);
}
console.log(`No knife visible (attempt ${attempt + 1}/15), waiting for respawn...`);
}
await sleep(2000); // Wait 2s for potential respawn
}
// If no knife on ground, check if we can buy from a shop that sells knives
// General stores don't always stock knives, but some specialty shops do
if (!hasKnife(ctx)) {
console.log('No knife found on ground. Checking for alternative sources...');
// Check if there's a knife anywhere nearby we missed
const allGroundItems = ctx.sdk.getState()?.groundItems ?? [];
console.log(`All visible ground items (${allGroundItems.length}): ${allGroundItems.map(i => i.name).join(', ')}`);
}
return hasKnife(ctx);
}
async function chopAndFletch(ctx: ScriptContext): Promise<void> {
const { bot, sdk } = ctx;
console.log(`Walking to trees at (${LUMBRIDGE_TREES.x}, ${LUMBRIDGE_TREES.z})...`);
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
let logsChopped = 0;
let itemsFletched = 0;
let stuckCount = 0;
const MAX_STUCK = 10;
const LOGS_PER_BATCH = 5; // Chop this many logs before fletching
while (getFletchingLevel(ctx) < TARGET_LEVEL) {
const state = ctx.sdk.getState();
if (!state) {
await sleep(500);
continue;
}
// Dismiss any level-up dialogs
if (state.dialog.isOpen) {
console.log('Dismissing dialog...');
await sdk.sendClickDialog(0);
await sleep(300);
continue;
}
const freeSlots = getInventoryFreeSlots(ctx);
const logs = countLogs(ctx);
const level = getFletchingLevel(ctx);
// Determine best product based on level
// Level 1-4: Arrow shafts only
// Level 5-9: Shortbows (5 XP each)
// Level 10+: Longbows (10 XP each)
let product: string;
if (level >= 10) {
product = 'long bow';
} else if (level >= 5) {
product = 'short bow';
} else {
product = 'arrow shafts';
}
// Drop fletched items if inventory is getting full
if (freeSlots <= 2) {
await dropFletchedItems(ctx);
continue;
}
// If we have logs, fletch them
if (logs >= LOGS_PER_BATCH || (logs > 0 && freeSlots <= 5)) {
console.log(`Fletching ${logs} logs into ${product} (level ${level})...`);
let fletchedThisBatch = 0;
let consecutiveFailures = 0;
while (countLogs(ctx) > 0 && consecutiveFailures < 3) {
// Dismiss any dialogs
if (ctx.sdk.getState()?.dialog.isOpen) {
await sdk.sendClickDialog(0);
await sleep(200);
continue;
}
const xpBefore = getFletchingXp(ctx);
const result = await bot.fletchLogs(product);
if (result.success) {
fletchedThisBatch++;
itemsFletched++;
consecutiveFailures = 0;
const newLevel = getFletchingLevel(ctx);
if (newLevel > level) {
console.log(`LEVEL UP! Fletching is now level ${newLevel}`);
}
} else {
consecutiveFailures++;
console.log(`Fletch failed (${consecutiveFailures}/3): ${result.message}`);
await sleep(500);
}
}
if (fletchedThisBatch > 0) {
console.log(`[Lvl ${getFletchingLevel(ctx)}] Fletched ${fletchedThisBatch} items, Total: ${itemsFletched}`);
}
continue;
}
// Otherwise, chop more trees
const tree = sdk.findNearbyLoc(/^tree$/i);
if (!tree) {
console.log('No tree nearby, walking to tree area...');
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
stuckCount++;
if (stuckCount > MAX_STUCK) {
throw new Error('Unable to find trees after multiple attempts');
}
continue;
}
// Chop the tree
const result = await bot.chopTree(tree);
if (result.success) {
logsChopped++;
stuckCount = 0;
console.log(`[Lvl ${getFletchingLevel(ctx)}] Chopped tree! Logs: ${countLogs(ctx)}, Total chopped: ${logsChopped}`);
} else {
console.log(`Chop failed: ${result.message}`);
stuckCount++;
if (stuckCount > MAX_STUCK) {
throw new Error(`Stuck: ${stuckCount} failed chop attempts`);
}
}
await sleep(200);
}
console.log(`=== GOAL ACHIEVED: Fletching level ${getFletchingLevel(ctx)}! ===`);
}
// Main script
async function main() {
const username = `fl${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
console.log('=== Fletching Trainer ===');
console.log(`Goal: Level ${TARGET_LEVEL}`);
// Wait for state to initialize
await sleep(2000);
const state = ctx.sdk.getState();
if (!state?.player) {
console.error('No player state');
return;
}
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
console.log(`Current Fletching level: ${getFletchingLevel(ctx)}`);
// Dismiss any startup dialogs
await ctx.bot.dismissBlockingUI();
// Step 1: Get a knife
if (!hasKnife(ctx)) {
console.log('No knife found, acquiring one...');
const gotKnife = await acquireKnife(ctx);
if (!gotKnife) {
console.error('Failed to acquire knife, cannot continue');
return;
}
} else {
console.log('Already have a knife!');
}
// Verify we have an axe
if (!hasAxe(ctx)) {
console.error('No axe found! The preset should include a bronze axe.');
return;
}
console.log('Have axe - ready to chop trees');
// Step 2: Chop trees and fletch until target level
console.log('Starting fletching training...');
await chopAndFletch(ctx);
console.log('=== Script Complete ===');
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000, // 10 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
// Helper
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
-145
View File
@@ -1,145 +0,0 @@
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
/**
* Test if ladders/stairs work in other buildings.
* If Wizard Tower ladder works, we can work around the Lumbridge Castle stair bug.
*/
async function main() {
const username = `lt${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx: ScriptContext) => {
console.log('=== Ladder/Stair Functionality Test ===');
const startPos = ctx.sdk.getState()?.player;
console.log(`Starting at: (${startPos?.worldX}, ${startPos?.worldZ}), floor: ${startPos?.level}`);
// Walk toward Wizard's Tower (south of Draynor Village)
// It's a long walk, so let's use waypoints
const waypoints = [
[3222, 3218], // Start
[3230, 3220], // East
[3230, 3200], // South
[3220, 3180], // Continue south
[3200, 3170], // West-ish toward Draynor
[3150, 3170], // West to Draynor area
[3109, 3168], // Wizard Tower entrance
];
for (const [x, z] of waypoints) {
console.log(`Walking to (${x}, ${z})...`);
try {
await ctx.bot.walkTo(x!, z!);
await new Promise(r => setTimeout(r, 500));
const pos = ctx.sdk.getState()?.player;
console.log(` -> Now at (${pos?.worldX}, ${pos?.worldZ})`);
} catch (e) {
console.warn(`Walk failed: ${e}`);
}
}
// Check if we're near Wizard's Tower
const pos = ctx.sdk.getState()?.player;
console.log(`\nFinal position: (${pos?.worldX}, ${pos?.worldZ})`);
// Look for ladder
const ladders = ctx.sdk.getState()?.nearbyLocs.filter(l => /ladder/i.test(l.name)) ?? [];
console.log(`Ladders found: ${ladders.map(l => `${l.name}@(${l.x},${l.z}) dist=${l.distance} opts:[${l.optionsWithIndex.map(o => o.text).join(',')}]`).join('; ')}`);
// Look for any climbable object
const climbables = ctx.sdk.getState()?.nearbyLocs.filter(l =>
l.optionsWithIndex.some(o => /climb/i.test(o.text))
) ?? [];
console.log(`Climbable objects: ${climbables.map(l => `${l.name}@(${l.x},${l.z})`).join(', ')}`);
// Try to enter the tower
console.log('\nTrying to enter Wizard Tower...');
await ctx.bot.walkTo(3109, 3168);
await new Promise(r => setTimeout(r, 500));
// Check for door
const door = ctx.sdk.getState()?.nearbyLocs.find(l =>
/door/i.test(l.name) &&
l.optionsWithIndex.some(o => /open/i.test(o.text))
);
if (door) {
console.log(`Opening door at (${door.x}, ${door.z})...`);
const opt = door.optionsWithIndex.find(o => /open/i.test(o.text));
if (opt) {
await ctx.sdk.sendInteractLoc(door.x, door.z, door.id, opt.opIndex);
await new Promise(r => setTimeout(r, 1000));
}
}
// Walk inside
await ctx.bot.walkTo(3104, 3162);
await new Promise(r => setTimeout(r, 500));
// Find ladder to basement
const ladder = ctx.sdk.getState()?.nearbyLocs.find(l =>
/ladder/i.test(l.name) &&
l.optionsWithIndex.some(o => /climb.*down/i.test(o.text))
);
if (!ladder) {
console.warn('No ladder found inside Wizard Tower');
// Log what IS nearby
const nearby = ctx.sdk.getState()?.nearbyLocs.slice(0, 10) ?? [];
console.log(`Nearby objects: ${nearby.map(l => `${l.name}@(${l.x},${l.z})`).join(', ')}`);
return;
}
console.log(`\nFound ladder at (${ladder.x}, ${ladder.z}), distance=${ladder.distance}`);
console.log(`Options: ${ladder.optionsWithIndex.map(o => `${o.opIndex}:${o.text}`).join(', ')}`);
// Try climbing down
const priorLevel = ctx.sdk.getState()?.player?.level ?? 0;
console.log(`\nAttempting to climb down (current floor: ${priorLevel})...`);
const climbOpt = ladder.optionsWithIndex.find(o => /climb.*down/i.test(o.text));
if (climbOpt) {
const result = await ctx.sdk.sendInteractLoc(ladder.x, ladder.z, ladder.id, climbOpt.opIndex);
console.log(`Interaction result: ${JSON.stringify(result)}`);
// Wait for floor change
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 1000));
const currLevel = ctx.sdk.getState()?.player?.level ?? 0;
const currPos = ctx.sdk.getState()?.player;
console.log(` Check ${i+1}: floor=${currLevel}, pos=(${currPos?.worldX}, ${currPos?.worldZ})`);
if (currLevel !== priorLevel) {
console.log('\n*** LADDER WORKS! Floor changed! ***');
// Look for Sedridor in basement
const npcs = ctx.sdk.getState()?.nearbyNpcs.slice(0, 5) ?? [];
console.log(`NPCs in basement: ${npcs.map(n => n.name).join(', ')}`);
return;
}
}
// Check game messages
const msgs = ctx.sdk.getState()?.gameMessages ?? [];
console.log(`\nGame messages: ${msgs.slice(-5).map(m => m.text).join(' | ')}`);
if (msgs.some(m => /can't reach/i.test(m.text))) {
console.warn('LADDER ALSO FAILS with "can\'t reach" - this is a widespread bug');
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 120_000,
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-173
View File
@@ -1,173 +0,0 @@
# Lab Log: magic
Magic training script using combat spells on NPCs.
## Goal
Train Magic to level 10+ from a fresh Lumbridge spawn using Wind Strike on chickens.
## Starting Conditions (LUMBRIDGE_SPAWN preset)
- Air rune x25
- Mind rune x15
- Water rune x6
- Earth rune x4
- Body rune x2
## XP Analysis
- Wind Strike = 1 Air + 1 Mind = 5.5 XP per cast
- 15 Mind runes = max 15 Wind Strikes = 82.5 XP max
- Level 10 requires 1,154 XP = ~210 Wind Strikes
- With 15 runes, can only reach ~level 2-3
**Limitation**: The preset doesn't provide enough runes to reach level 10 with magic alone.
---
## Run 001 - Initial Test
**Outcome**: SUCCESS
**Duration**: 32.1s
**Final Level**: 11 (from 1)
### What Happened
- Script walked to chicken coop successfully
- Cast Wind Strike 3 times
- Each hit gave 475 XP (not 5.5 XP - server has boosted rates)
- Hit level 11 with only 3 runes used
- Dialog handling worked correctly (dismissed level-up dialogs)
### Key Discovery
**Server XP rates are ~86x normal** - Wind Strike gives 475 XP per hit instead of 5.5 XP.
This completely changes the calculation:
- 15 Mind runes = 15 casts = 7,125 XP potential (if all hit)
- Can easily reach level 10+ with starting runes
### What Worked Well
1. `sendSpellOnNpc()` worked correctly
2. Dialog dismissal handled level-up popups
3. Target selection found chickens
4. Progress tracking was accurate
### Remaining runes
Air=22, Mind=12, Water=6, Earth=4 (12 runes left = plenty for future use)
---
## Run 002 - Verification Run
**Outcome**: SUCCESS
**Duration**: 105.2s
**Final Level**: 11 (from 1)
### What Happened
- Script worked reliably on fresh browser start
- Cast 31 times, hit 4 times (87% splash rate at low levels)
- XP per hit varied: 275, 375, 375, 475 XP
- Still reached level 11 despite high splash rate
### Observations
1. XP per hit scales with damage dealt (not fixed)
2. Splash rate is high at level 1 (expected - low magic accuracy)
3. Even with many splashes, goal achieved comfortably
4. Rune usage: 4 successful hits used 4 of each rune (21 air, 11 mind remaining)
### Browser Note
Had to kill stuck browser between runs - shared browser mode can get stuck sometimes.
---
## Learnings
### 1. Strategic Findings
- **XP rates are boosted** - ~50-100x normal rates on this server
- **Splash rate is high at low levels** - expected, but hits still give enough XP
- **Chickens are ideal targets** - easy to find, don't fight back hard, weak
- **Dialog handling is essential** - level-up dialogs block all actions
### 2. Process & Tooling Reflections
- Script-runner infrastructure worked well for logging
- Dialog dismissal loop is critical for combat magic training
- `sendSpellOnNpc()` API is straightforward and reliable
### 3. SDK Issues & Gaps
- None encountered - the magic combat API works correctly
- `sendSpellOnNpc(npcIndex, spellComponent)` is intuitive
- Spell constants should be documented somewhere (currently in save-generator.ts)
---
## Run 003 - Gate Fix
**Outcome**: ISSUE IDENTIFIED
**Issue**: "I can't reach that" errors - chicken coop gate was closed
### Root Cause
The chicken coop at Lumbridge has a gate that can be closed. When closed, the player can't cast spells on chickens inside due to line-of-sight blocking (fence).
### Fix Applied
1. Open the gate after walking to the chicken coop using `bot.openDoor(/gate/i)`
2. Walk inside the coop after opening
3. Handle "can't reach" errors by trying to open the gate again
```typescript
// Open the gate to get inside the coop
ctx.log('Opening gate...');
const gateResult = await ctx.bot.openDoor(/gate/i);
if (gateResult.success) {
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
}
```
---
## Run 004 - Verification After Fix
**Outcome**: SUCCESS
**Duration**: 39.6s
**Final Level**: 10 (from 1)
### What Worked
- Gate opened successfully: `Gate opened: Opened Gate`
- Casting from inside coop at dist=1
- 5 casts, 3 hits (2 splashes at low level is normal)
- No "can't reach" errors
### Key Learning
- **Gates/fences block line-of-sight for magic** - must be inside the area
- `bot.openDoor(/gate/i)` handles gates as well as doors
- Always consider obstacles when casting spells on NPCs in enclosed areas
---
## Run 005 - New `castSpellOnNpc` API
**Outcome**: SUCCESS
**Duration**: 110.2s
**Final Level**: 10 (from 1)
### What Changed
Added high-level `bot.castSpellOnNpc()` method to bot-actions.ts that:
1. Sends the spell
2. Waits for result (XP gain = hit, error message = blocked, timeout = splash)
3. Returns structured result with `hit`, `xpGained`, `reason`
```typescript
const result = await ctx.bot.castSpellOnNpc(target, spell.spell);
if (result.success) {
if (result.hit) {
ctx.log(`HIT! ${result.message}`); // "Hit Chicken for 275 Magic XP"
}
} else if (result.reason === 'out_of_reach') {
// Try opening gate
}
```
### Benefits
- **Structured error handling** - `reason: 'out_of_reach' | 'no_runes' | 'npc_not_found'`
- **Hit/splash detection** - via XP gain, not just message parsing
- **Cleaner script code** - no manual game message parsing needed
- **Consistent with other bot methods** - follows `attackNpc` pattern
### Results
- 23 casts, 3 hits (20 splashes - normal at low magic level)
- Goal reached successfully
- Gate handling ready if needed
-338
View File
@@ -1,338 +0,0 @@
/**
* Magic Trainer Script
*
* Goal: Train Magic to level 10+ starting from a fresh Lumbridge spawn.
*
* Strategy:
* - LUMBRIDGE_SPAWN preset provides: Air x25, Mind x15, Water x6, Earth x4, Body x2
* - Wind Strike = 1 Air + 1 Mind = 5.5 XP per cast
* - 15 Mind runes = 15 casts = 82.5 XP (gets to ~level 2-3)
* - Level 10 requires 1,154 XP = ~210 Wind Strikes
* - Need to find more runes or supplement with melee
*
* Plan:
* 1. Walk to chicken coop near Lumbridge (3235, 3295)
* 2. Cast Wind Strike on chickens until runes run out
* 3. Log progress and track XP gains
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Spell component IDs
const Spells = {
WIND_STRIKE: 1152,
WATER_STRIKE: 1154,
EARTH_STRIKE: 1156,
};
// Locations
const CHICKEN_COOP = { x: 3235, z: 3295 }; // Near Lumbridge
// XP required for each level
const MAGIC_XP_TABLE = [
0, 0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, // 0-10
1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, // 11-20
];
/**
* Find the best target for magic training
*/
function findTarget(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
// Prefer chickens (weakest), then rats, then goblins
const targets = state.nearbyNpcs
.filter(npc =>
/chicken/i.test(npc.name) ||
/rat/i.test(npc.name) ||
/goblin/i.test(npc.name)
)
.filter(npc => !npc.inCombat) // Don't target NPCs already fighting
.filter(npc => npc.distance <= 10) // Within reasonable range
.sort((a, b) => {
// Priority: chicken > rat > goblin
const priority = (name: string) => {
if (/chicken/i.test(name)) return 0;
if (/rat/i.test(name)) return 1;
return 2;
};
const pDiff = priority(a.name) - priority(b.name);
if (pDiff !== 0) return pDiff;
return a.distance - b.distance;
});
return targets[0] ?? null;
}
/**
* Get rune counts
*/
function getRuneCounts(ctx: ScriptContext): { air: number; mind: number; water: number; earth: number } {
const airRunes = ctx.sdk.findInventoryItem(/^air rune$/i);
const mindRunes = ctx.sdk.findInventoryItem(/^mind rune$/i);
const waterRunes = ctx.sdk.findInventoryItem(/^water rune$/i);
const earthRunes = ctx.sdk.findInventoryItem(/^earth rune$/i);
return {
air: airRunes?.count ?? 0,
mind: mindRunes?.count ?? 0,
water: waterRunes?.count ?? 0,
earth: earthRunes?.count ?? 0,
};
}
/**
* Determine which spell to cast based on available runes and magic level
*/
function getBestSpell(ctx: ScriptContext): { spell: number; name: string; xp: number } | null {
const runes = getRuneCounts(ctx);
const magicLevel = ctx.sdk.getSkill('Magic')?.baseLevel ?? 1;
// Wind Strike: 1 Air + 1 Mind, level 1, 5.5 XP
if (runes.air >= 1 && runes.mind >= 1) {
return { spell: Spells.WIND_STRIKE, name: 'Wind Strike', xp: 5.5 };
}
// Water Strike: 1 Water + 1 Air + 1 Mind, level 5, 7.5 XP
if (magicLevel >= 5 && runes.water >= 1 && runes.air >= 1 && runes.mind >= 1) {
return { spell: Spells.WATER_STRIKE, name: 'Water Strike', xp: 7.5 };
}
// Earth Strike: 2 Earth + 1 Air + 1 Mind, level 9, 9.5 XP
if (magicLevel >= 9 && runes.earth >= 2 && runes.air >= 1 && runes.mind >= 1) {
return { spell: Spells.EARTH_STRIKE, name: 'Earth Strike', xp: 9.5 };
}
return null;
}
interface MagicStats {
startXp: number;
startLevel: number;
casts: number;
hits: number;
misses: number; // Splashes
lastCastTime: number;
startTime: number;
}
/**
* Main magic training loop
*/
async function magicTrainingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
// Initialize stats
const magicSkill = ctx.sdk.getSkill('Magic');
const stats: MagicStats = {
startXp: magicSkill?.experience ?? 0,
startLevel: magicSkill?.baseLevel ?? 1,
casts: 0,
hits: 0,
misses: 0,
lastCastTime: 0,
startTime: Date.now(),
};
console.log('=== Magic Trainer ===');
console.log(`Goal: Train Magic from level ${stats.startLevel} (need 1,154 XP for level 10)`);
// Log rune inventory
const runes = getRuneCounts(ctx);
console.log(`Runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
console.log(`Potential casts: ~${Math.min(runes.air, runes.mind)} Wind Strikes (~${Math.min(runes.air, runes.mind) * 5.5} XP)`);
// Walk to chicken coop
console.log('Walking to chicken coop...');
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
// Open the gate to get inside the coop
console.log('Opening gate...');
const gateResult = await ctx.bot.openDoor(/gate/i);
if (gateResult.success) {
console.log(`Gate opened: ${gateResult.message}`);
// Walk inside the coop
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
} else {
console.log(`Gate: ${gateResult.message}`);
}
let noTargetCount = 0;
let failedCastCount = 0;
const MAX_NO_TARGET_ATTEMPTS = 30; // Exit if no targets for too long
const MAX_FAILED_CASTS = 5; // Exit if too many failed casts in a row
// Main loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Dismiss any blocking dialogs (level-up, etc.)
if (currentState.dialog.isOpen) {
console.log('Dismissing dialog...');
await ctx.sdk.sendClickDialog(0);
continue;
}
// Check if we've reached level 10
const currentMagic = ctx.sdk.getSkill('Magic');
const currentLevel = currentMagic?.baseLevel ?? 1;
if (currentLevel >= 10) {
console.log('*** GOAL REACHED: Magic level 10! ***');
break;
}
// Check if we have runes
const spell = getBestSpell(ctx);
if (!spell) {
console.log('Out of runes! Cannot cast any more spells.');
break;
}
// Find a target
const target = findTarget(ctx);
if (!target) {
noTargetCount++;
if (noTargetCount >= MAX_NO_TARGET_ATTEMPTS) {
console.log(`No targets found after ${noTargetCount} attempts, exiting.`);
break;
}
if (noTargetCount % 5 === 0) {
console.log(`No targets nearby (attempt ${noTargetCount}/${MAX_NO_TARGET_ATTEMPTS}), walking around...`);
// Walk to chicken coop center
await ctx.bot.walkTo(
CHICKEN_COOP.x + Math.floor(Math.random() * 6) - 3,
CHICKEN_COOP.z + Math.floor(Math.random() * 6) - 3
);
}
await new Promise(r => setTimeout(r, 500));
continue;
}
noTargetCount = 0;
// Don't spam casts - wait between attempts
const now = Date.now();
if (now - stats.lastCastTime < 2500) {
await new Promise(r => setTimeout(r, 300));
continue;
}
// Walk closer if target is far (magic range is ~10 but need clear LOS)
if (target.distance > 5) {
console.log(`Walking toward ${target.name} at (${target.x}, ${target.z}), dist: ${target.distance}`);
// Walk to within ~3 tiles of the target
await ctx.bot.walkTo(target.x, target.z);
await new Promise(r => setTimeout(r, 500));
continue;
}
// Cast spell on target using high-level API
if (stats.casts % 5 === 0 || stats.casts === 0) {
const currentRunes = getRuneCounts(ctx);
console.log(`Casting ${spell.name} on ${target.name} (cast #${stats.casts + 1}, dist=${target.distance}, runes: air=${currentRunes.air}, mind=${currentRunes.mind})`);
}
const castResult = await ctx.bot.castSpellOnNpc(target, spell.spell);
stats.casts++;
stats.lastCastTime = now;
if (castResult.success) {
failedCastCount = 0; // Reset on success
if (castResult.hit) {
stats.hits++;
console.log(`HIT! ${castResult.message}`);
}
// Splash is still success, just no XP
} else if (castResult.reason === 'out_of_reach') {
// Gate likely closed - try to open it
console.log(`Can't reach target - trying to open gate`);
const gateResult = await ctx.bot.openDoor(/gate/i);
console.log(`Gate: ${gateResult.message}`);
if (gateResult.success) {
await ctx.bot.walkTo(CHICKEN_COOP.x - 3, CHICKEN_COOP.z);
}
failedCastCount++;
} else if (castResult.reason === 'no_runes') {
console.log('Out of runes!');
break;
} else {
// Unknown failure reason
failedCastCount++;
console.log(`Cast failed: ${castResult.message} (reason: ${castResult.reason})`);
}
// Exit if too many consecutive failures
if (failedCastCount >= MAX_FAILED_CASTS) {
console.log(`Too many failed casts (${failedCastCount}), exiting.`);
break;
}
await new Promise(r => setTimeout(r, 500));
}
// Final report
logFinalStats(ctx, stats);
}
/**
* Log final training statistics
*/
function logFinalStats(ctx: ScriptContext, stats: MagicStats): void {
const currentMagic = ctx.sdk.getSkill('Magic');
const finalXp = currentMagic?.experience ?? 0;
const finalLevel = currentMagic?.baseLevel ?? 1;
const xpGained = finalXp - stats.startXp;
const elapsed = (Date.now() - stats.startTime) / 1000;
const xpPerHour = elapsed > 0 ? Math.round((xpGained / elapsed) * 3600) : 0;
console.log('=== Final Results ===');
console.log(`Magic: Level ${stats.startLevel} -> ${finalLevel}`);
console.log(`XP: ${stats.startXp} -> ${finalXp} (+${xpGained})`);
console.log(`Casts: ${stats.casts} (Hits: ${stats.hits}, Splashes: ${stats.casts - stats.hits})`);
console.log(`Duration: ${elapsed.toFixed(1)}s`);
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
// Log remaining runes
const runes = getRuneCounts(ctx);
console.log(`Remaining runes: Air=${runes.air}, Mind=${runes.mind}, Water=${runes.water}, Earth=${runes.earth}`);
}
// Main script
async function main() {
// Create fresh account
const username = `mg${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await magicTrainingLoop(ctx);
} finally {
// Log final state
const state = ctx.sdk.getState();
if (state) {
const magic = state.skills.find(s => s.name === 'Magic');
console.log(`Final Magic: Level ${magic?.baseLevel ?? '?'}, XP ${magic?.experience ?? '?'}`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 5 * 60 * 1000, // 5 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-204
View File
@@ -1,204 +0,0 @@
# Lab Log: mining-trainer
Goal: Maximize Mining level in 5 minutes starting from a fresh character.
## Strategy
- Spawn directly at SE Varrock mine (pathfinding from Lumbridge failed)
- Bronze pickaxe only, Mining level 1
- Mine tin/copper rocks continuously (1750 XP each - server has 100x rates)
- Drop ore when inventory full (28 slots)
---
## Run 001 - 2026-01-24 (v1/v2 - pathfinding failed)
**Outcome**: timeout (no mining occurred)
**Duration**: 5 minutes
### What Happened
- Spawned at Lumbridge with TUTORIAL_COMPLETE preset
- Tried to walk to SE Varrock mine (160 tiles) - pathfinder failed
- Tried Lumbridge Swamp mine (70 tiles) - also failed
- Bot stuck at (3244, 3270), never found any rocks
### Root Cause
Pathfinder couldn't find paths to distant locations. Possible causes:
- Pathfinder search radius too limited (~100 tiles)
- Missing collision data in some areas
- Buildings/obstacles blocking direct paths
### Fix
v3: Spawn directly at the mine using custom saveConfig instead of walking there.
---
## Run 002 - 2026-01-24 23:53 (v3 - success!)
**Outcome**: success (ran full 5 minutes)
**Duration**: 5m 0s
### Results
| Metric | Start | End |
|--------|-------|-----|
| Mining Level | 1 | 44 |
| Mining XP | 0 | 57,750 |
| Ores Mined | 0 | ~33 |
### What Worked
- Spawning at mine bypassed pathfinding issues
- Rock detection worked (found "Rocks" at distance 1)
- Mining action worked (sendInteractLoc with "Mine" option)
- Dialog dismissal worked (level-up popups closed)
- Inventory management worked (dropped 27 ores when full)
- XP-based success detection worked reliably
### XP Rates
Server has 100x XP rates: 1750 XP per tin ore instead of 17.5
### Mining Speed Analysis
- 33 ores in 300 seconds = ~9 seconds per ore
- This includes:
- Mining animation (~4-5 seconds)
- Rock respawn wait
- Movement to next rock
- Occasional level-up dialogs
### Optimization Ideas
1. **Mine iron ore at level 15+** - Higher XP per ore (3500 XP at 100x)
2. **Power-mine closest rock** - Reduce movement time
3. **Better pickaxe** - Faster mining speed (if available)
4. **Prospect rocks** - Identify highest-value rocks nearby
---
## Run 003 - 2026-01-25 00:00 (v4 - regression)
**Outcome**: partial success (wandered from mine)
**Duration**: 5m 0s
### Results
| Metric | Start | End |
|--------|-------|-----|
| Mining Level | 1 | 36 |
| Mining XP | 0 | 26,250 |
### What Happened
- Bot drifted to (3245, 3270) - 40 tiles from mine!
- Random wandering logic accumulated over time
- Got stuck in cow/duck area with no rocks nearby
### Root Cause
The "wander to find rocks" logic when no rocks available sent the bot progressively further from the mine.
---
## Run 004 - 2026-01-25 00:06 (v5 - success!)
**Outcome**: success (ran full 5 minutes at mine)
**Duration**: 5m 0s
### Results
| Metric | Start | End |
|--------|-------|-----|
| Mining Level | 1 | 44 |
| Mining XP | 0 | 57,750 |
| Ores Mined | 0 | ~33 |
### What Worked
- Fixed drift by returning to mine center if >5 tiles away
- Waiting for rock respawn instead of wandering
- Bot stayed at (3284, 3365) throughout the run
---
## Script Evolution
| Version | Change | Result |
|---------|--------|--------|
| v1 | Walk from Lumbridge to SE Varrock | Failed (path not found) |
| v2 | Walk to Lumbridge Swamp mine | Failed (path not found) |
| v3 | Spawn at SE Varrock mine | Success! Level 44 |
| v4 | Prioritize closest rock + clean up | Regression: Level 36 (drifted away) |
| v5 | Fix drift with mine center reset | Success! Level 44 |
---
## Run 005 - 2026-01-25 03:49 (v8 - Al Kharid route)
**Outcome**: partial success (died to scorpions)
**Duration**: 5m 0s (timeout)
### What Happened
- Walked from Lumbridge → General Store → Toll Gate → Al Kharid mine
- Sold Bronze sword for 10gp toll
- Reached copper/tin area and started mining
- **Died to scorpions** (level 14 aggressive) - respawned in Lumbridge
- Spent rest of time walking back
### Results
| Metric | Start | End |
|--------|-------|-----|
| Mining Level | 1 | 21 |
| Mining XP | 0 | 5,250 |
### Learning
- Al Kharid mine has aggressive scorpions (level 14)
- Low-level characters die quickly without combat gear/food
- Need safer mine for level 1 characters
---
## Run 006 - 2026-01-25 03:57 (v9 - SE Varrock, honest walk)
**Outcome**: success (full 5 minutes mining)
**Duration**: 5m 0s
### Strategy
- Walk from Lumbridge to SE Varrock mine using waypoints
- No teleporting/spawning at mine (honest approach)
- Mine tin/copper rocks continuously
### Results
| Metric | Start | End |
|--------|-------|-----|
| Mining Level | 1 | 37 |
| Mining XP | 0 | 28,000 |
| Ores Mined | 0 | 16+ |
### What Worked
- Waypoint-based walking reached mine (~45s travel time)
- Prospecting identified rock types (tin, copper, etc.)
- Ore caching avoided re-prospecting same rock IDs
- No death (SE Varrock has no aggressive mobs)
- Inventory management (dropped 11 ores when full)
### Comparison: Honest vs Spawn-at-Mine
| Approach | Final Level | XP | Notes |
|----------|-------------|-----|-------|
| Spawn at mine | 44 | 57,750 | "Cheating" - instant start |
| Honest walk | 37 | 28,000 | ~45s travel time |
---
## Script Evolution (Full)
| Version | Change | Result |
|---------|--------|--------|
| v1-v2 | Walk from Lumbridge | Failed (path not found) |
| v3-v5 | Spawn at SE Varrock mine | Success: Level 44 |
| v6-v7 | Waypoint navigation (south) | Failed (blocked paths) |
| v8 | Al Kharid via toll gate | Died to scorpions: Level 21 |
| v9 | SE Varrock via waypoints | Success: Level 37 (honest!) |
## Final Summary
**Best Honest Result**: Mining Level 37 (28,000 XP) in 5 minutes
**Best Overall Result**: Mining Level 44 (57,750 XP) when spawning at mine
### Key Learnings
1. Server pathfinder has ~100 tile search radius - use waypoints for long distances
2. Some routes blocked (Lumbridge south) - needed to go north
3. Al Kharid mine has aggressive scorpions - dangerous for low levels
4. Prospecting identifies ore types - essential for finding copper/tin
5. Ore ID caching prevents re-prospecting same rock types
-535
View File
@@ -1,535 +0,0 @@
/**
* Mining Training Script v9
*
* Goal: Maximize Mining level in 5 minutes
*
* Strategy:
* - Start at Lumbridge (natural spawn) with tutorial-complete gear
* - Walk north to SE Varrock mine (no toll needed, no aggressive mobs)
* - Mine copper/tin rocks continuously
* - Drop ore when inventory is full
*
* v9 Changes:
* - Switch to SE Varrock mine (safer, no scorpions)
* - No need to sell items or pay toll
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// SE Varrock mine - has copper and tin, no aggressive monsters
const TARGET_MINE = { x: 3285, z: 3365 };
// Waypoints from Lumbridge to SE Varrock mine
const WAYPOINTS_TO_MINE = [
{ x: 3222, z: 3230 }, // North from Lumbridge
{ x: 3222, z: 3250 }, // Continue north
{ x: 3230, z: 3280 }, // Northeast
{ x: 3250, z: 3310 }, // Continue NE
{ x: 3270, z: 3340 }, // Getting closer
{ x: 3285, z: 3360 }, // Near mine
TARGET_MINE, // SE Varrock mine
];
// Get mining level and XP
function getMiningStats(ctx: ScriptContext): { level: number; xp: number } {
const state = ctx.sdk.getState();
const mining = state?.skills.find(s => s.name === 'Mining');
return {
level: mining?.baseLevel ?? 1,
xp: mining?.experience ?? 0
};
}
// Log mining progress
function logProgress(ctx: ScriptContext, label: string): void {
const stats = getMiningStats(ctx);
const invCount = ctx.sdk.getState()?.inventory.length ?? 0;
console.log(`[${label}] Mining Lv${stats.level} (${stats.xp} XP) | Inv: ${invCount}/28`);
}
// Rock info with ore type from prospecting
interface RockInfo {
x: number;
z: number;
id: number;
name: string;
mineOpIndex: number;
prospectOpIndex: number;
distance: number;
oreType?: string; // Set after prospecting
}
// Find all nearby rocks with Mine option
function findRocks(ctx: ScriptContext): RockInfo[] {
const state = ctx.sdk.getState();
if (!state) return [];
const rocks: RockInfo[] = [];
for (const loc of state.nearbyLocs) {
if (/rocks?/i.test(loc.name) && !/rockslide/i.test(loc.name)) {
const mineOpt = loc.optionsWithIndex.find(o => /mine/i.test(o.text));
const prospectOpt = loc.optionsWithIndex.find(o => /prospect/i.test(o.text));
if (mineOpt && prospectOpt) {
rocks.push({
x: loc.x,
z: loc.z,
id: loc.id,
name: loc.name,
mineOpIndex: mineOpt.opIndex,
prospectOpIndex: prospectOpt.opIndex,
distance: loc.distance
});
}
}
}
// Sort by distance
return rocks.sort((a, b) => a.distance - b.distance);
}
// Prospect a rock to find ore type
async function prospectRock(ctx: ScriptContext, rock: RockInfo): Promise<string | null> {
const { sdk } = ctx;
const startTick = ctx.sdk.getState()?.tick ?? 0;
await sdk.sendInteractLoc(rock.x, rock.z, rock.id, rock.prospectOpIndex);
// Wait for prospect message
try {
const state = await sdk.waitForCondition(s => {
for (const msg of s.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
// Look for "This rock contains X" messages
if (text.includes('rock contains') || text.includes('ore')) {
return true;
}
if (text.includes('no ore') || text.includes('nothing')) {
return true;
}
}
}
return false;
}, 5000);
// Extract ore type from message
for (const msg of state.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
if (text.includes('copper')) return 'copper';
if (text.includes('tin')) return 'tin';
if (text.includes('iron')) return 'iron';
if (text.includes('gold')) return 'gold';
if (text.includes('silver')) return 'silver';
if (text.includes('coal')) return 'coal';
if (text.includes('mithril')) return 'mithril';
if (text.includes('adamant')) return 'adamant';
if (text.includes('rune') || text.includes('runite')) return 'runite';
if (text.includes('no ore') || text.includes('nothing')) return 'empty';
}
}
} catch {
return null;
}
return null;
}
// Known mineable ores by level
const MINEABLE_AT_LEVEL: Record<number, string[]> = {
1: ['copper', 'tin'],
15: ['iron'],
20: ['silver'],
30: ['coal'],
40: ['gold'],
55: ['mithril'],
70: ['adamant'],
85: ['runite']
};
function canMineOre(oreType: string, miningLevel: number): boolean {
for (const [reqLevel, ores] of Object.entries(MINEABLE_AT_LEVEL)) {
if (ores.includes(oreType) && miningLevel >= parseInt(reqLevel)) {
return true;
}
}
return false;
}
// Drop all ore from inventory
async function dropOre(ctx: ScriptContext): Promise<number> {
const state = ctx.sdk.getState();
if (!state) return 0;
let dropped = 0;
for (const item of state.inventory) {
// Drop copper, tin, iron ore (and any other ore)
if (/ore/i.test(item.name)) {
await ctx.sdk.sendDropItem(item.slot);
dropped++;
}
}
return dropped;
}
// Check if inventory is full
function isInventoryFull(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
return (state?.inventory.length ?? 0) >= 28;
}
// Walk using small direct steps (bypasses server pathfinder issues)
async function walkSmallSteps(
ctx: ScriptContext,
targetX: number,
targetZ: number,
stepSize: number = 12
): Promise<{ x: number; z: number }> {
const { sdk } = ctx;
const MAX_STEPS = 20;
for (let step = 0; step < MAX_STEPS; step++) {
const state = ctx.sdk.getState();
if (!state?.player) break;
const currentX = state.player.worldX;
const currentZ = state.player.worldZ;
const dist = Math.sqrt(Math.pow(targetX - currentX, 2) + Math.pow(targetZ - currentZ, 2));
if (dist <= 3) {
return { x: currentX, z: currentZ };
}
// Calculate next step towards target
const ratio = Math.min(stepSize / dist, 1);
const nextX = Math.round(currentX + (targetX - currentX) * ratio);
const nextZ = Math.round(currentZ + (targetZ - currentZ) * ratio);
// Use direct walk command (client-side pathfinding)
await sdk.sendWalk(nextX, nextZ, true);
// Wait for movement
await new Promise(r => setTimeout(r, 800));
// Check if we moved
const newState = ctx.sdk.getState();
if (newState?.player) {
const movedDist = Math.sqrt(
Math.pow(newState.player.worldX - currentX, 2) +
Math.pow(newState.player.worldZ - currentZ, 2)
);
if (movedDist < 1) {
// Stuck - try a slightly different direction
const jitterX = nextX + Math.round((Math.random() - 0.5) * 4);
const jitterZ = nextZ + Math.round((Math.random() - 0.5) * 4);
await sdk.sendWalk(jitterX, jitterZ, true);
await new Promise(r => setTimeout(r, 800));
}
}
}
const finalState = ctx.sdk.getState();
return {
x: finalState?.player?.worldX ?? 0,
z: finalState?.player?.worldZ ?? 0
};
}
// Walk to a destination using waypoints with small steps
// Returns early if rocks are found along the way
async function walkWithWaypoints(
ctx: ScriptContext,
waypoints: Array<{ x: number; z: number }>,
label: string
): Promise<{ foundRocks: boolean }> {
const { bot, sdk } = ctx;
for (let i = 0; i < waypoints.length; i++) {
const wp = waypoints[i];
if (!wp) continue;
const state = ctx.sdk.getState();
const playerPos = state?.player ? `(${state.player.worldX}, ${state.player.worldZ})` : '?';
console.log(`Walking to waypoint ${i + 1}/${waypoints.length}: (${wp.x}, ${wp.z}) from ${playerPos}`);
// Don't stop early for rocks - we need to reach the copper/tin area
// (The mine has gold/coal near the entrance, copper/tin further in)
// Try bot.walkTo first (uses server pathfinder)
const result = await bot.walkTo(wp.x, wp.z);
let newState = ctx.sdk.getState();
let currentX = newState?.player?.worldX ?? 0;
let currentZ = newState?.player?.worldZ ?? 0;
let dist = Math.sqrt(Math.pow(wp.x - currentX, 2) + Math.pow(wp.z - currentZ, 2));
// If walkTo failed and we're still far, try small steps
if (!result.success && dist > 5) {
console.log(`Server pathfinder failed, trying small steps...`);
const pos = await walkSmallSteps(ctx, wp.x, wp.z);
currentX = pos.x;
currentZ = pos.z;
dist = Math.sqrt(Math.pow(wp.x - currentX, 2) + Math.pow(wp.z - currentZ, 2));
// Continue walking to target - don't stop for rocks
}
console.log(`Waypoint ${i + 1}: now at (${currentX}, ${currentZ}), ${dist.toFixed(0)} tiles from target`);
// Brief pause between waypoints
await new Promise(r => setTimeout(r, 300));
}
const finalState = ctx.sdk.getState();
const finalPos = finalState?.player ? `(${finalState.player.worldX}, ${finalState.player.worldZ})` : '?';
console.log(`${label} - final position: ${finalPos}`);
return { foundRocks: false };
}
async function main() {
// Create fresh account
const username = `mn${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
const { bot, sdk } = ctx;
// Log initial state
logProgress(ctx, 'START');
let oresMined = 0;
let lastXp = getMiningStats(ctx).xp;
// Check for pickaxe
let pickaxe = sdk.findInventoryItem(/pickaxe/i);
if (!pickaxe) {
// Check if already equipped
pickaxe = sdk.findEquipmentItem(/pickaxe/i);
if (!pickaxe) {
console.log('ERROR: No pickaxe in inventory or equipped!');
return;
}
console.log(`Found ${pickaxe.name} already equipped`);
} else {
console.log(`Found ${pickaxe.name} in inventory - equipping it...`);
const equipResult = await bot.equipItem(pickaxe);
if (equipResult.success) {
console.log('Pickaxe equipped!');
} else {
console.log(`Failed to equip pickaxe: ${equipResult.message} - will try mining anyway`);
}
}
// Walk to SE Varrock mine (safer than Al Kharid - no scorpions)
console.log('Walking to SE Varrock mine...');
await walkWithWaypoints(ctx, WAYPOINTS_TO_MINE, 'ARRIVED AT MINE');
// Check current position and look for rocks
const posState = ctx.sdk.getState();
const currentPos = posState?.player
? { x: posState.player.worldX, z: posState.player.worldZ }
: TARGET_MINE;
// Update mine location to where we actually ended up (for drift detection)
const actualMineLocation = { ...currentPos };
// Look for rocks in the area - if none found, try walking around
let rocks = findRocks(ctx);
if (rocks.length === 0) {
console.log(`No rocks at (${currentPos.x}, ${currentPos.z}), searching nearby...`);
// Try walking to nearby locations to find rocks
const searchOffsets = [
{ x: -10, z: 0 }, { x: 10, z: 0 },
{ x: 0, z: -10 }, { x: 0, z: 10 },
{ x: -15, z: -15 }, { x: 15, z: -15 },
];
for (const offset of searchOffsets) {
const searchX = currentPos.x + offset.x;
const searchZ = currentPos.z + offset.z;
console.log(`Searching at (${searchX}, ${searchZ})...`);
await walkSmallSteps(ctx, searchX, searchZ, 8);
rocks = findRocks(ctx);
if (rocks.length > 0) {
const newPos = ctx.sdk.getState()?.player;
if (newPos) {
actualMineLocation.x = newPos.worldX;
actualMineLocation.z = newPos.worldZ;
}
console.log(`Found ${rocks.length} rocks at (${actualMineLocation.x}, ${actualMineLocation.z})!`);
break;
}
}
}
logProgress(ctx, 'READY TO MINE');
// Cache of rock ID -> ore type (from prospecting)
const oreTypeCache = new Map<number, string>();
let consecutiveFailures = 0;
const MAX_FAILURES = 5;
// Main mining loop
while (true) {
const state = ctx.sdk.getState();
if (!state?.player) {
await new Promise(r => setTimeout(r, 1000));
continue;
}
const currentStats = getMiningStats(ctx);
// Check for XP gain (successful mine)
if (currentStats.xp > lastXp) {
const xpGained = currentStats.xp - lastXp;
oresMined++;
lastXp = currentStats.xp;
consecutiveFailures = 0; // Reset on success
if (oresMined % 5 === 0) {
logProgress(ctx, `MINED #${oresMined}`);
}
}
// Dismiss any dialogs (level-up messages)
if (state.dialog.isOpen) {
await sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Check if inventory is full - drop ore
if (isInventoryFull(ctx)) {
console.log('Inventory full, dropping ore...');
const dropped = await dropOre(ctx);
console.log(`Dropped ${dropped} ores`);
continue;
}
// Find rocks
const rocks = findRocks(ctx);
if (rocks.length === 0) {
// Return to mine center
const player = state.player;
const distFromMine = Math.sqrt(
Math.pow(player.worldX - actualMineLocation.x, 2) +
Math.pow(player.worldZ - actualMineLocation.z, 2)
);
if (distFromMine > 5) {
console.log(`No rocks nearby, returning to mine center (${distFromMine.toFixed(0)} tiles away)...`);
await walkSmallSteps(ctx, actualMineLocation.x, actualMineLocation.z, 8);
} else {
console.log('No rocks nearby, waiting for respawn...');
await new Promise(r => setTimeout(r, 1500));
}
continue;
}
// Find a mineable rock - only prospect rocks within reasonable range
const MAX_PROSPECT_DIST = 8;
let targetRock: RockInfo | null = null;
for (const rock of rocks) {
// Check cache first
const cachedOre = oreTypeCache.get(rock.id);
if (cachedOre) {
if (cachedOre === 'empty') continue;
if (!canMineOre(cachedOre, currentStats.level)) {
continue; // Skip rocks we can't mine
}
targetRock = rock;
targetRock.oreType = cachedOre;
break;
}
// Only prospect nearby rocks (don't walk too far)
if (rock.distance > MAX_PROSPECT_DIST) {
continue;
}
// Unknown rock - prospect it
console.log(`Prospecting rock id=${rock.id} dist=${rock.distance}...`);
const oreType = await prospectRock(ctx, rock);
if (oreType) {
console.log(`Rock ${rock.id} = ${oreType}`);
oreTypeCache.set(rock.id, oreType);
if (oreType !== 'empty' && canMineOre(oreType, currentStats.level)) {
targetRock = rock;
targetRock.oreType = oreType;
break;
}
}
}
if (!targetRock) {
consecutiveFailures++;
console.log(`No mineable rocks found (failure ${consecutiveFailures}/${MAX_FAILURES})`);
if (consecutiveFailures >= MAX_FAILURES) {
console.log('ERROR: No mineable rocks at this location! Need copper/tin at level 1.');
console.log('Cached ore types: ' + Array.from(oreTypeCache.entries()).map(([id, ore]) => `${id}=${ore}`).join(', '));
return; // Exit script - wrong mine location
}
await new Promise(r => setTimeout(r, 2000));
continue;
}
// Mine the rock
console.log(`Mining ${targetRock.oreType} rock id=${targetRock.id} dist=${targetRock.distance}`);
const startXp = currentStats.xp;
const startTick = ctx.sdk.getState()?.tick ?? 0;
const mineResult = await sdk.sendInteractLoc(
targetRock.x, targetRock.z, targetRock.id, targetRock.mineOpIndex
);
if (!mineResult.success) {
console.log(`Mine action failed: ${mineResult.message}`);
consecutiveFailures++;
await new Promise(r => setTimeout(r, 500));
continue;
}
// Wait for mining to complete
try {
await sdk.waitForCondition(state => {
// Success: XP gained
const miningXp = state.skills.find(s => s.name === 'Mining')?.experience ?? 0;
if (miningXp > startXp) return true;
// Rock depleted
const rockNow = state.nearbyLocs.find(l =>
l.x === targetRock!.x && l.z === targetRock!.z
);
if (!rockNow || rockNow.id !== targetRock!.id) return true;
// Dismiss dialogs
if (state.dialog.isOpen) {
sdk.sendClickDialog(0).catch(() => {});
}
return false;
}, 8000);
consecutiveFailures = 0;
} catch {
consecutiveFailures++;
console.log(`Mining timeout (failure ${consecutiveFailures}/${MAX_FAILURES})`);
}
await new Promise(r => setTimeout(r, 200));
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 5 * 60 * 1000, // 5 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-142
View File
@@ -1,142 +0,0 @@
# Lab Log: prayer
## Goal
Train Prayer from level 1 to level 10+ by killing chickens near Lumbridge and burying their bones.
## Strategy
1. Spawn fresh character in Lumbridge with TestPresets.LUMBRIDGE_SPAWN
2. Equip bronze sword and wooden shield for faster kills
3. Walk to chicken coop east of Lumbridge castle (~3237, 3295)
4. Enter through the gate
5. Kill chickens → pick up bones → bury bones → repeat
6. Continue until Prayer level 10
## XP Math
- Prayer XP per bone: 4.5 XP
- XP needed for level 10: 1,154 XP
- Bones needed: ~257 bones
---
## Run 001 - Initial test
**Outcome**: stall
**Duration**: ~3min (external timeout)
### Observations
- Script walked to chicken coop successfully
- Hit "I can't reach that!" error - fence blocks access
### Root Cause
Chicken coop is fenced. Need to open the gate to enter.
### Fix
Added gate opening logic:
1. Walk to entrance coordinates (3237, 3295)
2. Call `ctx.bot.openDoor(/gate/i)`
3. Walk inside to (3232, 3295)
---
## Run 002 - With gate fix
**Outcome**: partial success (level 7, 675 XP)
**Duration**: ~3min (external timeout)
### Observations
- Gate opening worked
- Entered coop successfully
- Bones collected and buried
- Reached level 7 before external timeout
### What Worked
- Gate entry
- Combat with chickens
- Bone pickup
- Bone burying
---
## Run 003 - Dialog issue
**Outcome**: stall
**Duration**: ~3min (external timeout)
### Observations
- Collected bones (4+)
- Prayer stayed at 0 XP
- Dialog was open at exit
- Bones not being buried
### Root Cause
Level-up or other dialog blocking bury action. Dialog handling not aggressive enough.
### Fix
1. Added `dismissDialog()` helper function
2. Enhanced dialog dismissal in main loop (multiple clicks)
3. Added dialog checks in `buryAllBones()`
---
## Run 004 - Better dialog handling
**Outcome**: stall
**Duration**: ~2min (external timeout)
### Observations
- 20 items in inventory
- 4 bones collected
- Prayer still 0 XP
- Bones not being buried
### Root Cause
Burying threshold too high. Condition was `bonesInInv.length >= 5 || inventory.length >= 25`.
With 20 items and 4 bones, neither condition met.
### Fix
Changed to bury immediately when bones >= 1:
```typescript
if (bonesInInv.length >= 1) {
await buryAllBones(ctx, stats);
}
```
---
## Run 005 - SUCCESS!
**Outcome**: success
**Duration**: ~2-3min
### What Happened
- Equipped sword and shield
- Walked to chicken coop entrance
- Opened gate
- Entered coop
- Killed chickens, picked up bones, buried bones
- Reached Prayer level 10 with 1350 XP
### What Worked
- Immediate bone burying (don't accumulate)
- Gate handling
- Dialog dismissal
- Combat flow
---
## Learnings
### 1. Strategic Findings
- **Bury immediately**: Don't wait to accumulate bones. Burying one at a time is just as efficient as batching, and prevents dialog/state issues.
- **Chickens are ideal**: Level 1 NPCs die very fast, drop bones every kill, and chicken coop is nearby.
- **Gate navigation**: Lumbridge chicken coop requires opening a gate to enter.
### 2. Process & Tooling Reflections
- External timeouts can be misleading - "Bot not connected" errors were from shell timeout, not script issues.
- The `[delta]` logs are very helpful for seeing what changed.
- Running with shorter timeouts initially (2-3 min) is good for quick iteration.
### 3. SDK Issues & Gaps
- None encountered - the SDK handled everything well.
- `bot.openDoor()` worked perfectly for the gate.
- `sdk.sendUseItem()` for burying bones worked as expected.
-331
View File
@@ -1,331 +0,0 @@
/**
* Prayer Trainer Script
*
* Goal: Train Prayer from level 1 to level 10+
*
* Strategy:
* - Walk to chicken coop near Lumbridge (east of castle)
* - Kill chickens (they drop bones)
* - Pick up bones and bury them
* - Repeat until Prayer level 10
*
* Prayer XP per bone buried: 4.5 XP
* XP needed for level 10: 1,154 XP
* Bones needed: ~257 bones (1154 / 4.5)
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Chicken coop is east of Lumbridge castle, near the farm
// The coop is fenced - need to enter through the gate
const CHICKEN_COOP_ENTRANCE = { x: 3237, z: 3295 }; // Just outside gate
const CHICKEN_COOP_INSIDE = { x: 3232, z: 3295 }; // Inside the coop
// Prayer level goal
const TARGET_PRAYER_LEVEL = 5;
// Stats tracking
interface PrayerStats {
bonesCollected: number;
bonesBuried: number;
chickensKilled: number;
startXp: number;
startTime: number;
}
/**
* Find the best chicken to attack.
* Prioritizes chickens not in combat and closest to player.
*/
function findBestChicken(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const chickens = state.nearbyNpcs
.filter(npc => /^chicken$/i.test(npc.name))
.filter(npc => npc.options.some(o => /attack/i.test(o)))
// Filter out chickens already in combat
.filter(npc => !npc.inCombat || npc.targetIndex === -1)
.sort((a, b) => {
// Prefer chickens not in combat
if (a.inCombat !== b.inCombat) {
return a.inCombat ? 1 : -1;
}
// Then by distance
return a.distance - b.distance;
});
return chickens[0] ?? null;
}
/**
* Wait for combat to end (chicken dies).
* Chickens are level 1 and die very quickly.
*/
async function waitForCombatEnd(
ctx: ScriptContext,
targetNpc: NearbyNpc,
stats: PrayerStats
): Promise<'kill' | 'fled' | 'lost_target'> {
const maxWaitMs = 15000; // 15 seconds max for a chicken
const startTime = Date.now();
// Initial delay for combat to start
await new Promise(r => setTimeout(r, 600));
while (Date.now() - startTime < maxWaitMs) {
await new Promise(r => setTimeout(r, 300));
const state = ctx.sdk.getState();
if (!state) return 'lost_target';
// Dismiss any level-up dialogs
if (state.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
continue;
}
// Find the chicken we're fighting
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
if (!target) {
// Chicken disappeared - it died
stats.chickensKilled++;
return 'kill';
}
// Check if chicken has 0 HP (dead)
if (target.maxHp > 0 && target.hp === 0) {
stats.chickensKilled++;
return 'kill';
}
}
return 'lost_target';
}
/**
* Dismiss any open dialog (level-up, etc.)
*/
async function dismissDialog(ctx: ScriptContext): Promise<boolean> {
const state = ctx.sdk.getState();
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
return true;
}
return false;
}
/**
* Bury all bones in inventory.
*/
async function buryAllBones(ctx: ScriptContext, stats: PrayerStats): Promise<void> {
// Dismiss any dialogs before starting
for (let i = 0; i < 5; i++) {
if (!(await dismissDialog(ctx))) break;
}
// Re-fetch inventory after dismissing dialogs (state may have changed)
let inventory = ctx.sdk.getInventory();
let bones = inventory.filter(i => /^bones$/i.test(i.name));
for (const bone of bones) {
const state = ctx.sdk.getState();
if (!state) break;
// Dismiss any dialogs that appeared (level-up, etc.)
if (state.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue; // Re-check state after dismissing
}
// Re-check bone still exists (may have been buried in dialog handling)
const currentInv = ctx.sdk.getInventory();
const currentBone = currentInv.find(i => i.slot === bone.slot && /^bones$/i.test(i.name));
if (!currentBone) continue;
// Find "Bury" option on bone
const buryOpt = currentBone.optionsWithIndex.find(o => /bury/i.test(o.text));
if (buryOpt) {
await ctx.sdk.sendUseItem(currentBone.slot, buryOpt.opIndex);
stats.bonesBuried++;
// Wait for bury animation and XP to register
await new Promise(r => setTimeout(r, 700));
// Log progress periodically
if (stats.bonesBuried % 10 === 0) {
const prayerSkill = ctx.sdk.getState()?.skills.find(s => s.name === 'Prayer');
console.log(`Buried ${stats.bonesBuried} bones - Prayer: Level ${prayerSkill?.baseLevel} (${prayerSkill?.experience} XP)`);
}
}
}
}
/**
* Main prayer training loop
*/
async function prayerTrainingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
const prayerSkill = state.skills.find(s => s.name === 'Prayer');
const stats: PrayerStats = {
bonesCollected: 0,
bonesBuried: 0,
chickensKilled: 0,
startXp: prayerSkill?.experience ?? 0,
startTime: Date.now(),
};
console.log('=== Prayer Trainer ===');
console.log(`Goal: Train Prayer to level ${TARGET_PRAYER_LEVEL}`);
console.log(`Starting Prayer: Level ${prayerSkill?.baseLevel ?? 1} (${prayerSkill?.experience ?? 0} XP)`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
// Dismiss any initial dialogs
await ctx.bot.dismissBlockingUI();
// Equip starting gear for faster kills
const sword = ctx.sdk.findInventoryItem(/bronze sword/i);
if (sword) {
console.log('Equipping bronze sword...');
await ctx.bot.equipItem(sword);
}
const shield = ctx.sdk.findInventoryItem(/wooden shield/i);
if (shield) {
console.log('Equipping wooden shield...');
await ctx.bot.equipItem(shield);
}
// Walk to chicken coop entrance and enter through gate
console.log(`Walking to chicken coop entrance at (${CHICKEN_COOP_ENTRANCE.x}, ${CHICKEN_COOP_ENTRANCE.z})...`);
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
await ctx.bot.walkTo(CHICKEN_COOP_ENTRANCE.x, CHICKEN_COOP_ENTRANCE.z);
// Open the gate to enter the chicken coop
console.log('Opening chicken coop gate...');
const gateResult = await ctx.bot.openDoor(/gate/i);
if (!gateResult.success && gateResult.reason !== 'already_open') {
console.warn(`Gate issue: ${gateResult.message}`);
}
// Walk inside the coop
console.log(`Walking inside coop to (${CHICKEN_COOP_INSIDE.x}, ${CHICKEN_COOP_INSIDE.z})...`);
await ctx.bot.walkTo(CHICKEN_COOP_INSIDE.x, CHICKEN_COOP_INSIDE.z);
// Main loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Check if we've reached target level
const prayer = currentState.skills.find(s => s.name === 'Prayer');
if (prayer && prayer.baseLevel >= TARGET_PRAYER_LEVEL) {
console.log(`*** Goal achieved! Prayer level ${prayer.baseLevel} ***`);
break;
}
// Dismiss any dialogs (level-up, etc.) - click multiple times to clear multi-page dialogs
if (currentState.dialog.isOpen) {
for (let i = 0; i < 3; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) break;
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
continue;
}
// Check inventory for bones - bury them as soon as we have any
const inventory = ctx.sdk.getInventory();
const bonesInInv = inventory.filter(i => /^bones$/i.test(i.name));
// Bury bones immediately when we have 1+ (don't wait to accumulate)
// This is faster than waiting and prevents inventory from filling
if (bonesInInv.length >= 1) {
if (bonesInInv.length > 1) {
console.log(`Burying ${bonesInInv.length} bones...`);
}
await buryAllBones(ctx, stats);
continue;
}
// Pick up nearby bones (prioritize closest)
const groundBones = ctx.sdk.getGroundItems()
.filter(i => /^bones$/i.test(i.name))
.filter(i => i.distance <= 5)
.sort((a, b) => a.distance - b.distance);
if (groundBones.length > 0) {
const bone = groundBones[0]!;
const result = await ctx.bot.pickupItem(bone);
if (result.success) {
stats.bonesCollected++;
}
continue;
}
// Find a chicken to attack
const chicken = findBestChicken(ctx);
if (!chicken) {
console.log('No chickens nearby - walking back inside coop...');
await ctx.bot.walkTo(CHICKEN_COOP_INSIDE.x, CHICKEN_COOP_INSIDE.z);
continue;
}
// Attack the chicken
const attackResult = await ctx.bot.attackNpc(chicken);
if (!attackResult.success) {
console.warn(`Attack failed: ${attackResult.message}`);
// Try opening gate if blocked
if (attackResult.reason === 'out_of_reach') {
await ctx.bot.openDoor(/gate/i);
}
continue;
}
// Wait for chicken to die
await waitForCombatEnd(ctx, chicken, stats);
}
}
async function main() {
const username = `pr${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await prayerTrainingLoop(ctx);
} finally {
// Log final stats
const state = ctx.sdk.getState();
if (state) {
const prayer = state.skills.find(s => s.name === 'Prayer');
console.log('=== Final Results ===');
console.log(`Prayer: Level ${prayer?.baseLevel ?? '?'} (${prayer?.experience ?? '?'} XP)`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 3 * 60 * 1000, // 3 minutes for testing
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-107
View File
@@ -1,107 +0,0 @@
# Lab Log: ranged
## Goal
Train Ranged skill from level 1 to level 10+ using the shortbow and bronze arrows provided by LUMBRIDGE_SPAWN preset.
## Strategy
- Equip shortbow and bronze arrows at start
- Walk to Lumbridge chicken coop (3235, 3295)
- Open gate to enter coop
- Kill chickens using ranged attacks (level 1, easy targets)
- Pick up bronze arrows from the ground to conserve ammo
- Continue until Ranged level 10
## Equipment
- Shortbow (from preset)
- Bronze arrows x25 (from preset)
---
## Run 001 - Initial Test (Cow Field)
**Timestamp**: 2026-01-27
**Duration**: ~2 min
**Outcome**: FAIL - Context limit hit
### What Happened
- Previous version targeted cows at cow field
- Got stuck trying to pick up arrows inside fence (unreachable)
- Script stalled repeatedly
### Root Cause
1. Arrows land inside fenced areas and are unreachable
2. No retry limit on failed pickups - kept trying same unreachable arrows
3. Cow field has more obstacles/fences than chicken coop
### Fix Applied
- Switch to Lumbridge chicken coop (fewer obstacles)
- Add MAX_PICKUP_RETRIES = 2 to limit attempts on same arrow location
- Add gate opening logic for chicken coop entry
---
## Run 002 - Chicken Coop Strategy
**Timestamp**: 2026-01-27 19:06
**Duration**: ~2 min
**Outcome**: PARTIAL SUCCESS (Level 6, then disconnect)
### What Happened
- Successfully equipped gear and walked to chicken coop
- Opened gate, entered coop
- Started killing chickens, leveled up to Ranged 3 quickly
- Gained XP rapidly (chickens die fast to ranged)
- Reached level 6 before bot disconnect error
### Notes
- "Bot not connected" error caused script to terminate
- This appears to be a connection issue, not script logic
---
## Run 003 - Full Success
**Timestamp**: 2026-01-27 19:08
**Duration**: ~1 min
**Outcome**: SUCCESS - Reached Ranged Level 10
### What Happened
- Equipped shortbow and bronze arrows
- Walked to chicken coop gate
- Opened gate successfully
- Killed 2 chickens total
- Level progression: 1 -> 3 -> 6 -> 10 (very fast)
- Picked up arrows successfully after kills
- Goal achieved with 20 arrows remaining
### Stats
- Final Level: 10
- Final XP: 1200
- Arrows used: 5 (25 - 20)
- Arrows recovered: Some (pickup working)
- Time: ~1 minute
### What Worked
1. Chicken coop is excellent for ranged training - enclosed area, easy targets
2. Gate opening logic works correctly
3. Arrow pickup with retry limit prevents getting stuck on unreachable arrows
4. Dialog dismissal during combat works well
---
## Learnings
### 1. Strategic Findings
- **Chicken coop > Cow field for ranged**: Enclosed area, level 1 targets, fewer obstacles
- **Ranged XP is very fast**: Level 1->10 in 2 kills with accurate style
- **Arrow conservation works**: Pickup logic recovers most arrows
- **Gate handling is essential**: Must open gate to enter coop
### 2. Process & Tooling Reflections
- State delta logging made it easy to see level progression
- Short runs (1-2 min) were sufficient to validate the approach
- Connection issues can terminate scripts unexpectedly - may need retry logic
### 3. SDK Issues & Gaps
- "Bot not connected" error during combat suggests connection stability issues
- Pickup failed arrows could benefit from pathfinding check before attempting
-459
View File
@@ -1,459 +0,0 @@
/**
* Ranged Trainer Script
*
* Goal: Train Ranged to level 10+ using the shortbow and bronze arrows from LUMBRIDGE_SPAWN.
*
* Strategy:
* - Equip shortbow and bronze arrows
* - Kill chickens at Lumbridge chicken coop (level 1, easy targets)
* - Open gate to enter coop
* - Pick up bronze arrows from loot to conserve ammo (with retry limits)
* - Continue until Ranged level 10
*
* LUMBRIDGE_SPAWN preset includes:
* - Shortbow (1)
* - Bronze arrows (25)
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Training location - Lumbridge chicken coop (inside)
const CHICKEN_COOP = { x: 3235, z: 3295 };
// Ranged attack style index (for shortbow, all styles train ranged)
const RANGED_STYLE = 0; // Accurate
// Track combat statistics
interface RangedStats {
kills: number;
arrowsUsed: number;
arrowsRecovered: number;
startXp: number;
startTime: number;
lastProgressTime: number;
}
// Track failed pickup locations to avoid retrying (position -> retry count)
const failedPickups = new Map<string, number>();
const MAX_PICKUP_RETRIES = 2;
/**
* Find the best chicken to attack.
* Prioritize chickens that aren't in combat and are close.
*/
function findBestTarget(ctx: ScriptContext): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const targets = state.nearbyNpcs
.filter(npc => /^chicken$/i.test(npc.name))
.filter(npc => npc.options.some(o => /attack/i.test(o)))
// Prefer NPCs not in combat
.filter(npc => {
if (npc.targetIndex === -1) return true;
return !npc.inCombat;
})
.sort((a, b) => {
// Prefer NPCs not in combat
if (a.inCombat !== b.inCombat) {
return a.inCombat ? 1 : -1;
}
// Then by distance (but ranged can attack from further)
return a.distance - b.distance;
});
return targets[0] ?? null;
}
/**
* Count bronze arrows in inventory + equipped
*/
function countArrows(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
if (!state) return 0;
// Check inventory
const invArrows = state.inventory
.filter(i => /bronze arrow/i.test(i.name))
.reduce((sum, i) => sum + i.count, 0);
// Check equipment (ammo slot is index 10)
const equippedArrows = state.equipment
.filter(e => /bronze arrow/i.test(e.name))
.reduce((sum, e) => sum + e.count, 0);
return invArrows + equippedArrows;
}
/**
* Get current ranged level
*/
function getRangedLevel(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
return state?.skills.find(s => s.name === 'Ranged')?.baseLevel ?? 1;
}
/**
* Get current ranged XP
*/
function getRangedXp(ctx: ScriptContext): number {
const state = ctx.sdk.getState();
return state?.skills.find(s => s.name === 'Ranged')?.experience ?? 0;
}
/**
* Wait for combat to end (NPC dies or we need to heal)
*/
async function waitForCombatEnd(
ctx: ScriptContext,
targetNpc: NearbyNpc,
stats: RangedStats
): Promise<'kill' | 'fled' | 'lost_target' | 'need_heal'> {
let combatStarted = false;
let ticksSinceCombatEnded = 0;
let loopCount = 0;
const startXp = getRangedXp(ctx);
const maxWaitMs = 30000;
const startTime = Date.now();
// Initial delay to let combat start
await new Promise(r => setTimeout(r, 800));
while (Date.now() - startTime < maxWaitMs) {
await new Promise(r => setTimeout(r, 400));
loopCount++;
const state = ctx.sdk.getState();
if (!state) return 'lost_target';
// Check for dialog (level up, etc)
if (state.dialog.isOpen) {
console.log('Dismissing dialog during combat...');
await ctx.sdk.sendClickDialog(0);
continue;
}
// Check XP gains as combat indicator
const currentXp = getRangedXp(ctx);
const xpGained = currentXp - startXp;
if (xpGained > 0) {
combatStarted = true;
}
// Find our target NPC
const target = state.nearbyNpcs.find(n => n.index === targetNpc.index);
if (!target) {
// NPC disappeared - count as kill if we gained XP
if (combatStarted || xpGained > 0 || loopCount >= 2) {
stats.kills++;
return 'kill';
}
return 'lost_target';
}
// Check NPC health - if 0, it died
if (target.maxHp > 0 && target.hp === 0) {
stats.kills++;
return 'kill';
}
// Track combat via combatCycle
const currentTick = state.tick;
const npcInCombat = target.combatCycle > currentTick;
const playerInCombat = state.player?.combat?.inCombat ?? false;
const inActiveCombat = playerInCombat || npcInCombat || xpGained > 0;
if (inActiveCombat) {
combatStarted = true;
ticksSinceCombatEnded = 0;
} else if (combatStarted) {
ticksSinceCombatEnded++;
if (ticksSinceCombatEnded >= 4) {
return 'fled';
}
} else if (loopCount >= 8) {
// Combat never started after ~4 seconds
return 'lost_target';
}
}
return 'lost_target';
}
/**
* Log current statistics
*/
function logStats(ctx: ScriptContext, stats: RangedStats): void {
const state = ctx.sdk.getState();
if (!state) return;
const ranged = state.skills.find(s => s.name === 'Ranged');
const currentXp = ranged?.experience ?? 0;
const xpGained = currentXp - stats.startXp;
const arrows = countArrows(ctx);
const elapsedMs = Date.now() - stats.startTime;
const elapsedMinutes = Math.round(elapsedMs / 60_000);
const xpPerHour = elapsedMs > 60_000 ? Math.round(xpGained / (elapsedMs / 3_600_000)) : 0;
console.log(`--- Stats after ${elapsedMinutes}m / ${stats.kills} kills ---`);
console.log(`Ranged: Level ${ranged?.baseLevel} (${currentXp} XP, +${xpGained})`);
console.log(`Arrows: ${arrows} remaining`);
console.log(`XP/hour: ~${xpPerHour.toLocaleString()}`);
}
/**
* Main ranged training loop
*/
async function rangedTrainingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
const now = Date.now();
const stats: RangedStats = {
kills: 0,
arrowsUsed: 0,
arrowsRecovered: 0,
startXp: getRangedXp(ctx),
startTime: now,
lastProgressTime: now,
};
console.log('=== Ranged Trainer - Chicken Coop Strategy ===');
console.log(`Goal: Reach Ranged level 10`);
console.log(`Starting Ranged Level: ${getRangedLevel(ctx)}`);
console.log(`Starting XP: ${stats.startXp}`);
console.log(`Starting Arrows: ${countArrows(ctx)}`);
// Equip shortbow if not already equipped
const shortbow = ctx.sdk.findInventoryItem(/shortbow/i);
if (shortbow) {
console.log('Equipping shortbow...');
await ctx.bot.equipItem(shortbow);
}
// Equip bronze arrows if not already equipped
const arrows = ctx.sdk.findInventoryItem(/bronze arrow/i);
if (arrows) {
console.log('Equipping bronze arrows...');
await ctx.bot.equipItem(arrows);
}
// Set ranged attack style
console.log('Setting ranged attack style...');
await ctx.sdk.sendSetCombatStyle(RANGED_STYLE);
// Dismiss any blocking UI
await ctx.bot.dismissBlockingUI();
// Walk to chicken coop and open gate if needed
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
// // Open the gate to enter - retry if needed
// for (let attempt = 0; attempt < 3; attempt++) {
// const gateResult = await ctx.bot.openDoor(/gate/i);
// if (gateResult.success) {
// console.log('Opened gate to chicken coop');
// await new Promise(r => setTimeout(r, 600)); // Wait for gate to open
// break;
// }
// console.log(`Gate open attempt ${attempt + 1} failed, retrying...`);
// await new Promise(r => setTimeout(r, 300));
// }
// // Walk inside the coop
// console.log('Walking inside coop...');
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
// Verify we're inside - if not, try again
// const pos = ctx.sdk.getState();
// if (pos && (pos.player?.worldX !== CHICKEN_COOP.x || pos.player?.worldZ !== CHICKEN_COOP.z)) {
// console.log(`Not at target (${pos.player?.worldX}, ${pos.player?.worldZ}), retrying entry...`);
// await ctx.bot.openDoor(/gate/i);
// await new Promise(r => setTimeout(r, 600));
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
// }
let lastStatsLog = 0;
// Main training loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Check if we've reached level 10
const rangedLevel = getRangedLevel(ctx);
if (rangedLevel >= 10) {
console.log(`*** GOAL REACHED: Ranged Level ${rangedLevel}! ***`);
break;
}
// Check arrows remaining
const arrowCount = countArrows(ctx);
if (arrowCount <= 0) {
throw new Error('Out of arrows!');
}
// Dismiss any blocking dialogs (level-up, etc) - critical to check frequently!
if (currentState.dialog.isOpen) {
console.log('Dismissing dialog...');
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Log stats periodically (every 5 kills or 2 minutes)
const timeSinceLastLog = Date.now() - lastStatsLog;
const shouldLogByKills = stats.kills > 0 && stats.kills % 5 === 0 && stats.kills !== lastStatsLog;
const shouldLogByTime = timeSinceLastLog >= 2 * 60_000;
if (shouldLogByKills || shouldLogByTime || lastStatsLog === 0) {
lastStatsLog = stats.kills > 0 ? stats.kills : Date.now();
logStats(ctx, stats);
}
// Pick up arrows from the ground (conserve ammo!) - but only ones we can reach
// Skip arrows that we've already failed to pick up too many times
const groundArrows = ctx.sdk.getGroundItems()
.filter(i => /bronze arrow/i.test(i.name))
.filter(i => i.distance <= 4) // Only close ones to avoid fence issues
.filter(i => {
const key = `${i.x},${i.z}`;
const retries = failedPickups.get(key) ?? 0;
return retries < MAX_PICKUP_RETRIES;
})
.sort((a, b) => a.distance - b.distance);
if (groundArrows.length > 0) {
const arrowPile = groundArrows[0]!;
const key = `${arrowPile.x},${arrowPile.z}`;
console.log(`Picking up ${arrowPile.count ?? 1} bronze arrows at (${arrowPile.x}, ${arrowPile.z})...`);
const result = await ctx.bot.pickupItem(arrowPile);
if (result.success) {
stats.arrowsRecovered += arrowPile.count ?? 1;
// Clear retry count on success
failedPickups.delete(key);
// Re-equip arrows if they went to inventory
const invArrows = ctx.sdk.findInventoryItem(/bronze arrow/i);
if (invArrows) {
await ctx.bot.equipItem(invArrows);
}
} else if (result.reason === 'cant_reach' || result.reason === 'timeout') {
// Increment retry count for this position
const retries = failedPickups.get(key) ?? 0;
failedPickups.set(key, retries + 1);
console.log(`Arrow pickup failed (retry ${retries + 1}/${MAX_PICKUP_RETRIES}): ${result.reason}`);
}
continue;
}
// Find a chicken to attack
const target = findBestTarget(ctx);
if (!target) {
console.log('No chickens nearby - walking to chicken coop...');
// Try to open gate and enter
await ctx.bot.openDoor(/gate/i);
await new Promise(r => setTimeout(r, 400));
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
continue;
}
// For ranged, we can attack from further away
// Walk closer if target is too far (ranged has ~7 tile range)
if (target.distance > 7) {
console.log(`Chicken too far (${target.distance} tiles), walking to coop...`);
// Try to open gate first if needed
await ctx.bot.openDoor(/gate/i);
await new Promise(r => setTimeout(r, 400));
// Walk to coop
await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
continue;
}
// Check if already fighting
const playerCombat = currentState.player?.combat;
if (playerCombat?.inCombat && playerCombat.targetIndex === target.index) {
await waitForCombatEnd(ctx, target, stats);
continue;
}
// Dismiss any blocking UI (level-up dialogs, etc.) before attacking
await ctx.bot.dismissBlockingUI();
// Attack the cow
const arrowsBefore = countArrows(ctx);
const attackResult = await ctx.bot.attackNpc(target);
if (!attackResult.success) {
console.warn(`Attack failed: ${attackResult.message}`);
// If timeout, check for dialog that may have appeared during attack attempt
if (attackResult.reason === 'timeout') {
const s = ctx.sdk.getState();
if (s?.dialog.isOpen) {
console.log('Dismissing dialog that appeared during attack...');
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
}
// Try opening gate and re-entering if blocked
// if (attackResult.reason === 'out_of_reach' || attackResult.reason === 'timeout') {
// console.log('Cannot reach target - trying to enter coop...');
// await ctx.bot.openDoor(/gate/i);
// await new Promise(r => setTimeout(r, 400));
// await ctx.bot.walkTo(CHICKEN_COOP.x, CHICKEN_COOP.z);
// }
continue;
}
// Wait for combat to complete
const combatResult = await waitForCombatEnd(ctx, target, stats);
// Track arrows used
const arrowsAfter = countArrows(ctx);
if (arrowsAfter < arrowsBefore) {
stats.arrowsUsed += arrowsBefore - arrowsAfter;
}
if (combatResult === 'kill') {
console.log(`Kill #${stats.kills}! (Ranged: Lv${getRangedLevel(ctx)}, Arrows: ${arrowsAfter})`);
}
}
}
async function main() {
const username = `rg${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await rangedTrainingLoop(ctx);
} finally {
// Log final stats
const state = ctx.sdk.getState();
if (state) {
const ranged = state.skills.find(s => s.name === 'Ranged');
console.log('=== Final Results ===');
console.log(`Ranged: Level ${ranged?.baseLevel} (${ranged?.experience} XP)`);
console.log(`Arrows remaining: ${countArrows(ctx)}`);
console.log(`Position: (${state.player?.worldX}, ${state.player?.worldZ})`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 30 * 60 * 1000, // 30 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-76
View File
@@ -1,76 +0,0 @@
# Script Best Practices
Lessons learned from developing automation scripts. Reference this when writing new scripts to avoid common pitfalls.
## Dialogs & UI Blocking
### Level-Up Dialogs
When a player gains enough XP to level up, a congratulations dialog appears that **blocks all actions**. Your script must handle this:
```typescript
// Check for and dismiss dialogs in your main loop
if (currentState.dialog.isOpen) {
ctx.log('Dismissing dialog...');
await ctx.sdk.sendClickDialog(0);
continue;
}
```
All BotActions methods automatically dismiss blocking UI (level-up dialogs, etc.). Manual dismissal is only needed when using low-level sdk methods directly:
```typescript
await ctx.bot.dismissBlockingUI();
```
### Other Blocking Dialogs
- Welcome messages on login
- NPC conversation dialogs
- Shop interfaces
- Bank interfaces
## State & Activity Detection
### Animation State
The SDK exposes animation IDs for both player and NPCs:
```typescript
// Player animation (-1 = idle/none)
state.player?.animId // Current animation sequence ID
state.player?.spotanimId // Spot animation (spell effects, combat hits, etc.)
// NPC animation
const npc = state.nearbyNpcs[0];
npc.animId // -1 = idle
npc.spotanimId // -1 = none
```
**Common animation checks:**
```typescript
// Check if player is doing something (not idle)
const isActive = state.player?.animId !== -1;
// Check if player is idle
const isIdle = state.player?.animId === -1;
```
Note: Animation IDs are raw sequence IDs from the game. -1 always means idle/none.
### Other Ways to Detect Player Activity
Animation state is useful, but you can also detect activity through:
- **XP changes** - check if skill XP increased
- **Inventory changes** - check if items appeared/disappeared
- **Game messages** - check `state.gameMessages` for "You catch...", "You mine...", etc.
- **Just keep clicking** - the game queues actions, so continuous clicking often works best
## Inventory Management
### Drop Items to Make Space
For gathering skills, drop items when inventory fills:
```typescript
if (currentState.inventory.length > 20) {
for (const item of itemsToDrop) {
await ctx.sdk.sendDropItem(item.slot);
await new Promise(r => setTimeout(r, 100));
}
}
```
-183
View File
@@ -1,183 +0,0 @@
# Lab Log: smithing
Goal: Train Smithing to level 10+ starting from a fresh Lumbridge spawn.
## Strategy
Smithing requires:
1. **Mining copper and tin ore** - Both are level 1, found at various mines
2. **Smelting at a furnace** - Creates bronze bars (1 copper + 1 tin = 1 bronze bar)
3. **Smithing at an anvil** - Uses bronze bars + hammer to make items
### Location Analysis
**Ore Sources:**
- SE Varrock mine (3285, 3365) - Has copper and tin, safe area
- Al-Kharid mine (3300, 3310) - Has copper and tin, scorpions nearby
**Furnaces:**
- Al-Kharid furnace (3274, 3186) - Closest to Lumbridge, requires 10gp toll
- Lumbridge furnace - Inside castle, but may require quest completion?
- Falador furnace - Too far
**Anvils:**
- Varrock anvil (3188, 3427) - West Varrock, near west bank
- Al-Kharid anvil - Need to verify location
### Initial Approach (v1)
1. Start at Lumbridge with bronze pickaxe
2. Walk to SE Varrock mine (no toll required)
3. Mine copper and tin ore (alternating)
4. When inventory has ore, walk to Al-Kharid furnace
- Need 10gp for toll - sell shortbow at Lumbridge general store
5. Smelt bronze bars at furnace
6. Smith items at nearby anvil (if exists) or Varrock anvil
7. Repeat until level 10
**XP per action:**
- Smelting bronze bar: 6.25 XP
- Smithing bronze item: 12.5 XP per bar used
- Bronze dagger = 1 bar = 12.5 XP
- Level 10 requires ~4,470 XP total
---
## Run History
### Run 001 - 2026-01-27 17:36
**Outcome**: Partial success (disconnected)
**Duration**: ~3 minutes
**Version**: v2
**What Happened:**
- Equipped bronze pickaxe ✓
- Sold shortbow at Lumbridge shop (got 20gp) ✓
- Walked to SE Varrock mine ✓
- Mined copper and tin ore using prospecting ✓
- Entered Al-Kharid (paid toll) ✓
- Smelted 3 bronze bars at furnace ✓
- **Smithing went from Level 1 to Level 8 (930 XP)!**
**Insights:**
- Smelting works! XP rate is ~310 XP per bar (server has boosted rates)
- With 310 XP per bar, only need ~15 bars for level 10
- Script disconnected during iteration 2 ("Bot not connected")
---
### Run 002-005 - 2026-01-27 17:42-17:59
**Outcome**: Various disconnects
**Versions**: v2, v3, v4
**Issues Encountered:**
1. **Al-Kharid mine (v3)** - WRONG LOCATION
- Has iron and gold rocks, NOT copper/tin
- Has aggressive scorpions (HP dropped from 11 to 2)
- Don't use Al-Kharid mine for level 1 mining
2. **Server instability**
- Multiple "Bot not connected" errors
- "Game tick frozen for 16s" - server tick freeze
- These are infrastructure issues, not script bugs
3. **SE Varrock mine locations**
- Center-east area (~3287, 3365) has copper/tin
- Northern area (~3285, 3370) has more iron
- Target the correct sub-area
**What Works:**
- Prospecting to identify ore type ✓
- Mining when we find copper/tin ✓
- Walking routes ✓
- Toll gate payment ✓
- Smelting at Al-Kharid furnace ✓ (from Run 001)
---
## Learnings
### 1. Strategic Findings
**Effective approaches:**
- SE Varrock mine copper/tin is around (3286-3290, 3361-3366)
- Use prospecting to identify ore type before mining
- Al-Kharid furnace is reliable for smelting
- XP rates are boosted (~310 XP per bronze bar smelt)
**Failed strategies:**
- Al-Kharid mine is NOT suitable for level 1 (has iron/gold + scorpions)
- Walking directly to Al-Kharid mine center leads to iron rocks
**Optimal parameters:**
- Mine ~8 ore pairs before traveling to smelt
- Leave 2-3 inventory slots free
- 10 second timeout for mining actions
### 2. Process & Tooling Reflections
- Script successfully completed one full smelting cycle on first run
- Disconnects appear to be server instability, not script issues
- The prospecting approach works well for finding correct rocks
- Consider adding reconnection logic or retry mechanisms
### 3. SDK Issues & Gaps
- No obvious SDK bugs encountered
- `sendUseItemOnLoc` works for ore + furnace
- `waitForCondition` with game messages works for prospecting
---
### Run 006 - 2026-01-27 18:36
**Outcome**: Partial success - mined 5 pairs, walked to furnace, then disconnected
**Duration**: ~2.5 minutes
**Version**: v4 (simplified)
**What Happened:**
- Iteration 1: Walk from Lumbridge failed (obstacle at 3240, 3262)
- Iteration 2: Walk from Al-Kharid worked directly (139 tiles)
- Successfully mined 5 copper, 7 tin (5 pairs) ✓
- Started walking to Al-Kharid furnace
- Disconnected mid-walk at (3275, 3295)
**Key Insights:**
- Pathfinding works FROM Al-Kharid but has issues FROM Lumbridge
- The SE Varrock mine copper/tin area IS correct (~3282-3286, 3365-3368)
- Mining successfully detects copper vs tin after prospecting
- Need to go to Al-Kharid FIRST, then walk to mine (smoother path)
---
## Current Status
**Script Functionality:** WORKING (with caveats)
The script successfully:
1. ✅ Equips pickaxe
2. ✅ Gets coins by selling shortbow
3. ✅ Enters Al-Kharid (pays toll)
4. ✅ Walks to SE Varrock mine (from Al-Kharid)
5. ✅ Mines copper and tin ore (using prospecting to filter iron)
6. ✅ Walks to Al-Kharid furnace
7. ✅ Smelts bronze bars (demonstrated in Run 001)
8. ⏳ Repeats until level 10 (blocked by server disconnects)
**Remaining Issues:**
- Server instability causes frequent disconnects
- Walking from Lumbridge to mine is blocked by obstacles
- Server XP rates are boosted (310 XP per bar vs 6.25 standard)
**Recommended Flow:**
1. Sell shortbow → get coins
2. Go to Al-Kharid first (pay toll)
3. Walk to SE Varrock mine from Al-Kharid (more reliable path)
4. Mine copper/tin
5. Walk to furnace (in Al-Kharid)
6. Smelt bars
7. Repeat
-720
View File
@@ -1,720 +0,0 @@
/**
* Smithing Training Script v4
*
* Goal: Train Smithing to level 10+ from a fresh Lumbridge spawn
*
* Strategy (v4):
* 1. Sell shortbow for coins (toll money)
* 2. Walk to SE Varrock mine copper/tin area (around 3289, 3363)
* 3. Mine copper/tin ore (balanced)
* 4. Walk to Al-Kharid and pay toll
* 5. Smelt bronze bars at Al-Kharid furnace
* 6. Repeat until level 10
*
* v4 Changes:
* - Back to SE Varrock mine (has copper/tin, Al-Kharid has iron/gold/scorpions)
* - Better copper/tin detection by checking specific rock locations
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Locations
const LUMBRIDGE_SHOP = { x: 3211, z: 3247 };
const AL_KHARID_GATE = { x: 3268, z: 3228 };
const AL_KHARID_FURNACE = { x: 3274, z: 3186 };
const AL_KHARID_INSIDE = { x: 3277, z: 3227 };
// SE Varrock mine - copper/tin area is in the central-east section
// Copper rocks around (3287-3290, 3361-3366)
// Tin rocks around (3282-3286, 3364-3368)
const SE_VARROCK_MINE_COPPERTIN = { x: 3286, z: 3365 };
// Waypoints from Lumbridge to SE Varrock mine copper/tin area
// Note: We use all waypoints to ensure we reach the correct spot
const WAYPOINTS_TO_MINE = [
{ x: 3222, z: 3250 }, // North of Lumbridge
{ x: 3235, z: 3290 }, // Continue north
{ x: 3255, z: 3320 }, // Approaching mine
{ x: 3275, z: 3350 }, // Near mine entrance
SE_VARROCK_MINE_COPPERTIN, // Final destination: 3286, 3365
];
// Get smithing stats
function getSmithingStats(ctx: ScriptContext): { level: number; xp: number } {
const state = ctx.sdk.getState();
const smithing = state?.skills.find(s => s.name === 'Smithing');
return {
level: smithing?.baseLevel ?? 1,
xp: smithing?.experience ?? 0
};
}
// Get current coin count
function getCoins(ctx: ScriptContext): number {
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
return coins?.count ?? 0;
}
// Check if inside Al Kharid
function isInsideAlKharid(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
return (state?.player?.worldX ?? 0) >= 3270;
}
// Count ore in inventory
function countOre(ctx: ScriptContext): { copper: number; tin: number; bronzeBars: number } {
const inv = ctx.sdk.getState()?.inventory ?? [];
return {
copper: inv.filter(i => /copper ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
tin: inv.filter(i => /tin ore/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
bronzeBars: inv.filter(i => /bronze bar/i.test(i.name)).reduce((sum, i) => sum + i.count, 0),
};
}
// Get coins by selling items at Lumbridge shop
async function getCoinsFromShop(ctx: ScriptContext): Promise<boolean> {
console.log('Selling items for toll money...');
await ctx.bot.walkTo(LUMBRIDGE_SHOP.x, LUMBRIDGE_SHOP.z);
const openResult = await ctx.bot.openShop(/shop keeper/i);
if (!openResult.success) {
console.log(`Failed to open shop: ${openResult.message}`);
return false;
}
// Sell shortbow (worth ~20gp)
const sellResult = await ctx.bot.sellToShop(/shortbow/i, 1);
if (sellResult.success) {
console.log(sellResult.message);
}
await ctx.bot.closeShop();
const coins = getCoins(ctx);
console.log(`Have ${coins}gp after selling`);
return coins >= 10;
}
// Walk to SE Varrock mine
// The pathfinder should handle routing automatically
async function walkToMine(ctx: ScriptContext): Promise<void> {
console.log('Walking to SE Varrock mine...');
const currentPos = ctx.sdk.getState()?.player;
console.log(`From (${currentPos?.worldX}, ${currentPos?.worldZ}) to (${SE_VARROCK_MINE_COPPERTIN.x}, ${SE_VARROCK_MINE_COPPERTIN.z})`);
// Try direct walk to mine
let result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
if (!result.success) {
console.log(`Direct walk failed: ${result.message}`);
// Try via an intermediate point (around Varrock east)
console.log('Trying via Varrock east gate area...');
const intermediate = { x: 3275, z: 3340 };
result = await ctx.bot.walkTo(intermediate.x, intermediate.z);
if (result.success) {
result = await ctx.bot.walkTo(SE_VARROCK_MINE_COPPERTIN.x, SE_VARROCK_MINE_COPPERTIN.z);
}
}
// Dismiss any dialogs
const state = ctx.sdk.getState();
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
const pos = ctx.sdk.getState()?.player;
console.log(`At mine: (${pos?.worldX}, ${pos?.worldZ})`);
}
// Prospect a rock to find what ore it contains
async function prospectRock(ctx: ScriptContext, rock: { x: number; z: number; id: number; optionsWithIndex: Array<{ text: string; opIndex: number }> }): Promise<string | null> {
const prospectOpt = rock.optionsWithIndex.find(o => /prospect/i.test(o.text));
if (!prospectOpt) return null;
const startTick = ctx.sdk.getState()?.tick ?? 0;
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, prospectOpt.opIndex);
await new Promise(r => setTimeout(r, 800)); // Brief wait for action
try {
const state = await ctx.sdk.waitForCondition(s => {
for (const msg of s.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
if (text.includes('rock contains') || text.includes('ore') || text.includes('no ore') || text.includes('nothing')) {
return true;
}
}
}
return false;
}, 3000); // Reduced from 5000
// Extract ore type from message
for (const msg of state.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
if (text.includes('copper')) return 'copper';
if (text.includes('tin')) return 'tin';
if (text.includes('iron')) return 'iron';
if (text.includes('gold')) return 'gold';
if (text.includes('coal')) return 'coal';
if (text.includes('no ore') || text.includes('nothing')) return 'empty';
}
}
} catch {
return null;
}
return null;
}
// Find and mine a rock (copper or tin)
// Use prospecting to filter out iron/gold, but don't try to balance
async function mineRock(ctx: ScriptContext): Promise<boolean> {
const state = ctx.sdk.getState();
if (!state) return false;
// Dismiss dialogs first
if (state.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
}
// Find rocks with Mine option - sort by distance
const rocks = state.nearbyLocs.filter(loc =>
/rocks?/i.test(loc.name) && !/rockslide/i.test(loc.name) &&
loc.optionsWithIndex.some(o => /mine/i.test(o.text))
).sort((a, b) => a.distance - b.distance);
if (rocks.length === 0) {
console.log('No rocks nearby');
return false;
}
// Try prospecting and mining rocks
for (const rock of rocks.slice(0, 5)) {
// Prospect to check ore type
const oreType = await prospectRock(ctx, rock);
// Skip non-copper/tin or empty rocks
if (!oreType || oreType === 'empty') continue;
if (oreType !== 'copper' && oreType !== 'tin') {
console.log(`Skip ${oreType} rock`);
continue;
}
// Mine the rock immediately after prospecting
const mineOpt = rock.optionsWithIndex.find(o => /mine/i.test(o.text));
if (!mineOpt) continue;
const xpBefore = ctx.sdk.getState()?.skills.find(s => s.name === 'Mining')?.experience ?? 0;
const invCountBefore = ctx.sdk.getState()?.inventory.length ?? 0;
console.log(`Mining ${oreType} at (${rock.x}, ${rock.z})...`);
await ctx.sdk.sendInteractLoc(rock.x, rock.z, rock.id, mineOpt.opIndex);
// Wait for mining to complete
try {
await ctx.sdk.waitForCondition(state => {
// Dismiss any dialogs
if (state.dialog.isOpen) {
ctx.sdk.sendClickDialog(0).catch(() => {});
}
// Check for XP gain or inventory change
const miningXp = state.skills.find(s => s.name === 'Mining')?.experience ?? 0;
return miningXp > xpBefore || state.inventory.length > invCountBefore;
}, 8000);
const currentOre = countOre(ctx);
console.log(`Mined! (${currentOre.copper} copper, ${currentOre.tin} tin)`);
return true;
} catch {
console.log('Mining timeout');
}
}
return false;
}
// Mine ore until we have enough copper and tin
async function mineOre(ctx: ScriptContext, targetPairs: number): Promise<void> {
console.log(`Mining ${targetPairs} pairs of ore...`);
let consecutiveFails = 0;
while (consecutiveFails < 5) {
const ore = countOre(ctx);
const pairs = Math.min(ore.copper, ore.tin);
if (pairs >= targetPairs) {
console.log(`Have ${pairs} ore pairs, done mining`);
break;
}
// Check inventory space
const invCount = ctx.sdk.getState()?.inventory.length ?? 0;
if (invCount >= 26) { // Leave some space
console.log('Inventory nearly full');
break;
}
// Log current ore counts
console.log(`Mining... (copper: ${ore.copper}, tin: ${ore.tin})`);
// Just mine any copper or tin we find - don't try to balance
// The preference logic was causing issues where rocks respawn as different ore
const result = await mineRock(ctx);
if (!result) {
consecutiveFails++;
console.log(`Mining failed (${consecutiveFails}/5)`);
// Walk around to find rocks
const currentPos = ctx.sdk.getState()?.player;
if (currentPos) {
const offsetX = (Math.random() - 0.5) * 15;
const offsetZ = (Math.random() - 0.5) * 15;
await ctx.bot.walkTo(
Math.round(SE_VARROCK_MINE_COPPERTIN.x + offsetX),
Math.round(SE_VARROCK_MINE_COPPERTIN.z + offsetZ)
);
}
await new Promise(r => setTimeout(r, 1000));
} else {
consecutiveFails = 0;
}
}
}
// Pay toll and enter Al Kharid
async function enterAlKharid(ctx: ScriptContext): Promise<boolean> {
if (isInsideAlKharid(ctx)) {
console.log('Already in Al Kharid');
return true;
}
console.log('Walking to toll gate...');
await ctx.bot.walkTo(AL_KHARID_GATE.x, AL_KHARID_GATE.z);
// Find and interact with gate
const state = ctx.sdk.getState();
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name) && l.distance < 10);
if (!gate) {
console.log('Gate not found!');
return false;
}
const openOpt = gate.optionsWithIndex.find(o => /pay|open/i.test(o.text));
if (!openOpt) {
console.log('No open option on gate');
return false;
}
console.log('Interacting with gate...');
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 800));
// Handle dialog - pay toll
for (let i = 0; i < 20; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) {
await new Promise(r => setTimeout(r, 150));
continue;
}
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
break;
}
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
await new Promise(r => setTimeout(r, 500));
// Dismiss remaining dialogs
for (let i = 0; i < 5; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) break;
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
await new Promise(r => setTimeout(r, 500));
// Walk through gate
for (let attempt = 0; attempt < 3; attempt++) {
await ctx.bot.walkTo(AL_KHARID_INSIDE.x, AL_KHARID_INSIDE.z);
await new Promise(r => setTimeout(r, 500));
if (isInsideAlKharid(ctx)) {
console.log('Entered Al Kharid!');
return true;
}
}
return isInsideAlKharid(ctx);
}
// Smelt bronze bars at furnace
async function smeltBars(ctx: ScriptContext): Promise<number> {
console.log('Walking to furnace...');
await ctx.bot.walkTo(AL_KHARID_FURNACE.x, AL_KHARID_FURNACE.z);
let barsSmelted = 0;
while (true) {
const ore = countOre(ctx);
const pairs = Math.min(ore.copper, ore.tin);
if (pairs === 0) {
console.log('No more ore pairs');
break;
}
// Dismiss any dialogs
const state = ctx.sdk.getState();
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Find furnace
const furnace = state?.nearbyLocs.find(l => /furnace/i.test(l.name));
if (!furnace) {
console.log('Furnace not found');
break;
}
// Find copper ore in inventory
const copper = ctx.sdk.findInventoryItem(/copper ore/i);
if (!copper) {
console.log('No copper ore');
break;
}
const smithingXpBefore = getSmithingStats(ctx).xp;
// Use copper ore on furnace
console.log('Using ore on furnace...');
await ctx.sdk.sendUseItemOnLoc(copper.slot, furnace.x, furnace.z, furnace.id);
await new Promise(r => setTimeout(r, 1000));
// Handle the smelting interface/dialog
// The smelting interface should appear - click to make bronze bar
for (let i = 0; i < 10; i++) {
const s = ctx.sdk.getState();
// Check if dialog opened (smelting menu)
if (s?.dialog.isOpen) {
// Look for bronze bar option
const bronzeOpt = s.dialog.options.find(o => /bronze/i.test(o.text));
if (bronzeOpt) {
console.log('Selecting bronze bar...');
await ctx.sdk.sendClickDialog(bronzeOpt.index);
break;
}
// Look for "Make" or "Ok" button
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
if (makeOpt) {
await ctx.sdk.sendClickDialog(makeOpt.index);
break;
}
await ctx.sdk.sendClickDialog(0);
}
await new Promise(r => setTimeout(r, 300));
}
// Wait for smelting to complete (XP gain)
try {
await ctx.sdk.waitForCondition(state => {
if (state.dialog.isOpen) {
ctx.sdk.sendClickDialog(0).catch(() => {});
}
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
return smithingXp > smithingXpBefore;
}, 10000);
barsSmelted++;
console.log(`Smelted bar #${barsSmelted}`);
} catch {
console.log('Smelting timeout');
await new Promise(r => setTimeout(r, 500));
}
}
return barsSmelted;
}
// Find and use anvil to smith items
async function smithItems(ctx: ScriptContext): Promise<number> {
// Check for anvil near furnace first
let anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
if (!anvil) {
// Try walking around Al Kharid to find anvil
console.log('Looking for anvil in Al Kharid...');
// Known Al Kharid anvil location (near general store)
const alKharidAnvil = { x: 3290, z: 3179 };
await ctx.bot.walkTo(alKharidAnvil.x, alKharidAnvil.z);
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
}
if (!anvil) {
console.log('No anvil found, trying Varrock...');
// Walk to Varrock west anvil
await ctx.bot.walkTo(3188, 3427);
anvil = ctx.sdk.getState()?.nearbyLocs.find(l => /anvil/i.test(l.name));
}
if (!anvil) {
console.log('Cannot find anvil!');
return 0;
}
console.log(`Found anvil at (${anvil.x}, ${anvil.z})`);
// Check for hammer
const hammer = ctx.sdk.findInventoryItem(/hammer/i);
if (!hammer) {
console.log('No hammer in inventory!');
return 0;
}
let itemsSmithed = 0;
while (true) {
const ore = countOre(ctx);
if (ore.bronzeBars === 0) {
console.log('No bronze bars left');
break;
}
// Dismiss any dialogs
const state = ctx.sdk.getState();
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Find bronze bar
const bar = ctx.sdk.findInventoryItem(/bronze bar/i);
if (!bar) {
console.log('No bronze bar');
break;
}
const smithingXpBefore = getSmithingStats(ctx).xp;
// Use bar on anvil
console.log('Using bar on anvil...');
await ctx.sdk.sendUseItemOnLoc(bar.slot, anvil.x, anvil.z, anvil.id);
await new Promise(r => setTimeout(r, 1000));
// Handle smithing interface
// Smithing interface (id 994) uses iop not buttonType
// We need to click a specific component to make items
for (let i = 0; i < 15; i++) {
const s = ctx.sdk.getState();
// Check dialog
if (s?.dialog.isOpen) {
// Look for bronze dagger (1 bar, level 1)
const daggerOpt = s.dialog.options.find(o => /dagger/i.test(o.text));
if (daggerOpt) {
console.log('Selecting dagger...');
await ctx.sdk.sendClickDialog(daggerOpt.index);
break;
}
// Try clicking first "Make" or "Ok" option
const makeOpt = s.dialog.options.find(o => /make|ok/i.test(o.text));
if (makeOpt) {
await ctx.sdk.sendClickDialog(makeOpt.index);
break;
}
// Try first option
if (s.dialog.options.length > 0) {
await ctx.sdk.sendClickDialog(s.dialog.options[0]?.index ?? 0);
}
}
// Check interface state
if (s?.interface?.isOpen) {
console.log(`Interface open: ${s.interface.interfaceId}, options: ${s.interface.options.map(o => o.text).join(', ')}`);
// Try clicking first option
if (s.interface.options.length > 0) {
await ctx.sdk.sendClickInterfaceOption(0);
break;
}
}
await new Promise(r => setTimeout(r, 300));
}
// Wait for smithing (XP gain)
try {
await ctx.sdk.waitForCondition(state => {
if (state.dialog.isOpen) {
ctx.sdk.sendClickDialog(0).catch(() => {});
}
const smithingXp = state.skills.find(s => s.name === 'Smithing')?.experience ?? 0;
return smithingXp > smithingXpBefore;
}, 10000);
itemsSmithed++;
console.log(`Smithed item #${itemsSmithed}`);
} catch {
console.log('Smithing timeout');
await new Promise(r => setTimeout(r, 500));
}
}
return itemsSmithed;
}
// Drop items to make space
async function dropItems(ctx: ScriptContext, pattern: RegExp): Promise<void> {
const items = ctx.sdk.getState()?.inventory.filter(i => pattern.test(i.name)) ?? [];
for (const item of items) {
await ctx.sdk.sendDropItem(item.slot);
await new Promise(r => setTimeout(r, 100));
}
}
// Equip pickaxe if not already equipped
async function equipPickaxe(ctx: ScriptContext): Promise<boolean> {
const equipped = ctx.sdk.findEquipmentItem(/pickaxe/i);
if (equipped) {
console.log('Pickaxe already equipped');
return true;
}
const pickaxe = ctx.sdk.findInventoryItem(/pickaxe/i);
if (!pickaxe) {
console.log('No pickaxe in inventory!');
return false;
}
console.log(`Equipping ${pickaxe.name}...`);
const result = await ctx.bot.equipItem(pickaxe);
return result.success;
}
// Main script
async function main() {
const username = `sm${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
console.log('=== Smithing Training Script v2 ===');
const startStats = getSmithingStats(ctx);
console.log(`Starting Smithing: Level ${startStats.level} (${startStats.xp} XP)`);
// Equip pickaxe first
await equipPickaxe(ctx);
// Check for hammer (needed for smithing at anvil)
let hammer = ctx.sdk.findInventoryItem(/hammer/i);
if (!hammer) {
console.log('No hammer - will only be able to smelt bars');
// Smelting still gives XP, so we can train without a hammer
}
// Main loop
let iterations = 0;
while (iterations < 20) { // Safety limit
iterations++;
const stats = getSmithingStats(ctx);
if (stats.level >= 10) {
console.log(`Goal achieved! Smithing level ${stats.level}`);
break;
}
console.log(`\n=== Iteration ${iterations}: Smithing Level ${stats.level} (${stats.xp} XP) ===`);
// Step 1: Get coins if we don't have enough (for toll)
if (getCoins(ctx) < 10 && !isInsideAlKharid(ctx)) {
await getCoinsFromShop(ctx);
}
// Step 2: Mine ore if we don't have enough
const ore = countOre(ctx);
console.log(`Inventory: ${ore.copper} copper, ${ore.tin} tin, ${ore.bronzeBars} bars`);
const pairs = Math.min(ore.copper, ore.tin);
if (pairs < 5) {
await walkToMine(ctx);
await mineOre(ctx, 8); // Mine 8 pairs
}
// Step 3: Travel to Al-Kharid for smelting
if (!isInsideAlKharid(ctx)) {
const entered = await enterAlKharid(ctx);
if (!entered) {
console.log('Failed to enter Al Kharid, retrying...');
continue;
}
}
// Step 4: Smelt bars
const oreAfterMining = countOre(ctx);
const pairsToSmelt = Math.min(oreAfterMining.copper, oreAfterMining.tin);
if (pairsToSmelt > 0) {
console.log(`Smelting ${pairsToSmelt} bronze bars...`);
const smelted = await smeltBars(ctx);
console.log(`Smelted ${smelted} bars`);
}
// Step 5: Smith items (if we have a hammer)
hammer = ctx.sdk.findInventoryItem(/hammer/i);
const barsCount = countOre(ctx);
if (hammer && barsCount.bronzeBars > 0) {
console.log(`Smithing with ${barsCount.bronzeBars} bars...`);
const smithed = await smithItems(ctx);
console.log(`Smithed ${smithed} items`);
}
// Drop smithed items to make inventory space
await dropItems(ctx, /bronze (dagger|sword|axe|mace|med helm|bolts)/i);
}
const endStats = getSmithingStats(ctx);
console.log(`\n=== Complete: Level ${endStats.level} (${endStats.xp} XP) ===`);
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 15 * 60 * 1000, // 15 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-508
View File
@@ -1,508 +0,0 @@
# Lab Log: thieving-gp
Goal: Maximize coins earned through thieving in 5 minutes.
## Strategy Considerations
### Thieving Targets (by level requirement)
| Target | Level | Coins/Success | XP | Notes |
|--------|-------|---------------|-----|-------|
| Man/Woman | 1 | 3 coins | 8 | Easy, ~50% success at lvl 1 |
| Farmer | 10 | 9 coins | 14.5 | Need to train first |
| Warrior | 25 | 18 coins | 26 | Higher level, more damage when stunned |
### Stalls (alternative)
| Stall | Level | Loot | XP | Sell Value | Notes |
|-------|-------|------|-----|------------|-------|
| Tea | 5 | Cup of tea | 16 | 10gp? | Need shop nearby |
| Cake | 5 | Cake | 16 | - | Food source! |
| Silk | 20 | Silk | 24 | 60gp | High value but needs level |
### Damage Mitigation Strategies
1. **Use tutorial food** - Bread heals 5 HP, Shrimps heal 3 HP
2. **Fish for food first** - Net fishing at Al-Kharid is safe
3. **Train HP via combat first** - More HP = more attempts before death
4. **Steal cakes** - Level 5 req, but provides free food
### Expected Performance (v1)
- Starting HP: 10
- Damage per failed pickpocket: 1
- Success rate at level 1: ~50%
- Average attempts before needing food: ~10 (5 failures = 5 damage)
- Tutorial bread: 1 piece = 5 HP = 5 more attempts
---
## Run 001 - Initial Implementation
**Date**: 2026-01-24
**Strategy**: Pickpocket men at Lumbridge, use tutorial bread for healing
**Hypothesis**: Can earn ~100-200 coins in 5 minutes by pickpocketing men.
### Observations
1. **XP gain is massive**: Went from level 1 to level 38 Thieving in 5 minutes (32,000 XP)
2. **GP gain is modest**: Only 120 coins total (~3 coins per successful pickpocket)
3. **Success rate improved with level**: Started at ~50%, ended near 70%+
4. **Stun time is the major bottleneck**: ~4.8 seconds per failed attempt
5. **Dialog dismissal overhead**: Level-up dialogs appeared frequently and added delay
6. **Food usage was minimal**: Only ate once (Shrimps) - tutorial items are sufficient
7. **Men give 3 coins, not 8**: The 8 was XP per success, not coins
### Results
| Metric | Value |
|--------|-------|
| Final GP | 120 |
| Thieving Level | 38 (from 1) |
| Thieving XP | 32,000 |
| Attempts | ~60 |
| Success Rate | ~66% average |
| Food Eaten | 1 |
| Stun Time | ~48+ seconds |
### Outcome
**PARTIAL SUCCESS** - Hypothesis confirmed (100-200 GP range), but lower end. The XP gain is exceptional but GP is bottlenecked by:
1. Low coins per pickpocket (3 GP)
2. Stun time wasting ~15-20% of total time
3. Dialog overhead
### Analysis
The real issue is that Men only give 3 coins. At 60 successes in 5 minutes:
- Men: 60 * 3 = 180 GP theoretical (actual: 120 GP due to missed time)
- Farmers (level 10): 60 * 9 = 540 GP
- Warriors (level 25): 60 * 18 = 1080 GP
**Key insight**: We hit level 10 in ~2 minutes. If we switched to Farmers then, we'd earn 3x more per pickpocket for the remaining 3 minutes!
### Next Steps
1. **v2**: Implement dynamic target switching based on thieving level
2. Reduce dialog dismissal overhead (batch or async)
3. Consider walking to Farmers when level 10 is reached
4. Profile time spent in each activity
---
## Run 002 - Dynamic Target Switching (v2)
**Date**: 2026-01-24
**Strategy**: Start with Men, switch to Farmers at level 10, Warriors at level 25
**Hypothesis**: Can earn 300-500 coins by switching to higher-value targets as level increases.
### Observations
1. **Target selection works correctly** - Code checks for Farmers/Warriors first, falls back to Men
2. **Problem: No Farmers in Lumbridge castle area** - Only Men, Rats, Hans, Cook nearby
3. **Never left Lumbridge** - Always found Men nearby, so never triggered the "walk to find targets" code
4. **Same performance as v1** - ~129 GP, level 39, because we only pickpocketed Men
### Results
| Metric | v1 | v2 |
|--------|----|----|
| Final GP | 120 | 129 |
| Thieving Level | 38 | 39 |
| Thieving XP | 32,000 | 34,400 |
### Root Cause Analysis
The `findPickpocketTarget()` function correctly prioritizes higher-value targets, BUT:
- It only checks NPCs that are **already nearby**
- Since Men are always nearby in Lumbridge, it never triggers the "walk to find targets" branch
- The code to walk to Farmer locations at level 10+ is never executed because Men are found first
### Fix Required
Need **proactive location changes** - not just "if no targets, walk" but "if level >= 10 AND not at Farmer area, walk there FIRST before looking for targets".
---
## Run 003 - Proactive Target Location (v3)
**Date**: 2026-01-24
**Strategy**:
- Level 1-9: Stay at Lumbridge, pickpocket Men
- Level 10+: Walk to Fred's Farm and pickpocket Farmer
- Level 25+: Walk to Al-Kharid and pickpocket Warriors
**Hypothesis**: By proactively moving to higher-value target areas, can earn 400-600 coins.
### Observations
1. **Zone switching triggered correctly** at level 10
2. **FAILED: Pathing issues** - Bot couldn't navigate to farm area
3. **Gates/doors blocked path** - No door opening logic during walkTo
4. **"Fred the Farmer" name mismatch** - Initial regex was `/^farmer$/` which didn't match
5. **Wasted entire run** wandering around, only 15 GP earned
### Results
| Metric | v1 | v2 | v3 |
|--------|----|----|----|
| Final GP | 120 | 129 | 15 |
| Thieving Level | 38 | 39 | 16 |
| Outcome | Timeout | Timeout | Timeout (stuck) |
### Root Cause
1. Navigation to remote zones requires opening gates/doors
2. `walkTo()` doesn't handle obstacles automatically
3. Even with pathfinding, closed gates block movement
### Conclusion
**Zone switching is not viable without door/gate handling.** Better to optimize the Men pickpocketing loop which works reliably.
---
## Run 004 - Optimized Men Loop (v4)
**Date**: 2026-01-24
**Strategy**: Stay at Lumbridge, but optimize the pickpocketing loop:
- Remove zone switching complexity
- Faster dialog dismissal (don't wait between dismissals)
- Track actual GP/hour rate
- Consider reducing HP eating threshold
**Hypothesis**: Optimized Men loop can reach 150-180 GP in 5 minutes.
### Implementation Notes
- Removed all zone switching code
- Simplified to just Men/Women targeting
### Results
| Metric | v4 |
|--------|-----|
| Final GP | 96 |
| Thieving Level | ~31 |
| Thieving XP | 25,600 |
| Success Rate | 53% |
| Food Eaten | 2 |
### Observations
1. **Ran out of food** - Only had 2 food items (Shrimps + Bread from tutorial)
2. **"HP low but no food!"** warnings at end of run
3. **Lower success rate** than v1/v2 (53% vs 65%) - could be variance
4. **Script stalled** when HP was low and no food available
### Root Cause
The bottleneck is **not enough food**. Tutorial items (1 Bread + 1 Shrimps) only last ~30 attempts worth of failures.
### Fix Required
Need to **source more food**:
1. Buy bread from Lumbridge general store
2. Or steal cakes from cake stall (level 5) - provides food AND thieving XP
---
## Run 005 - Shop Buying Attempt (v5)
**Date**: 2026-01-24
**Strategy**: Buy bread from Lumbridge general store when food runs low
### Results
- Final GP: 108
- Shop opened successfully
- **Failed**: "Item not found in shop: /bread/i"
- Lumbridge General Store sells tools, NOT food!
### Root Cause
Wrong shop - general stores don't sell food. Would need:
- Al-Kharid kebab shop (but gates block path)
- Fish and cook (time consuming)
- Just accept limited food
---
## Run 006 - Conservative Eating (v6)
**Date**: 2026-01-24
**Strategy**:
- Remove shop logic (doesn't work)
- Lower eat threshold to HP <= 3 (more conservative)
- Accept that food is limited
### Results
| Metric | v6 | Best Previous |
|--------|----|---------------|
| **Final GP** | **180** | 129 (v2) |
| Thieving Level | 42 | 39 |
| Thieving XP | 48,000 | 34,400 |
| Success Rate | 83% | 65% |
| Attempts | 70 | ~60 |
| Food Eaten | 0 | 2 |
### Observations
1. **50% improvement over v1/v2!** (180 GP vs ~120 GP)
2. **Zero food needed** - lucky RNG, fewer failures caused damage
3. **Higher success rate** - 83% vs 65% in earlier runs
4. **More attempts** - 70 vs 60 = more efficient loop
5. **Level 42 thieving** achieved
### Analysis
The improvement came from:
1. **Simpler code** - fewer conditionals, faster loop
2. **Good RNG** - fewer failed pickpockets
3. **Higher thieving level = higher success rate** - compounds over time
---
## Learnings (Final)
### 1. Strategic Findings
- **Simple is best**: v6 (simplest code) achieved 180 GP, 50% better than complex v2/v3
- **Men pickpocketing is reliable**: Consistent 3 GP per pickpocket
- **Zone switching not viable**: walkTo doesn't handle doors/gates
- **Shop buying failed**: Lumbridge general store doesn't sell food
- **70 pickpockets in 5 min**: ~14 per minute when optimized
- **Level 42 thieving achievable**: 48,000 XP in 5 minutes
- **Success rate improves with level**: 50% at level 1 → 85%+ at level 40
### 2. Process & Tooling Reflections
- **Lab log methodology works**: Systematic hypothesis → run → observe → iterate
- **Verify assumptions first**: "Fred the Farmer" name, shop inventory, gate navigation
- **events.jsonl is essential**: Shows exactly what happened
- **Simpler code often wins**: Complex zone switching failed, simple loop succeeded
### 3. SDK Issues & Gaps
- **walkTo doesn't handle doors/gates**: Major limitation for navigation
- **Shop inventory unknown upfront**: Had to discover Lumbridge store doesn't sell food
- **Consider adding walkToWithDoors()**: High-level API that opens obstacles
### Summary Table
| Version | Strategy | GP | Level | Notes |
|---------|----------|-----|-------|-------|
| v1 | Basic Men loop | 120 | 38 | Baseline |
| v2 | Dynamic targeting | 129 | 39 | No Farmers nearby |
| v3 | Zone switching | 15 | 16 | Failed - gates |
| v4 | Optimized loop | 96 | 31 | Ran out of food |
| v5 | Shop buying | 108 | - | Shop has no food |
| v6 | Conservative eat | 180 | 42 | Good RNG, Men only |
| v7 | Zone + doors | 339 | 44 | First big win - Farmers |
| v8 | Al-Kharid kebabs | 206 | 43 | Regression - Men, not Warriors |
| v9 | Warrior targeting | 36 | 26 | Failed - Warriors not found |
| v10 | Farmers only | 195 | 38 | Regression - zone re-walking |
| **v11** | **Optimized zones** | **393** | **45** | **NEW BEST - 327% of baseline!** |
---
## Run 007 - Zone Switching with Door Handling (v7)
**Date**: 2026-01-24
**Strategy**:
- Level 1-9: Pickpocket Men at Lumbridge (3 GP each)
- Level 10+: Walk to Farmers, opening doors/gates along the way (9 GP each)
- Implemented `walkToWithDoors()` helper that detects and opens gates
### Key Fixes
1. **Door handling**: Check for nearby doors with "Open" option after failed walks
2. **Correct Farmer location**: (3167, 3283) near crop patches, NOT Fred's Farm
3. **Fred the Farmer ≠ Farmer**: Fred is a quest NPC, not thievable!
### Results
| Metric | v7 | v6 | Improvement |
|--------|----|----|-------------|
| **Final GP** | **339** | 180 | +88% |
| Thieving Level | 44 | 42 | +2 |
| Thieving XP | 56,700 | 48,000 | +18% |
| Success Rate | 60% | 83% | Lower on Farmers |
| Food Eaten | 2 | 0 | More damage |
### Observations
1. **Zone switching succeeded!** Bot navigated through gates to reach Farmers
2. **9 GP per Farmer** vs 3 GP per Man = 3x coins per pickpocket
3. **Lower success rate on Farmers** (60% vs 83% on Men) but offset by higher GP
4. **~38 Farmer pickpockets** × 9 GP = 342 GP (matches actual result)
### Key Insight
The user's suggestion to "just look for doors while walking and open them" was the breakthrough. Simple solution, huge improvement.
---
## Run 008 - Al-Kharid Kebabs for Unlimited Food (v8)
**Date**: 2026-01-25
**Strategy**:
- Level 1-9: Pickpocket Men at Lumbridge (build up coins + levels)
- Level 10+: Sell bronze sword (10gp) → Pay Al-Kharid toll → Buy kebabs
- With kebabs: Can sustain pickpocketing indefinitely without HP issues
**Hypothesis**: With unlimited kebab food, can push for 400+ GP by never running out of food.
### Implementation Notes
From script_best_practices.md:
1. **Sell bronze sword** at Lumbridge general store for 10gp
2. **Toll gate dialog handling** - must click through dialog and select "Yes" option
3. **Al-Kharid detection**: `x >= 3270` means inside Al-Kharid
4. **Kebab shop location**: Need to find in Al-Kharid
### Results
| Metric | v8 | v7 | Change |
|--------|----|----|--------|
| Final GP | **206** | 339 | **-39%** |
| Thieving Level | 43 | 44 | -1 |
| Thieving XP | 53,600 | 56,700 | -5% |
| Food Eaten | 1 | 2 | -1 |
| Attempts | ~82 | ~60 | +37% |
| Success Rate | 81% | 60% | +35% |
### Observations
1. **REGRESSION**: 206 GP vs 339 GP (v7) - Al-Kharid setup didn't pay off
2. **Kebab buying partially failed**: Only bought 1 kebab despite trying for 5 (dialog flow issues)
3. **Still pickpocketing Men (3 GP)**: Even in Al-Kharid, we're getting Men not Warriors
4. **Zone wandering**: "No targets in zone - moving around..." cost significant time
5. **Setup overhead**: ~40 seconds lost on selling shortbow, paying toll, walking
6. **Ate Shrimps, not Kebab**: Food system worked but only needed 1 eat
### Root Cause Analysis
**Why v8 was worse than v7:**
| Factor | v7 (Farmers) | v8 (Al-Kharid) |
|--------|--------------|----------------|
| Target | Farmers (9 GP) | Men (3 GP) |
| GP per pickpocket | 9 | 3 |
| Setup time | ~30s walk | ~60s (sell + toll + walk) |
| Location stability | Good | Wandering issues |
The fundamental flaw: **We entered Al-Kharid but still pickpocketed Men (3 GP) instead of Warriors (18 GP)**. The regex pattern includes Warriors but:
- Warriors need level 25+ (we had it)
- But `findPickpocketTarget()` finds the nearest match - Men are more common than Warriors
- We should have **prioritized by value**, not distance
### Fix Ideas for v9
1. **Prioritize high-value targets**: Sort by GP/pickpocket, not distance
2. **Skip Al-Kharid unless targeting Warriors**: If Men-only, Farmers are 3x better
3. **Walk TO Warriors specifically**: Warriors spawn near Al-Kharid palace guards
4. **Or revert to v7 strategy**: Farmers gave 339 GP, Al-Kharid Men gave 206 GP
---
## Run 009 - Warrior Targeting (v9)
**Date**: 2026-01-25
**Strategy**:
- Level 1-9: Men at Lumbridge (3 GP)
- Level 10-24: Farmers (9 GP)
- Level 25+: Enter Al-Kharid for Warriors (18 GP)
### Results
| Metric | v9 |
|--------|-----|
| Final GP | **36** |
| Thieving Level | 26 |
| Notes | FAILURE - Warriors not found |
### Root Cause
Warriors at (3301, 3175) don't exist or don't have Pickpocket option. The script spent the entire remaining time in "No targets in zone - moving around..." loop.
---
## Run 010 - Farmers Only (v10)
**Date**: 2026-01-25
**Strategy**: Remove Al-Kharid, just use Men→Farmers (same as v7)
### Results
| Metric | v10 |
|--------|-----|
| Final GP | **195** |
| Thieving Level | ~38 |
| Success Rate | 46% |
| Notes | REGRESSION - constant zone re-walking |
### Root Cause
Zone radius (20 tiles) too small - player keeps leaving zone during pickpocketing, triggering unnecessary walks. Many "Level X: Moving to Farmers" messages.
---
## Run 011 - Optimized Zone Handling (v11) ⭐ NEW BEST
**Date**: 2026-01-25
**Strategy**:
- Same as v10 (Men→Farmers)
- Larger zone radius (40 tiles instead of 20)
- Walk TO zone center when no targets (not random)
### Results
| Metric | v11 | v7 (baseline) | Improvement |
|--------|-----|---------------|-------------|
| **Final GP** | **393** | 339 | **+16%** |
| Thieving Level | 45 | 44 | +1 |
| Thieving XP | 65,400 | 56,700 | +15% |
| Success Rate | 61% | 60% | +1% |
| Attempts | ~73 | ~60 | +22% |
| Food Eaten | 2 | 2 | Same |
### Key Changes That Worked
1. **Larger zone radius (40 vs 20)**: Eliminated constant zone re-walking
2. **Directed walking**: Walk TO Farmer spawn point instead of random movement
3. **No Al-Kharid overhead**: Stayed with proven Farmer strategy
### Analysis
v11 is the new best with **393 GP**, beating v7's 339 GP by 16%! The key insight was that zone radius and walking behavior were causing inefficiency in v10 - players would leave the small zone during pickpocketing and waste time walking back.
---
## Learnings (Updated after v11)
### 1. Strategic Findings
**What Works:**
- **Farmers at 9 GP each** are the optimal target (3x Men, reliable)
- **Large zone radius (40 tiles)** prevents wasteful re-walking
- **Directed walking** (to spawn point, not random) finds targets faster
- **Simple strategies beat complex ones**: v11 (393 GP) vs v8 (206 GP)
**What Doesn't Work:**
- **Al-Kharid Warriors** - Either don't exist at expected location or can't be pickpocketed
- **Small zone radius (20 tiles)** - Players drift during pickpocketing, causing re-walks
- **Random wandering** - Walk TO known spawn points instead
- **Complex setup** (toll, kebabs) wastes time if target isn't better
**Key Numbers:**
| Target | GP/Success | Level Req | Reliability |
|--------|------------|-----------|-------------|
| Men | 3 GP | 1 | High |
| Farmers | 9 GP | 10 | High |
| Warriors | 18 GP | 25 | UNRELIABLE |
### 2. Process & Tooling Reflections
- **Iterative debugging works**: v8→v9→v10→v11 each identified a specific issue
- **Version tracking essential**: Easy to compare and identify regressions
- **"No targets in zone" log message** was key to diagnosing wandering issues
- **Success rate + GP rate** together tell the full story
### 3. SDK Issues & Gaps
- **Browser stability**: Multiple "Bot not connected" crashes during runs
- **NPC locations unclear**: Warriors expected at (3301, 3175) didn't exist
- **Kebab seller dialog** requires manual handling (not shop API)
- **Zone detection** needs larger radius for roaming NPCs
### 4. Evolution Summary
```
v1 (120 GP) → Basic loop
v7 (339 GP) → Farmers + doors = 183% improvement
v11 (393 GP) → Optimized zones = 327% improvement over v1
```
The biggest wins came from:
1. **Switching to Farmers** (3x GP per pickpocket)
2. **Door handling** (enables zone transitions)
3. **Zone optimization** (prevents wasteful re-walking)
-639
View File
@@ -1,639 +0,0 @@
/**
* Thieving GP Maximizer Script
*
* Goal: Maximize coins earned through thieving in 5 minutes.
*
* Strategy v11:
* - Level 1-9: Pickpocket Men at Lumbridge (3 GP each)
* - Level 10+: Walk to Farmers (9 GP each) - proven strategy from v7
* - Larger zone radius (40 tiles) to avoid constant zone re-entry
* - Walk TO Farmer spawn when no target found (not random)
*
* v7: 339 GP (Farmers with door handling) - BASELINE
* v8: 206 GP (REGRESSION - Men in Al-Kharid)
* v9: 36 GP (FAILURE - Warriors not found)
* v10: 195 GP (REGRESSION - constant zone re-walking)
* v11 Target: 350+ GP with Farmers only, optimized zone handling
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
import type { NearbyNpc } from '../../sdk/types';
// Thieving stats tracking
interface ThievingStats {
pickpocketAttempts: number;
successfulPickpockets: number;
failedPickpockets: number;
coinsEarned: number;
startCoins: number;
startThievingXp: number;
foodEaten: number;
stunTimeMs: number;
}
// Stun duration in ticks (~4.5 seconds = 7-8 game ticks)
const STUN_TICKS = 8;
const STUN_MS = 4800;
// v10: Simple zone switching - Men then Farmers (proven reliable)
const ZONES = {
lumbridge: { x: 3222, z: 3218, targetPattern: /^(man|woman)$/i, levelReq: 1, coins: 3 },
// Farmer area near crop patches (proven in v7: 339 GP)
farmers: { x: 3167, z: 3283, targetPattern: /^farmer$/i, levelReq: 10, coins: 9 },
};
// Key locations
const LOCATIONS = {
lumbridgeGeneralStore: { x: 3211, z: 3247 },
alKharidTollGate: { x: 3268, z: 3228 },
alKharidInside: { x: 3277, z: 3227 },
kebabSeller: { x: 3273, z: 3180 }, // Karim the kebab seller (dialog, not shop)
};
// State tracking for one-time setup
interface SetupState {
soldShortbow: boolean;
paidToll: boolean;
boughtKebabs: boolean;
tollAttempted: boolean; // Track if we've tried to enter Al-Kharid
}
/**
* Walk to destination, opening any doors/gates encountered along the way
*/
async function walkToWithDoors(ctx: ScriptContext, destX: number, destZ: number, maxAttempts: number = 5): Promise<boolean> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const state = ctx.sdk.getState();
if (!state?.player) return false;
// Check if we're close enough
const dx = Math.abs(state.player.worldX - destX);
const dz = Math.abs(state.player.worldZ - destZ);
if (dx <= 5 && dz <= 5) {
return true; // Arrived!
}
// Try walking
const walkResult = await ctx.bot.walkTo(destX, destZ);
// Check if we made progress
const stateAfter = ctx.sdk.getState();
if (stateAfter?.player) {
const dxAfter = Math.abs(stateAfter.player.worldX - destX);
const dzAfter = Math.abs(stateAfter.player.worldZ - destZ);
if (dxAfter <= 5 && dzAfter <= 5) {
return true; // Arrived!
}
}
// If walk failed or we're stuck, look for a door/gate to open
const doors = ctx.sdk.getNearbyLocs()
.filter(loc => /door|gate/i.test(loc.name))
.filter(loc => loc.options.some(o => /^open$/i.test(o)))
.sort((a, b) => a.distance - b.distance);
if (doors.length > 0) {
const door = doors[0]!;
console.log(`Opening ${door.name} at (${door.x}, ${door.z})...`);
const openResult = await ctx.bot.openDoor(door);
if (openResult.success) {
console.log(`Opened ${door.name}`);
continue; // Try walking again
} else {
console.warn(`Failed to open ${door.name}: ${openResult.message}`);
}
}
// If no doors found and walk failed, we might be stuck
if (!walkResult.success) {
console.warn(`Walk failed: ${walkResult.message}`);
return false;
}
}
return false;
}
/**
* Check if we're in Al-Kharid (x >= 3270)
*/
function isInAlKharid(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
return (state?.player?.worldX ?? 0) >= 3270;
}
/**
* Sell shortbow at Lumbridge general store for 20gp (for toll + extra)
* From script_best_practices.md: shortbow sells for 20gp, better than bronze sword (10gp)
*/
async function sellShortbow(ctx: ScriptContext): Promise<boolean> {
const shortbow = ctx.sdk.findInventoryItem(/^shortbow$/i);
if (!shortbow) {
console.log('No shortbow to sell');
return false;
}
console.log('Selling shortbow for toll money (20gp)...');
// Walk to general store
await walkToWithDoors(ctx, LOCATIONS.lumbridgeGeneralStore.x, LOCATIONS.lumbridgeGeneralStore.z);
// Open shop
const shopResult = await ctx.bot.openShop(/shop keeper|shop assistant/i);
if (!shopResult.success) {
console.warn(`Failed to open shop: ${shopResult.message}`);
return false;
}
// Sell the shortbow
const sellResult = await ctx.bot.sellToShop(/shortbow/i);
if (!sellResult.success) {
console.warn(`Failed to sell shortbow: ${sellResult.message}`);
}
// Close shop
await ctx.bot.closeShop();
console.log('Sold shortbow for 20gp');
return true;
}
/**
* Pay toll and enter Al-Kharid (from script_best_practices.md)
*/
async function payTollAndEnterAlKharid(ctx: ScriptContext): Promise<boolean> {
const coins = countCoins(ctx);
if (coins < 10) {
console.warn(`Not enough coins for toll (have ${coins}, need 10)`);
return false;
}
console.log('Walking to Al-Kharid toll gate...');
await walkToWithDoors(ctx, LOCATIONS.alKharidTollGate.x, LOCATIONS.alKharidTollGate.z);
// Find and click the gate
const state = ctx.sdk.getState();
const gate = state?.nearbyLocs.find(l => /gate/i.test(l.name));
if (!gate) {
console.warn('Cannot find toll gate');
return false;
}
const openOpt = gate.optionsWithIndex.find(o => /open|pay/i.test(o.text));
if (!openOpt) {
console.warn('Cannot find open option on gate');
return false;
}
console.log('Opening toll gate...');
await ctx.sdk.sendInteractLoc(gate.x, gate.z, gate.id, openOpt.opIndex);
await new Promise(r => setTimeout(r, 800));
// Handle dialog - click through until "Yes" option appears
for (let i = 0; i < 20; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) {
await new Promise(r => setTimeout(r, 150));
continue;
}
const yesOpt = s.dialog.options.find(o => /yes/i.test(o.text));
if (yesOpt) {
console.log('Paying 10gp toll...');
await ctx.sdk.sendClickDialog(yesOpt.index);
break;
}
await ctx.sdk.sendClickDialog(0); // Click to continue
await new Promise(r => setTimeout(r, 200));
}
// Dismiss any remaining dialogs
for (let i = 0; i < 5; i++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) break;
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
// Wait and walk through aggressively
await new Promise(r => setTimeout(r, 600));
for (let i = 0; i < 5; i++) {
// Check if already in
if (isInAlKharid(ctx)) {
console.log('Entered Al-Kharid!');
return true;
}
console.log(`Walking through toll gate (attempt ${i + 1})...`);
await ctx.bot.walkTo(LOCATIONS.alKharidInside.x, LOCATIONS.alKharidInside.z);
await new Promise(r => setTimeout(r, 800));
}
return isInAlKharid(ctx);
}
/**
* Buy kebabs from Al-Kharid kebab seller (uses DIALOG, not shop!)
* From script_best_practices.md - Karim uses dialog system
*/
async function buyKebabs(ctx: ScriptContext, quantity: number = 5): Promise<boolean> {
console.log(`Buying ${quantity} kebabs via dialog...`);
// Walk to kebab seller (3273, 3180)
await walkToWithDoors(ctx, 3273, 3180);
const kebabsBefore = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
for (let i = 0; i < quantity; i++) {
// Check if we have enough coins (1gp per kebab)
const coins = countCoins(ctx);
if (coins < 1) {
console.warn('Not enough coins to buy kebab');
break;
}
// Find kebab seller (Karim)
const seller = ctx.sdk.findNearbyNpc(/kebab/i);
if (!seller) {
console.warn('Cannot find kebab seller');
return false;
}
// Talk to kebab seller
const talkOpt = seller.optionsWithIndex.find(o => /talk/i.test(o.text));
if (!talkOpt) {
console.warn('Cannot find talk option on kebab seller');
return false;
}
await ctx.sdk.sendInteractNpc(seller.index, talkOpt.opIndex);
await new Promise(r => setTimeout(r, 1000));
// Handle dialog - click through and select "Yes please."
let bought = false;
for (let j = 0; j < 15; j++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) {
await new Promise(r => setTimeout(r, 200));
continue;
}
const buyOpt = s.dialog.options.find(o => /yes/i.test(o.text));
if (buyOpt) {
await ctx.sdk.sendClickDialog(buyOpt.index); // Buy kebab (1gp)
bought = true;
} else {
await ctx.sdk.sendClickDialog(0); // Click to continue
}
await new Promise(r => setTimeout(r, 300));
// Check if dialog closed after buying
if (bought) {
const sAfter = ctx.sdk.getState();
if (!sAfter?.dialog.isOpen) break;
}
}
// Dismiss any remaining dialogs
for (let j = 0; j < 3; j++) {
const s = ctx.sdk.getState();
if (!s?.dialog.isOpen) break;
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 200));
}
await new Promise(r => setTimeout(r, 200));
}
const kebabsAfter = ctx.sdk.findInventoryItem(/^kebab$/i)?.count ?? 0;
const bought = kebabsAfter - kebabsBefore;
console.log(`Bought ${bought} kebabs (now have ${kebabsAfter})`);
return bought > 0;
}
/**
* Get the best zone for current thieving level
* v10: Simple - Men then Farmers (proven reliable from v7)
*/
function getBestZone(thievingLevel: number) {
// Level 10+: Farmers (9 GP) - proven in v7 with 339 GP
if (thievingLevel >= ZONES.farmers.levelReq) {
return ZONES.farmers;
}
// Level 1-9: Lumbridge Men (3 GP)
return ZONES.lumbridge;
}
/**
* Check if we're in a zone
* v11: Larger radius (40 tiles) to avoid constant zone re-entry
*/
function isInZone(ctx: ScriptContext, zone: typeof ZONES.lumbridge): boolean {
const state = ctx.sdk.getState();
if (!state?.player) return false;
const dx = Math.abs(state.player.worldX - zone.x);
const dz = Math.abs(state.player.worldZ - zone.z);
return dx <= 40 && dz <= 40; // Large radius - stay in zone once entered
}
/**
* Find nearest pickpocket target matching the pattern
*/
function findPickpocketTarget(ctx: ScriptContext, pattern: RegExp): NearbyNpc | null {
const state = ctx.sdk.getState();
if (!state) return null;
const targets = state.nearbyNpcs
.filter(npc => pattern.test(npc.name))
.filter(npc => npc.options.some(o => /pickpocket/i.test(o)))
.sort((a, b) => a.distance - b.distance);
return targets[0] ?? null;
}
/**
* Check if we should eat food based on HP
*/
function shouldEat(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state) return false;
const hp = state.skills.find(s => s.name === 'Hitpoints');
if (!hp) return false;
// v6: Be more conservative - only eat at HP <= 3 to preserve food
// Success rate improves with thieving level, so later attempts are safer
return hp.level <= 3;
}
/**
* Check if we have food available
*/
function hasFood(ctx: ScriptContext): boolean {
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
return food !== null;
}
/**
* Count coins in inventory
*/
function countCoins(ctx: ScriptContext): number {
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
return coins?.count ?? 0;
}
/**
* Attempt to pickpocket an NPC
* Returns result and coins gained
*/
async function attemptPickpocket(
ctx: ScriptContext,
target: NearbyNpc,
stats: ThievingStats
): Promise<'success' | 'failed' | 'stunned' | 'error'> {
const startTick = ctx.sdk.getState()?.tick ?? 0;
const coinsBefore = countCoins(ctx);
// Find the Pickpocket option
const pickpocketOpt = target.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
if (!pickpocketOpt) {
console.warn(`No pickpocket option on ${target.name}`);
return 'error';
}
// Send the pickpocket action
const result = await ctx.sdk.sendInteractNpc(target.index, pickpocketOpt.opIndex);
if (!result.success) {
console.warn(`Pickpocket action failed: ${result.message}`);
return 'error';
}
stats.pickpocketAttempts++;
// Wait for result - either coins increase or we get stunned
// Check for game messages about success/failure
try {
await ctx.sdk.waitForCondition(state => {
// Check for success message or coins increasing
for (const msg of state.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
// Success messages
if (text.includes('you pick') || text.includes("you steal")) {
return true;
}
// Failure/stun messages
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
return true;
}
}
}
// Also check if coins increased
const currentCoins = state.inventory.find(i => /^coins$/i.test(i.name))?.count ?? 0;
if (currentCoins > coinsBefore) {
return true;
}
return false;
}, 5000);
// Check what happened
const currentState = ctx.sdk.getState();
if (!currentState) return 'error';
const coinsAfter = countCoins(ctx);
if (coinsAfter > coinsBefore) {
// Success!
const gained = coinsAfter - coinsBefore;
stats.successfulPickpockets++;
stats.coinsEarned += gained;
console.log(`Pickpocketed ${target.name}! +${gained} coins (total: ${stats.coinsEarned})`);
return 'success';
}
// Check for failure message
for (const msg of currentState.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
if (text.includes("you fail") || text.includes("stunned") || text.includes("caught")) {
stats.failedPickpockets++;
console.log(`Failed pickpocket - stunned for ${STUN_MS}ms`);
// Wait out the stun duration
await new Promise(r => setTimeout(r, STUN_MS));
stats.stunTimeMs += STUN_MS;
return 'stunned';
}
}
}
// Unknown result
return 'failed';
} catch {
return 'error';
}
}
/**
* Main thieving loop
*/
async function thievingLoop(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) throw new Error('No initial state');
// Initialize stats
const stats: ThievingStats = {
pickpocketAttempts: 0,
successfulPickpockets: 0,
failedPickpockets: 0,
coinsEarned: 0,
startCoins: countCoins(ctx),
startThievingXp: state.skills.find(s => s.name === 'Thieving')?.experience ?? 0,
foodEaten: 0,
stunTimeMs: 0,
};
// v8: Setup state for Al-Kharid access
const setup: SetupState = {
soldShortbow: false,
paidToll: false,
boughtKebabs: false,
tollAttempted: false,
};
console.log('=== Thieving GP Maximizer v11 Started ===');
console.log(`Starting coins: ${stats.startCoins}`);
console.log(`Starting thieving XP: ${stats.startThievingXp}`);
// Main loop
while (true) {
const currentState = ctx.sdk.getState();
if (!currentState) {
console.warn('Lost game state');
break;
}
// Dismiss any dialogs first
if (currentState.dialog.isOpen) {
console.log('Dismissing dialog...');
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 600));
continue;
}
// Check HP and eat if needed (conservative - only eat at HP <= 3)
if (shouldEat(ctx)) {
const food = ctx.sdk.findInventoryItem(/^(bread|shrimps?|cooked meat|anchovies|trout|salmon|lobster|swordfish|cake|chocolate cake|kebab)$/i);
if (food) {
console.log(`HP low - eating ${food.name}`);
await ctx.bot.eatFood(food);
stats.foodEaten++;
continue;
}
// No food - if in Al-Kharid, try to buy more kebabs
if (setup.paidToll && isInAlKharid(ctx)) {
const coins = countCoins(ctx);
if (coins >= 5) {
console.log('Out of food in Al-Kharid - buying more kebabs...');
await buyKebabs(ctx, 5);
continue;
}
}
// No food and can't buy - continue anyway
}
// v10: Simple zone switching based on level
// Level 1-9: Men (3 GP), Level 10+: Farmers (9 GP)
const thievingLevel = currentState.skills.find(s => s.name === 'Thieving')?.baseLevel ?? 1;
// Determine best zone based on level (no setup needed!)
const bestZone = getBestZone(thievingLevel);
// Move to best zone if not there
if (!isInZone(ctx, bestZone)) {
const zoneName = bestZone === ZONES.farmers ? 'Farmers' : 'Lumbridge';
console.log(`Level ${thievingLevel}: Moving to ${zoneName} (${bestZone.coins} GP each)...`);
const arrived = await walkToWithDoors(ctx, bestZone.x, bestZone.z);
if (!arrived) {
console.warn('Failed to reach zone, trying again...');
}
continue;
}
// Find target in current zone
const target = findPickpocketTarget(ctx, bestZone.targetPattern);
if (!target) {
// No targets nearby - walk TO the zone center (not random)
// This is where the target spawns, so walking there helps
console.log(`No targets nearby - walking to ${bestZone === ZONES.farmers ? 'Farmer spawn' : 'zone'}...`);
await ctx.bot.walkTo(bestZone.x, bestZone.z);
continue;
}
// Attempt pickpocket
const result = await attemptPickpocket(ctx, target, stats);
// Log periodic stats every 10 attempts
if (stats.pickpocketAttempts % 10 === 0 && stats.pickpocketAttempts > 0) {
logStats(ctx, stats);
}
}
}
/**
* Log current thieving statistics
*/
function logStats(ctx: ScriptContext, stats: ThievingStats): void {
const state = ctx.sdk.getState();
if (!state) return;
const currentThievingXp = state.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
const xpGained = currentThievingXp - stats.startThievingXp;
const successRate = stats.pickpocketAttempts > 0
? ((stats.successfulPickpockets / stats.pickpocketAttempts) * 100).toFixed(1)
: '0.0';
console.log(`--- Stats after ${stats.pickpocketAttempts} attempts ---`);
console.log(`Success rate: ${successRate}% (${stats.successfulPickpockets}/${stats.pickpocketAttempts})`);
console.log(`Coins earned: ${stats.coinsEarned} GP`);
console.log(`Thieving XP gained: +${xpGained}`);
console.log(`Food eaten: ${stats.foodEaten}`);
console.log(`Time stunned: ${(stats.stunTimeMs / 1000).toFixed(1)}s`);
}
// Main script
async function main() {
const username = `tg${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
try {
await thievingLoop(ctx);
} finally {
// Log final stats
const state = ctx.sdk.getState();
if (state) {
const startThievingXp = 0; // We start at 0
const thieving = state.skills.find(s => s.name === 'Thieving');
const coins = ctx.sdk.findInventoryItem(/^coins$/i);
console.log('=== Final Results ===');
console.log(`Thieving: Level ${thieving?.baseLevel ?? 1} (${thieving?.experience ?? 0} XP)`);
console.log(`Total coins in inventory: ${coins?.count ?? 0}`);
}
}
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 5 * 60 * 1000, // 5 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-41
View File
@@ -1,41 +0,0 @@
# Lab Log: thieving
## Run 001 - Initial Implementation
**Outcome**: SUCCESS
**Duration**: ~1 minute (very fast due to spawn location)
### Strategy
- Start at Lumbridge spawn (3222, 3218 - already near Men NPCs)
- Find Men/Women NPCs (they have Pickpocket option)
- Use sendInteractNpc with Pickpocket option index
- Handle stuns by waiting ~5 seconds
- Continue until Thieving level 10
### What Worked
- Script found Man NPC immediately at spawn
- sendInteractNpc with Pickpocket option works correctly
- XP detection for success/failure works
- Stun handling with 5s delay was effective
- Reached level 10 with 1200 XP
### Notes
- The Lumbridge spawn (3222, 3218) is already in a good spot for pickpocketing
- Men NPCs are plentiful in the Lumbridge area
- Mix of successes and stuns is normal at low levels
---
## Learnings
### 1. Strategic Findings
- Pickpocketing Men/Women is effective for early Thieving training
- Lumbridge spawn location is ideal - no need to walk anywhere
- Stun duration of ~5 seconds handles the failed attempt penalty
### 2. Process & Tooling Reflections
- Script completed on first attempt - pattern was straightforward
- Using optionsWithIndex to find "Pickpocket" option worked well
### 3. SDK Issues & Gaps
- None encountered - sendInteractNpc with custom option index works as expected
-179
View File
@@ -1,179 +0,0 @@
/**
* Thieving Training Script v1
*
* Goal: Train Thieving to level 10+ from a fresh Lumbridge spawn
*
* Strategy:
* 1. Find Men/Women NPCs in Lumbridge
* 2. Pickpocket them repeatedly
* 3. Handle stuns (brief delay after failed pickpocket)
* 4. Continue until level 10
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Get thieving stats
function getThievingStats(ctx: ScriptContext): { level: number; xp: number } {
const state = ctx.sdk.getState();
const thieving = state?.skills.find(s => s.name === 'Thieving');
return {
level: thieving?.baseLevel ?? 1,
xp: thieving?.experience ?? 0
};
}
// Find a Man or Woman NPC to pickpocket
function findTarget(ctx: ScriptContext): { npc: { index: number; name: string; optionsWithIndex: Array<{ text: string; opIndex: number }> }; pickpocketOpt: { text: string; opIndex: number } } | null {
const state = ctx.sdk.getState();
if (!state) return null;
// Look for Man or Woman NPCs
for (const npc of state.nearbyNpcs) {
if (/^(man|woman)$/i.test(npc.name)) {
const pickpocketOpt = npc.optionsWithIndex.find(o => /pickpocket/i.test(o.text));
if (pickpocketOpt) {
return { npc, pickpocketOpt };
}
}
}
return null;
}
// Pickpocket a target NPC
async function pickpocket(ctx: ScriptContext): Promise<boolean> {
const target = findTarget(ctx);
if (!target) {
console.log('No pickpocket target found');
return false;
}
const { npc, pickpocketOpt } = target;
const xpBefore = getThievingStats(ctx).xp;
const startTick = ctx.sdk.getState()?.tick ?? 0;
console.log(`Pickpocketing ${npc.name}...`);
await ctx.sdk.sendInteractNpc(npc.index, pickpocketOpt.opIndex);
// Wait for XP gain (success) or stun message (failure)
try {
const finalState = await ctx.sdk.waitForCondition(state => {
// Check for XP gain (success)
const thievingXp = state.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
if (thievingXp > xpBefore) {
return true;
}
// Check for stun/failure messages
for (const msg of state.gameMessages) {
if (msg.tick > startTick) {
const text = msg.text.toLowerCase();
if (text.includes('stunned') || text.includes('stun') ||
text.includes('caught') || text.includes('failed') ||
text.includes("what do you think you're doing")) {
return true;
}
}
}
// Dismiss level-up dialogs
if (state.dialog.isOpen) {
ctx.sdk.sendClickDialog(0).catch(() => {});
}
return false;
}, 8000);
// Check what happened
const thievingXpAfter = finalState.skills.find(s => s.name === 'Thieving')?.experience ?? 0;
if (thievingXpAfter > xpBefore) {
console.log('Pickpocket success!');
return true;
}
// We got stunned - wait for stun to wear off
console.log('Stunned! Waiting...');
await new Promise(r => setTimeout(r, 5000)); // Stun lasts ~5 seconds
return true; // Still count as progress since we attempted
} catch {
console.log('Pickpocket timeout');
return false;
}
}
// Main script
async function main() {
// Create fresh account
const username = `th${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
console.log('=== Thieving Training Script v1 ===');
const startStats = getThievingStats(ctx);
console.log(`Starting Thieving: Level ${startStats.level} (${startStats.xp} XP)`);
let consecutiveFails = 0;
while (true) {
const stats = getThievingStats(ctx);
if (stats.level >= 10) {
console.log(`Goal achieved! Thieving level ${stats.level}`);
break;
}
// Dismiss any blocking dialogs
const state = ctx.sdk.getState();
if (state?.dialog.isOpen) {
await ctx.sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Check if we have a target nearby
const target = findTarget(ctx);
if (!target) {
console.log('No targets nearby, walking to Lumbridge center...');
// Walk to Lumbridge center where Men/Women spawn
await ctx.bot.walkTo(3222, 3218);
consecutiveFails++;
if (consecutiveFails >= 5) {
console.log('Too many consecutive fails, aborting');
break;
}
continue;
}
// Attempt pickpocket
const success = await pickpocket(ctx);
if (success) {
consecutiveFails = 0;
} else {
consecutiveFails++;
}
// Small delay between attempts
await new Promise(r => setTimeout(r, 600));
}
const endStats = getThievingStats(ctx);
console.log(`\n=== Complete: Level ${endStats.level} (${endStats.xp} XP) ===`);
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 15 * 60 * 1000, // 15 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-83
View File
@@ -1,83 +0,0 @@
# Lab Log: varrock-travel
## Purpose
Test long-distance navigation using `bot.walkTo()` from Lumbridge to Varrock (~220 tiles).
## Configuration
- Start: Lumbridge spawn (3222, 3218)
- Destination: Varrock West Bank (3185, 3436)
- Distance: ~220 tiles
- Time limit: 3 minutes
- Stall timeout: 60s
---
## Run 001 - Direct walkTo (FAILED)
**Outcome**: error
**Duration**: 16.1s
### What Happened
- Single `walkTo(3212, 3428)` call
- Bot walked from (3222, 3218) to (3212, 3270) - about 52 tiles
- Pathfinder returned "No path found"
- Stuck at z=3270 (roughly cow field area)
### Root Cause
Direct walkTo over 200+ tiles fails. Pathfinder has ~100 tile search radius and intermediate waypoint calculation (60 tiles) still gets stuck in problematic terrain.
### Fix
Use explicit waypoints with 20-30 tile segments, with retry logic per waypoint.
---
## Run 002 - Waypoint-based (SUCCESS)
**Outcome**: success
**Duration**: 81.7s
### What Happened
- Used 8 waypoints from Lumbridge to Varrock
- Most waypoints reached on first attempt
- Two retries needed:
- Champion Guild area: stuck at (3260, 3277), retry succeeded
- South Varrock: stuck at (3269, 3343), retry succeeded
- Arrived at (3212, 3418) - within Varrock area (z >= 3400)
### What Worked
- 20-30 tile waypoint segments
- 8-tile tolerance for intermediate waypoints
- Retry logic (3 attempts per waypoint)
- Early exit when reaching Varrock area (z >= 3400)
### Waypoints Used
```
1. (3232, 3245) - North of Lumbridge
2. (3250, 3270) - Near cow field
3. (3260, 3300) - North of cow field
4. (3255, 3330) - Champion Guild area
5. (3245, 3360) - South Varrock
6. (3230, 3390) - Varrock south entrance
7. (3210, 3420) - Near Varrock square
8. (3185, 3436) - Varrock West Bank
```
---
## Learnings
### 1. Strategic Findings
- **Direct walkTo fails for >100 tile distances** - pathfinder search radius is limited
- **Waypoints of 20-30 tiles work reliably** - within pathfinder range
- **Retry logic is essential** - some waypoints get stuck but succeed on retry
- **Known stuck points**: around (3260, 3277) and (3269, 3343) - terrain/collision issues
### 2. Process & Tooling Reflections
- Console output clearly showed where pathfinding failed
- Explicit waypoint names in logs make debugging easier
- Bot actions auto-track progress during long walks
### 3. SDK Issues & Gaps
- `walkTo` returns "No path found" rather than a more informative partial-progress message
- Would be useful if `walkTo` returned how far it got when failing
- Intermediate waypoint calculation (60 tiles) in bot-actions.ts could use smaller steps
-179
View File
@@ -1,179 +0,0 @@
/**
* Varrock Travel Script
*
* Goal: Navigate from Lumbridge to Varrock using bot.walkTo()
*
* Uses waypoint-based navigation for long distances (>30 tiles).
* Route goes through the cow field area and up to Varrock.
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Varrock West Bank area (ground floor, reliable destination)
const VARROCK = { x: 3185, z: 3436 };
// Waypoints from Lumbridge to Varrock (~210 tiles total)
// Each segment is 20-30 tiles to avoid pathfinding issues
const WAYPOINTS = [
{ x: 3232, z: 3245, name: 'North of Lumbridge' }, // First step north
{ x: 3250, z: 3270, name: 'Near cow field' }, // East to avoid castle
{ x: 3260, z: 3300, name: 'North of cow field' }, // Continue north
{ x: 3255, z: 3330, name: 'Champion Guild area' }, // Past champions guild
{ x: 3245, z: 3360, name: 'South Varrock' }, // Approaching Varrock
{ x: 3230, z: 3390, name: 'Varrock south entrance' }, // South of Varrock
{ x: 3210, z: 3420, name: 'Near Varrock square' }, // Into Varrock
{ x: 3185, z: 3436, name: 'Varrock West Bank' }, // Final destination
];
// Arrival tolerance in tiles
const WAYPOINT_TOLERANCE = 8;
const FINAL_TOLERANCE = 10;
/**
* Check if we've reached Varrock (north of z=3400)
*/
function isInVarrock(ctx: ScriptContext): boolean {
const state = ctx.sdk.getState();
if (!state?.player) return false;
return state.player.worldZ >= 3400;
}
/**
* Get distance between player and a point
*/
function distanceTo(ctx: ScriptContext, x: number, z: number): number {
const state = ctx.sdk.getState();
if (!state?.player) return Infinity;
const dx = x - state.player.worldX;
const dz = z - state.player.worldZ;
return Math.sqrt(dx * dx + dz * dz);
}
/**
* Walk to a waypoint with retries
*/
async function walkToWaypoint(
ctx: ScriptContext,
x: number,
z: number,
name: string,
tolerance: number
): Promise<boolean> {
const MAX_ATTEMPTS = 3;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
const dist = distanceTo(ctx, x, z);
if (dist <= tolerance) {
console.log(`Already at ${name}`);
return true;
}
console.log(`Walking to ${name} (${x}, ${z}) - attempt ${attempt}/${MAX_ATTEMPTS}`);
const result = await ctx.bot.walkTo(x, z, tolerance);
const newDist = distanceTo(ctx, x, z);
if (newDist <= tolerance) {
console.log(`Reached ${name}`);
return true;
}
if (!result.success) {
console.warn(`Walk attempt ${attempt} failed: ${result.message}`);
}
// Small delay before retry
await new Promise(r => setTimeout(r, 500));
}
const finalDist = distanceTo(ctx, x, z);
console.warn(`Could not reach ${name} - distance: ${finalDist.toFixed(0)} tiles`);
return finalDist <= tolerance * 2; // Allow some leeway to continue
}
/**
* Main travel function using waypoints
*/
async function travelToVarrock(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state?.player) throw new Error('No initial state');
const startX = state.player.worldX;
const startZ = state.player.worldZ;
const startTime = Date.now();
console.log(`Start: (${startX}, ${startZ})`);
console.log(`Destination: Varrock (${VARROCK.x}, ${VARROCK.z})`);
console.log(`Total waypoints: ${WAYPOINTS.length}`);
if (isInVarrock(ctx)) {
console.log('Already in Varrock!');
return;
}
// Follow waypoints
for (let i = 0; i < WAYPOINTS.length; i++) {
const wp = WAYPOINTS[i];
if (!wp) continue;
// Skip waypoints we're already past
if (distanceTo(ctx, wp.x, wp.z) <= WAYPOINT_TOLERANCE) {
console.log(`Skipping ${wp.name} - already there`);
continue;
}
const isLast = i === WAYPOINTS.length - 1;
const tolerance = isLast ? FINAL_TOLERANCE : WAYPOINT_TOLERANCE;
const success = await walkToWaypoint(ctx, wp.x, wp.z, wp.name, tolerance);
if (!success && !isInVarrock(ctx)) {
// Log position and continue trying
const pos = ctx.sdk.getState()?.player;
console.warn(`Stuck at (${pos?.worldX}, ${pos?.worldZ}) - trying next waypoint`);
}
// Check if we've reached Varrock early
if (isInVarrock(ctx)) {
console.log('Reached Varrock area!');
break;
}
}
const elapsed = (Date.now() - startTime) / 1000;
const endState = ctx.sdk.getState();
const endX = endState?.player?.worldX ?? 0;
const endZ = endState?.player?.worldZ ?? 0;
const finalDist = distanceTo(ctx, VARROCK.x, VARROCK.z);
console.log(`End position: (${endX}, ${endZ})`);
console.log(`Distance to target: ${finalDist.toFixed(0)} tiles`);
console.log(`Time: ${elapsed.toFixed(1)}s`);
if (!isInVarrock(ctx) && finalDist > FINAL_TOLERANCE * 2) {
throw new Error(`Failed to reach Varrock - stuck at (${endX}, ${endZ})`);
}
console.log('Successfully reached Varrock!');
}
// Run the script
async function main() {
const username = `vt${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
await travelToVarrock(ctx);
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 3 * 60 * 1000, // 3 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
-130
View File
@@ -1,130 +0,0 @@
# Lab Log: woodcutting
Goal: Train Woodcutting to level 10+ from fresh Lumbridge spawn.
Strategy:
1. Check for existing axe (LUMBRIDGE_SPAWN preset includes bronze axe)
2. If no axe: Walk to Bob's Axes (3230, 3203), buy bronze axe for 16gp
3. Walk to trees west of Lumbridge castle (3200, 3230)
4. Chop trees, drop logs when full
5. Continue until target level
---
## Run 001 - 2026-01-27 17:07
**Outcome**: SUCCESS
**Duration**: ~20 seconds
**Target**: Level 10
### What Happened
1. Script detected existing bronze axe in inventory (from preset)
2. Walked to trees at (3200, 3230)
3. Chopped one tree
4. XP jump: 1250xp (level 1 → 10) - server has accelerated XP rates
### Observations
- LUMBRIDGE_SPAWN preset includes: bronze axe, tinderbox, fishing net, pickaxe, and other starter items
- Buy-axe logic was skipped (axe already present)
- Server appears to have 50x XP rates (normal tree = 25xp, received 1250xp)
---
## Run 002 - 2026-01-27 17:08
**Outcome**: FAILED (connection error)
**Duration**: ~5 seconds
### What Happened
- Bot disconnected during walkTo action
- ConnectionClosedError from Puppeteer
- Transient browser/connection issue
---
## Run 003 - 2026-01-27 17:08
**Outcome**: SUCCESS
**Duration**: ~15 seconds
**Target**: Level 10
### What Happened
- Same as Run 001 - confirms consistent behavior
---
## Run 004 - 2026-01-27 17:10
**Outcome**: SUCCESS
**Duration**: ~30 seconds
**Target**: Level 15
### What Happened
1. Walked to trees
2. Chopped 2 trees
3. Dismissed level-up dialog 3 times
4. Reached level 15
### Observations
- Dialog dismissal working correctly
- HP dropped from 10 → 7 (nearby enemies?)
---
## Run 005 - 2026-01-27 17:10
**Outcome**: SUCCESS
**Duration**: ~60 seconds
**Target**: Level 30
### What Happened
1. Chopped trees continuously
2. Bot moved between trees as they were cut/despawned
3. Dropped 8 logs when inventory hit 26 items
4. Dismissed multiple level-up dialogs
5. Reached level 30
### Observations
- **Inventory management working**: "Dropping 8 logs..." triggered at 26 items
- **Dialog dismissal working**: Multiple dismissals throughout
- **HP concern**: Dropped from 10 → 1 during training
- Trees are near dark wizards area (3200, 3230)
- Bot drifted north toward (3187, 3241) during training
- For longer runs, may need food or different location
### Final Stats
- Trees chopped: ~12
- Max level reached: 30
- HP at end: 1/10
---
## Summary
The script is **fully functional**:
| Feature | Status | Notes |
|---------|--------|-------|
| Axe detection | ✅ | Detects preset axe, has buy fallback |
| Buy from shop | ⚠️ | Code present but untested (preset includes axe) |
| Tree chopping | ✅ | Uses bot.chopTree() reliably |
| Dialog dismissal | ✅ | Handles level-up congratulations |
| Inventory management | ✅ | Drops logs at 26 items |
| Level detection | ✅ | Stops at target level |
| Stall detection | ✅ | Has stuck counter + StallError |
### Known Issues
1. **HP drain**: Location near dark wizards causes HP loss. For extended training:
- Add food eating logic
- Move to safer tree area (further north)
- Add HP monitoring with retreat
2. **Buy-axe untested**: LUMBRIDGE_SPAWN includes axe, so shop logic never executes
### Potential Improvements
1. Add eating when HP low
2. Move to safer tree location
3. Chop oak trees at level 15+ for faster XP
4. Bank logs instead of dropping (for later use)
-273
View File
@@ -1,273 +0,0 @@
/**
* Woodcutting Trainer Script
*
* Goal: Train Woodcutting to level 10+ starting from a fresh Lumbridge spawn.
*
* Strategy:
* 1. Walk to Bob's Axes in Lumbridge
* 2. Buy a bronze axe (16gp - fresh accounts have 25gp)
* 3. Walk to trees near Lumbridge
* 4. Chop trees until level 10
* 5. Drop logs when inventory fills
*/
import { runScript, type ScriptContext } from '../../sdk/runner';
import { generateSave, TestPresets } from '../../sdk/test/utils/save-generator';
import { launchBotWithSDK } from '../../sdk/test/utils/browser';
// Locations
const BOBS_AXES = { x: 3230, z: 3203 };
const LUMBRIDGE_TREES = { x: 3200, z: 3230 }; // Trees west of Lumbridge castle
// Configuration
const TARGET_LEVEL = 10;
const BRONZE_AXE_COST = 16;
function getWoodcuttingLevel(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.baseLevel ?? 1;
}
function getWoodcuttingXp(ctx: ScriptContext): number {
return ctx.sdk.getState()?.skills.find(s => s.name === 'Woodcutting')?.experience ?? 0;
}
function countLogs(ctx: ScriptContext): number {
const inv = ctx.sdk.getState()?.inventory ?? [];
return inv.filter(i => /logs/i.test(i.name)).length;
}
function hasAxe(ctx: ScriptContext): boolean {
const inv = ctx.sdk.getState()?.inventory ?? [];
const equip = ctx.sdk.getState()?.equipment ?? [];
return inv.some(i => /axe/i.test(i.name)) || equip.some(i => /axe/i.test(i.name));
}
function getCoins(ctx: ScriptContext): number {
const coins = ctx.sdk.getState()?.inventory.find(i => /coins/i.test(i.name));
return coins?.count ?? 0;
}
async function dropAllLogs(ctx: ScriptContext): Promise<void> {
const state = ctx.sdk.getState();
if (!state) return;
const logsItems = state.inventory.filter(item => /logs/i.test(item.name));
console.log(`Dropping ${logsItems.length} logs...`);
for (const item of logsItems) {
await ctx.sdk.sendDropItem(item.slot);
await new Promise(r => setTimeout(r, 150));
}
}
async function buyAxeFromBob(ctx: ScriptContext): Promise<boolean> {
const { bot, sdk } = ctx;
// Check if we have enough coins
const gp = getCoins(ctx);
console.log(`Current GP: ${gp}`);
if (gp < BRONZE_AXE_COST) {
console.error(`Not enough GP for bronze axe (need ${BRONZE_AXE_COST}, have ${gp})`);
return false;
}
// Walk to Bob's Axes
console.log(`Walking to Bob's Axes at (${BOBS_AXES.x}, ${BOBS_AXES.z})...`);
await bot.walkTo(BOBS_AXES.x, BOBS_AXES.z);
// Dismiss any dialogs
await bot.dismissBlockingUI();
// Log nearby NPCs for debugging
const state = ctx.sdk.getState();
console.log(`Position: (${state?.player?.worldX}, ${state?.player?.worldZ})`);
const nearbyNpcs = state?.nearbyNpcs?.slice(0, 5) ?? [];
console.log(`Nearby NPCs: ${nearbyNpcs.map(n => n.name).join(', ')}`);
// Open Bob's shop
console.log('Opening shop...');
let shopResult = await bot.openShop(/bob/i);
if (!shopResult.success) {
// Try finding any NPC with trade option
console.log('Bob not found by name, looking for trade option...');
const npcWithTrade = state?.nearbyNpcs.find(n =>
n.optionsWithIndex.some(o => /trade/i.test(o.text))
);
if (npcWithTrade) {
console.log(`Found tradeable NPC: ${npcWithTrade.name}`);
const tradeOpt = npcWithTrade.optionsWithIndex.find(o => /trade/i.test(o.text));
if (tradeOpt) {
await sdk.sendInteractNpc(npcWithTrade.index, tradeOpt.opIndex);
await new Promise(r => setTimeout(r, 1500));
}
}
}
// Check if shop opened
await new Promise(r => setTimeout(r, 500));
const shopState = ctx.sdk.getState()?.shop;
if (!shopState?.isOpen) {
console.error('Failed to open shop');
return false;
}
// Log shop inventory
console.log(`Shop open with ${shopState.shopItems.length} items`);
const axes = shopState.shopItems.filter(i => /axe/i.test(i.name));
for (const axe of axes) {
console.log(` ${axe.name} - ${axe.buyPrice}gp (stock: ${axe.count})`);
}
// Buy bronze axe
console.log('Buying bronze axe...');
const buyResult = await bot.buyFromShop(/bronze axe/i, 1);
console.log(`Buy result: ${buyResult.message}`);
// Close shop by walking away
await sdk.sendWalk(3225, 3210, true);
await new Promise(r => setTimeout(r, 1000));
// Verify we got the axe
const gotAxe = hasAxe(ctx);
if (gotAxe) {
console.log('SUCCESS: Got bronze axe!');
} else {
console.error('Failed to buy bronze axe');
}
return gotAxe;
}
async function chopTrees(ctx: ScriptContext): Promise<void> {
const { bot, sdk } = ctx;
console.log(`Walking to trees at (${LUMBRIDGE_TREES.x}, ${LUMBRIDGE_TREES.z})...`);
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
let lastXp = getWoodcuttingXp(ctx);
let logsChopped = 0;
let stuckCount = 0;
const MAX_STUCK = 10;
while (getWoodcuttingLevel(ctx) < TARGET_LEVEL) {
const state = ctx.sdk.getState();
if (!state) {
await new Promise(r => setTimeout(r, 500));
continue;
}
// Dismiss any level-up dialogs
if (state.dialog.isOpen) {
console.log('Dismissing dialog...');
await sdk.sendClickDialog(0);
await new Promise(r => setTimeout(r, 300));
continue;
}
// Drop logs if inventory is getting full
if (state.inventory.length >= 26) {
await dropAllLogs(ctx);
continue;
}
// Find nearest tree
const tree = sdk.findNearbyLoc(/^tree$/i);
if (!tree) {
console.log('No tree nearby, walking to tree area...');
await bot.walkTo(LUMBRIDGE_TREES.x, LUMBRIDGE_TREES.z);
stuckCount++;
if (stuckCount > MAX_STUCK) {
throw new Error('Unable to find trees after multiple attempts');
}
continue;
}
// Chop the tree
const result = await bot.chopTree(tree);
const currentXp = getWoodcuttingXp(ctx);
if (currentXp > lastXp) {
logsChopped++;
stuckCount = 0; // Reset stuck counter on success
console.log(`[Lvl ${getWoodcuttingLevel(ctx)}] Chopped tree! Logs: ${countLogs(ctx)}, Total: ${logsChopped}, XP: ${currentXp}`);
lastXp = currentXp;
} else if (!result.success) {
console.log(`Chop failed: ${result.message}`);
stuckCount++;
if (stuckCount > MAX_STUCK) {
throw new Error(`Stuck: ${stuckCount} failed chop attempts`);
}
}
// Small delay between actions
await new Promise(r => setTimeout(r, 200));
}
console.log(`=== GOAL ACHIEVED: Woodcutting level ${getWoodcuttingLevel(ctx)}! ===`);
}
// Main script
async function main() {
// Create fresh account
const username = `wc${Math.random().toString(36).slice(2, 7)}`;
await generateSave(username, TestPresets.LUMBRIDGE_SPAWN);
// Launch browser
const session = await launchBotWithSDK(username, { usePuppeteer: true });
try {
await runScript(async (ctx) => {
console.log('=== Woodcutting Trainer ===');
console.log(`Goal: Level ${TARGET_LEVEL}`);
// Wait for state to initialize
await new Promise(r => setTimeout(r, 2000));
const state = ctx.sdk.getState();
if (!state?.player) {
console.error('No player state');
return;
}
console.log(`Starting at (${state.player.worldX}, ${state.player.worldZ})`);
console.log(`Current Woodcutting level: ${getWoodcuttingLevel(ctx)}`);
// Dismiss any startup dialogs
await ctx.bot.dismissBlockingUI();
// Step 1: Get an axe if we don't have one
if (!hasAxe(ctx)) {
console.log('No axe found, buying from Bob...');
const gotAxe = await buyAxeFromBob(ctx);
if (!gotAxe) {
console.error('Failed to acquire axe, cannot continue');
return;
}
} else {
console.log('Already have an axe!');
}
// Step 2: Chop trees until target level
console.log('Starting woodcutting training...');
await chopTrees(ctx);
console.log('=== Script Complete ===');
}, {
connection: { bot: session.bot, sdk: session.sdk },
timeout: 10 * 60 * 1000, // 10 minutes
});
} finally {
await session.cleanup();
}
}
main().catch(console.error);
+3 -6
View File
@@ -197,12 +197,9 @@ await sdk.waitForConnection(60000);
## Example Script
See `scripts/example-remote.ts` for a complete example.
See `bots/_template/script.ts` for a complete starter example, or create a new
bot with `bun bots/create-bot.ts <username>` and run its generated script:
```bash
# Run locally
bun scripts/example-remote.ts
# Connect to remote server
GATEWAY_HOST=game.example.com BOT_USERNAME=player1 bun scripts/example-remote.ts
bun bots/<username>/script.ts
```
-169
View File
@@ -1,169 +0,0 @@
# SDK Design Notes
## Architecture
The system has four layers:
1. **BotActions (porcelain)**
- High level ergonomic functions skipTutorial, chopTree, attackNpc, etc.
- Returns when intended action is complete
- Legible failure messages
- Pathfinding orchestration
- encodes domain knowledge, actively constructed and maintained as this project evolves
- *Can be updated without any serverside changes*
2. **BotSDK (plumbing)**
- Returns when server acknowledges it's been sent
- State queries, waitForCondition
- Lower domain knowledge, similar to actual game client
- Should evolve more slowly
3. **Gateway**
- Routing, session management, multi-client broadcast
4. **Bot Client (executor)**
- Executes primitive actions (mapping directly to game packets)
- Sends raw state
### Design Principle
**Bot client should only have pure primitives** - actions that map 1:1 to game protocol packets.
All "smart" behavior (multi-step sequences, decision-making) lives in BotActions.
### Plumbing (`index.ts` - BotSDK)
- Low-level API where actions resolve on game acknowledgment (fast)
- Includes basic state queries and local pathfinding
- Protocol changes require gateway/client updates, should change more slowly
```typescript
await sdk.sendInteractLoc(tree.x, tree.z, tree.id, 1);
await sdk.sendWalk(x, z, running);
await sdk.sendClickComponent(componentId); // IF_BUTTON packet
await sdk.sendClickComponentWithOption(componentId, 1); // INV_BUTTON packet
```
### Porcelain (`actions.ts` - BotActions)
- High-level, domain-aware API that wraps plumbing
- Actions resolve when the **effect is complete** (more ergonomic to develop against)
- Tries to encode domain knowledge and handles edge cases
```typescript
const result = await bot.chopTree(); // Waits for logs in inventory
if(!result.success) {
console.log(result.message);
}
await bot.walkTo(x, z); // Pathfinding + arrival confirmation
await bot.skipTutorial(); // Dialog navigation + NPC interaction
await bot.smithAtAnvil('dagger'); // Full smithing workflow
```
## Interface Component Actions
Three SDK methods for clicking interface components:
| Action | Packet | Use Case |
|--------|--------|----------|
| `sendClickComponent(id)` | IF_BUTTON | Simple buttons, spellcasting |
| `sendClickComponentWithOption(id, opt)` | INV_BUTTON | Components with options (smithing, crafting, make-x) |
| `sendClickInterfaceOption(index)` | IF_BUTTON | Convenience - looks up componentId from `state.interface.options[index]` |
## Known Limitations / TODOs
### `acceptCharacterDesign`
Currently uses hidden client state (`designGender`, `designKits`, `designColours`).
The SDK cannot set design values before accepting.
**Future improvement**: Parameterize as `acceptCharacterDesign(gender, kits[7], colours[5])`
so the action is fully self-contained.
## Key Learnings
### 1. Game Messages Persist in Buffer
Old messages like "You can't light a fire here" persist. Filter by tick:
```typescript
const startTick = this.sdk.getState()?.tick || 0;
// Only check messages where msg.tick > startTick
```
### 2. Level-Up Dialogs Are Multi-Page
Keep clicking every few ticks while open:
```typescript
if (state.dialog.isOpen && (state.tick - lastClick) >= 3) {
this.sdk.sendClickDialog(0).catch(() => {});
}
```
### 3. Choose the Right Success Signal
| Action | Reliable Signal |
|--------|-----------------|
| Firemaking | XP gain |
| Woodcutting | Logs in inventory OR tree disappears |
| Pickup | Item in inventory |
| Walking | Player position matches destination |
| Shop Buy | Item appears in inventory |
| Equip | Item leaves inventory |
## Available BotActions Methods
### Movement & Interaction
| Method | Description |
|--------|-------------|
| `walkTo(x, z, tolerance?)` | Pathfinding + arrival |
| `talkTo(target)` | Talk to NPC, opens dialog |
| `openDoor(target?)` | Opens a door |
| `navigateDialog(choices)` | Click through dialog options |
| `skipTutorial()` | Navigate tutorial dialogs/NPCs |
### Skills & Resources
| Method | Description |
|--------|-------------|
| `chopTree(target?)` | Chops tree, waits for logs |
| `burnLogs(target?)` | Burns logs with tinderbox |
| `pickupItem(target)` | Picks up ground item |
| `fletchLogs(product?)` | Fletches logs into items |
| `craftLeather(product?)` | Crafts leather items |
| `smithAtAnvil(product?)` | Smiths bars into items |
### Shop & Bank
| Method | Description |
|--------|-------------|
| `openShop(target?)` | Opens shop interface |
| `buyFromShop(target, amount?)` | Buys from shop |
| `sellToShop(target, amount?)` | Sells to shop |
| `closeShop()` | Closes shop interface |
| `openBank()` | Opens bank interface |
| `depositItem(target, amount?)` | Deposits to bank |
| `withdrawItem(slot, amount?)` | Withdraws from bank |
| `closeBank()` | Closes bank interface |
### Equipment & Combat
| Method | Description |
|--------|-------------|
| `equipItem(target)` | Equips from inventory |
| `unequipItem(target)` | Unequips item |
| `eatFood(target)` | Eats food |
| `attackNpc(target, timeout?)` | Attacks NPC |
| `castSpellOnNpc(target, spell, timeout?)` | Casts spell on NPC |
### Helpers
| Method | Description |
|--------|-------------|
| `dismissBlockingUI()` | Closes dialogs/modals |
| `waitForSkillLevel(skill, level)` | Waits for skill level |
| `waitForInventoryItem(pattern)` | Waits for item |
| `waitForDialogClose()` | Waits for dialog to close |
| `waitForIdle()` | Waits for player to be idle |
## Files
| File | Purpose |
|------|---------|
| `sdk/index.ts` | BotSDK - low-level WebSocket API |
| `sdk/actions.ts` | BotActions - high-level domain actions |
| `sdk/types.ts` | Type definitions |
| `sdk/pathfinding.ts` | Pathfinding utilities |
| `server/gateway/gateway.ts` | Gateway - routing, sessions |
| `server/webclient/src/bot/BotSDK.ts` | Bot Client - pure executor |
-19
View File
@@ -1,19 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "@rs-agent/sdk",
"dependencies": {
"@2004scape/rsmod-pathfinder": "github:MaxBittker/rsmod-pathfinder#df7ec08",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@2004scape/rsmod-pathfinder": ["@2004scape/rsmod-pathfinder@github:MaxBittker/rsmod-pathfinder#df7ec08", {}, "MaxBittker-rsmod-pathfinder-df7ec08"],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
}
}
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env bun
/**
* Logic test for sdk.clickDialogByText() — no bot required.
*
* Verifies the helper resolves dialog options by visible text and dispatches
* the server-assigned (1-based) index, sidestepping the array-position
* footgun that exists with raw sendClickDialog().
*/
import { BotSDK } from '../index';
import type { ActionResult, BotWorldState, DialogOption } from '../types';
let failures = 0;
function check(label: string, ok: boolean, detail?: string): void {
if (ok) {
console.log(` ok ${label}`);
} else {
failures++;
console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`);
}
}
function makeOption(index: number, text: string): DialogOption {
return { index, text, componentId: 1000 + index, buttonType: 0 };
}
function makeState(opts: { dialogOpen: boolean; options?: DialogOption[] }): BotWorldState {
// Only the dialog field matters — cast the rest. clickDialogByText only
// touches state.dialog.
return {
dialog: {
isOpen: opts.dialogOpen,
options: opts.options ?? [],
isWaiting: false,
},
} as unknown as BotWorldState;
}
function mountSdk(state: BotWorldState | null): { sdk: BotSDK; calls: number[] } {
const sdk = new BotSDK({ botUsername: 'test' });
(sdk as any).state = state;
// Stub the protocol-level call so we can record what the helper dispatches.
const calls: number[] = [];
(sdk as any).sendClickDialog = async (option: number): Promise<ActionResult> => {
calls.push(option);
return { success: true, message: 'stub-clicked' };
};
return { sdk, calls };
}
async function run(): Promise<void> {
console.log('clickDialogByText():');
// 1. No dialog open → returns no_dialog
{
const { sdk, calls } = mountSdk(makeState({ dialogOpen: false }));
const r = await sdk.clickDialogByText(/yes/i);
check('returns no_dialog when dialog is closed',
!r.success && r.reason === 'no_dialog' && calls.length === 0,
JSON.stringify(r));
}
// 2. Null state → returns no_dialog
{
const { sdk, calls } = mountSdk(null);
const r = await sdk.clickDialogByText(/yes/i);
check('returns no_dialog when state is null',
!r.success && r.reason === 'no_dialog' && calls.length === 0,
JSON.stringify(r));
}
// 3. Pattern matches first option → dispatches its server index (1, NOT 0)
// This is the core footgun fix: array position 0 maps to server index 1.
{
const options = [
makeOption(1, 'Yes, ok.'),
makeOption(2, 'No thanks.'),
];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText(/yes/i);
check('dispatches server-assigned index 1 for first option (NOT array position 0)',
r.success && calls.length === 1 && calls[0] === 1,
`result=${JSON.stringify(r)} calls=${JSON.stringify(calls)}`);
}
// 4. Pattern matches a later option → dispatches that option's index
{
const options = [
makeOption(1, 'Yes, ok.'),
makeOption(2, 'Is there anything down this alleyway?'),
makeOption(3, 'No thanks.'),
];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText('alleyway');
check('dispatches index 2 for the alleyway option',
r.success && calls.length === 1 && calls[0] === 2,
`calls=${JSON.stringify(calls)}`);
}
// 5. String patterns are case-insensitive
{
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText('YES');
check('string pattern is case-insensitive',
r.success && calls[0] === 1,
`calls=${JSON.stringify(calls)}`);
}
// 6. Caller-supplied RegExp flags are respected (e.g. case-sensitive when
// explicitly constructed without /i)
{
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText(/YES/); // no /i, won't match "Yes"
check('explicit RegExp without /i is honoured (no match)',
!r.success && r.reason === 'no_match' && calls.length === 0,
`result=${JSON.stringify(r)}`);
}
// 7. No match → returns no_match with available options listed
{
const options = [makeOption(1, 'Yes, ok.'), makeOption(2, 'No thanks.')];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText(/maybe/i);
check('returns no_match when no option matches',
!r.success && r.reason === 'no_match' && calls.length === 0
&& r.message.includes('Yes, ok.') && r.message.includes('No thanks.'),
JSON.stringify(r));
}
// 8. Empty options list → returns no_match (with "(none)")
{
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options: [] }));
const r = await sdk.clickDialogByText(/anything/);
check('returns no_match when option list is empty',
!r.success && r.reason === 'no_match' && calls.length === 0
&& r.message.includes('(none)'),
JSON.stringify(r));
}
// 9. Non-contiguous server indices are passed through verbatim. (Real
// dialogs always assign 1..N contiguously today, but the SDK should
// never assume that — it must trust DialogOption.index.)
{
const options = [
makeOption(7, 'Yes, ok.'),
makeOption(42, 'Tell me more.'),
];
const { sdk, calls } = mountSdk(makeState({ dialogOpen: true, options }));
const r = await sdk.clickDialogByText(/tell me/i);
check('passes through non-contiguous server-assigned indices',
r.success && calls[0] === 42,
`calls=${JSON.stringify(calls)}`);
}
console.log('');
if (failures === 0) {
console.log('PASSED — all 9 cases');
process.exit(0);
} else {
console.log(`FAILED — ${failures} case(s) failed`);
process.exit(1);
}
}
run().catch(e => {
console.error('Fatal:', e);
process.exit(1);
});
-8
View File
@@ -1,8 +0,0 @@
# Failing Tests
### long-distance-navigation.ts
`Zone not allocated` - collision data sparse, pathfinder needs all zones
### navigation.ts
Timeout at Taverley gate (~2906, 3312) - gates not visible until close range
+2
View File
@@ -25,6 +25,8 @@ coverage/
# these are artifacts generated from other repos
public/loader.jar
public/runescape*.jar
# built clients are served from webclient/out; a copy here shadows fresh builds
public/client/*.js
public/client/*.js.map
### Windows ###
+1 -1
View File
@@ -26,6 +26,6 @@
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": ["*.ts", "server/gateway/**/*.ts", "bot_arcs/**/*.ts", "sdk/**/*.ts", "stream/**/*.ts"],
"include": ["*.ts", "server/gateway/**/*.ts", "sdk/**/*.ts"],
"exclude": ["server/engine", "server/webclient", "node_modules"]
}