diff --git a/.envrc b/.envrc deleted file mode 100644 index 3550a30f2..000000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/.gitignore b/.gitignore index 08b1ce832..720e2787b 100644 --- a/.gitignore +++ b/.gitignore @@ -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/*/ diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 83517651f..4461750e4 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -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 "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 diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index ccf002c6c..000000000 --- a/GEMINI.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 000000000..bac85c9fc --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +claude.md \ No newline at end of file diff --git a/agents.md b/agents.md deleted file mode 100644 index ccf002c6c..000000000 --- a/agents.md +++ /dev/null @@ -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! \ No newline at end of file diff --git a/agents.md b/agents.md new file mode 120000 index 000000000..bac85c9fc --- /dev/null +++ b/agents.md @@ -0,0 +1 @@ +claude.md \ No newline at end of file diff --git a/bun.lock b/bun.lock index 65dfda1e5..934fba071 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 67aa939ea..000000000 --- a/flake.lock +++ /dev/null @@ -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 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 0a69a0264..000000000 --- a/flake.nix +++ /dev/null @@ -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 - ]; - }; - }); - }; -} diff --git a/package.json b/package.json index b7e5b256c..520559d25 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/scripts/agility/lab_log.md b/scripts/agility/lab_log.md deleted file mode 100644 index 87ba1fdba..000000000 --- a/scripts/agility/lab_log.md +++ /dev/null @@ -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 { - 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 { - // Walk to gate area waypoints - // Use findAndUseGate() when blocked -} -``` diff --git a/scripts/agility/script.ts b/scripts/agility/script.ts deleted file mode 100644 index d97cc1c75..000000000 --- a/scripts/agility/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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); diff --git a/scripts/al-kharid-travel/lab_log.md b/scripts/al-kharid-travel/lab_log.md deleted file mode 100644 index cc3509bb6..000000000 --- a/scripts/al-kharid-travel/lab_log.md +++ /dev/null @@ -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) diff --git a/scripts/al-kharid-travel/script.ts b/scripts/al-kharid-travel/script.ts deleted file mode 100644 index a7dfbb65e..000000000 --- a/scripts/al-kharid-travel/script.ts +++ /dev/null @@ -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); diff --git a/scripts/combat-trainer/lab_log.md b/scripts/combat-trainer/lab_log.md deleted file mode 100644 index 308016cd5..000000000 --- a/scripts/combat-trainer/lab_log.md +++ /dev/null @@ -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 - ---- diff --git a/scripts/combat-trainer/script.ts b/scripts/combat-trainer/script.ts deleted file mode 100644 index 60bc21fd3..000000000 --- a/scripts/combat-trainer/script.ts +++ /dev/null @@ -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 { - 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(); -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(); // 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 { - // 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 { - 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 { - 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 { - 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); diff --git a/scripts/cooking/lab_log.md b/scripts/cooking/lab_log.md deleted file mode 100644 index 03b5cd0ca..000000000 --- a/scripts/cooking/lab_log.md +++ /dev/null @@ -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 - diff --git a/scripts/cooking/script.ts b/scripts/cooking/script.ts deleted file mode 100644 index 71ac755d7..000000000 --- a/scripts/cooking/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - // 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 { - // 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 { - 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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/cowhide-banking/lab_log.md b/scripts/cowhide-banking/lab_log.md deleted file mode 100644 index 98a36d49d..000000000 --- a/scripts/cowhide-banking/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/cowhide-banking/script.ts b/scripts/cowhide-banking/script.ts deleted file mode 100644 index 3c1da24f8..000000000 --- a/scripts/cowhide-banking/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/crafting/lab_log.md b/scripts/crafting/lab_log.md deleted file mode 100644 index 19ca3978d..000000000 --- a/scripts/crafting/lab_log.md +++ /dev/null @@ -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()` diff --git a/scripts/crafting/script.ts b/scripts/crafting/script.ts deleted file mode 100644 index f9190f609..000000000 --- a/scripts/crafting/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/firemaking/lab_log.md b/scripts/firemaking/lab_log.md deleted file mode 100644 index 2e9df6d82..000000000 --- a/scripts/firemaking/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/firemaking/script.ts b/scripts/firemaking/script.ts deleted file mode 100644 index 78ed4a3ec..000000000 --- a/scripts/firemaking/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/fishing-speedrun/lab_log.md b/scripts/fishing-speedrun/lab_log.md deleted file mode 100644 index d3f9ee184..000000000 --- a/scripts/fishing-speedrun/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/fishing-speedrun/script.ts b/scripts/fishing-speedrun/script.ts deleted file mode 100644 index 2bd92b5cd..000000000 --- a/scripts/fishing-speedrun/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - // 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 { - 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 { - // 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 { - // 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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/fletching-gp/lab_log.md b/scripts/fletching-gp/lab_log.md deleted file mode 100644 index c7c7b52c6..000000000 --- a/scripts/fletching-gp/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/fletching-gp/script.ts b/scripts/fletching-gp/script.ts deleted file mode 100644 index 4f8dadc92..000000000 --- a/scripts/fletching-gp/script.ts +++ /dev/null @@ -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 { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/scripts/fletching/lab_log.md b/scripts/fletching/lab_log.md deleted file mode 100644 index a9f6a37a3..000000000 --- a/scripts/fletching/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/fletching/script.ts b/scripts/fletching/script.ts deleted file mode 100644 index 9b6c50a13..000000000 --- a/scripts/fletching/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/scripts/ladder-test/script.ts b/scripts/ladder-test/script.ts deleted file mode 100644 index 263d50371..000000000 --- a/scripts/ladder-test/script.ts +++ /dev/null @@ -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); diff --git a/scripts/magic/lab_log.md b/scripts/magic/lab_log.md deleted file mode 100644 index d0c4f9476..000000000 --- a/scripts/magic/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/magic/script.ts b/scripts/magic/script.ts deleted file mode 100644 index 8172a6156..000000000 --- a/scripts/magic/script.ts +++ /dev/null @@ -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 { - 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); diff --git a/scripts/mining-trainer/lab_log.md b/scripts/mining-trainer/lab_log.md deleted file mode 100644 index d29619bc5..000000000 --- a/scripts/mining-trainer/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/mining-trainer/script.ts b/scripts/mining-trainer/script.ts deleted file mode 100644 index 59ced79f0..000000000 --- a/scripts/mining-trainer/script.ts +++ /dev/null @@ -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 { - 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 = { - 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 { - 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(); - 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); diff --git a/scripts/prayer/lab_log.md b/scripts/prayer/lab_log.md deleted file mode 100644 index dc4fb7e65..000000000 --- a/scripts/prayer/lab_log.md +++ /dev/null @@ -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. diff --git a/scripts/prayer/script.ts b/scripts/prayer/script.ts deleted file mode 100644 index f58b36fca..000000000 --- a/scripts/prayer/script.ts +++ /dev/null @@ -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 { - 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 { - // 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 { - 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); diff --git a/scripts/ranged/lab_log.md b/scripts/ranged/lab_log.md deleted file mode 100644 index bd7aa16ca..000000000 --- a/scripts/ranged/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/ranged/script.ts b/scripts/ranged/script.ts deleted file mode 100644 index 1d92c2466..000000000 --- a/scripts/ranged/script.ts +++ /dev/null @@ -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(); -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 { - 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); diff --git a/scripts/script_best_practices.md b/scripts/script_best_practices.md deleted file mode 100644 index 013843f42..000000000 --- a/scripts/script_best_practices.md +++ /dev/null @@ -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)); - } -} -``` diff --git a/scripts/smithing/lab_log.md b/scripts/smithing/lab_log.md deleted file mode 100644 index fcf11aaf3..000000000 --- a/scripts/smithing/lab_log.md +++ /dev/null @@ -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 - diff --git a/scripts/smithing/script.ts b/scripts/smithing/script.ts deleted file mode 100644 index c6864e481..000000000 --- a/scripts/smithing/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - // 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 { - 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 { - 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); diff --git a/scripts/thieving-gp/lab_log.md b/scripts/thieving-gp/lab_log.md deleted file mode 100644 index e29b35641..000000000 --- a/scripts/thieving-gp/lab_log.md +++ /dev/null @@ -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) - diff --git a/scripts/thieving-gp/script.ts b/scripts/thieving-gp/script.ts deleted file mode 100644 index a7e5ca9b6..000000000 --- a/scripts/thieving-gp/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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); diff --git a/scripts/thieving/lab_log.md b/scripts/thieving/lab_log.md deleted file mode 100644 index 4380f8447..000000000 --- a/scripts/thieving/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/thieving/script.ts b/scripts/thieving/script.ts deleted file mode 100644 index 4987a37e7..000000000 --- a/scripts/thieving/script.ts +++ /dev/null @@ -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 { - 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); diff --git a/scripts/varrock-travel/lab_log.md b/scripts/varrock-travel/lab_log.md deleted file mode 100644 index ed707bd94..000000000 --- a/scripts/varrock-travel/lab_log.md +++ /dev/null @@ -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 diff --git a/scripts/varrock-travel/script.ts b/scripts/varrock-travel/script.ts deleted file mode 100644 index 81e7bc5c1..000000000 --- a/scripts/varrock-travel/script.ts +++ /dev/null @@ -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 { - 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 { - 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); diff --git a/scripts/woodcutting/lab_log.md b/scripts/woodcutting/lab_log.md deleted file mode 100644 index 2fa009065..000000000 --- a/scripts/woodcutting/lab_log.md +++ /dev/null @@ -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) diff --git a/scripts/woodcutting/script.ts b/scripts/woodcutting/script.ts deleted file mode 100644 index 1ad2b729c..000000000 --- a/scripts/woodcutting/script.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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); diff --git a/sdk/README.md b/sdk/README.md index ce81d20bc..587fbf251 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -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 ` 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//script.ts ``` diff --git a/sdk/SDK_DESIGN.md b/sdk/SDK_DESIGN.md deleted file mode 100644 index 7b568f778..000000000 --- a/sdk/SDK_DESIGN.md +++ /dev/null @@ -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 | diff --git a/sdk/bun.lock b/sdk/bun.lock deleted file mode 100644 index d4099a9e5..000000000 --- a/sdk/bun.lock +++ /dev/null @@ -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=="], - } -} diff --git a/sdk/test/click-dialog-by-text.test.ts b/sdk/test/click-dialog-by-text.test.ts new file mode 100644 index 000000000..aa2228c71 --- /dev/null +++ b/sdk/test/click-dialog-by-text.test.ts @@ -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 => { + calls.push(option); + return { success: true, message: 'stub-clicked' }; + }; + + return { sdk, calls }; +} + +async function run(): Promise { + 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); +}); diff --git a/sdk/test/test_status.md b/sdk/test/test_status.md deleted file mode 100644 index 0bc34d7a0..000000000 --- a/sdk/test/test_status.md +++ /dev/null @@ -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 - diff --git a/server/engine/.gitignore b/server/engine/.gitignore index c2a0a6fa3..4c9dfa80a 100644 --- a/server/engine/.gitignore +++ b/server/engine/.gitignore @@ -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 ### diff --git a/tsconfig.json b/tsconfig.json index a2bb26c40..e8b658da9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }